@hasna/todos 0.11.86 → 0.11.87

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 (39) hide show
  1. package/dist/cli/cloud-router.d.ts +33 -2
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/task-commands.d.ts +2 -0
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/index.js +934 -412
  7. package/dist/contracts.js +1 -1
  8. package/dist/db/comments.d.ts.map +1 -1
  9. package/dist/index.js +167 -4
  10. package/dist/mcp/index.js +407 -38
  11. package/dist/registry.js +1 -1
  12. package/dist/release-provenance.json +3 -3
  13. package/dist/sdk/index.d.ts +1 -1
  14. package/dist/sdk/index.d.ts.map +1 -1
  15. package/dist/sdk/index.js +21 -0
  16. package/dist/sdk/v1.generated.d.ts +54 -0
  17. package/dist/sdk/v1.generated.d.ts.map +1 -1
  18. package/dist/server/cloud.d.ts +13 -0
  19. package/dist/server/cloud.d.ts.map +1 -1
  20. package/dist/server/index.js +824 -383
  21. package/dist/server/openapi.d.ts +171 -0
  22. package/dist/server/openapi.d.ts.map +1 -1
  23. package/dist/server/v1.d.ts +7 -1
  24. package/dist/server/v1.d.ts.map +1 -1
  25. package/dist/storage/comment-redaction-backfill.d.ts +32 -0
  26. package/dist/storage/comment-redaction-backfill.d.ts.map +1 -0
  27. package/dist/storage/index.d.ts +4 -2
  28. package/dist/storage/index.d.ts.map +1 -1
  29. package/dist/storage/interfaces.d.ts +16 -0
  30. package/dist/storage/interfaces.d.ts.map +1 -1
  31. package/dist/storage/local-sqlite.d.ts.map +1 -1
  32. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  33. package/dist/storage/postgres-sync.d.ts +6 -0
  34. package/dist/storage/postgres-sync.d.ts.map +1 -1
  35. package/dist/storage.d.ts +3 -3
  36. package/dist/storage.d.ts.map +1 -1
  37. package/dist/storage.js +171 -4
  38. package/package.json +2 -1
  39. package/vendor/hasna-contracts-0.5.1.tgz +0 -0
package/dist/mcp/index.js CHANGED
@@ -14085,7 +14085,7 @@ function getComment(id, db) {
14085
14085
  }
14086
14086
  function listComments(taskId, db) {
14087
14087
  const d = db || getDatabase();
14088
- return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
14088
+ return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at, rowid").all(taskId);
14089
14089
  }
14090
14090
  function updateComment(id, input, db) {
14091
14091
  const d = db || getDatabase();
@@ -22792,14 +22792,18 @@ function unwrapTask(raw) {
22792
22792
  }
22793
22793
  function toListQuery(filter = {}) {
22794
22794
  const query = {};
22795
- if (typeof filter.status === "string")
22796
- query["status"] = filter.status;
22797
- if (typeof filter.priority === "string")
22798
- query["priority"] = filter.priority;
22795
+ if (filter.status)
22796
+ query["status"] = Array.isArray(filter.status) ? filter.status.join(",") : filter.status;
22797
+ if (filter.priority)
22798
+ query["priority"] = Array.isArray(filter.priority) ? filter.priority.join(",") : filter.priority;
22799
22799
  if (filter.project_id)
22800
22800
  query["project_id"] = filter.project_id;
22801
+ if (filter.parent_id !== undefined)
22802
+ query["parent_id"] = filter.parent_id ?? "";
22801
22803
  if (filter.plan_id)
22802
22804
  query["plan_id"] = filter.plan_id;
22805
+ if (filter.task_list_id)
22806
+ query["task_list_id"] = filter.task_list_id;
22803
22807
  if (filter.assigned_to)
22804
22808
  query["assigned_to"] = filter.assigned_to;
22805
22809
  if (filter.agent_id)
@@ -22849,10 +22853,19 @@ async function cloudListProjects(client) {
22849
22853
  }
22850
22854
  async function cloudAddComment(client, taskId, input) {
22851
22855
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/comments`, input);
22852
- if (raw && typeof raw === "object" && "comment" in raw) {
22853
- return raw.comment;
22854
- }
22855
- return raw;
22856
+ const comment = raw && typeof raw === "object" && "comment" in raw ? raw.comment : raw;
22857
+ if (!isTaskComment(comment))
22858
+ throw new Error("Invalid cloud comment response");
22859
+ return redactComment(comment);
22860
+ }
22861
+ function isTaskComment(value) {
22862
+ if (!value || typeof value !== "object" || Array.isArray(value))
22863
+ return false;
22864
+ const comment = value;
22865
+ return typeof comment["id"] === "string" && typeof comment["task_id"] === "string" && (comment["agent_id"] === null || typeof comment["agent_id"] === "string") && (comment["session_id"] === null || typeof comment["session_id"] === "string") && typeof comment["content"] === "string" && (comment["type"] === "comment" || comment["type"] === "progress" || comment["type"] === "note") && (comment["progress_pct"] === null || typeof comment["progress_pct"] === "number") && typeof comment["created_at"] === "string";
22866
+ }
22867
+ function redactComment(comment) {
22868
+ return { ...comment, content: redactEvidenceText(comment.content) };
22856
22869
  }
22857
22870
  async function cloudCountTasks(client, filter = {}) {
22858
22871
  const { limit: _drop, offset: _o, ...rest } = filter;
@@ -22885,6 +22898,7 @@ async function cloudReleaseAgent(client, idOrName, sessionId) {
22885
22898
  var _cache;
22886
22899
  var init_cloud_router = __esm(() => {
22887
22900
  init_storage();
22901
+ init_redaction();
22888
22902
  });
22889
22903
 
22890
22904
  // src/mcp/tools/task-crud.ts
@@ -46148,6 +46162,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
46148
46162
  list: (filter = {}) => listTasks(filter, database()),
46149
46163
  count: (filter = {}) => countTasks(filter, database()),
46150
46164
  update: (id, input) => updateTask(id, input, database()),
46165
+ unlock: (id, agentId) => {
46166
+ unlockTask(id, agentId, database());
46167
+ return true;
46168
+ },
46151
46169
  delete: (id) => deleteTask(id, database()),
46152
46170
  start: (id, agentId) => startTask(id, agentId, database()),
46153
46171
  complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
@@ -46199,6 +46217,20 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
46199
46217
  logTaskChange: (taskId, action, field, oldValue, newValue, agentId) => logTaskChange(taskId, action, field, oldValue, newValue, agentId, database()),
46200
46218
  addComment: (input) => addComment(input, database()),
46201
46219
  getComments: (taskId) => listComments(taskId, database()),
46220
+ getCommentsPage: (taskId, options2) => {
46221
+ if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
46222
+ throw new Error("Comment limit must be an integer between 1 and 1001");
46223
+ }
46224
+ let comments = listComments(taskId, database());
46225
+ comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
46226
+ if (options2?.before) {
46227
+ const before = options2.before;
46228
+ comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
46229
+ }
46230
+ if (options2?.limit !== undefined)
46231
+ comments = comments.slice(-options2.limit);
46232
+ return comments;
46233
+ },
46202
46234
  getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
46203
46235
  getRecentActivity: (limit) => getRecentActivity(limit, database())
46204
46236
  },
@@ -46256,6 +46288,12 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
46256
46288
  )`
46257
46289
  ];
46258
46290
  }
46291
+ function postgresTodosCommentCursorIndexSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
46292
+ assertSafeIdentifier(tableName);
46293
+ return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_comment_task_created_idx
46294
+ ON ${tableName} (service, (payload->>'task_id'), (payload->>'created_at'), object_id)
46295
+ WHERE object_type = 'comments' AND deleted_at IS NULL`;
46296
+ }
46259
46297
 
46260
46298
  class PostgresTodosSyncStore {
46261
46299
  client;
@@ -48240,7 +48278,24 @@ function createPostgresTodosStorageAdapter(options) {
48240
48278
  audit: {
48241
48279
  logTaskChange: (taskId, action, field, oldValue, newValue, agentId, context) => logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context),
48242
48280
  addComment: (input, context) => addComment2(input, store, context),
48243
- getComments: async (taskId) => (await store.list("comments")).filter((comment) => comment.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
48281
+ getComments: async (taskId) => {
48282
+ const pages = [];
48283
+ let before;
48284
+ while (true) {
48285
+ const page = await store.listComments(taskId, { limit: 1000, ...before ? { before } : {} });
48286
+ if (page.length === 0)
48287
+ break;
48288
+ pages.unshift(page);
48289
+ if (page.length < 1000)
48290
+ break;
48291
+ const oldest = page[0];
48292
+ before = { created_at: oldest.created_at, id: oldest.id };
48293
+ }
48294
+ return pages.flat().map(redactComment2).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
48295
+ },
48296
+ getCommentsPage: async (taskId, options2) => {
48297
+ return (await store.listComments(taskId, options2)).map(redactComment2).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
48298
+ },
48244
48299
  getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
48245
48300
  getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
48246
48301
  },
@@ -48290,6 +48345,27 @@ class PostgresJsonRecordStore {
48290
48345
  async list(type) {
48291
48346
  return (await this.listRecords(type)).map((record) => record.payload);
48292
48347
  }
48348
+ async listComments(taskId, options = {}) {
48349
+ await this.ensureSchema();
48350
+ const limit = options.limit ?? 100;
48351
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
48352
+ throw new Error("Postgres comment limit must be an integer between 1 and 1001");
48353
+ }
48354
+ const params = [this.service, taskId];
48355
+ let cursorPredicate = "";
48356
+ if (options.before) {
48357
+ params.push(options.before.created_at, options.before.id);
48358
+ cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
48359
+ }
48360
+ params.push(limit);
48361
+ const result = await this.options.client.query(`/* todos:list-comments */ SELECT payload FROM ${this.tableName}
48362
+ WHERE service = $1 AND object_type = 'comments' AND deleted_at IS NULL
48363
+ AND payload->>'task_id' = $2
48364
+ ${cursorPredicate}
48365
+ ORDER BY payload->>'created_at' DESC, object_id DESC
48366
+ LIMIT $${params.length}`, params);
48367
+ return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
48368
+ }
48293
48369
  async listRecords(type) {
48294
48370
  await this.ensureSchema();
48295
48371
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
@@ -48683,7 +48759,7 @@ async function lockTask2(id, agentId, store) {
48683
48759
  async function unlockTask2(id, agentId, store) {
48684
48760
  const task2 = await requireRecord("tasks", id, store);
48685
48761
  if (agentId && task2.locked_by && task2.locked_by !== agentId) {
48686
- throw new Error(`Task ${id} is locked by ${task2.locked_by}, not ${agentId}`);
48762
+ throw new LockError(id, task2.locked_by);
48687
48763
  }
48688
48764
  await patchTask(task2, { locked_by: null, locked_at: null }, store);
48689
48765
  return true;
@@ -49048,13 +49124,16 @@ async function addComment2(input, store, context) {
49048
49124
  task_id: input.task_id,
49049
49125
  agent_id: input.agent_id ?? context?.agentId ?? null,
49050
49126
  session_id: input.session_id ?? context?.sessionId ?? null,
49051
- content: input.content,
49127
+ content: redactEvidenceText(input.content),
49052
49128
  type: input.type ?? "comment",
49053
49129
  progress_pct: input.progress_pct ?? null,
49054
49130
  created_at: new Date().toISOString()
49055
49131
  };
49056
49132
  return store.upsert("comments", comment, context);
49057
49133
  }
49134
+ function redactComment2(comment) {
49135
+ return { ...comment, content: redactEvidenceText(comment.content) };
49136
+ }
49058
49137
  async function exportSnapshot(store) {
49059
49138
  return {
49060
49139
  exportedAt: new Date().toISOString(),
@@ -49200,7 +49279,106 @@ function numberValue3(value) {
49200
49279
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
49201
49280
  }
49202
49281
  var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_BY = "ORDER BY 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";
49203
- var init_postgres_adapter = () => {};
49282
+ var init_postgres_adapter = __esm(() => {
49283
+ init_types();
49284
+ init_redaction();
49285
+ });
49286
+
49287
+ // src/storage/comment-redaction-backfill.ts
49288
+ function assertSafeIdentifier2(value) {
49289
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
49290
+ throw new Error(`Unsafe Postgres identifier: ${value}`);
49291
+ }
49292
+ }
49293
+ function payloadObject(value) {
49294
+ if (value && typeof value === "object" && !Array.isArray(value))
49295
+ return value;
49296
+ if (typeof value !== "string")
49297
+ return null;
49298
+ try {
49299
+ const parsed = JSON.parse(value);
49300
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
49301
+ } catch {
49302
+ return null;
49303
+ }
49304
+ }
49305
+ async function backfillPostgresCommentRedaction(client, options = {}) {
49306
+ const apply = options.apply === true;
49307
+ if (apply && options.confirmation !== COMMENT_REDACTION_BACKFILL_CONFIRMATION) {
49308
+ throw new Error(`Applying the comment redaction backfill requires confirmation ${COMMENT_REDACTION_BACKFILL_CONFIRMATION}`);
49309
+ }
49310
+ const tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
49311
+ assertSafeIdentifier2(tableName);
49312
+ const service = options.service ?? "todos";
49313
+ const batchSize = options.batchSize ?? 100;
49314
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 500) {
49315
+ throw new Error("Comment redaction backfill batchSize must be an integer between 1 and 500");
49316
+ }
49317
+ const result = {
49318
+ dry_run: !apply,
49319
+ scanned: 0,
49320
+ candidates: 0,
49321
+ updated: 0,
49322
+ conflicts: 0,
49323
+ batches: 0,
49324
+ remaining_candidates: 0
49325
+ };
49326
+ let afterId = "";
49327
+ while (true) {
49328
+ const page = await client.query(`/* todos:comment-redaction-backfill-scan */
49329
+ SELECT object_id, payload
49330
+ FROM ${tableName}
49331
+ WHERE service = $1 AND object_type = 'comments'
49332
+ AND object_id > $2
49333
+ ORDER BY object_id ASC
49334
+ LIMIT $3`, [service, afterId, batchSize]);
49335
+ if (page.rows.length === 0)
49336
+ break;
49337
+ result.batches += 1;
49338
+ for (const row of page.rows) {
49339
+ afterId = row.object_id;
49340
+ result.scanned += 1;
49341
+ const payload = payloadObject(row.payload);
49342
+ const original = payload?.["content"];
49343
+ if (typeof original !== "string")
49344
+ continue;
49345
+ const redacted = redactEvidenceText(original);
49346
+ if (redacted === original)
49347
+ continue;
49348
+ result.candidates += 1;
49349
+ if (!apply)
49350
+ continue;
49351
+ const nextPayload = { ...payload, content: redacted };
49352
+ const update = await client.query(`/* todos:comment-redaction-backfill-apply */
49353
+ UPDATE ${tableName}
49354
+ SET payload = $3::jsonb
49355
+ WHERE service = $1 AND object_type = 'comments' AND object_id = $2
49356
+ AND payload = $4::jsonb
49357
+ RETURNING object_id`, [service, row.object_id, nextPayload, row.payload]);
49358
+ if (update.rows.length === 1)
49359
+ result.updated += 1;
49360
+ else
49361
+ result.conflicts += 1;
49362
+ }
49363
+ if (page.rows.length < batchSize)
49364
+ break;
49365
+ }
49366
+ if (!apply) {
49367
+ result.remaining_candidates = result.candidates;
49368
+ return result;
49369
+ }
49370
+ const verification = await backfillPostgresCommentRedaction(client, {
49371
+ ...options,
49372
+ apply: false,
49373
+ confirmation: undefined
49374
+ });
49375
+ result.remaining_candidates = verification.candidates;
49376
+ return result;
49377
+ }
49378
+ var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
49379
+ var init_comment_redaction_backfill = __esm(() => {
49380
+ init_redaction();
49381
+ });
49204
49382
 
49205
49383
  // src/server/cloud.ts
49206
49384
  var exports_cloud = {};
@@ -49214,7 +49392,9 @@ __export(exports_cloud, {
49214
49392
  getCloudStorageAdapter: () => getCloudStorageAdapter,
49215
49393
  getApiKeyStore: () => getApiKeyStore,
49216
49394
  ensureCloudSchema: () => ensureCloudSchema,
49395
+ ensureCloudCommentCursorIndex: () => ensureCloudCommentCursorIndex,
49217
49396
  closeCloud: () => closeCloud,
49397
+ backfillCloudCommentRedaction: () => backfillCloudCommentRedaction,
49218
49398
  TODOS_APP_SLUG: () => TODOS_APP_SLUG
49219
49399
  });
49220
49400
  function resolveCloudDatabaseUrl(env = process.env) {
@@ -49292,6 +49472,9 @@ async function ensureCloudSchema() {
49292
49472
  })();
49293
49473
  return schemaEnsured;
49294
49474
  }
49475
+ async function ensureCloudCommentCursorIndex() {
49476
+ await getClient().query(postgresTodosCommentCursorIndexSql());
49477
+ }
49295
49478
  async function normalizeCloudPayloads() {
49296
49479
  const client = getClient();
49297
49480
  const res = await client.query(`UPDATE todos_sync_records
@@ -49300,6 +49483,9 @@ async function normalizeCloudPayloads() {
49300
49483
  RETURNING object_id AS id`);
49301
49484
  return res.rows.length;
49302
49485
  }
49486
+ function backfillCloudCommentRedaction(options = {}) {
49487
+ return backfillPostgresCommentRedaction(getClient(), { ...options, service: TODOS_APP_SLUG });
49488
+ }
49303
49489
  async function pingCloud() {
49304
49490
  const client = getClient();
49305
49491
  const res = await client.query("select 1 as ok");
@@ -49320,6 +49506,7 @@ var init_cloud = __esm(() => {
49320
49506
  init_auth();
49321
49507
  init_cloud_client();
49322
49508
  init_postgres_adapter();
49509
+ init_comment_redaction_backfill();
49323
49510
  });
49324
49511
 
49325
49512
  // src/server/openapi.ts
@@ -49343,6 +49530,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
49343
49530
  schemas: {
49344
49531
  Task: taskSchema,
49345
49532
  Project: projectSchema,
49533
+ TaskComment: taskCommentSchema,
49346
49534
  CreateTaskInput: {
49347
49535
  type: "object",
49348
49536
  required: ["title"],
@@ -49377,6 +49565,17 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
49377
49565
  description: { type: "string" },
49378
49566
  task_prefix: { type: "string" }
49379
49567
  }
49568
+ },
49569
+ CreateTaskCommentInput: {
49570
+ type: "object",
49571
+ required: ["content"],
49572
+ properties: {
49573
+ content: { type: "string", minLength: 1 },
49574
+ agent_id: { type: "string" },
49575
+ session_id: { type: "string" },
49576
+ type: { type: "string", enum: ["comment", "progress", "note"] },
49577
+ progress_pct: { type: "number" }
49578
+ }
49380
49579
  }
49381
49580
  }
49382
49581
  },
@@ -49475,6 +49674,61 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
49475
49674
  }
49476
49675
  }
49477
49676
  },
49677
+ "/v1/tasks/{id}/comments": {
49678
+ get: {
49679
+ operationId: "listTaskComments",
49680
+ summary: "List a bounded page of task comments",
49681
+ description: "Returns the newest page in oldest-to-newest display order. Use next_cursor to request older pages; count is the page size, not a total. Pagination-aware clients must send limit during the mixed-version rollout.",
49682
+ parameters: [
49683
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
49684
+ { name: "limit", in: "query", required: true, schema: { type: "integer", minimum: 1, maximum: 500, default: 100 } },
49685
+ { name: "cursor", in: "query", schema: { type: "string" } }
49686
+ ],
49687
+ responses: {
49688
+ "200": {
49689
+ content: {
49690
+ "application/json": {
49691
+ schema: {
49692
+ type: "object",
49693
+ required: ["comments", "count", "has_more", "next_cursor"],
49694
+ properties: {
49695
+ comments: { type: "array", maxItems: 500, items: { $ref: "#/components/schemas/TaskComment" } },
49696
+ count: { type: "integer", minimum: 0, maximum: 500 },
49697
+ has_more: { type: "boolean" },
49698
+ next_cursor: { type: "string", nullable: true }
49699
+ }
49700
+ }
49701
+ }
49702
+ }
49703
+ },
49704
+ "426": {
49705
+ description: "Upgrade required: a predecessor client omitted limit and the complete legacy history exceeds 500 comments, or the configured storage adapter lacks cursor pagination support."
49706
+ }
49707
+ }
49708
+ },
49709
+ post: {
49710
+ operationId: "createTaskComment",
49711
+ summary: "Create a task comment",
49712
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
49713
+ requestBody: {
49714
+ required: true,
49715
+ content: { "application/json": { schema: { $ref: "#/components/schemas/CreateTaskCommentInput" } } }
49716
+ },
49717
+ responses: {
49718
+ "201": {
49719
+ content: {
49720
+ "application/json": {
49721
+ schema: {
49722
+ type: "object",
49723
+ required: ["comment"],
49724
+ properties: { comment: { $ref: "#/components/schemas/TaskComment" } }
49725
+ }
49726
+ }
49727
+ }
49728
+ }
49729
+ }
49730
+ }
49731
+ },
49478
49732
  "/v1/tasks/{id}/start": {
49479
49733
  post: {
49480
49734
  operationId: "startTask",
@@ -49593,7 +49847,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
49593
49847
  }
49594
49848
  };
49595
49849
  }
49596
- var taskSchema, projectSchema;
49850
+ var taskSchema, projectSchema, taskCommentSchema;
49597
49851
  var init_openapi = __esm(() => {
49598
49852
  init_package_version();
49599
49853
  taskSchema = {
@@ -49624,6 +49878,20 @@ var init_openapi = __esm(() => {
49624
49878
  updated_at: { type: "string" }
49625
49879
  }
49626
49880
  };
49881
+ taskCommentSchema = {
49882
+ type: "object",
49883
+ required: ["id", "task_id", "agent_id", "session_id", "content", "type", "progress_pct", "created_at"],
49884
+ properties: {
49885
+ id: { type: "string" },
49886
+ task_id: { type: "string" },
49887
+ agent_id: { type: "string", nullable: true },
49888
+ session_id: { type: "string", nullable: true },
49889
+ content: { type: "string" },
49890
+ type: { type: "string", enum: ["comment", "progress", "note"] },
49891
+ progress_pct: { type: "number", nullable: true },
49892
+ created_at: { type: "string", format: "date-time" }
49893
+ }
49894
+ };
49627
49895
  });
49628
49896
 
49629
49897
  // src/server/v1.ts
@@ -49653,6 +49921,29 @@ function contextFromPrincipal(principal, body) {
49653
49921
  const agentId = body?.agent_id || principal.agent || undefined;
49654
49922
  return agentId ? { agentId } : {};
49655
49923
  }
49924
+ function redactComment3(comment) {
49925
+ return { ...comment, content: redactEvidenceText(comment.content) };
49926
+ }
49927
+ function encodeCommentCursor(comment) {
49928
+ return Buffer.from(JSON.stringify({ created_at: comment.created_at, id: comment.id }), "utf8").toString("base64url");
49929
+ }
49930
+ function decodeCommentCursor(value) {
49931
+ if (value.length > 1024)
49932
+ throw new Error("invalid comment cursor");
49933
+ let parsed;
49934
+ try {
49935
+ parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
49936
+ } catch {
49937
+ throw new Error("invalid comment cursor");
49938
+ }
49939
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
49940
+ throw new Error("invalid comment cursor");
49941
+ const cursor = parsed;
49942
+ if (typeof cursor["created_at"] !== "string" || cursor["created_at"].length > 64 || !Number.isFinite(Date.parse(cursor["created_at"])) || typeof cursor["id"] !== "string" || !cursor["id"] || cursor["id"].length > 256) {
49943
+ throw new Error("invalid comment cursor");
49944
+ }
49945
+ return { created_at: cursor["created_at"], id: cursor["id"] };
49946
+ }
49656
49947
  function normalizeImportSnapshot(raw) {
49657
49948
  const body = raw && typeof raw === "object" ? raw : {};
49658
49949
  const arr = (v) => Array.isArray(v) ? v : [];
@@ -49673,7 +49964,7 @@ function normalizeImportSnapshot(raw) {
49673
49964
  function countSnapshotRecords(s) {
49674
49965
  return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
49675
49966
  }
49676
- async function handleV1Request(req, url) {
49967
+ async function handleV1Request(req, url, dependencies = {}) {
49677
49968
  const path = url.pathname;
49678
49969
  if (path !== "/v1" && !path.startsWith("/v1/"))
49679
49970
  return null;
@@ -49682,7 +49973,7 @@ async function handleV1Request(req, url) {
49682
49973
  const requiredScopes = [isWrite ? "todos:write" : "todos:read"];
49683
49974
  let verifier;
49684
49975
  try {
49685
- verifier = getCloudVerifier();
49976
+ verifier = (dependencies.getVerifier ?? getCloudVerifier)();
49686
49977
  } catch (e) {
49687
49978
  return error(503, e.message);
49688
49979
  }
@@ -49691,8 +49982,8 @@ async function handleV1Request(req, url) {
49691
49982
  return error(decision.status, decision.message, { reason: decision.reason });
49692
49983
  }
49693
49984
  const principal = decision.principal;
49694
- await ensureCloudSchema();
49695
- const store = getCloudStorageAdapter();
49985
+ await (dependencies.ensureSchema ?? ensureCloudSchema)();
49986
+ const store = (dependencies.getStorageAdapter ?? getCloudStorageAdapter)();
49696
49987
  const segments = path.split("/").filter(Boolean);
49697
49988
  const resource = segments[1];
49698
49989
  const id = segments[2];
@@ -49778,10 +50069,16 @@ async function handleV1Request(req, url) {
49778
50069
  if (!id) {
49779
50070
  if (method === "GET") {
49780
50071
  const filter = {
49781
- ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
49782
- ...url.searchParams.get("priority") ? { priority: url.searchParams.get("priority") } : {},
50072
+ ...url.searchParams.get("status") ? {
50073
+ status: url.searchParams.get("status").includes(",") ? url.searchParams.get("status").split(",") : url.searchParams.get("status")
50074
+ } : {},
50075
+ ...url.searchParams.get("priority") ? {
50076
+ priority: url.searchParams.get("priority").includes(",") ? url.searchParams.get("priority").split(",") : url.searchParams.get("priority")
50077
+ } : {},
49783
50078
  ...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
50079
+ ...url.searchParams.has("parent_id") ? { parent_id: url.searchParams.get("parent_id") || null, include_subtasks: true } : {},
49784
50080
  ...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {},
50081
+ ...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
49785
50082
  ...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
49786
50083
  ...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
49787
50084
  ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
@@ -49805,8 +50102,47 @@ async function handleV1Request(req, url) {
49805
50102
  if (action) {
49806
50103
  if (action === "comments") {
49807
50104
  if (method === "GET") {
49808
- const comments = await store.audit.getComments(id);
49809
- return json2({ comments, count: comments.length });
50105
+ if (!await store.tasks.get(id))
50106
+ return error(404, "task not found");
50107
+ const rawLimit = url.searchParams.get("limit");
50108
+ const cursor = url.searchParams.get("cursor");
50109
+ if (rawLimit === null && cursor === null) {
50110
+ const storageContext = contextFromPrincipal(principal);
50111
+ const legacyPage = (await (store.audit.getCommentsPage ? store.audit.getCommentsPage(id, { limit: LEGACY_COMMENT_RESPONSE_LIMIT + 1 }, storageContext) : store.audit.getComments(id, storageContext))).map(redactComment3);
50112
+ if (legacyPage.length > LEGACY_COMMENT_RESPONSE_LIMIT) {
50113
+ return error(426, "task has too many comments for this client; upgrade @hasna/todos to use cursor pagination");
50114
+ }
50115
+ return json2({
50116
+ comments: legacyPage,
50117
+ count: legacyPage.length,
50118
+ has_more: false,
50119
+ next_cursor: null
50120
+ });
50121
+ }
50122
+ const limit = rawLimit === null ? DEFAULT_COMMENT_PAGE_SIZE : Number(rawLimit);
50123
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_COMMENT_PAGE_SIZE) {
50124
+ return error(400, `limit must be an integer between 1 and ${MAX_COMMENT_PAGE_SIZE}`);
50125
+ }
50126
+ let before;
50127
+ if (cursor) {
50128
+ try {
50129
+ before = decodeCommentCursor(cursor);
50130
+ } catch {
50131
+ return error(400, "invalid comment cursor");
50132
+ }
50133
+ }
50134
+ if (!store.audit.getCommentsPage) {
50135
+ return error(426, "storage adapter must be upgraded to support cursor-paginated comments");
50136
+ }
50137
+ const page = (await store.audit.getCommentsPage(id, { limit: limit + 1, ...before ? { before } : {} }, contextFromPrincipal(principal))).map(redactComment3);
50138
+ const hasMore = page.length > limit;
50139
+ const comments = hasMore ? page.slice(1) : page;
50140
+ return json2({
50141
+ comments,
50142
+ count: comments.length,
50143
+ has_more: hasMore,
50144
+ next_cursor: hasMore && comments[0] ? encodeCommentCursor(comments[0]) : null
50145
+ });
49810
50146
  }
49811
50147
  if (method === "POST") {
49812
50148
  const body2 = await readJson(req) ?? {};
@@ -49824,7 +50160,7 @@ async function handleV1Request(req, url) {
49824
50160
  type: body2.type,
49825
50161
  progress_pct: body2.progress_pct
49826
50162
  }, contextFromPrincipal(principal, body2));
49827
- return json2({ comment }, 201);
50163
+ return json2({ comment: redactComment3(comment) }, 201);
49828
50164
  }
49829
50165
  return error(405, `method ${method} not allowed on /v1/tasks/:id/comments`);
49830
50166
  }
@@ -49839,17 +50175,30 @@ async function handleV1Request(req, url) {
49839
50175
  if (action === "lock" || action === "unlock") {
49840
50176
  if (method !== "POST")
49841
50177
  return error(405, `method ${method} not allowed on /v1/tasks/:id/${action}`);
49842
- if (typeof store.tasks.lock !== "function" || typeof store.tasks.unlock !== "function") {
49843
- return error(501, "task locking is not supported by this storage backend");
49844
- }
49845
50178
  const body2 = await readJson(req) ?? {};
49846
50179
  if (!await store.tasks.get(id))
49847
50180
  return error(404, "task not found");
49848
50181
  if (action === "lock") {
49849
- const agentId2 = body2.agent_id || principal.agent || "todos-serve";
49850
- return json2({ result: await store.tasks.lock(id, agentId2) });
50182
+ if (typeof store.tasks.lock !== "function")
50183
+ return error(501, "task locking is not supported by this storage backend");
50184
+ const agentId3 = body2.agent_id || principal.agent || "todos-serve";
50185
+ return json2({ result: await store.tasks.lock(id, agentId3) });
50186
+ }
50187
+ if (typeof store.tasks.unlock !== "function")
50188
+ return error(501, "task unlocking is not supported by this storage backend");
50189
+ if (body2.force === true) {
50190
+ if (!principal.scopes.includes("todos:*"))
50191
+ return error(403, "force unlock requires todos:* scope");
50192
+ const released2 = await store.tasks.unlock(id);
50193
+ return json2({ success: released2 });
50194
+ }
50195
+ if (body2.agent_id && principal.agent && body2.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
50196
+ return error(403, "unlock agent_id must match the authenticated agent");
49851
50197
  }
49852
- const released = await store.tasks.unlock(id, body2.agent_id || principal.agent || undefined);
50198
+ const agentId2 = principal.agent || body2.agent_id;
50199
+ if (!agentId2)
50200
+ return error(403, "unlock requires an agent-bound key or force=true");
50201
+ const released = await store.tasks.unlock(id, agentId2);
49853
50202
  return json2({ success: released });
49854
50203
  }
49855
50204
  if (action === "dependencies") {
@@ -50132,12 +50481,28 @@ async function handleV1Request(req, url) {
50132
50481
  const activity = await store.audit.getRecentActivity(limit);
50133
50482
  return json2({ activity, count: activity.length });
50134
50483
  }
50135
- if (resource === "task-lists" && !id) {
50136
- if (method !== "GET")
50137
- return error(405, `method ${method} not allowed on /v1/task-lists`);
50138
- const projectId = url.searchParams.get("project_id") ?? undefined;
50139
- const taskLists = await store.taskLists.list(projectId);
50140
- return json2({ task_lists: taskLists, count: taskLists.length });
50484
+ if (resource === "task-lists") {
50485
+ if (!id && method === "GET") {
50486
+ const projectId = url.searchParams.get("project_id") ?? undefined;
50487
+ const taskLists = await store.taskLists.list(projectId);
50488
+ return json2({ task_lists: taskLists, count: taskLists.length });
50489
+ }
50490
+ if (!id && method === "POST") {
50491
+ const body = await readJson(req);
50492
+ if (!body || typeof body.name !== "string" || !body.name.trim())
50493
+ return error(400, "name is required");
50494
+ const taskList = await store.taskLists.create(body, contextFromPrincipal(principal));
50495
+ return json2({ task_list: taskList }, 201);
50496
+ }
50497
+ if (id && method === "GET") {
50498
+ const taskList = await store.taskLists.get(id);
50499
+ return taskList ? json2({ task_list: taskList }) : error(404, "task list not found");
50500
+ }
50501
+ if (id && method === "DELETE") {
50502
+ const deleted = await store.taskLists.delete(id, contextFromPrincipal(principal));
50503
+ return deleted ? json2({ deleted: true, id }) : error(404, "task list not found");
50504
+ }
50505
+ return error(405, `method ${method} not allowed on /v1/task-lists${id ? "/:id" : ""}`);
50141
50506
  }
50142
50507
  if (resource === "dependencies" && !id) {
50143
50508
  if (method !== "GET")
@@ -50145,8 +50510,8 @@ async function handleV1Request(req, url) {
50145
50510
  if (typeof store.dependencies?.listAll !== "function") {
50146
50511
  return error(501, "dependency edge listing is not supported by this storage backend");
50147
50512
  }
50148
- const dependencies = await store.dependencies.listAll();
50149
- return json2({ dependencies, count: dependencies.length });
50513
+ const dependencies2 = await store.dependencies.listAll();
50514
+ return json2({ dependencies: dependencies2, count: dependencies2.length });
50150
50515
  }
50151
50516
  if (resource === "commits" && id) {
50152
50517
  if (method !== "GET")
@@ -50203,12 +50568,16 @@ async function handleV1Request(req, url) {
50203
50568
  }
50204
50569
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
50205
50570
  } catch (e) {
50571
+ if (e instanceof LockError)
50572
+ return error(409, e.message, { code: LockError.code });
50206
50573
  return error(500, e.message || "internal error");
50207
50574
  }
50208
50575
  }
50209
- var JSON_HEADERS;
50576
+ var JSON_HEADERS, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500;
50210
50577
  var init_v1 = __esm(() => {
50578
+ init_types();
50211
50579
  init_cloud();
50580
+ init_redaction();
50212
50581
  JSON_HEADERS = { "Content-Type": "application/json" };
50213
50582
  });
50214
50583