@hasna/todos 0.11.85 → 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 +55 -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 +1106 -417
  7. package/dist/contracts.js +1 -1
  8. package/dist/db/comments.d.ts.map +1 -1
  9. package/dist/index.js +180 -5
  10. package/dist/mcp/index.js +483 -39
  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 +900 -384
  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 +23 -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 +184 -5
  38. package/package.json +2 -1
  39. package/vendor/hasna-contracts-0.5.1.tgz +0 -0
package/dist/storage.js CHANGED
@@ -9424,7 +9424,7 @@ function getComment(id, db) {
9424
9424
  }
9425
9425
  function listComments(taskId, db) {
9426
9426
  const d = db || getDatabase();
9427
- return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
9427
+ return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at, rowid").all(taskId);
9428
9428
  }
9429
9429
  function deleteComment(id, db) {
9430
9430
  const d = db || getDatabase();
@@ -11552,6 +11552,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
11552
11552
  list: (filter = {}) => listTasks(filter, database()),
11553
11553
  count: (filter = {}) => countTasks(filter, database()),
11554
11554
  update: (id, input) => updateTask(id, input, database()),
11555
+ unlock: (id, agentId) => {
11556
+ unlockTask(id, agentId, database());
11557
+ return true;
11558
+ },
11555
11559
  delete: (id) => deleteTask(id, database()),
11556
11560
  start: (id, agentId) => startTask(id, agentId, database()),
11557
11561
  complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
@@ -11603,6 +11607,20 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
11603
11607
  logTaskChange: (taskId, action, field, oldValue, newValue, agentId) => logTaskChange(taskId, action, field, oldValue, newValue, agentId, database()),
11604
11608
  addComment: (input) => addComment(input, database()),
11605
11609
  getComments: (taskId) => listComments(taskId, database()),
11610
+ getCommentsPage: (taskId, options2) => {
11611
+ if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
11612
+ throw new Error("Comment limit must be an integer between 1 and 1001");
11613
+ }
11614
+ let comments = listComments(taskId, database());
11615
+ comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
11616
+ if (options2?.before) {
11617
+ const before = options2.before;
11618
+ comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
11619
+ }
11620
+ if (options2?.limit !== undefined)
11621
+ comments = comments.slice(-options2.limit);
11622
+ return comments;
11623
+ },
11606
11624
  getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
11607
11625
  getRecentActivity: (limit) => getRecentActivity(limit, database())
11608
11626
  },
@@ -11650,6 +11668,12 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
11650
11668
  )`
11651
11669
  ];
11652
11670
  }
11671
+ function postgresTodosCommentCursorIndexSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
11672
+ assertSafeIdentifier(tableName);
11673
+ return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_comment_task_created_idx
11674
+ ON ${tableName} (service, (payload->>'task_id'), (payload->>'created_at'), object_id)
11675
+ WHERE object_type = 'comments' AND deleted_at IS NULL`;
11676
+ }
11653
11677
 
11654
11678
  class PostgresTodosSyncStore {
11655
11679
  client;
@@ -11882,7 +11906,9 @@ function createHybridTodosStorageAdapter(options) {
11882
11906
  }
11883
11907
 
11884
11908
  // src/storage/postgres-adapter.ts
11909
+ init_types();
11885
11910
  import { randomUUID as randomUUID3 } from "crypto";
11911
+ init_redaction();
11886
11912
  function createPostgresTodosStorageAdapter(options) {
11887
11913
  const store = new PostgresJsonRecordStore(options);
11888
11914
  const adapter = {
@@ -11909,7 +11935,8 @@ function createPostgresTodosStorageAdapter(options) {
11909
11935
  getActiveWork: (filters) => getActiveWork2(filters, store),
11910
11936
  getChangedSince: (since, filters) => getChangedSince(since, filters, store),
11911
11937
  lock: (id, agentId) => lockTask2(id, agentId, store),
11912
- unlock: (id, agentId) => unlockTask2(id, agentId, store)
11938
+ unlock: (id, agentId) => unlockTask2(id, agentId, store),
11939
+ getByFingerprint: (fingerprint) => store.getTaskByFingerprint(fingerprint)
11913
11940
  },
11914
11941
  dependencies: {
11915
11942
  add: (taskId, dependsOn, context) => addDependency2(taskId, dependsOn, store, context),
@@ -11977,7 +12004,24 @@ function createPostgresTodosStorageAdapter(options) {
11977
12004
  audit: {
11978
12005
  logTaskChange: (taskId, action, field, oldValue, newValue, agentId, context) => logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context),
11979
12006
  addComment: (input, context) => addComment2(input, store, context),
11980
- getComments: async (taskId) => (await store.list("comments")).filter((comment) => comment.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
12007
+ getComments: async (taskId) => {
12008
+ const pages = [];
12009
+ let before;
12010
+ while (true) {
12011
+ const page = await store.listComments(taskId, { limit: 1000, ...before ? { before } : {} });
12012
+ if (page.length === 0)
12013
+ break;
12014
+ pages.unshift(page);
12015
+ if (page.length < 1000)
12016
+ break;
12017
+ const oldest = page[0];
12018
+ before = { created_at: oldest.created_at, id: oldest.id };
12019
+ }
12020
+ return pages.flat().map(redactComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
12021
+ },
12022
+ getCommentsPage: async (taskId, options2) => {
12023
+ return (await store.listComments(taskId, options2)).map(redactComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
12024
+ },
11981
12025
  getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
11982
12026
  getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
11983
12027
  },
@@ -12027,6 +12071,27 @@ class PostgresJsonRecordStore {
12027
12071
  async list(type) {
12028
12072
  return (await this.listRecords(type)).map((record) => record.payload);
12029
12073
  }
12074
+ async listComments(taskId, options = {}) {
12075
+ await this.ensureSchema();
12076
+ const limit = options.limit ?? 100;
12077
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
12078
+ throw new Error("Postgres comment limit must be an integer between 1 and 1001");
12079
+ }
12080
+ const params = [this.service, taskId];
12081
+ let cursorPredicate = "";
12082
+ if (options.before) {
12083
+ params.push(options.before.created_at, options.before.id);
12084
+ cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
12085
+ }
12086
+ params.push(limit);
12087
+ const result = await this.options.client.query(`/* todos:list-comments */ SELECT payload FROM ${this.tableName}
12088
+ WHERE service = $1 AND object_type = 'comments' AND deleted_at IS NULL
12089
+ AND payload->>'task_id' = $2
12090
+ ${cursorPredicate}
12091
+ ORDER BY payload->>'created_at' DESC, object_id DESC
12092
+ LIMIT $${params.length}`, params);
12093
+ return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
12094
+ }
12030
12095
  async listRecords(type) {
12031
12096
  await this.ensureSchema();
12032
12097
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
@@ -12098,6 +12163,17 @@ class PostgresJsonRecordStore {
12098
12163
  const result = await this.options.client.query(sql, params);
12099
12164
  return result.rows.map((row) => payloadRecord2(row.payload));
12100
12165
  }
12166
+ async getTaskByFingerprint(fingerprint) {
12167
+ await this.ensureSchema();
12168
+ const sql = `/* todos:task-by-fingerprint */ SELECT payload FROM ${this.tableName}
12169
+ WHERE service = $1 AND object_type = $2 AND deleted_at IS NULL
12170
+ AND payload->'metadata'->>'fingerprint' = $3
12171
+ ORDER BY payload->>'created_at' ASC
12172
+ LIMIT 1`;
12173
+ const result = await this.options.client.query(sql, [this.service, "tasks", fingerprint]);
12174
+ const row = result.rows[0];
12175
+ return row ? payloadRecord2(row.payload) : null;
12176
+ }
12101
12177
  async countTasks(filter) {
12102
12178
  await this.ensureSchema();
12103
12179
  const { where, params } = this.buildTaskFilterSql(filter);
@@ -12410,7 +12486,7 @@ async function lockTask2(id, agentId, store) {
12410
12486
  async function unlockTask2(id, agentId, store) {
12411
12487
  const task = await requireRecord("tasks", id, store);
12412
12488
  if (agentId && task.locked_by && task.locked_by !== agentId) {
12413
- throw new Error(`Task ${id} is locked by ${task.locked_by}, not ${agentId}`);
12489
+ throw new LockError(id, task.locked_by);
12414
12490
  }
12415
12491
  await patchTask(task, { locked_by: null, locked_at: null }, store);
12416
12492
  return true;
@@ -12776,13 +12852,16 @@ async function addComment2(input, store, context) {
12776
12852
  task_id: input.task_id,
12777
12853
  agent_id: input.agent_id ?? context?.agentId ?? null,
12778
12854
  session_id: input.session_id ?? context?.sessionId ?? null,
12779
- content: input.content,
12855
+ content: redactEvidenceText(input.content),
12780
12856
  type: input.type ?? "comment",
12781
12857
  progress_pct: input.progress_pct ?? null,
12782
12858
  created_at: new Date().toISOString()
12783
12859
  };
12784
12860
  return store.upsert("comments", comment, context);
12785
12861
  }
12862
+ function redactComment(comment) {
12863
+ return { ...comment, content: redactEvidenceText(comment.content) };
12864
+ }
12786
12865
  async function exportSnapshot(store) {
12787
12866
  return {
12788
12867
  exportedAt: new Date().toISOString(),
@@ -13714,6 +13793,102 @@ async function closeRuntimeShadowCloud() {
13714
13793
  await cloud.close().catch(() => {});
13715
13794
  }
13716
13795
  }
13796
+ // src/storage/comment-redaction-backfill.ts
13797
+ init_redaction();
13798
+ var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
13799
+ function assertSafeIdentifier2(value) {
13800
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
13801
+ throw new Error(`Unsafe Postgres identifier: ${value}`);
13802
+ }
13803
+ }
13804
+ function payloadObject(value) {
13805
+ if (value && typeof value === "object" && !Array.isArray(value))
13806
+ return value;
13807
+ if (typeof value !== "string")
13808
+ return null;
13809
+ try {
13810
+ const parsed = JSON.parse(value);
13811
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
13812
+ } catch {
13813
+ return null;
13814
+ }
13815
+ }
13816
+ async function backfillPostgresCommentRedaction(client, options = {}) {
13817
+ const apply = options.apply === true;
13818
+ if (apply && options.confirmation !== COMMENT_REDACTION_BACKFILL_CONFIRMATION) {
13819
+ throw new Error(`Applying the comment redaction backfill requires confirmation ${COMMENT_REDACTION_BACKFILL_CONFIRMATION}`);
13820
+ }
13821
+ const tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
13822
+ assertSafeIdentifier2(tableName);
13823
+ const service = options.service ?? "todos";
13824
+ const batchSize = options.batchSize ?? 100;
13825
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 500) {
13826
+ throw new Error("Comment redaction backfill batchSize must be an integer between 1 and 500");
13827
+ }
13828
+ const result = {
13829
+ dry_run: !apply,
13830
+ scanned: 0,
13831
+ candidates: 0,
13832
+ updated: 0,
13833
+ conflicts: 0,
13834
+ batches: 0,
13835
+ remaining_candidates: 0
13836
+ };
13837
+ let afterId = "";
13838
+ while (true) {
13839
+ const page = await client.query(`/* todos:comment-redaction-backfill-scan */
13840
+ SELECT object_id, payload
13841
+ FROM ${tableName}
13842
+ WHERE service = $1 AND object_type = 'comments'
13843
+ AND object_id > $2
13844
+ ORDER BY object_id ASC
13845
+ LIMIT $3`, [service, afterId, batchSize]);
13846
+ if (page.rows.length === 0)
13847
+ break;
13848
+ result.batches += 1;
13849
+ for (const row of page.rows) {
13850
+ afterId = row.object_id;
13851
+ result.scanned += 1;
13852
+ const payload = payloadObject(row.payload);
13853
+ const original = payload?.["content"];
13854
+ if (typeof original !== "string")
13855
+ continue;
13856
+ const redacted = redactEvidenceText(original);
13857
+ if (redacted === original)
13858
+ continue;
13859
+ result.candidates += 1;
13860
+ if (!apply)
13861
+ continue;
13862
+ const nextPayload = { ...payload, content: redacted };
13863
+ const update = await client.query(`/* todos:comment-redaction-backfill-apply */
13864
+ UPDATE ${tableName}
13865
+ SET payload = $3::jsonb
13866
+ WHERE service = $1 AND object_type = 'comments' AND object_id = $2
13867
+ AND payload = $4::jsonb
13868
+ RETURNING object_id`, [service, row.object_id, nextPayload, row.payload]);
13869
+ if (update.rows.length === 1)
13870
+ result.updated += 1;
13871
+ else
13872
+ result.conflicts += 1;
13873
+ }
13874
+ if (page.rows.length < batchSize)
13875
+ break;
13876
+ }
13877
+ if (!apply) {
13878
+ result.remaining_candidates = result.candidates;
13879
+ return result;
13880
+ }
13881
+ const verification = await backfillPostgresCommentRedaction(client, {
13882
+ ...options,
13883
+ apply: false,
13884
+ confirmation: undefined
13885
+ });
13886
+ result.remaining_candidates = verification.candidates;
13887
+ return result;
13888
+ }
13889
+ function isCommentRedactionBackfillComplete(result) {
13890
+ return !result.dry_run && result.conflicts === 0 && result.remaining_candidates === 0;
13891
+ }
13717
13892
  // src/storage/s3-artifacts.ts
13718
13893
  import { createHash as createHash3, createHmac as createHmac2 } from "crypto";
13719
13894
  function createTodosS3ArtifactStore(options) {
@@ -14146,6 +14321,7 @@ export {
14146
14321
  signAwsV4Request,
14147
14322
  registerShadowExitFlush,
14148
14323
  postgresTodosSyncSchemaSql,
14324
+ postgresTodosCommentCursorIndexSql,
14149
14325
  planRunArtifactsS3Sync,
14150
14326
  parseStorageMode,
14151
14327
  maybeInstallShadowCapture2 as maybeInstallShadowCapture,
@@ -14153,6 +14329,7 @@ export {
14153
14329
  loadStorageConfig,
14154
14330
  isTodosShadowEnabled,
14155
14331
  isTodosRemoteStorageEnabled,
14332
+ isCommentRedactionBackfillComplete,
14156
14333
  installShadowOutboxSchema,
14157
14334
  importSqliteTodosStorageSnapshot,
14158
14335
  getTodosStorageShadowEnvName,
@@ -14180,6 +14357,7 @@ export {
14180
14357
  closeRuntimeShadowCloud,
14181
14358
  buildS3ObjectUrl,
14182
14359
  buildS3ObjectKey,
14360
+ backfillPostgresCommentRedaction,
14183
14361
  assertTodosShadowConfig,
14184
14362
  assertTodosRemoteStorageConfig,
14185
14363
  TodosShadowOutbox,
@@ -14188,6 +14366,7 @@ export {
14188
14366
  TODOS_STORAGE_FALLBACK_ENV,
14189
14367
  TODOS_STORAGE_ENV,
14190
14368
  STORAGE_TABLES,
14369
+ COMMENT_REDACTION_BACKFILL_CONFIRMATION,
14191
14370
  CANONICAL_TODOS_RDS_RUNTIME_PATH,
14192
14371
  CANONICAL_TODOS_RDS_DATABASE,
14193
14372
  CANONICAL_TODOS_RDS_CLUSTER
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.11.85",
3
+ "version": "0.11.87",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -50,6 +50,7 @@
50
50
  "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 && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts --outdir dist --target bun && tsc --emitDeclarationOnly --outDir dist",
51
51
  "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 && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts --outdir dist --target bun",
52
52
  "migrate": "bun run src/server/index.ts migrate",
53
+ "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
53
54
  "generate:sdk": "bun run scripts/generate-sdk.ts",
54
55
  "build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
55
56
  "typecheck": "tsc --noEmit",
Binary file