@hasna/todos 0.15.47 → 0.15.50

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.
Files changed (38) hide show
  1. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  2. package/dist/cli/index.js +296 -81
  3. package/dist/contracts.js +56 -5
  4. package/dist/db/task-crud.d.ts +6 -0
  5. package/dist/db/task-crud.d.ts.map +1 -1
  6. package/dist/db/tasks.d.ts +1 -1
  7. package/dist/db/tasks.d.ts.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +187 -70
  11. package/dist/lib/dedupe-projection.d.ts +48 -0
  12. package/dist/lib/dedupe-projection.d.ts.map +1 -0
  13. package/dist/lib/redaction.d.ts.map +1 -1
  14. package/dist/lib/task-dedupe.d.ts +5 -9
  15. package/dist/lib/task-dedupe.d.ts.map +1 -1
  16. package/dist/mcp/index.js +246 -80
  17. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  18. package/dist/mcp.js +3 -2
  19. package/dist/project-registration.js +128 -68
  20. package/dist/registry.js +56 -5
  21. package/dist/release-provenance.json +5 -5
  22. package/dist/server/cloud.d.ts.map +1 -1
  23. package/dist/server/index.js +248 -82
  24. package/dist/server/routes.d.ts.map +1 -1
  25. package/dist/server/v1.d.ts.map +1 -1
  26. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  27. package/dist/storage/postgres-errors.d.ts +15 -0
  28. package/dist/storage/postgres-errors.d.ts.map +1 -0
  29. package/dist/storage/postgres-sync.d.ts +7 -0
  30. package/dist/storage/postgres-sync.d.ts.map +1 -1
  31. package/dist/storage/shadow-outbox.d.ts.map +1 -1
  32. package/dist/storage/shadow.d.ts.map +1 -1
  33. package/dist/storage.js +139 -66
  34. package/dist/task-manifest.js +118 -47
  35. package/dist/task-subtree-transfer.js +326 -44
  36. package/dist/types/index.d.ts +2 -2
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/package.json +3 -2
@@ -623,13 +623,15 @@ function upsertSecretSafetyConfig(input) {
623
623
  saveConfig({ ...config, secret_safety: next });
624
624
  return next;
625
625
  }
626
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
626
+ var XAI_PREFIX, DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
627
627
  var init_redaction = __esm(() => {
628
628
  init_config();
629
+ XAI_PREFIX = ["x", "ai", "-"].join("");
629
630
  DEFAULT_SECRET_PATTERNS = [
630
631
  { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
631
632
  { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
632
633
  { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
634
+ { name: `${XAI_PREFIX}token`, regex: /\bxai[-][A-Za-z0-9]{20,80}\b/g, replacement: "[REDACTED_TOKEN]" },
633
635
  { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
634
636
  { name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
635
637
  { name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
@@ -9933,6 +9935,12 @@ function listTasks2(filter = {}, db) {
9933
9935
  const d = db || getDatabase();
9934
9936
  const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
9935
9937
  clearExpiredLocks2(d);
9938
+ if (filter.limit !== undefined) {
9939
+ const limit = filter.limit;
9940
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > MAX_TASK_LIST_LIMIT) {
9941
+ throw new TypeError(`listTasks limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT}; got ${String(limit)}`);
9942
+ }
9943
+ }
9936
9944
  const conditions = [];
9937
9945
  const params = [];
9938
9946
  if (filter.project_id) {
@@ -10466,6 +10474,7 @@ function deleteTask(id, db) {
10466
10474
  }
10467
10475
  return result.changes > 0;
10468
10476
  }
10477
+ var MAX_TASK_LIST_LIMIT = 1e5;
10469
10478
  var init_task_crud = __esm(() => {
10470
10479
  init_types();
10471
10480
  init_database();
@@ -13074,7 +13083,7 @@ import { createHash as createHash7 } from "crypto";
13074
13083
  // package.json
13075
13084
  var package_default = {
13076
13085
  name: "@hasna/todos",
13077
- version: "0.15.47",
13086
+ version: "0.15.50",
13078
13087
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
13079
13088
  type: "module",
13080
13089
  main: "dist/index.js",
@@ -13138,6 +13147,7 @@ var package_default = {
13138
13147
  ],
13139
13148
  scripts: {
13140
13149
  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",
13150
+ "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",
13141
13151
  "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/*'",
13142
13152
  migrate: "bun run src/server/index.ts migrate",
13143
13153
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
@@ -13192,7 +13202,7 @@ var package_default = {
13192
13202
  author: "Andrei Hasna <andrei@hasna.com>",
13193
13203
  license: "Apache-2.0",
13194
13204
  dependencies: {
13195
- "@hasna/contracts": "0.13.4",
13205
+ "@hasna/contracts": "0.14.0",
13196
13206
  "@hasna/events": "^0.1.11",
13197
13207
  "@modelcontextprotocol/sdk": "^1.12.1",
13198
13208
  chalk: "^5.4.1",
@@ -13341,6 +13351,30 @@ function assertPlanProjectLinkReceipt(value) {
13341
13351
  // src/storage/postgres-adapter.ts
13342
13352
  init_stale_lock_handoff();
13343
13353
 
13354
+ // src/storage/postgres-sync.ts
13355
+ init_types();
13356
+
13357
+ // src/storage/postgres-errors.ts
13358
+ function isPostgresUniqueViolation(error) {
13359
+ if (typeof error !== "object" || error === null)
13360
+ return false;
13361
+ const candidate = error;
13362
+ const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
13363
+ if (typeof candidate.cause === "object" && candidate.cause !== null) {
13364
+ const cause = candidate.cause;
13365
+ states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
13366
+ }
13367
+ return states.some((state) => String(state) === "23505");
13368
+ }
13369
+ function postgresConstraintName(error) {
13370
+ if (typeof error !== "object" || error === null)
13371
+ return "";
13372
+ const candidate = error;
13373
+ const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
13374
+ const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
13375
+ return typeof constraint === "string" ? constraint : "";
13376
+ }
13377
+
13344
13378
  // src/storage/postgres-sync.ts
13345
13379
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
13346
13380
  var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
@@ -13543,53 +13577,98 @@ class PostgresTodosSyncStore {
13543
13577
  if (routingErrors.length > 0) {
13544
13578
  throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
13545
13579
  }
13546
- const existing = await this.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
13547
- FROM ${this.tableName}
13548
- WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
13549
- const existingProjects = [];
13550
- const existingTaskLists = [];
13551
- for (const row of existing.rows) {
13552
- const payload = payloadRecord(row.payload);
13553
- if (row.object_type === "projects")
13554
- existingProjects.push(payload);
13555
- if (row.object_type === "task_lists")
13556
- existingTaskLists.push(payload);
13557
- }
13558
- const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
13559
- if (destinationErrors.length > 0) {
13560
- throw new Error(`Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
13561
- }
13562
- const result = { records: 0, objectTypes: {} };
13563
- const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
13564
- for (const entry of snapshotEntries(snapshot)) {
13565
- if (entry.deletedAt === null)
13566
- assertCanonicalScopedSlugEntry(entry);
13567
- await this.client.query(`INSERT INTO ${this.tableName} (
13568
- service, object_type, object_id, payload, updated_at,
13569
- deleted_at, source_machine_id, version
13570
- ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
13571
- ON CONFLICT (service, object_type, object_id) DO UPDATE SET
13572
- payload = EXCLUDED.payload,
13573
- updated_at = EXCLUDED.updated_at,
13574
- deleted_at = EXCLUDED.deleted_at,
13575
- source_machine_id = EXCLUDED.source_machine_id,
13576
- version = EXCLUDED.version
13577
- WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
13578
- OR (${this.tableName}.updated_at = EXCLUDED.updated_at
13579
- AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
13580
- this.service,
13581
- entry.type,
13582
- entry.id,
13583
- entry.payload,
13584
- entry.updatedAt,
13585
- entry.deletedAt,
13586
- sourceMachineId,
13587
- entry.version
13588
- ]);
13589
- result.records += 1;
13590
- result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
13580
+ const push = async (client) => {
13581
+ const existing = await client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
13582
+ FROM ${this.tableName}
13583
+ WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
13584
+ const existingProjects = [];
13585
+ const existingTaskLists = [];
13586
+ for (const row of existing.rows) {
13587
+ const payload = payloadRecord(row.payload);
13588
+ if (row.object_type === "projects")
13589
+ existingProjects.push(payload);
13590
+ if (row.object_type === "task_lists")
13591
+ existingTaskLists.push(payload);
13592
+ }
13593
+ const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
13594
+ if (destinationErrors.length > 0) {
13595
+ throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
13596
+ }
13597
+ const result = { records: 0, objectTypes: {} };
13598
+ const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
13599
+ for (const entry of snapshotEntries(snapshot)) {
13600
+ if (entry.deletedAt === null)
13601
+ assertCanonicalScopedSlugEntry(entry);
13602
+ try {
13603
+ await client.query(`INSERT INTO ${this.tableName} (
13604
+ service, object_type, object_id, payload, updated_at,
13605
+ deleted_at, source_machine_id, version
13606
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
13607
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
13608
+ payload = EXCLUDED.payload,
13609
+ updated_at = EXCLUDED.updated_at,
13610
+ deleted_at = EXCLUDED.deleted_at,
13611
+ source_machine_id = EXCLUDED.source_machine_id,
13612
+ version = EXCLUDED.version
13613
+ WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
13614
+ OR (${this.tableName}.updated_at = EXCLUDED.updated_at
13615
+ AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
13616
+ this.service,
13617
+ entry.type,
13618
+ entry.id,
13619
+ entry.payload,
13620
+ entry.updatedAt,
13621
+ entry.deletedAt,
13622
+ sourceMachineId,
13623
+ entry.version
13624
+ ]);
13625
+ } catch (error) {
13626
+ await this.classifySyncInsertConflict(error, entry);
13627
+ }
13628
+ result.records += 1;
13629
+ result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
13630
+ }
13631
+ return result;
13632
+ };
13633
+ if (typeof this.client.transaction === "function") {
13634
+ return this.client.transaction((client) => push(client));
13591
13635
  }
13592
- return result;
13636
+ return push(this.client);
13637
+ }
13638
+ async classifySyncInsertConflict(error, entry) {
13639
+ if (!isPostgresUniqueViolation(error))
13640
+ throw error;
13641
+ const payload = entry.payload;
13642
+ const taskListSlug = String(payload["slug"] ?? "");
13643
+ const projectSlug = String(payload["task_list_id"] ?? "");
13644
+ const constraintName = postgresConstraintName(error);
13645
+ if (entry.type === "projects" && constraintName.includes("project_task_list_slug_uidx")) {
13646
+ throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
13647
+ }
13648
+ if (entry.type === "task_lists" && constraintName.includes("task_list_scope_slug_uidx")) {
13649
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
13650
+ }
13651
+ if (entry.type === "task_lists") {
13652
+ const scope = typeof payload["project_id"] === "string" ? payload["project_id"] : "";
13653
+ const conflict = await this.client.query(`/* todos:classify-sync-task-list-conflict */ SELECT EXISTS (
13654
+ SELECT 1 FROM ${this.tableName}
13655
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id <> $2
13656
+ AND deleted_at IS NULL AND COALESCE(payload->>'project_id','') = $3 AND payload->>'slug' = $4
13657
+ ) AS conflict`, [this.service, entry.id, scope, taskListSlug]);
13658
+ if (conflict.rows[0]?.conflict) {
13659
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
13660
+ }
13661
+ } else if (entry.type === "projects") {
13662
+ const conflict = await this.client.query(`/* todos:classify-sync-project-conflict */ SELECT EXISTS (
13663
+ SELECT 1 FROM ${this.tableName}
13664
+ WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
13665
+ AND deleted_at IS NULL AND payload->>'task_list_id' = $3
13666
+ ) AS conflict`, [this.service, entry.id, projectSlug]);
13667
+ if (conflict.rows[0]?.conflict) {
13668
+ throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
13669
+ }
13670
+ }
13671
+ throw error;
13593
13672
  }
13594
13673
  async pullSnapshot(options = {}) {
13595
13674
  const params = [this.service];
@@ -16566,25 +16645,6 @@ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
16566
16645
  }
16567
16646
  throw lastError;
16568
16647
  }
16569
- function isPostgresUniqueViolation(error) {
16570
- if (typeof error !== "object" || error === null)
16571
- return false;
16572
- const candidate = error;
16573
- const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
16574
- if (typeof candidate.cause === "object" && candidate.cause !== null) {
16575
- const cause = candidate.cause;
16576
- states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
16577
- }
16578
- return states.some((state) => String(state) === "23505");
16579
- }
16580
- function postgresConstraintName(error) {
16581
- if (typeof error !== "object" || error === null)
16582
- return "";
16583
- const candidate = error;
16584
- const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
16585
- const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
16586
- return typeof constraint === "string" ? constraint : "";
16587
- }
16588
16648
  // src/project-registration/types.ts
16589
16649
  var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1";
16590
16650
  var TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1";
package/dist/registry.js CHANGED
@@ -5531,13 +5531,15 @@ function upsertSecretSafetyConfig(input) {
5531
5531
  saveConfig({ ...config, secret_safety: next });
5532
5532
  return next;
5533
5533
  }
5534
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
5534
+ var XAI_PREFIX, DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
5535
5535
  var init_redaction = __esm(() => {
5536
5536
  init_config2();
5537
+ XAI_PREFIX = ["x", "ai", "-"].join("");
5537
5538
  DEFAULT_SECRET_PATTERNS = [
5538
5539
  { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
5539
5540
  { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
5540
5541
  { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
5542
+ { name: `${XAI_PREFIX}token`, regex: /\bxai[-][A-Za-z0-9]{20,80}\b/g, replacement: "[REDACTED_TOKEN]" },
5541
5543
  { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
5542
5544
  { name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
5543
5545
  { name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
@@ -9933,6 +9935,12 @@ function listTasks(filter = {}, db) {
9933
9935
  const d = db || getDatabase();
9934
9936
  const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
9935
9937
  clearExpiredLocks2(d);
9938
+ if (filter.limit !== undefined) {
9939
+ const limit = filter.limit;
9940
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > MAX_TASK_LIST_LIMIT) {
9941
+ throw new TypeError(`listTasks limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT}; got ${String(limit)}`);
9942
+ }
9943
+ }
9936
9944
  const conditions = [];
9937
9945
  const params = [];
9938
9946
  if (filter.project_id) {
@@ -10466,6 +10474,7 @@ function deleteTask(id, db) {
10466
10474
  }
10467
10475
  return result.changes > 0;
10468
10476
  }
10477
+ var MAX_TASK_LIST_LIMIT = 1e5;
10469
10478
  var init_task_crud = __esm(() => {
10470
10479
  init_types();
10471
10480
  init_database();
@@ -13071,7 +13080,7 @@ var init_tasks = __esm(() => {
13071
13080
  // package.json
13072
13081
  var package_default = {
13073
13082
  name: "@hasna/todos",
13074
- version: "0.15.47",
13083
+ version: "0.15.50",
13075
13084
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
13076
13085
  type: "module",
13077
13086
  main: "dist/index.js",
@@ -13135,6 +13144,7 @@ var package_default = {
13135
13144
  ],
13136
13145
  scripts: {
13137
13146
  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",
13147
+ "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",
13138
13148
  "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/*'",
13139
13149
  migrate: "bun run src/server/index.ts migrate",
13140
13150
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
@@ -13189,7 +13199,7 @@ var package_default = {
13189
13199
  author: "Andrei Hasna <andrei@hasna.com>",
13190
13200
  license: "Apache-2.0",
13191
13201
  dependencies: {
13192
- "@hasna/contracts": "0.13.4",
13202
+ "@hasna/contracts": "0.14.0",
13193
13203
  "@hasna/events": "^0.1.11",
13194
13204
  "@modelcontextprotocol/sdk": "^1.12.1",
13195
13205
  chalk: "^5.4.1",
@@ -22016,6 +22026,47 @@ function autoDetectFileRelationships(taskId, db) {
22016
22026
 
22017
22027
  // src/lib/task-dedupe.ts
22018
22028
  init_tasks();
22029
+
22030
+ // src/lib/dedupe-projection.ts
22031
+ init_redaction();
22032
+ var DEDUPE_SOURCE_KEY_ALLOWLIST = [
22033
+ "github_url",
22034
+ "github_issue_url",
22035
+ "github_pr_url",
22036
+ "source_url",
22037
+ "url",
22038
+ "external_url",
22039
+ "issue_url",
22040
+ "github_owner",
22041
+ "github_repo",
22042
+ "github_number"
22043
+ ];
22044
+ function projectTasksForDedupe(tasks) {
22045
+ return tasks.map((task2) => {
22046
+ const metadata = {};
22047
+ for (const key of DEDUPE_SOURCE_KEY_ALLOWLIST) {
22048
+ if (key in task2.metadata) {
22049
+ metadata[key] = redactValue(task2.metadata[key]);
22050
+ }
22051
+ }
22052
+ return {
22053
+ id: task2.id,
22054
+ short_id: task2.short_id,
22055
+ title: redactEvidenceText(task2.title),
22056
+ description: task2.description === null || task2.description === undefined ? null : redactEvidenceText(task2.description),
22057
+ status: task2.status,
22058
+ created_at: task2.created_at,
22059
+ updated_at: task2.updated_at,
22060
+ project_id: task2.project_id,
22061
+ task_list_id: task2.task_list_id,
22062
+ assigned_to: task2.assigned_to,
22063
+ priority: task2.priority,
22064
+ metadata
22065
+ };
22066
+ });
22067
+ }
22068
+
22069
+ // src/lib/task-dedupe.ts
22019
22070
  var DEFAULT_THRESHOLD = 0.74;
22020
22071
  var STOP_WORDS = new Set([
22021
22072
  "a",
@@ -22208,10 +22259,10 @@ function olderFirst(left, right) {
22208
22259
  function findDuplicateTasks(options = {}, db) {
22209
22260
  const d = db || getDatabase();
22210
22261
  const threshold = options.threshold ?? DEFAULT_THRESHOLD;
22211
- const fingerprints = listTasks({
22262
+ const fingerprints = projectTasksForDedupe(listTasks({
22212
22263
  include_archived: Boolean(options.include_archived),
22213
22264
  limit: options.limit ?? 1000
22214
- }, d).map(fingerprint);
22265
+ }, d)).map(fingerprint);
22215
22266
  const candidates = [];
22216
22267
  for (let i = 0;i < fingerprints.length; i++) {
22217
22268
  for (let j = i + 1;j < fingerprints.length; j++) {
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.15.47",
3
+ "packageVersion": "0.15.50",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "79cfb6879757ed256e01a8e873ec2c61675e3b68",
6
- "gitTree": "3071c5617a4bc07dfc75dc1460d2020bb21d68a0",
7
- "sourceTreeSha256": "0f1b1a26ecedf68f4aaa2d9cf955913c9dd16c72797f88605fe065bb703bcc33",
8
- "generatedAt": "2026-08-23T10:40:26.000Z"
5
+ "gitCommit": "d1c3300ee75f88ed5c96669a646459e2fe75f7d0",
6
+ "gitTree": "95740bb8e3519601b1fc002199a875f122308a90",
7
+ "sourceTreeSha256": "d4a4e6676850b53226707c4a8720a411555ed37a56faacc2796399bec6afdf1a",
8
+ "generatedAt": "2026-08-25T21:08:07.000Z"
9
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/server/cloud.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAwB,MAAM,uBAAuB,CAAC;AAG1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAKvD,OAAO,EAGL,KAAK,iCAAiC,EACvC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAGL,KAAK,iCAAiC,EACvC,MAAM,mCAAmC,CAAC;AAQ3C,OAAO,EAEL,KAAK,+BAA+B,EACpC,KAAK,8BAA8B,EACpC,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC7B,MAAM,kCAAkC,CAAC;AAE1C,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC,uFAAuF;AACvF,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,SAAS,CAOhG;AAED,+DAA+D;AAC/D,wBAAgB,oBAAoB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,SAAS,CAO7F;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEzF;AA4BD,4EAA4E;AAC5E,wBAAgB,sBAAsB,IAAI,mBAAmB,CAK5D;AAED,gFAAgF;AAChF,wBAAgB,qBAAqB,IAAI,aAAa,CAIrD;AAED,iFAAiF;AACjF,wBAAgB,oCAAoC,IAAI,iCAAiC,CAYxF;AAED,gFAAgF;AAChF,wBAAgB,6BAA6B,IAAI,0BAA0B,CAO1E;AAED,wFAAwF;AACxF,wBAAgB,oCAAoC,IAAI,iCAAiC,CAOxF;AAuBD,wBAAgB,cAAc,IAAI,WAAW,CAI5C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,cAAc,CAoBjD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkCvD;AAED;;;;GAIG;AACH,wBAAsB,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnE;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC,CAEjE;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,IAAI,CAAC,CAElE;AAED,uFAAuF;AACvF,wBAAsB,kCAAkC,IAAI,OAAO,CAAC,IAAI,CAAC,CAExE;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAS9D;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,8BAA8B,CAAC,CAEzC;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED,iEAAiE;AACjE,wBAAsB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAIlD;AAED,4BAA4B;AAC5B,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAWhD"}
1
+ {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/server/cloud.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAwB,MAAM,uBAAuB,CAAC;AAG1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAKvD,OAAO,EAGL,KAAK,iCAAiC,EACvC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAGL,KAAK,iCAAiC,EACvC,MAAM,mCAAmC,CAAC;AAQ3C,OAAO,EAEL,KAAK,+BAA+B,EACpC,KAAK,8BAA8B,EACpC,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC7B,MAAM,kCAAkC,CAAC;AAE1C,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC,uFAAuF;AACvF,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,SAAS,CAOhG;AAED,+DAA+D;AAC/D,wBAAgB,oBAAoB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,SAAS,CAO7F;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEzF;AAgDD,4EAA4E;AAC5E,wBAAgB,sBAAsB,IAAI,mBAAmB,CAK5D;AAED,gFAAgF;AAChF,wBAAgB,qBAAqB,IAAI,aAAa,CAIrD;AAED,iFAAiF;AACjF,wBAAgB,oCAAoC,IAAI,iCAAiC,CAYxF;AAED,gFAAgF;AAChF,wBAAgB,6BAA6B,IAAI,0BAA0B,CAO1E;AAED,wFAAwF;AACxF,wBAAgB,oCAAoC,IAAI,iCAAiC,CAOxF;AAuBD,wBAAgB,cAAc,IAAI,WAAW,CAI5C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,cAAc,CAoBjD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAiDvD;AAED;;;;GAIG;AACH,wBAAsB,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnE;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC,CAEjE;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,IAAI,CAAC,CAElE;AAED,uFAAuF;AACvF,wBAAsB,kCAAkC,IAAI,OAAO,CAAC,IAAI,CAAC,CAExE;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAS9D;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,8BAA8B,CAAC,CAEzC;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED,iEAAiE;AACjE,wBAAsB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAIlD;AAED,4BAA4B;AAC5B,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAahD"}