@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
package/dist/mcp/index.js CHANGED
@@ -9582,13 +9582,15 @@ function upsertSecretSafetyConfig(input) {
9582
9582
  saveConfig({ ...config, secret_safety: next });
9583
9583
  return next;
9584
9584
  }
9585
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
9585
+ var XAI_PREFIX, DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
9586
9586
  var init_redaction = __esm(() => {
9587
9587
  init_config2();
9588
+ XAI_PREFIX = ["x", "ai", "-"].join("");
9588
9589
  DEFAULT_SECRET_PATTERNS = [
9589
9590
  { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
9590
9591
  { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
9591
9592
  { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
9593
+ { name: `${XAI_PREFIX}token`, regex: /\bxai[-][A-Za-z0-9]{20,80}\b/g, replacement: "[REDACTED_TOKEN]" },
9592
9594
  { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
9593
9595
  { name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
9594
9596
  { name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
@@ -14571,6 +14573,12 @@ function listTasks(filter = {}, db) {
14571
14573
  const d = db || getDatabase();
14572
14574
  const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
14573
14575
  clearExpiredLocks2(d);
14576
+ if (filter.limit !== undefined) {
14577
+ const limit = filter.limit;
14578
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > MAX_TASK_LIST_LIMIT) {
14579
+ throw new TypeError(`listTasks limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT}; got ${String(limit)}`);
14580
+ }
14581
+ }
14574
14582
  const conditions = [];
14575
14583
  const params = [];
14576
14584
  if (filter.project_id) {
@@ -15104,6 +15112,7 @@ function deleteTask(id, db) {
15104
15112
  }
15105
15113
  return result.changes > 0;
15106
15114
  }
15115
+ var MAX_TASK_LIST_LIMIT = 1e5;
15107
15116
  var init_task_crud = __esm(() => {
15108
15117
  init_types();
15109
15118
  init_database();
@@ -18018,7 +18027,8 @@ __export(exports_tasks, {
18018
18027
  buildTaskBoardSnapshot: () => buildTaskBoardSnapshot,
18019
18028
  archiveTasks: () => archiveTasks,
18020
18029
  archiveCompletedTasks: () => archiveCompletedTasks,
18021
- addDependency: () => addDependency
18030
+ addDependency: () => addDependency,
18031
+ MAX_TASK_LIST_LIMIT: () => MAX_TASK_LIST_LIMIT
18022
18032
  });
18023
18033
  var init_tasks = __esm(() => {
18024
18034
  init_task_crud();
@@ -22441,8 +22451,8 @@ function registerTaskCrudTools(server, ctx) {
22441
22451
  tags: exports_external.array(exports_external.string()).optional().describe("Filter by tags (AND logic)"),
22442
22452
  created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
22443
22453
  created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
22444
- limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
22445
- offset: exports_external.number().optional().describe("Pagination offset"),
22454
+ limit: exports_external.number().int().min(1).optional().describe("Max results (default: 50, max 500)"),
22455
+ offset: exports_external.number().int().min(0).optional().describe("Pagination offset"),
22446
22456
  metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
22447
22457
  }, async (params) => {
22448
22458
  try {
@@ -25792,6 +25802,48 @@ var init_task_relationships = __esm(() => {
25792
25802
  ];
25793
25803
  });
25794
25804
 
25805
+ // src/lib/dedupe-projection.ts
25806
+ function projectTasksForDedupe(tasks) {
25807
+ return tasks.map((task) => {
25808
+ const metadata = {};
25809
+ for (const key of DEDUPE_SOURCE_KEY_ALLOWLIST) {
25810
+ if (key in task.metadata) {
25811
+ metadata[key] = redactValue(task.metadata[key]);
25812
+ }
25813
+ }
25814
+ return {
25815
+ id: task.id,
25816
+ short_id: task.short_id,
25817
+ title: redactEvidenceText(task.title),
25818
+ description: task.description === null || task.description === undefined ? null : redactEvidenceText(task.description),
25819
+ status: task.status,
25820
+ created_at: task.created_at,
25821
+ updated_at: task.updated_at,
25822
+ project_id: task.project_id,
25823
+ task_list_id: task.task_list_id,
25824
+ assigned_to: task.assigned_to,
25825
+ priority: task.priority,
25826
+ metadata
25827
+ };
25828
+ });
25829
+ }
25830
+ var DEDUPE_SOURCE_KEY_ALLOWLIST;
25831
+ var init_dedupe_projection = __esm(() => {
25832
+ init_redaction();
25833
+ DEDUPE_SOURCE_KEY_ALLOWLIST = [
25834
+ "github_url",
25835
+ "github_issue_url",
25836
+ "github_pr_url",
25837
+ "source_url",
25838
+ "url",
25839
+ "external_url",
25840
+ "issue_url",
25841
+ "github_owner",
25842
+ "github_repo",
25843
+ "github_number"
25844
+ ];
25845
+ });
25846
+
25795
25847
  // src/lib/task-dedupe.ts
25796
25848
  function asObject(value) {
25797
25849
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -25956,10 +26008,10 @@ function olderFirst(left, right) {
25956
26008
  function findDuplicateTasks(options = {}, db) {
25957
26009
  const d = db || getDatabase();
25958
26010
  const threshold = options.threshold ?? DEFAULT_THRESHOLD;
25959
- const fingerprints = listTasks({
26011
+ const fingerprints = projectTasksForDedupe(listTasks({
25960
26012
  include_archived: Boolean(options.include_archived),
25961
26013
  limit: options.limit ?? 1000
25962
- }, d).map(fingerprint);
26014
+ }, d)).map(fingerprint);
25963
26015
  const candidates = [];
25964
26016
  for (let i = 0;i < fingerprints.length; i++) {
25965
26017
  for (let j = i + 1;j < fingerprints.length; j++) {
@@ -26194,6 +26246,7 @@ var init_task_dedupe = __esm(() => {
26194
26246
  init_database();
26195
26247
  init_task_relationships();
26196
26248
  init_tasks();
26249
+ init_dedupe_projection();
26197
26250
  STOP_WORDS = new Set([
26198
26251
  "a",
26199
26252
  "an",
@@ -36364,7 +36417,7 @@ var package_default;
36364
36417
  var init_package = __esm(() => {
36365
36418
  package_default = {
36366
36419
  name: "@hasna/todos",
36367
- version: "0.15.47",
36420
+ version: "0.15.50",
36368
36421
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
36369
36422
  type: "module",
36370
36423
  main: "dist/index.js",
@@ -36428,6 +36481,7 @@ var init_package = __esm(() => {
36428
36481
  ],
36429
36482
  scripts: {
36430
36483
  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",
36484
+ "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",
36431
36485
  "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/*'",
36432
36486
  migrate: "bun run src/server/index.ts migrate",
36433
36487
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
@@ -36482,7 +36536,7 @@ var init_package = __esm(() => {
36482
36536
  author: "Andrei Hasna <andrei@hasna.com>",
36483
36537
  license: "Apache-2.0",
36484
36538
  dependencies: {
36485
- "@hasna/contracts": "0.13.4",
36539
+ "@hasna/contracts": "0.14.0",
36486
36540
  "@hasna/events": "^0.1.11",
36487
36541
  "@modelcontextprotocol/sdk": "^1.12.1",
36488
36542
  chalk: "^5.4.1",
@@ -46738,6 +46792,27 @@ var init_local_sqlite = __esm(() => {
46738
46792
  TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
46739
46793
  });
46740
46794
 
46795
+ // src/storage/postgres-errors.ts
46796
+ function isPostgresUniqueViolation(error) {
46797
+ if (typeof error !== "object" || error === null)
46798
+ return false;
46799
+ const candidate = error;
46800
+ const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
46801
+ if (typeof candidate.cause === "object" && candidate.cause !== null) {
46802
+ const cause = candidate.cause;
46803
+ states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
46804
+ }
46805
+ return states.some((state) => String(state) === "23505");
46806
+ }
46807
+ function postgresConstraintName(error) {
46808
+ if (typeof error !== "object" || error === null)
46809
+ return "";
46810
+ const candidate = error;
46811
+ const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
46812
+ const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
46813
+ return typeof constraint === "string" ? constraint : "";
46814
+ }
46815
+
46741
46816
  // src/storage/postgres-sync.ts
46742
46817
  function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE, cursorTableName = DEFAULT_TODOS_POSTGRES_CURSOR_TABLE) {
46743
46818
  assertSafeIdentifier(tableName);
@@ -46932,53 +47007,98 @@ class PostgresTodosSyncStore {
46932
47007
  if (routingErrors.length > 0) {
46933
47008
  throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
46934
47009
  }
46935
- const existing = await this.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
46936
- FROM ${this.tableName}
46937
- WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
46938
- const existingProjects = [];
46939
- const existingTaskLists = [];
46940
- for (const row of existing.rows) {
46941
- const payload = payloadRecord(row.payload);
46942
- if (row.object_type === "projects")
46943
- existingProjects.push(payload);
46944
- if (row.object_type === "task_lists")
46945
- existingTaskLists.push(payload);
46946
- }
46947
- const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
46948
- if (destinationErrors.length > 0) {
46949
- throw new Error(`Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
46950
- }
46951
- const result = { records: 0, objectTypes: {} };
46952
- const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
46953
- for (const entry of snapshotEntries(snapshot)) {
46954
- if (entry.deletedAt === null)
46955
- assertCanonicalScopedSlugEntry(entry);
46956
- await this.client.query(`INSERT INTO ${this.tableName} (
46957
- service, object_type, object_id, payload, updated_at,
46958
- deleted_at, source_machine_id, version
46959
- ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
46960
- ON CONFLICT (service, object_type, object_id) DO UPDATE SET
46961
- payload = EXCLUDED.payload,
46962
- updated_at = EXCLUDED.updated_at,
46963
- deleted_at = EXCLUDED.deleted_at,
46964
- source_machine_id = EXCLUDED.source_machine_id,
46965
- version = EXCLUDED.version
46966
- WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
46967
- OR (${this.tableName}.updated_at = EXCLUDED.updated_at
46968
- AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
46969
- this.service,
46970
- entry.type,
46971
- entry.id,
46972
- entry.payload,
46973
- entry.updatedAt,
46974
- entry.deletedAt,
46975
- sourceMachineId,
46976
- entry.version
46977
- ]);
46978
- result.records += 1;
46979
- result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
47010
+ const push = async (client) => {
47011
+ const existing = await client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
47012
+ FROM ${this.tableName}
47013
+ WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
47014
+ const existingProjects = [];
47015
+ const existingTaskLists = [];
47016
+ for (const row of existing.rows) {
47017
+ const payload = payloadRecord(row.payload);
47018
+ if (row.object_type === "projects")
47019
+ existingProjects.push(payload);
47020
+ if (row.object_type === "task_lists")
47021
+ existingTaskLists.push(payload);
47022
+ }
47023
+ const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
47024
+ if (destinationErrors.length > 0) {
47025
+ throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
47026
+ }
47027
+ const result = { records: 0, objectTypes: {} };
47028
+ const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
47029
+ for (const entry of snapshotEntries(snapshot)) {
47030
+ if (entry.deletedAt === null)
47031
+ assertCanonicalScopedSlugEntry(entry);
47032
+ try {
47033
+ await client.query(`INSERT INTO ${this.tableName} (
47034
+ service, object_type, object_id, payload, updated_at,
47035
+ deleted_at, source_machine_id, version
47036
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
47037
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
47038
+ payload = EXCLUDED.payload,
47039
+ updated_at = EXCLUDED.updated_at,
47040
+ deleted_at = EXCLUDED.deleted_at,
47041
+ source_machine_id = EXCLUDED.source_machine_id,
47042
+ version = EXCLUDED.version
47043
+ WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
47044
+ OR (${this.tableName}.updated_at = EXCLUDED.updated_at
47045
+ AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
47046
+ this.service,
47047
+ entry.type,
47048
+ entry.id,
47049
+ entry.payload,
47050
+ entry.updatedAt,
47051
+ entry.deletedAt,
47052
+ sourceMachineId,
47053
+ entry.version
47054
+ ]);
47055
+ } catch (error) {
47056
+ await this.classifySyncInsertConflict(error, entry);
47057
+ }
47058
+ result.records += 1;
47059
+ result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
47060
+ }
47061
+ return result;
47062
+ };
47063
+ if (typeof this.client.transaction === "function") {
47064
+ return this.client.transaction((client) => push(client));
46980
47065
  }
46981
- return result;
47066
+ return push(this.client);
47067
+ }
47068
+ async classifySyncInsertConflict(error, entry) {
47069
+ if (!isPostgresUniqueViolation(error))
47070
+ throw error;
47071
+ const payload = entry.payload;
47072
+ const taskListSlug = String(payload["slug"] ?? "");
47073
+ const projectSlug = String(payload["task_list_id"] ?? "");
47074
+ const constraintName = postgresConstraintName(error);
47075
+ if (entry.type === "projects" && constraintName.includes("project_task_list_slug_uidx")) {
47076
+ throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
47077
+ }
47078
+ if (entry.type === "task_lists" && constraintName.includes("task_list_scope_slug_uidx")) {
47079
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
47080
+ }
47081
+ if (entry.type === "task_lists") {
47082
+ const scope = typeof payload["project_id"] === "string" ? payload["project_id"] : "";
47083
+ const conflict = await this.client.query(`/* todos:classify-sync-task-list-conflict */ SELECT EXISTS (
47084
+ SELECT 1 FROM ${this.tableName}
47085
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id <> $2
47086
+ AND deleted_at IS NULL AND COALESCE(payload->>'project_id','') = $3 AND payload->>'slug' = $4
47087
+ ) AS conflict`, [this.service, entry.id, scope, taskListSlug]);
47088
+ if (conflict.rows[0]?.conflict) {
47089
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
47090
+ }
47091
+ } else if (entry.type === "projects") {
47092
+ const conflict = await this.client.query(`/* todos:classify-sync-project-conflict */ SELECT EXISTS (
47093
+ SELECT 1 FROM ${this.tableName}
47094
+ WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
47095
+ AND deleted_at IS NULL AND payload->>'task_list_id' = $3
47096
+ ) AS conflict`, [this.service, entry.id, projectSlug]);
47097
+ if (conflict.rows[0]?.conflict) {
47098
+ throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
47099
+ }
47100
+ }
47101
+ throw error;
46982
47102
  }
46983
47103
  async pullSnapshot(options = {}) {
46984
47104
  const params = [this.service];
@@ -47135,6 +47255,7 @@ function assertSafeIdentifier(value) {
47135
47255
  }
47136
47256
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records", DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors", PostgresScopedSlugMigrationConflictError, PostgresScopedSlugIndexBuildError;
47137
47257
  var init_postgres_sync = __esm(() => {
47258
+ init_types();
47138
47259
  PostgresScopedSlugMigrationConflictError = class PostgresScopedSlugMigrationConflictError extends Error {
47139
47260
  conflicts;
47140
47261
  constructor(conflicts) {
@@ -47280,6 +47401,12 @@ class TodosShadowOutbox {
47280
47401
  this.onEvent?.({ type: "mirrored", objectType: row.object_type, id: row.object_id, lagMs });
47281
47402
  } catch (error) {
47282
47403
  const message = error instanceof Error ? error.message : String(error);
47404
+ if (error instanceof ResourceConflictError) {
47405
+ this.metrics.lastError = message;
47406
+ this.db.run(`UPDATE shadow_outbox SET attempts=?, last_error=?, status='failed' WHERE seq=? AND revision=?`, [row.attempts + 1, message, row.seq, row.revision]);
47407
+ this.onEvent?.({ type: "parked", objectType: row.object_type, id: row.object_id, error: message });
47408
+ return;
47409
+ }
47283
47410
  this.metrics.retries += 1;
47284
47411
  this.metrics.lastError = message;
47285
47412
  const attempts = row.attempts + 1;
@@ -47402,6 +47529,7 @@ function emptySnapshot() {
47402
47529
  }
47403
47530
  var MAX_BACKOFF_MS;
47404
47531
  var init_shadow_outbox = __esm(() => {
47532
+ init_types();
47405
47533
  init_local_sqlite();
47406
47534
  init_postgres_sync();
47407
47535
  init_shadow_outbox_schema();
@@ -50090,25 +50218,6 @@ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
50090
50218
  }
50091
50219
  throw lastError;
50092
50220
  }
50093
- function isPostgresUniqueViolation(error) {
50094
- if (typeof error !== "object" || error === null)
50095
- return false;
50096
- const candidate = error;
50097
- const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
50098
- if (typeof candidate.cause === "object" && candidate.cause !== null) {
50099
- const cause = candidate.cause;
50100
- states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
50101
- }
50102
- return states.some((state) => String(state) === "23505");
50103
- }
50104
- function postgresConstraintName(error) {
50105
- if (typeof error !== "object" || error === null)
50106
- return "";
50107
- const candidate = error;
50108
- const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
50109
- const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
50110
- return typeof constraint === "string" ? constraint : "";
50111
- }
50112
50221
  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;
50113
50222
  var init_postgres_adapter = __esm(() => {
50114
50223
  init_types();
@@ -56008,6 +56117,13 @@ function resolveSigningSecret(env = process.env) {
56008
56117
  function isPostgresBackendConfigured(env = process.env) {
56009
56118
  return Boolean(resolveCloudDatabaseUrl(env));
56010
56119
  }
56120
+ function schemaRetryMinIntervalMs(env = process.env) {
56121
+ const raw = env.HASNA_TODOS_SCHEMA_RETRY_MIN_MS;
56122
+ if (!raw)
56123
+ return DEFAULT_SCHEMA_RETRY_MIN_MS;
56124
+ const parsed = Number(raw);
56125
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_SCHEMA_RETRY_MIN_MS;
56126
+ }
56011
56127
  function getCloudTenantId() {
56012
56128
  return process.env.HASNA_TODOS_TENANT_ID ?? "default";
56013
56129
  }
@@ -56103,6 +56219,10 @@ function getCloudVerifier() {
56103
56219
  async function ensureCloudSchema() {
56104
56220
  if (schemaEnsured)
56105
56221
  return schemaEnsured;
56222
+ if (lastSchemaFailure !== null && Date.now() - lastSchemaAttemptAtMs < schemaRetryMinIntervalMs()) {
56223
+ throw lastSchemaFailure;
56224
+ }
56225
+ lastSchemaAttemptAtMs = Date.now();
56106
56226
  schemaEnsured = (async () => {
56107
56227
  const client = getClient();
56108
56228
  for (const sql of postgresTodosSyncSchemaSql()) {
@@ -56123,6 +56243,7 @@ async function ensureCloudSchema() {
56123
56243
  await getApiKeyStore().ensureSchema();
56124
56244
  })().catch((error) => {
56125
56245
  schemaEnsured = null;
56246
+ lastSchemaFailure = error;
56126
56247
  throw error;
56127
56248
  });
56128
56249
  return schemaEnsured;
@@ -56170,8 +56291,10 @@ async function closeCloud() {
56170
56291
  cachedTaskManifestAuthority = null;
56171
56292
  cachedTaskSubtreeTransferAuthority = null;
56172
56293
  schemaEnsured = null;
56294
+ lastSchemaAttemptAtMs = 0;
56295
+ lastSchemaFailure = null;
56173
56296
  }
56174
- var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, cachedTaskSubtreeTransferAuthority = null, schemaEnsured = null;
56297
+ 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;
56175
56298
  var init_cloud = __esm(() => {
56176
56299
  init_cloud_client();
56177
56300
  init_postgres_adapter();
@@ -56695,6 +56818,19 @@ function taskStatusQueryParam(url) {
56695
56818
  return { ok: false, message: result.message };
56696
56819
  return { ok: true, value: collapseEnumValues(result.values) };
56697
56820
  }
56821
+ function parsePaginationQueryParam(url, name) {
56822
+ const raw = url.searchParams.get(name);
56823
+ if (raw === null)
56824
+ return { ok: true, value: undefined };
56825
+ const min = name === "limit" ? 1 : 0;
56826
+ const message = name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer";
56827
+ if (!/^\d+$/.test(raw))
56828
+ return { ok: false, message };
56829
+ const value = Number(raw);
56830
+ if (!Number.isSafeInteger(value) || value < min)
56831
+ return { ok: false, message };
56832
+ return { ok: true, value };
56833
+ }
56698
56834
  async function handleListTasks(_req, url, _ctx, json5, taskToSummary2) {
56699
56835
  const statusParam = taskStatusQueryParam(url);
56700
56836
  if (!statusParam.ok)
@@ -56702,16 +56838,20 @@ async function handleListTasks(_req, url, _ctx, json5, taskToSummary2) {
56702
56838
  const projectId = url.searchParams.get("project_id") || undefined;
56703
56839
  const sessionId = url.searchParams.get("session_id") || undefined;
56704
56840
  const agentId = url.searchParams.get("agent_id") || undefined;
56705
- const limitParam = url.searchParams.get("limit");
56706
- const offsetParam = url.searchParams.get("offset");
56841
+ const limitParam = parsePaginationQueryParam(url, "limit");
56842
+ if (!limitParam.ok)
56843
+ return json5({ error: limitParam.message }, 400);
56844
+ const offsetParam = parsePaginationQueryParam(url, "offset");
56845
+ if (!offsetParam.ok)
56846
+ return json5({ error: offsetParam.message }, 400);
56707
56847
  const fields = parseFieldsParam(url);
56708
56848
  const tasks = listTasks({
56709
56849
  status: statusParam.value,
56710
56850
  project_id: projectId,
56711
56851
  session_id: sessionId,
56712
56852
  agent_id: agentId,
56713
- limit: limitParam ? parseInt(limitParam, 10) : undefined,
56714
- offset: offsetParam ? parseInt(offsetParam, 10) : undefined
56853
+ limit: limitParam.value,
56854
+ offset: offsetParam.value
56715
56855
  });
56716
56856
  return json5(tasks.map((t) => taskToSummary2(t, fields)));
56717
56857
  }
@@ -61580,6 +61720,26 @@ function parseSinceCursor(raw) {
61580
61720
  }
61581
61721
  return { ok: true, value: new Date(parsed).toISOString() };
61582
61722
  }
61723
+ function paginationQueryParam(url, name) {
61724
+ const raw = url.searchParams.get(name);
61725
+ if (raw === null)
61726
+ return { ok: true, value: undefined };
61727
+ const min = name === "limit" ? 1 : 0;
61728
+ if (!/^\d+$/.test(raw)) {
61729
+ return {
61730
+ ok: false,
61731
+ response: error(400, name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer")
61732
+ };
61733
+ }
61734
+ const value = Number(raw);
61735
+ if (!Number.isSafeInteger(value) || value < min) {
61736
+ return {
61737
+ ok: false,
61738
+ response: error(400, name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer")
61739
+ };
61740
+ }
61741
+ return { ok: true, value };
61742
+ }
61583
61743
  function validateTaskCompletion(value) {
61584
61744
  if (!value || typeof value !== "object" || Array.isArray(value))
61585
61745
  return { ok: false, message: "completion body must be an object" };
@@ -62108,6 +62268,12 @@ async function handleV1Request(req, url, dependencies = {}) {
62108
62268
  if (updatedAfter !== null && !updatedAfter.ok) {
62109
62269
  return error(400, updatedAfter.message);
62110
62270
  }
62271
+ const limitParam = paginationQueryParam(url, "limit");
62272
+ if (!limitParam.ok)
62273
+ return limitParam.response;
62274
+ const offsetParam = paginationQueryParam(url, "offset");
62275
+ if (!offsetParam.ok)
62276
+ return offsetParam.response;
62111
62277
  const filter = {
62112
62278
  ...updatedAfter !== null && updatedAfter.ok ? { updated_after: updatedAfter.value } : {},
62113
62279
  ...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
@@ -62124,8 +62290,8 @@ async function handleV1Request(req, url, dependencies = {}) {
62124
62290
  ...url.searchParams.get("tags") ? {
62125
62291
  tags: url.searchParams.get("tags").split(",").map((tag) => tag.trim()).filter(Boolean)
62126
62292
  } : {},
62127
- ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
62128
- ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
62293
+ ...limitParam.value !== undefined ? { limit: limitParam.value } : {},
62294
+ ...offsetParam.value !== undefined ? { offset: offsetParam.value } : {}
62129
62295
  };
62130
62296
  const tasks = await store.tasks.list(filter);
62131
62297
  const { limit: _l, offset: _o, ...countFilter } = filter;
@@ -1 +1 @@
1
- {"version":3,"file":"task-crud.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/task-crud.ts"],"names":[],"mappings":"AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAqBjD,UAAU,eAAe;IACvB,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IACxC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IACnC,gBAAgB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACvE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC1F,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACrE;AAuBD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,eAAe,QAga5E"}
1
+ {"version":3,"file":"task-crud.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/task-crud.ts"],"names":[],"mappings":"AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAqBjD,UAAU,eAAe;IACvB,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IACxC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IACnC,gBAAgB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACvE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC1F,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACrE;AAuBD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,eAAe,QAqa5E"}
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.47",
44
+ version: "0.15.50",
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",
@@ -105,6 +105,7 @@ var package_default = {
105
105
  ],
106
106
  scripts: {
107
107
  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",
108
+ "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",
108
109
  "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/*'",
109
110
  migrate: "bun run src/server/index.ts migrate",
110
111
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
@@ -159,7 +160,7 @@ var package_default = {
159
160
  author: "Andrei Hasna <andrei@hasna.com>",
160
161
  license: "Apache-2.0",
161
162
  dependencies: {
162
- "@hasna/contracts": "0.13.4",
163
+ "@hasna/contracts": "0.14.0",
163
164
  "@hasna/events": "^0.1.11",
164
165
  "@modelcontextprotocol/sdk": "^1.12.1",
165
166
  chalk: "^5.4.1",