@hasna/todos 0.11.86 → 0.11.88

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 +35 -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 +989 -416
  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
@@ -701,6 +701,128 @@ var init_cloud_client = __esm(() => {
701
701
  init_config();
702
702
  });
703
703
 
704
+ // src/types/index.ts
705
+ var TASK_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
706
+ var init_types = __esm(() => {
707
+ TASK_STATUSES = [
708
+ "pending",
709
+ "in_progress",
710
+ "completed",
711
+ "failed",
712
+ "cancelled"
713
+ ];
714
+ VersionConflictError = class VersionConflictError extends Error {
715
+ taskId;
716
+ expectedVersion;
717
+ actualVersion;
718
+ static code = "VERSION_CONFLICT";
719
+ static suggestion = "Fetch the task with get_task to get the current version before updating.";
720
+ constructor(taskId, expectedVersion, actualVersion) {
721
+ super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
722
+ this.taskId = taskId;
723
+ this.expectedVersion = expectedVersion;
724
+ this.actualVersion = actualVersion;
725
+ this.name = "VersionConflictError";
726
+ }
727
+ };
728
+ TaskNotFoundError = class TaskNotFoundError extends Error {
729
+ taskId;
730
+ static code = "TASK_NOT_FOUND";
731
+ static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
732
+ constructor(taskId) {
733
+ super(`Task not found: ${taskId}`);
734
+ this.taskId = taskId;
735
+ this.name = "TaskNotFoundError";
736
+ }
737
+ };
738
+ ProjectNotFoundError = class ProjectNotFoundError extends Error {
739
+ projectId;
740
+ static code = "PROJECT_NOT_FOUND";
741
+ static suggestion = "Use list_projects to see available projects.";
742
+ constructor(projectId) {
743
+ super(`Project not found: ${projectId}`);
744
+ this.projectId = projectId;
745
+ this.name = "ProjectNotFoundError";
746
+ }
747
+ };
748
+ PlanNotFoundError = class PlanNotFoundError extends Error {
749
+ planId;
750
+ static code = "PLAN_NOT_FOUND";
751
+ static suggestion = "Use list_plans to see available plans.";
752
+ constructor(planId) {
753
+ super(`Plan not found: ${planId}`);
754
+ this.planId = planId;
755
+ this.name = "PlanNotFoundError";
756
+ }
757
+ };
758
+ LockError = class LockError extends Error {
759
+ taskId;
760
+ lockedBy;
761
+ static code = "LOCK_ERROR";
762
+ static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
763
+ constructor(taskId, lockedBy) {
764
+ super(`Task ${taskId} is locked by ${lockedBy}`);
765
+ this.taskId = taskId;
766
+ this.lockedBy = lockedBy;
767
+ this.name = "LockError";
768
+ }
769
+ };
770
+ AgentNotFoundError = class AgentNotFoundError extends Error {
771
+ agentId;
772
+ static code = "AGENT_NOT_FOUND";
773
+ static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
774
+ constructor(agentId) {
775
+ super(`Agent not found: ${agentId}`);
776
+ this.agentId = agentId;
777
+ this.name = "AgentNotFoundError";
778
+ }
779
+ };
780
+ TaskListNotFoundError = class TaskListNotFoundError extends Error {
781
+ taskListId;
782
+ static code = "TASK_LIST_NOT_FOUND";
783
+ static suggestion = "Use list_task_lists to see available lists.";
784
+ constructor(taskListId) {
785
+ super(`Task list not found: ${taskListId}`);
786
+ this.taskListId = taskListId;
787
+ this.name = "TaskListNotFoundError";
788
+ }
789
+ };
790
+ DependencyCycleError = class DependencyCycleError extends Error {
791
+ taskId;
792
+ dependsOn;
793
+ static code = "DEPENDENCY_CYCLE";
794
+ static suggestion = "Check the dependency chain with get_task to avoid circular references.";
795
+ constructor(taskId, dependsOn) {
796
+ super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
797
+ this.taskId = taskId;
798
+ this.dependsOn = dependsOn;
799
+ this.name = "DependencyCycleError";
800
+ }
801
+ };
802
+ CompletionGuardError = class CompletionGuardError extends Error {
803
+ reason;
804
+ retryAfterSeconds;
805
+ static code = "COMPLETION_BLOCKED";
806
+ static suggestion = "Wait for the cooldown period, then retry.";
807
+ constructor(reason, retryAfterSeconds) {
808
+ super(reason);
809
+ this.reason = reason;
810
+ this.retryAfterSeconds = retryAfterSeconds;
811
+ this.name = "CompletionGuardError";
812
+ }
813
+ };
814
+ DispatchNotFoundError = class DispatchNotFoundError extends Error {
815
+ dispatchId;
816
+ static code = "DISPATCH_NOT_FOUND";
817
+ static suggestion = "Check the dispatch ID with list_dispatches.";
818
+ constructor(dispatchId) {
819
+ super(`Dispatch not found: ${dispatchId}`);
820
+ this.dispatchId = dispatchId;
821
+ this.name = "DispatchNotFoundError";
822
+ }
823
+ };
824
+ });
825
+
704
826
  // src/storage/postgres-sync.ts
705
827
  function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE, cursorTableName = DEFAULT_TODOS_POSTGRES_CURSOR_TABLE) {
706
828
  assertSafeIdentifier(tableName);
@@ -730,6 +852,12 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
730
852
  )`
731
853
  ];
732
854
  }
855
+ function postgresTodosCommentCursorIndexSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
856
+ assertSafeIdentifier(tableName);
857
+ return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_comment_task_created_idx
858
+ ON ${tableName} (service, (payload->>'task_id'), (payload->>'created_at'), object_id)
859
+ WHERE object_type = 'comments' AND deleted_at IS NULL`;
860
+ }
733
861
 
734
862
  class PostgresTodosSyncStore {
735
863
  client;
@@ -917,6 +1045,203 @@ function assertSafeIdentifier(value) {
917
1045
  }
918
1046
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records", DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
919
1047
 
1048
+ // src/lib/sync-utils.ts
1049
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "fs";
1050
+ import { join as join2 } from "path";
1051
+ function getHomeDir() {
1052
+ return process.env["HOME"] || process.env["USERPROFILE"] || "~";
1053
+ }
1054
+ function getTodosGlobalDir() {
1055
+ return join2(getHomeDir(), ".hasna", "todos");
1056
+ }
1057
+ function ensureDir(dir) {
1058
+ if (!existsSync2(dir))
1059
+ mkdirSync(dir, { recursive: true });
1060
+ }
1061
+ function readJsonFile(path) {
1062
+ try {
1063
+ return JSON.parse(readFileSync2(path, "utf-8"));
1064
+ } catch {
1065
+ return null;
1066
+ }
1067
+ }
1068
+ function writeJsonFile(path, data) {
1069
+ writeFileSync(path, JSON.stringify(data, null, 2) + `
1070
+ `);
1071
+ }
1072
+ function appendSyncConflict(metadata, conflict, limit = 5) {
1073
+ const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
1074
+ const next = [conflict, ...current].slice(0, limit);
1075
+ return { ...metadata, sync_conflicts: next };
1076
+ }
1077
+ var HOME;
1078
+ var init_sync_utils = __esm(() => {
1079
+ HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
1080
+ });
1081
+
1082
+ // src/lib/config.ts
1083
+ import { existsSync as existsSync3 } from "fs";
1084
+ import { dirname as dirname2, join as join3 } from "path";
1085
+ function getConfigPath() {
1086
+ return join3(getTodosGlobalDir(), "config.json");
1087
+ }
1088
+ function loadConfig() {
1089
+ if (cached)
1090
+ return cached;
1091
+ if (!existsSync3(getConfigPath())) {
1092
+ cached = {};
1093
+ return cached;
1094
+ }
1095
+ const config = readJsonFile(getConfigPath()) || {};
1096
+ if (typeof config.sync_agents === "string") {
1097
+ config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
1098
+ }
1099
+ cached = config;
1100
+ return cached;
1101
+ }
1102
+ function saveConfig(config) {
1103
+ const configPath = getConfigPath();
1104
+ ensureDir(dirname2(configPath));
1105
+ writeJsonFile(configPath, config);
1106
+ cached = config;
1107
+ return config;
1108
+ }
1109
+ function getAgentPoolForProject(workingDir) {
1110
+ const config = loadConfig();
1111
+ if (workingDir && config.project_pools) {
1112
+ let bestKey = null;
1113
+ let bestLen = 0;
1114
+ for (const key of Object.keys(config.project_pools)) {
1115
+ if (workingDir.startsWith(key) && key.length > bestLen) {
1116
+ bestKey = key;
1117
+ bestLen = key.length;
1118
+ }
1119
+ }
1120
+ if (bestKey && config.project_pools[bestKey]) {
1121
+ return config.project_pools[bestKey];
1122
+ }
1123
+ }
1124
+ return config.agent_pool || null;
1125
+ }
1126
+ function getCompletionGuardConfig(projectPath) {
1127
+ const config = loadConfig();
1128
+ const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
1129
+ if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
1130
+ return { ...global, ...config.project_overrides[projectPath].completion_guard };
1131
+ }
1132
+ return global;
1133
+ }
1134
+ var cached = null, GUARD_DEFAULTS;
1135
+ var init_config2 = __esm(() => {
1136
+ init_sync_utils();
1137
+ GUARD_DEFAULTS = {
1138
+ enabled: false,
1139
+ min_work_seconds: 30,
1140
+ max_completions_per_window: 5,
1141
+ window_minutes: 10,
1142
+ cooldown_seconds: 60
1143
+ };
1144
+ });
1145
+
1146
+ // src/lib/redaction.ts
1147
+ function unique(values) {
1148
+ return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
1149
+ }
1150
+ function cloneRegex(regex) {
1151
+ return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
1152
+ }
1153
+ function customPatterns() {
1154
+ return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
1155
+ try {
1156
+ return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
1157
+ } catch {
1158
+ return [];
1159
+ }
1160
+ });
1161
+ }
1162
+ function secretPatterns() {
1163
+ return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
1164
+ }
1165
+ function isSecretKey(key) {
1166
+ if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
1167
+ return false;
1168
+ if (DEFAULT_SECRET_KEY_PATTERN.test(key))
1169
+ return true;
1170
+ return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
1171
+ }
1172
+ function redactEvidenceText(value) {
1173
+ let redacted = value;
1174
+ for (const pattern of secretPatterns()) {
1175
+ const regex = cloneRegex(pattern.regex);
1176
+ const replacement = pattern.replacement ?? "[REDACTED]";
1177
+ redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
1178
+ }
1179
+ return redacted;
1180
+ }
1181
+ function redactValue(value) {
1182
+ if (typeof value === "string")
1183
+ return redactEvidenceText(value);
1184
+ if (Array.isArray(value))
1185
+ return value.map(redactValue);
1186
+ if (value && typeof value === "object") {
1187
+ const redacted = {};
1188
+ for (const [key, child] of Object.entries(value)) {
1189
+ if (isSecretKey(key)) {
1190
+ redacted[key] = "[REDACTED]";
1191
+ } else {
1192
+ redacted[key] = redactValue(child);
1193
+ }
1194
+ }
1195
+ return redacted;
1196
+ }
1197
+ return value;
1198
+ }
1199
+ function listSecretFindings(value) {
1200
+ const findings = [];
1201
+ for (const pattern of secretPatterns()) {
1202
+ const matches = value.match(cloneRegex(pattern.regex));
1203
+ if (matches?.length)
1204
+ findings.push({ pattern: pattern.name, count: matches.length });
1205
+ }
1206
+ return findings;
1207
+ }
1208
+ function getSecretSafetyConfig() {
1209
+ return {
1210
+ redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
1211
+ redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
1212
+ };
1213
+ }
1214
+ function upsertSecretSafetyConfig(input) {
1215
+ const config = loadConfig();
1216
+ const next = {
1217
+ redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
1218
+ redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
1219
+ };
1220
+ saveConfig({ ...config, secret_safety: next });
1221
+ return next;
1222
+ }
1223
+ var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
1224
+ var init_redaction = __esm(() => {
1225
+ init_config2();
1226
+ DEFAULT_SECRET_PATTERNS = [
1227
+ { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
1228
+ { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
1229
+ { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
1230
+ { name: "env-secret-assignment", regex: /\b([A-Za-z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*)\s*=\s*['"]?[^'"\s]{8,}/gi, replacement: "$1=[REDACTED]" },
1231
+ { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
1232
+ ];
1233
+ DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
1234
+ NON_SECRET_USAGE_KEYS = new Set([
1235
+ "tokens",
1236
+ "total_tokens",
1237
+ "token_count",
1238
+ "input_tokens",
1239
+ "output_tokens",
1240
+ "prompt_tokens",
1241
+ "completion_tokens"
1242
+ ]);
1243
+ });
1244
+
920
1245
  // src/storage/postgres-adapter.ts
921
1246
  import { randomUUID } from "crypto";
922
1247
  function createPostgresTodosStorageAdapter(options) {
@@ -1014,7 +1339,24 @@ function createPostgresTodosStorageAdapter(options) {
1014
1339
  audit: {
1015
1340
  logTaskChange: (taskId, action, field, oldValue, newValue, agentId, context) => logTaskChange(taskId, action, field, oldValue, newValue, agentId, store, context),
1016
1341
  addComment: (input, context) => addComment(input, store, context),
1017
- getComments: async (taskId) => (await store.list("comments")).filter((comment) => comment.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
1342
+ getComments: async (taskId) => {
1343
+ const pages = [];
1344
+ let before;
1345
+ while (true) {
1346
+ const page = await store.listComments(taskId, { limit: 1000, ...before ? { before } : {} });
1347
+ if (page.length === 0)
1348
+ break;
1349
+ pages.unshift(page);
1350
+ if (page.length < 1000)
1351
+ break;
1352
+ const oldest = page[0];
1353
+ before = { created_at: oldest.created_at, id: oldest.id };
1354
+ }
1355
+ return pages.flat().map(redactComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
1356
+ },
1357
+ getCommentsPage: async (taskId, options2) => {
1358
+ return (await store.listComments(taskId, options2)).map(redactComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
1359
+ },
1018
1360
  getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
1019
1361
  getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
1020
1362
  },
@@ -1064,6 +1406,27 @@ class PostgresJsonRecordStore {
1064
1406
  async list(type) {
1065
1407
  return (await this.listRecords(type)).map((record) => record.payload);
1066
1408
  }
1409
+ async listComments(taskId, options = {}) {
1410
+ await this.ensureSchema();
1411
+ const limit = options.limit ?? 100;
1412
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
1413
+ throw new Error("Postgres comment limit must be an integer between 1 and 1001");
1414
+ }
1415
+ const params = [this.service, taskId];
1416
+ let cursorPredicate = "";
1417
+ if (options.before) {
1418
+ params.push(options.before.created_at, options.before.id);
1419
+ cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
1420
+ }
1421
+ params.push(limit);
1422
+ const result = await this.options.client.query(`/* todos:list-comments */ SELECT payload FROM ${this.tableName}
1423
+ WHERE service = $1 AND object_type = 'comments' AND deleted_at IS NULL
1424
+ AND payload->>'task_id' = $2
1425
+ ${cursorPredicate}
1426
+ ORDER BY payload->>'created_at' DESC, object_id DESC
1427
+ LIMIT $${params.length}`, params);
1428
+ return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
1429
+ }
1067
1430
  async listRecords(type) {
1068
1431
  await this.ensureSchema();
1069
1432
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
@@ -1457,7 +1820,7 @@ async function lockTask(id, agentId, store) {
1457
1820
  async function unlockTask(id, agentId, store) {
1458
1821
  const task = await requireRecord("tasks", id, store);
1459
1822
  if (agentId && task.locked_by && task.locked_by !== agentId) {
1460
- throw new Error(`Task ${id} is locked by ${task.locked_by}, not ${agentId}`);
1823
+ throw new LockError(id, task.locked_by);
1461
1824
  }
1462
1825
  await patchTask(task, { locked_by: null, locked_at: null }, store);
1463
1826
  return true;
@@ -1822,13 +2185,16 @@ async function addComment(input, store, context) {
1822
2185
  task_id: input.task_id,
1823
2186
  agent_id: input.agent_id ?? context?.agentId ?? null,
1824
2187
  session_id: input.session_id ?? context?.sessionId ?? null,
1825
- content: input.content,
2188
+ content: redactEvidenceText(input.content),
1826
2189
  type: input.type ?? "comment",
1827
2190
  progress_pct: input.progress_pct ?? null,
1828
2191
  created_at: new Date().toISOString()
1829
2192
  };
1830
2193
  return store.upsert("comments", comment, context);
1831
2194
  }
2195
+ function redactComment(comment) {
2196
+ return { ...comment, content: redactEvidenceText(comment.content) };
2197
+ }
1832
2198
  async function exportSnapshot(store) {
1833
2199
  return {
1834
2200
  exportedAt: new Date().toISOString(),
@@ -1974,7 +2340,115 @@ function numberValue2(value) {
1974
2340
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
1975
2341
  }
1976
2342
  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";
1977
- var init_postgres_adapter = () => {};
2343
+ var init_postgres_adapter = __esm(() => {
2344
+ init_types();
2345
+ init_redaction();
2346
+ });
2347
+
2348
+ // src/storage/comment-redaction-backfill.ts
2349
+ var exports_comment_redaction_backfill = {};
2350
+ __export(exports_comment_redaction_backfill, {
2351
+ isCommentRedactionBackfillComplete: () => isCommentRedactionBackfillComplete,
2352
+ backfillPostgresCommentRedaction: () => backfillPostgresCommentRedaction,
2353
+ COMMENT_REDACTION_BACKFILL_CONFIRMATION: () => COMMENT_REDACTION_BACKFILL_CONFIRMATION
2354
+ });
2355
+ function assertSafeIdentifier2(value) {
2356
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
2357
+ throw new Error(`Unsafe Postgres identifier: ${value}`);
2358
+ }
2359
+ }
2360
+ function payloadObject(value) {
2361
+ if (value && typeof value === "object" && !Array.isArray(value))
2362
+ return value;
2363
+ if (typeof value !== "string")
2364
+ return null;
2365
+ try {
2366
+ const parsed = JSON.parse(value);
2367
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2368
+ } catch {
2369
+ return null;
2370
+ }
2371
+ }
2372
+ async function backfillPostgresCommentRedaction(client, options = {}) {
2373
+ const apply = options.apply === true;
2374
+ if (apply && options.confirmation !== COMMENT_REDACTION_BACKFILL_CONFIRMATION) {
2375
+ throw new Error(`Applying the comment redaction backfill requires confirmation ${COMMENT_REDACTION_BACKFILL_CONFIRMATION}`);
2376
+ }
2377
+ const tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
2378
+ assertSafeIdentifier2(tableName);
2379
+ const service = options.service ?? "todos";
2380
+ const batchSize = options.batchSize ?? 100;
2381
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 500) {
2382
+ throw new Error("Comment redaction backfill batchSize must be an integer between 1 and 500");
2383
+ }
2384
+ const result = {
2385
+ dry_run: !apply,
2386
+ scanned: 0,
2387
+ candidates: 0,
2388
+ updated: 0,
2389
+ conflicts: 0,
2390
+ batches: 0,
2391
+ remaining_candidates: 0
2392
+ };
2393
+ let afterId = "";
2394
+ while (true) {
2395
+ const page = await client.query(`/* todos:comment-redaction-backfill-scan */
2396
+ SELECT object_id, payload
2397
+ FROM ${tableName}
2398
+ WHERE service = $1 AND object_type = 'comments'
2399
+ AND object_id > $2
2400
+ ORDER BY object_id ASC
2401
+ LIMIT $3`, [service, afterId, batchSize]);
2402
+ if (page.rows.length === 0)
2403
+ break;
2404
+ result.batches += 1;
2405
+ for (const row of page.rows) {
2406
+ afterId = row.object_id;
2407
+ result.scanned += 1;
2408
+ const payload = payloadObject(row.payload);
2409
+ const original = payload?.["content"];
2410
+ if (typeof original !== "string")
2411
+ continue;
2412
+ const redacted = redactEvidenceText(original);
2413
+ if (redacted === original)
2414
+ continue;
2415
+ result.candidates += 1;
2416
+ if (!apply)
2417
+ continue;
2418
+ const nextPayload = { ...payload, content: redacted };
2419
+ const update = await client.query(`/* todos:comment-redaction-backfill-apply */
2420
+ UPDATE ${tableName}
2421
+ SET payload = $3::jsonb
2422
+ WHERE service = $1 AND object_type = 'comments' AND object_id = $2
2423
+ AND payload = $4::jsonb
2424
+ RETURNING object_id`, [service, row.object_id, nextPayload, row.payload]);
2425
+ if (update.rows.length === 1)
2426
+ result.updated += 1;
2427
+ else
2428
+ result.conflicts += 1;
2429
+ }
2430
+ if (page.rows.length < batchSize)
2431
+ break;
2432
+ }
2433
+ if (!apply) {
2434
+ result.remaining_candidates = result.candidates;
2435
+ return result;
2436
+ }
2437
+ const verification = await backfillPostgresCommentRedaction(client, {
2438
+ ...options,
2439
+ apply: false,
2440
+ confirmation: undefined
2441
+ });
2442
+ result.remaining_candidates = verification.candidates;
2443
+ return result;
2444
+ }
2445
+ function isCommentRedactionBackfillComplete(result) {
2446
+ return !result.dry_run && result.conflicts === 0 && result.remaining_candidates === 0;
2447
+ }
2448
+ var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
2449
+ var init_comment_redaction_backfill = __esm(() => {
2450
+ init_redaction();
2451
+ });
1978
2452
 
1979
2453
  // src/server/cloud.ts
1980
2454
  var exports_cloud = {};
@@ -1988,7 +2462,9 @@ __export(exports_cloud, {
1988
2462
  getCloudStorageAdapter: () => getCloudStorageAdapter,
1989
2463
  getApiKeyStore: () => getApiKeyStore,
1990
2464
  ensureCloudSchema: () => ensureCloudSchema,
2465
+ ensureCloudCommentCursorIndex: () => ensureCloudCommentCursorIndex,
1991
2466
  closeCloud: () => closeCloud,
2467
+ backfillCloudCommentRedaction: () => backfillCloudCommentRedaction,
1992
2468
  TODOS_APP_SLUG: () => TODOS_APP_SLUG
1993
2469
  });
1994
2470
  function resolveCloudDatabaseUrl(env = process.env) {
@@ -2066,6 +2542,9 @@ async function ensureCloudSchema() {
2066
2542
  })();
2067
2543
  return schemaEnsured;
2068
2544
  }
2545
+ async function ensureCloudCommentCursorIndex() {
2546
+ await getClient().query(postgresTodosCommentCursorIndexSql());
2547
+ }
2069
2548
  async function normalizeCloudPayloads() {
2070
2549
  const client = getClient();
2071
2550
  const res = await client.query(`UPDATE todos_sync_records
@@ -2074,6 +2553,9 @@ async function normalizeCloudPayloads() {
2074
2553
  RETURNING object_id AS id`);
2075
2554
  return res.rows.length;
2076
2555
  }
2556
+ function backfillCloudCommentRedaction(options = {}) {
2557
+ return backfillPostgresCommentRedaction(getClient(), { ...options, service: TODOS_APP_SLUG });
2558
+ }
2077
2559
  async function pingCloud() {
2078
2560
  const client = getClient();
2079
2561
  const res = await client.query("select 1 as ok");
@@ -2094,6 +2576,7 @@ var init_cloud = __esm(() => {
2094
2576
  init_auth();
2095
2577
  init_cloud_client();
2096
2578
  init_postgres_adapter();
2579
+ init_comment_redaction_backfill();
2097
2580
  });
2098
2581
 
2099
2582
  // src/db/migrations.ts
@@ -4395,7 +4878,7 @@ var init_schema = __esm(() => {
4395
4878
  });
4396
4879
 
4397
4880
  // src/db/machines.ts
4398
- import { existsSync as existsSync2 } from "fs";
4881
+ import { existsSync as existsSync4 } from "fs";
4399
4882
  import { hostname as osHostname, platform as osPlatform, arch as osArch } from "os";
4400
4883
  import { resolve } from "path";
4401
4884
  import { spawnSync } from "child_process";
@@ -4602,7 +5085,7 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
4602
5085
  message: `${project.name} has ${distinctPaths.length} different machine-local paths`
4603
5086
  });
4604
5087
  }
4605
- if (localRow && !existsSync2(localRow.path)) {
5088
+ if (localRow && !existsSync4(localRow.path)) {
4606
5089
  pathIssues.push({
4607
5090
  type: "path_missing",
4608
5091
  project_id: project.id,
@@ -4613,7 +5096,7 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
4613
5096
  message: `Local path does not exist on this machine: ${localRow.path}`
4614
5097
  });
4615
5098
  }
4616
- if (!localRow && project.path && machineById.has(localMachine.id) && !existsSync2(project.path)) {
5099
+ if (!localRow && project.path && machineById.has(localMachine.id) && !existsSync4(project.path)) {
4617
5100
  pathIssues.push({
4618
5101
  type: "path_missing",
4619
5102
  project_id: project.id,
@@ -4812,8 +5295,8 @@ __export(exports_database, {
4812
5295
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
4813
5296
  });
4814
5297
  import { Database } from "bun:sqlite";
4815
- import { existsSync as existsSync3, mkdirSync } from "fs";
4816
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
5298
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
5299
+ import { dirname as dirname3, join as join4, resolve as resolve2 } from "path";
4817
5300
  function isInMemoryDb(path) {
4818
5301
  return path === ":memory:" || path.startsWith("file::memory:");
4819
5302
  }
@@ -4822,12 +5305,12 @@ function findNearestProjectDb(startDir) {
4822
5305
  const stopAt = gitRoot ? resolve2(gitRoot) : resolve2(startDir);
4823
5306
  let dir = resolve2(startDir);
4824
5307
  while (true) {
4825
- const candidate = join2(dir, ".hasna", "todos", "todos.db");
4826
- if (existsSync3(candidate))
5308
+ const candidate = join4(dir, ".hasna", "todos", "todos.db");
5309
+ if (existsSync5(candidate))
4827
5310
  return candidate;
4828
5311
  if (dir === stopAt)
4829
5312
  break;
4830
- const parent = dirname2(dir);
5313
+ const parent = dirname3(dir);
4831
5314
  if (parent === dir)
4832
5315
  break;
4833
5316
  dir = parent;
@@ -4837,9 +5320,9 @@ function findNearestProjectDb(startDir) {
4837
5320
  function findGitRoot(startDir) {
4838
5321
  let dir = resolve2(startDir);
4839
5322
  while (true) {
4840
- if (existsSync3(join2(dir, ".git")))
5323
+ if (existsSync5(join4(dir, ".git")))
4841
5324
  return dir;
4842
- const parent = dirname2(dir);
5325
+ const parent = dirname3(dir);
4843
5326
  if (parent === dir)
4844
5327
  break;
4845
5328
  dir = parent;
@@ -4860,25 +5343,25 @@ function getDbPath() {
4860
5343
  if (process.env["TODOS_DB_SCOPE"] === "project") {
4861
5344
  const gitRoot = findGitRoot(cwd);
4862
5345
  if (gitRoot) {
4863
- return join2(gitRoot, ".hasna", "todos", "todos.db");
5346
+ return join4(gitRoot, ".hasna", "todos", "todos.db");
4864
5347
  }
4865
5348
  }
4866
5349
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4867
- return join2(home, ".hasna", "todos", "todos.db");
5350
+ return join4(home, ".hasna", "todos", "todos.db");
4868
5351
  }
4869
5352
  function getDatabasePath() {
4870
5353
  return getDbPath();
4871
5354
  }
4872
- function ensureDir(filePath) {
5355
+ function ensureDir2(filePath) {
4873
5356
  if (isInMemoryDb(filePath))
4874
5357
  return;
4875
- const dir = dirname2(resolve2(filePath));
4876
- if (!existsSync3(dir)) {
4877
- mkdirSync(dir, { recursive: true });
5358
+ const dir = dirname3(resolve2(filePath));
5359
+ if (!existsSync5(dir)) {
5360
+ mkdirSync2(dir, { recursive: true });
4878
5361
  }
4879
5362
  }
4880
5363
  function openDatabase(path) {
4881
- ensureDir(path);
5364
+ ensureDir2(path);
4882
5365
  const db = new Database(path);
4883
5366
  db.run("PRAGMA journal_mode = WAL");
4884
5367
  db.run("PRAGMA busy_timeout = 5000");
@@ -5053,226 +5536,6 @@ var init_api_keys = __esm(() => {
5053
5536
  init_database();
5054
5537
  });
5055
5538
 
5056
- // src/types/index.ts
5057
- var TASK_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
5058
- var init_types = __esm(() => {
5059
- TASK_STATUSES = [
5060
- "pending",
5061
- "in_progress",
5062
- "completed",
5063
- "failed",
5064
- "cancelled"
5065
- ];
5066
- VersionConflictError = class VersionConflictError extends Error {
5067
- taskId;
5068
- expectedVersion;
5069
- actualVersion;
5070
- static code = "VERSION_CONFLICT";
5071
- static suggestion = "Fetch the task with get_task to get the current version before updating.";
5072
- constructor(taskId, expectedVersion, actualVersion) {
5073
- super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
5074
- this.taskId = taskId;
5075
- this.expectedVersion = expectedVersion;
5076
- this.actualVersion = actualVersion;
5077
- this.name = "VersionConflictError";
5078
- }
5079
- };
5080
- TaskNotFoundError = class TaskNotFoundError extends Error {
5081
- taskId;
5082
- static code = "TASK_NOT_FOUND";
5083
- static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
5084
- constructor(taskId) {
5085
- super(`Task not found: ${taskId}`);
5086
- this.taskId = taskId;
5087
- this.name = "TaskNotFoundError";
5088
- }
5089
- };
5090
- ProjectNotFoundError = class ProjectNotFoundError extends Error {
5091
- projectId;
5092
- static code = "PROJECT_NOT_FOUND";
5093
- static suggestion = "Use list_projects to see available projects.";
5094
- constructor(projectId) {
5095
- super(`Project not found: ${projectId}`);
5096
- this.projectId = projectId;
5097
- this.name = "ProjectNotFoundError";
5098
- }
5099
- };
5100
- PlanNotFoundError = class PlanNotFoundError extends Error {
5101
- planId;
5102
- static code = "PLAN_NOT_FOUND";
5103
- static suggestion = "Use list_plans to see available plans.";
5104
- constructor(planId) {
5105
- super(`Plan not found: ${planId}`);
5106
- this.planId = planId;
5107
- this.name = "PlanNotFoundError";
5108
- }
5109
- };
5110
- LockError = class LockError extends Error {
5111
- taskId;
5112
- lockedBy;
5113
- static code = "LOCK_ERROR";
5114
- static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
5115
- constructor(taskId, lockedBy) {
5116
- super(`Task ${taskId} is locked by ${lockedBy}`);
5117
- this.taskId = taskId;
5118
- this.lockedBy = lockedBy;
5119
- this.name = "LockError";
5120
- }
5121
- };
5122
- AgentNotFoundError = class AgentNotFoundError extends Error {
5123
- agentId;
5124
- static code = "AGENT_NOT_FOUND";
5125
- static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
5126
- constructor(agentId) {
5127
- super(`Agent not found: ${agentId}`);
5128
- this.agentId = agentId;
5129
- this.name = "AgentNotFoundError";
5130
- }
5131
- };
5132
- TaskListNotFoundError = class TaskListNotFoundError extends Error {
5133
- taskListId;
5134
- static code = "TASK_LIST_NOT_FOUND";
5135
- static suggestion = "Use list_task_lists to see available lists.";
5136
- constructor(taskListId) {
5137
- super(`Task list not found: ${taskListId}`);
5138
- this.taskListId = taskListId;
5139
- this.name = "TaskListNotFoundError";
5140
- }
5141
- };
5142
- DependencyCycleError = class DependencyCycleError extends Error {
5143
- taskId;
5144
- dependsOn;
5145
- static code = "DEPENDENCY_CYCLE";
5146
- static suggestion = "Check the dependency chain with get_task to avoid circular references.";
5147
- constructor(taskId, dependsOn) {
5148
- super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
5149
- this.taskId = taskId;
5150
- this.dependsOn = dependsOn;
5151
- this.name = "DependencyCycleError";
5152
- }
5153
- };
5154
- CompletionGuardError = class CompletionGuardError extends Error {
5155
- reason;
5156
- retryAfterSeconds;
5157
- static code = "COMPLETION_BLOCKED";
5158
- static suggestion = "Wait for the cooldown period, then retry.";
5159
- constructor(reason, retryAfterSeconds) {
5160
- super(reason);
5161
- this.reason = reason;
5162
- this.retryAfterSeconds = retryAfterSeconds;
5163
- this.name = "CompletionGuardError";
5164
- }
5165
- };
5166
- DispatchNotFoundError = class DispatchNotFoundError extends Error {
5167
- dispatchId;
5168
- static code = "DISPATCH_NOT_FOUND";
5169
- static suggestion = "Check the dispatch ID with list_dispatches.";
5170
- constructor(dispatchId) {
5171
- super(`Dispatch not found: ${dispatchId}`);
5172
- this.dispatchId = dispatchId;
5173
- this.name = "DispatchNotFoundError";
5174
- }
5175
- };
5176
- });
5177
-
5178
- // src/lib/sync-utils.ts
5179
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "fs";
5180
- import { join as join3 } from "path";
5181
- function getHomeDir() {
5182
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
5183
- }
5184
- function getTodosGlobalDir() {
5185
- return join3(getHomeDir(), ".hasna", "todos");
5186
- }
5187
- function ensureDir2(dir) {
5188
- if (!existsSync4(dir))
5189
- mkdirSync2(dir, { recursive: true });
5190
- }
5191
- function readJsonFile(path) {
5192
- try {
5193
- return JSON.parse(readFileSync2(path, "utf-8"));
5194
- } catch {
5195
- return null;
5196
- }
5197
- }
5198
- function writeJsonFile(path, data) {
5199
- writeFileSync(path, JSON.stringify(data, null, 2) + `
5200
- `);
5201
- }
5202
- function appendSyncConflict(metadata, conflict, limit = 5) {
5203
- const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
5204
- const next = [conflict, ...current].slice(0, limit);
5205
- return { ...metadata, sync_conflicts: next };
5206
- }
5207
- var HOME;
5208
- var init_sync_utils = __esm(() => {
5209
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
5210
- });
5211
-
5212
- // src/lib/config.ts
5213
- import { existsSync as existsSync5 } from "fs";
5214
- import { dirname as dirname3, join as join4 } from "path";
5215
- function getConfigPath() {
5216
- return join4(getTodosGlobalDir(), "config.json");
5217
- }
5218
- function loadConfig() {
5219
- if (cached)
5220
- return cached;
5221
- if (!existsSync5(getConfigPath())) {
5222
- cached = {};
5223
- return cached;
5224
- }
5225
- const config = readJsonFile(getConfigPath()) || {};
5226
- if (typeof config.sync_agents === "string") {
5227
- config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
5228
- }
5229
- cached = config;
5230
- return cached;
5231
- }
5232
- function saveConfig(config) {
5233
- const configPath = getConfigPath();
5234
- ensureDir2(dirname3(configPath));
5235
- writeJsonFile(configPath, config);
5236
- cached = config;
5237
- return config;
5238
- }
5239
- function getAgentPoolForProject(workingDir) {
5240
- const config = loadConfig();
5241
- if (workingDir && config.project_pools) {
5242
- let bestKey = null;
5243
- let bestLen = 0;
5244
- for (const key of Object.keys(config.project_pools)) {
5245
- if (workingDir.startsWith(key) && key.length > bestLen) {
5246
- bestKey = key;
5247
- bestLen = key.length;
5248
- }
5249
- }
5250
- if (bestKey && config.project_pools[bestKey]) {
5251
- return config.project_pools[bestKey];
5252
- }
5253
- }
5254
- return config.agent_pool || null;
5255
- }
5256
- function getCompletionGuardConfig(projectPath) {
5257
- const config = loadConfig();
5258
- const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
5259
- if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
5260
- return { ...global, ...config.project_overrides[projectPath].completion_guard };
5261
- }
5262
- return global;
5263
- }
5264
- var cached = null, GUARD_DEFAULTS;
5265
- var init_config2 = __esm(() => {
5266
- init_sync_utils();
5267
- GUARD_DEFAULTS = {
5268
- enabled: false,
5269
- min_work_seconds: 30,
5270
- max_completions_per_window: 5,
5271
- window_minutes: 10,
5272
- cooldown_seconds: 60
5273
- };
5274
- });
5275
-
5276
5539
  // src/db/storage-tombstones.ts
5277
5540
  function recordStorageTombstone(input, db) {
5278
5541
  const d = db ?? getDatabase();
@@ -5709,105 +5972,6 @@ var init_event_emission_safety = __esm(() => {
5709
5972
  init_sync_utils();
5710
5973
  });
5711
5974
 
5712
- // src/lib/redaction.ts
5713
- function unique(values) {
5714
- return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
5715
- }
5716
- function cloneRegex(regex) {
5717
- return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
5718
- }
5719
- function customPatterns() {
5720
- return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
5721
- try {
5722
- return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
5723
- } catch {
5724
- return [];
5725
- }
5726
- });
5727
- }
5728
- function secretPatterns() {
5729
- return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
5730
- }
5731
- function isSecretKey(key) {
5732
- if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
5733
- return false;
5734
- if (DEFAULT_SECRET_KEY_PATTERN.test(key))
5735
- return true;
5736
- return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
5737
- }
5738
- function redactEvidenceText(value) {
5739
- let redacted = value;
5740
- for (const pattern of secretPatterns()) {
5741
- const regex = cloneRegex(pattern.regex);
5742
- const replacement = pattern.replacement ?? "[REDACTED]";
5743
- redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
5744
- }
5745
- return redacted;
5746
- }
5747
- function redactValue(value) {
5748
- if (typeof value === "string")
5749
- return redactEvidenceText(value);
5750
- if (Array.isArray(value))
5751
- return value.map(redactValue);
5752
- if (value && typeof value === "object") {
5753
- const redacted = {};
5754
- for (const [key, child] of Object.entries(value)) {
5755
- if (isSecretKey(key)) {
5756
- redacted[key] = "[REDACTED]";
5757
- } else {
5758
- redacted[key] = redactValue(child);
5759
- }
5760
- }
5761
- return redacted;
5762
- }
5763
- return value;
5764
- }
5765
- function listSecretFindings(value) {
5766
- const findings = [];
5767
- for (const pattern of secretPatterns()) {
5768
- const matches = value.match(cloneRegex(pattern.regex));
5769
- if (matches?.length)
5770
- findings.push({ pattern: pattern.name, count: matches.length });
5771
- }
5772
- return findings;
5773
- }
5774
- function getSecretSafetyConfig() {
5775
- return {
5776
- redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
5777
- redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
5778
- };
5779
- }
5780
- function upsertSecretSafetyConfig(input) {
5781
- const config = loadConfig();
5782
- const next = {
5783
- redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
5784
- redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
5785
- };
5786
- saveConfig({ ...config, secret_safety: next });
5787
- return next;
5788
- }
5789
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
5790
- var init_redaction = __esm(() => {
5791
- init_config2();
5792
- DEFAULT_SECRET_PATTERNS = [
5793
- { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
5794
- { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
5795
- { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
5796
- { name: "env-secret-assignment", regex: /\b([A-Za-z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*)\s*=\s*['"]?[^'"\s]{8,}/gi, replacement: "$1=[REDACTED]" },
5797
- { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
5798
- ];
5799
- DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
5800
- NON_SECRET_USAGE_KEYS = new Set([
5801
- "tokens",
5802
- "total_tokens",
5803
- "token_count",
5804
- "input_tokens",
5805
- "output_tokens",
5806
- "prompt_tokens",
5807
- "completion_tokens"
5808
- ]);
5809
- });
5810
-
5811
5975
  // src/lib/workspace-trust.ts
5812
5976
  import { relative, resolve as resolve4 } from "path";
5813
5977
  function normalizePath(path) {
@@ -11244,7 +11408,7 @@ function getComment(id, db) {
11244
11408
  }
11245
11409
  function listComments(taskId, db) {
11246
11410
  const d = db || getDatabase();
11247
- return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
11411
+ return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at, rowid").all(taskId);
11248
11412
  }
11249
11413
  function updateComment(id, input, db) {
11250
11414
  const d = db || getDatabase();
@@ -14978,6 +15142,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
14978
15142
  list: (filter = {}) => listTasks2(filter, database()),
14979
15143
  count: (filter = {}) => countTasks(filter, database()),
14980
15144
  update: (id, input) => updateTask2(id, input, database()),
15145
+ unlock: (id, agentId) => {
15146
+ unlockTask2(id, agentId, database());
15147
+ return true;
15148
+ },
14981
15149
  delete: (id) => deleteTask(id, database()),
14982
15150
  start: (id, agentId) => startTask2(id, agentId, database()),
14983
15151
  complete: (id, agentId, options2) => completeTask2(id, agentId, database(), options2),
@@ -15029,6 +15197,20 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
15029
15197
  logTaskChange: (taskId, action, field, oldValue, newValue, agentId) => logTaskChange2(taskId, action, field, oldValue, newValue, agentId, database()),
15030
15198
  addComment: (input) => addComment2(input, database()),
15031
15199
  getComments: (taskId) => listComments(taskId, database()),
15200
+ getCommentsPage: (taskId, options2) => {
15201
+ if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
15202
+ throw new Error("Comment limit must be an integer between 1 and 1001");
15203
+ }
15204
+ let comments = listComments(taskId, database());
15205
+ comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
15206
+ if (options2?.before) {
15207
+ const before = options2.before;
15208
+ comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
15209
+ }
15210
+ if (options2?.limit !== undefined)
15211
+ comments = comments.slice(-options2.limit);
15212
+ return comments;
15213
+ },
15032
15214
  getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
15033
15215
  getRecentActivity: (limit) => getRecentActivity(limit, database())
15034
15216
  },
@@ -15416,6 +15598,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
15416
15598
  schemas: {
15417
15599
  Task: taskSchema,
15418
15600
  Project: projectSchema,
15601
+ TaskComment: taskCommentSchema,
15419
15602
  CreateTaskInput: {
15420
15603
  type: "object",
15421
15604
  required: ["title"],
@@ -15450,6 +15633,17 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
15450
15633
  description: { type: "string" },
15451
15634
  task_prefix: { type: "string" }
15452
15635
  }
15636
+ },
15637
+ CreateTaskCommentInput: {
15638
+ type: "object",
15639
+ required: ["content"],
15640
+ properties: {
15641
+ content: { type: "string", minLength: 1 },
15642
+ agent_id: { type: "string" },
15643
+ session_id: { type: "string" },
15644
+ type: { type: "string", enum: ["comment", "progress", "note"] },
15645
+ progress_pct: { type: "number" }
15646
+ }
15453
15647
  }
15454
15648
  }
15455
15649
  },
@@ -15548,6 +15742,61 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
15548
15742
  }
15549
15743
  }
15550
15744
  },
15745
+ "/v1/tasks/{id}/comments": {
15746
+ get: {
15747
+ operationId: "listTaskComments",
15748
+ summary: "List a bounded page of task comments",
15749
+ 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.",
15750
+ parameters: [
15751
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
15752
+ { name: "limit", in: "query", required: true, schema: { type: "integer", minimum: 1, maximum: 500, default: 100 } },
15753
+ { name: "cursor", in: "query", schema: { type: "string" } }
15754
+ ],
15755
+ responses: {
15756
+ "200": {
15757
+ content: {
15758
+ "application/json": {
15759
+ schema: {
15760
+ type: "object",
15761
+ required: ["comments", "count", "has_more", "next_cursor"],
15762
+ properties: {
15763
+ comments: { type: "array", maxItems: 500, items: { $ref: "#/components/schemas/TaskComment" } },
15764
+ count: { type: "integer", minimum: 0, maximum: 500 },
15765
+ has_more: { type: "boolean" },
15766
+ next_cursor: { type: "string", nullable: true }
15767
+ }
15768
+ }
15769
+ }
15770
+ }
15771
+ },
15772
+ "426": {
15773
+ 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."
15774
+ }
15775
+ }
15776
+ },
15777
+ post: {
15778
+ operationId: "createTaskComment",
15779
+ summary: "Create a task comment",
15780
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
15781
+ requestBody: {
15782
+ required: true,
15783
+ content: { "application/json": { schema: { $ref: "#/components/schemas/CreateTaskCommentInput" } } }
15784
+ },
15785
+ responses: {
15786
+ "201": {
15787
+ content: {
15788
+ "application/json": {
15789
+ schema: {
15790
+ type: "object",
15791
+ required: ["comment"],
15792
+ properties: { comment: { $ref: "#/components/schemas/TaskComment" } }
15793
+ }
15794
+ }
15795
+ }
15796
+ }
15797
+ }
15798
+ }
15799
+ },
15551
15800
  "/v1/tasks/{id}/start": {
15552
15801
  post: {
15553
15802
  operationId: "startTask",
@@ -15666,7 +15915,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
15666
15915
  }
15667
15916
  };
15668
15917
  }
15669
- var taskSchema, projectSchema;
15918
+ var taskSchema, projectSchema, taskCommentSchema;
15670
15919
  var init_openapi = __esm(() => {
15671
15920
  init_package_version();
15672
15921
  taskSchema = {
@@ -15697,6 +15946,20 @@ var init_openapi = __esm(() => {
15697
15946
  updated_at: { type: "string" }
15698
15947
  }
15699
15948
  };
15949
+ taskCommentSchema = {
15950
+ type: "object",
15951
+ required: ["id", "task_id", "agent_id", "session_id", "content", "type", "progress_pct", "created_at"],
15952
+ properties: {
15953
+ id: { type: "string" },
15954
+ task_id: { type: "string" },
15955
+ agent_id: { type: "string", nullable: true },
15956
+ session_id: { type: "string", nullable: true },
15957
+ content: { type: "string" },
15958
+ type: { type: "string", enum: ["comment", "progress", "note"] },
15959
+ progress_pct: { type: "number", nullable: true },
15960
+ created_at: { type: "string", format: "date-time" }
15961
+ }
15962
+ };
15700
15963
  });
15701
15964
 
15702
15965
  // src/server/v1.ts
@@ -15726,6 +15989,29 @@ function contextFromPrincipal(principal, body) {
15726
15989
  const agentId = body?.agent_id || principal.agent || undefined;
15727
15990
  return agentId ? { agentId } : {};
15728
15991
  }
15992
+ function redactComment2(comment) {
15993
+ return { ...comment, content: redactEvidenceText(comment.content) };
15994
+ }
15995
+ function encodeCommentCursor(comment) {
15996
+ return Buffer.from(JSON.stringify({ created_at: comment.created_at, id: comment.id }), "utf8").toString("base64url");
15997
+ }
15998
+ function decodeCommentCursor(value) {
15999
+ if (value.length > 1024)
16000
+ throw new Error("invalid comment cursor");
16001
+ let parsed;
16002
+ try {
16003
+ parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
16004
+ } catch {
16005
+ throw new Error("invalid comment cursor");
16006
+ }
16007
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
16008
+ throw new Error("invalid comment cursor");
16009
+ const cursor = parsed;
16010
+ 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) {
16011
+ throw new Error("invalid comment cursor");
16012
+ }
16013
+ return { created_at: cursor["created_at"], id: cursor["id"] };
16014
+ }
15729
16015
  function normalizeImportSnapshot(raw) {
15730
16016
  const body = raw && typeof raw === "object" ? raw : {};
15731
16017
  const arr = (v) => Array.isArray(v) ? v : [];
@@ -15746,7 +16032,7 @@ function normalizeImportSnapshot(raw) {
15746
16032
  function countSnapshotRecords(s) {
15747
16033
  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);
15748
16034
  }
15749
- async function handleV1Request(req, url) {
16035
+ async function handleV1Request(req, url, dependencies = {}) {
15750
16036
  const path = url.pathname;
15751
16037
  if (path !== "/v1" && !path.startsWith("/v1/"))
15752
16038
  return null;
@@ -15755,7 +16041,7 @@ async function handleV1Request(req, url) {
15755
16041
  const requiredScopes = [isWrite ? "todos:write" : "todos:read"];
15756
16042
  let verifier;
15757
16043
  try {
15758
- verifier = getCloudVerifier();
16044
+ verifier = (dependencies.getVerifier ?? getCloudVerifier)();
15759
16045
  } catch (e) {
15760
16046
  return error(503, e.message);
15761
16047
  }
@@ -15764,8 +16050,8 @@ async function handleV1Request(req, url) {
15764
16050
  return error(decision.status, decision.message, { reason: decision.reason });
15765
16051
  }
15766
16052
  const principal = decision.principal;
15767
- await ensureCloudSchema();
15768
- const store = getCloudStorageAdapter();
16053
+ await (dependencies.ensureSchema ?? ensureCloudSchema)();
16054
+ const store = (dependencies.getStorageAdapter ?? getCloudStorageAdapter)();
15769
16055
  const segments = path.split("/").filter(Boolean);
15770
16056
  const resource = segments[1];
15771
16057
  const id = segments[2];
@@ -15851,10 +16137,16 @@ async function handleV1Request(req, url) {
15851
16137
  if (!id) {
15852
16138
  if (method === "GET") {
15853
16139
  const filter = {
15854
- ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
15855
- ...url.searchParams.get("priority") ? { priority: url.searchParams.get("priority") } : {},
16140
+ ...url.searchParams.get("status") ? {
16141
+ status: url.searchParams.get("status").includes(",") ? url.searchParams.get("status").split(",") : url.searchParams.get("status")
16142
+ } : {},
16143
+ ...url.searchParams.get("priority") ? {
16144
+ priority: url.searchParams.get("priority").includes(",") ? url.searchParams.get("priority").split(",") : url.searchParams.get("priority")
16145
+ } : {},
15856
16146
  ...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
16147
+ ...url.searchParams.has("parent_id") ? { parent_id: url.searchParams.get("parent_id") || null, include_subtasks: true } : {},
15857
16148
  ...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {},
16149
+ ...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
15858
16150
  ...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
15859
16151
  ...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
15860
16152
  ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
@@ -15878,8 +16170,47 @@ async function handleV1Request(req, url) {
15878
16170
  if (action) {
15879
16171
  if (action === "comments") {
15880
16172
  if (method === "GET") {
15881
- const comments = await store.audit.getComments(id);
15882
- return json2({ comments, count: comments.length });
16173
+ if (!await store.tasks.get(id))
16174
+ return error(404, "task not found");
16175
+ const rawLimit = url.searchParams.get("limit");
16176
+ const cursor = url.searchParams.get("cursor");
16177
+ if (rawLimit === null && cursor === null) {
16178
+ const storageContext = contextFromPrincipal(principal);
16179
+ const legacyPage = (await (store.audit.getCommentsPage ? store.audit.getCommentsPage(id, { limit: LEGACY_COMMENT_RESPONSE_LIMIT + 1 }, storageContext) : store.audit.getComments(id, storageContext))).map(redactComment2);
16180
+ if (legacyPage.length > LEGACY_COMMENT_RESPONSE_LIMIT) {
16181
+ return error(426, "task has too many comments for this client; upgrade @hasna/todos to use cursor pagination");
16182
+ }
16183
+ return json2({
16184
+ comments: legacyPage,
16185
+ count: legacyPage.length,
16186
+ has_more: false,
16187
+ next_cursor: null
16188
+ });
16189
+ }
16190
+ const limit = rawLimit === null ? DEFAULT_COMMENT_PAGE_SIZE : Number(rawLimit);
16191
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_COMMENT_PAGE_SIZE) {
16192
+ return error(400, `limit must be an integer between 1 and ${MAX_COMMENT_PAGE_SIZE}`);
16193
+ }
16194
+ let before;
16195
+ if (cursor) {
16196
+ try {
16197
+ before = decodeCommentCursor(cursor);
16198
+ } catch {
16199
+ return error(400, "invalid comment cursor");
16200
+ }
16201
+ }
16202
+ if (!store.audit.getCommentsPage) {
16203
+ return error(426, "storage adapter must be upgraded to support cursor-paginated comments");
16204
+ }
16205
+ const page = (await store.audit.getCommentsPage(id, { limit: limit + 1, ...before ? { before } : {} }, contextFromPrincipal(principal))).map(redactComment2);
16206
+ const hasMore = page.length > limit;
16207
+ const comments = hasMore ? page.slice(1) : page;
16208
+ return json2({
16209
+ comments,
16210
+ count: comments.length,
16211
+ has_more: hasMore,
16212
+ next_cursor: hasMore && comments[0] ? encodeCommentCursor(comments[0]) : null
16213
+ });
15883
16214
  }
15884
16215
  if (method === "POST") {
15885
16216
  const body2 = await readJson(req) ?? {};
@@ -15897,7 +16228,7 @@ async function handleV1Request(req, url) {
15897
16228
  type: body2.type,
15898
16229
  progress_pct: body2.progress_pct
15899
16230
  }, contextFromPrincipal(principal, body2));
15900
- return json2({ comment }, 201);
16231
+ return json2({ comment: redactComment2(comment) }, 201);
15901
16232
  }
15902
16233
  return error(405, `method ${method} not allowed on /v1/tasks/:id/comments`);
15903
16234
  }
@@ -15912,17 +16243,30 @@ async function handleV1Request(req, url) {
15912
16243
  if (action === "lock" || action === "unlock") {
15913
16244
  if (method !== "POST")
15914
16245
  return error(405, `method ${method} not allowed on /v1/tasks/:id/${action}`);
15915
- if (typeof store.tasks.lock !== "function" || typeof store.tasks.unlock !== "function") {
15916
- return error(501, "task locking is not supported by this storage backend");
15917
- }
15918
16246
  const body2 = await readJson(req) ?? {};
15919
16247
  if (!await store.tasks.get(id))
15920
16248
  return error(404, "task not found");
15921
16249
  if (action === "lock") {
15922
- const agentId2 = body2.agent_id || principal.agent || "todos-serve";
15923
- return json2({ result: await store.tasks.lock(id, agentId2) });
15924
- }
15925
- const released = await store.tasks.unlock(id, body2.agent_id || principal.agent || undefined);
16250
+ if (typeof store.tasks.lock !== "function")
16251
+ return error(501, "task locking is not supported by this storage backend");
16252
+ const agentId3 = body2.agent_id || principal.agent || "todos-serve";
16253
+ return json2({ result: await store.tasks.lock(id, agentId3) });
16254
+ }
16255
+ if (typeof store.tasks.unlock !== "function")
16256
+ return error(501, "task unlocking is not supported by this storage backend");
16257
+ if (body2.force === true) {
16258
+ if (!principal.scopes.includes("todos:*"))
16259
+ return error(403, "force unlock requires todos:* scope");
16260
+ const released2 = await store.tasks.unlock(id);
16261
+ return json2({ success: released2 });
16262
+ }
16263
+ if (body2.agent_id && principal.agent && body2.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
16264
+ return error(403, "unlock agent_id must match the authenticated agent");
16265
+ }
16266
+ const agentId2 = principal.agent || body2.agent_id;
16267
+ if (!agentId2)
16268
+ return error(403, "unlock requires an agent-bound key or force=true");
16269
+ const released = await store.tasks.unlock(id, agentId2);
15926
16270
  return json2({ success: released });
15927
16271
  }
15928
16272
  if (action === "dependencies") {
@@ -16205,12 +16549,28 @@ async function handleV1Request(req, url) {
16205
16549
  const activity = await store.audit.getRecentActivity(limit);
16206
16550
  return json2({ activity, count: activity.length });
16207
16551
  }
16208
- if (resource === "task-lists" && !id) {
16209
- if (method !== "GET")
16210
- return error(405, `method ${method} not allowed on /v1/task-lists`);
16211
- const projectId = url.searchParams.get("project_id") ?? undefined;
16212
- const taskLists = await store.taskLists.list(projectId);
16213
- return json2({ task_lists: taskLists, count: taskLists.length });
16552
+ if (resource === "task-lists") {
16553
+ if (!id && method === "GET") {
16554
+ const projectId = url.searchParams.get("project_id") ?? undefined;
16555
+ const taskLists = await store.taskLists.list(projectId);
16556
+ return json2({ task_lists: taskLists, count: taskLists.length });
16557
+ }
16558
+ if (!id && method === "POST") {
16559
+ const body = await readJson(req);
16560
+ if (!body || typeof body.name !== "string" || !body.name.trim())
16561
+ return error(400, "name is required");
16562
+ const taskList = await store.taskLists.create(body, contextFromPrincipal(principal));
16563
+ return json2({ task_list: taskList }, 201);
16564
+ }
16565
+ if (id && method === "GET") {
16566
+ const taskList = await store.taskLists.get(id);
16567
+ return taskList ? json2({ task_list: taskList }) : error(404, "task list not found");
16568
+ }
16569
+ if (id && method === "DELETE") {
16570
+ const deleted = await store.taskLists.delete(id, contextFromPrincipal(principal));
16571
+ return deleted ? json2({ deleted: true, id }) : error(404, "task list not found");
16572
+ }
16573
+ return error(405, `method ${method} not allowed on /v1/task-lists${id ? "/:id" : ""}`);
16214
16574
  }
16215
16575
  if (resource === "dependencies" && !id) {
16216
16576
  if (method !== "GET")
@@ -16218,8 +16578,8 @@ async function handleV1Request(req, url) {
16218
16578
  if (typeof store.dependencies?.listAll !== "function") {
16219
16579
  return error(501, "dependency edge listing is not supported by this storage backend");
16220
16580
  }
16221
- const dependencies = await store.dependencies.listAll();
16222
- return json2({ dependencies, count: dependencies.length });
16581
+ const dependencies2 = await store.dependencies.listAll();
16582
+ return json2({ dependencies: dependencies2, count: dependencies2.length });
16223
16583
  }
16224
16584
  if (resource === "commits" && id) {
16225
16585
  if (method !== "GET")
@@ -16276,12 +16636,16 @@ async function handleV1Request(req, url) {
16276
16636
  }
16277
16637
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
16278
16638
  } catch (e) {
16639
+ if (e instanceof LockError)
16640
+ return error(409, e.message, { code: LockError.code });
16279
16641
  return error(500, e.message || "internal error");
16280
16642
  }
16281
16643
  }
16282
- var JSON_HEADERS;
16644
+ var JSON_HEADERS, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500;
16283
16645
  var init_v1 = __esm(() => {
16646
+ init_types();
16284
16647
  init_cloud();
16648
+ init_redaction();
16285
16649
  JSON_HEADERS = { "Content-Type": "application/json" };
16286
16650
  });
16287
16651
 
@@ -19658,7 +20022,7 @@ class JSONSchemaGenerator {
19658
20022
  if (val === undefined) {
19659
20023
  if (this.unrepresentable === "throw") {
19660
20024
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
19661
- } else {}
20025
+ }
19662
20026
  } else if (typeof val === "bigint") {
19663
20027
  if (this.unrepresentable === "throw") {
19664
20028
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -44024,14 +44388,18 @@ function unwrapTask(raw) {
44024
44388
  }
44025
44389
  function toListQuery(filter = {}) {
44026
44390
  const query = {};
44027
- if (typeof filter.status === "string")
44028
- query["status"] = filter.status;
44029
- if (typeof filter.priority === "string")
44030
- query["priority"] = filter.priority;
44391
+ if (filter.status)
44392
+ query["status"] = Array.isArray(filter.status) ? filter.status.join(",") : filter.status;
44393
+ if (filter.priority)
44394
+ query["priority"] = Array.isArray(filter.priority) ? filter.priority.join(",") : filter.priority;
44031
44395
  if (filter.project_id)
44032
44396
  query["project_id"] = filter.project_id;
44397
+ if (filter.parent_id !== undefined)
44398
+ query["parent_id"] = filter.parent_id ?? "";
44033
44399
  if (filter.plan_id)
44034
44400
  query["plan_id"] = filter.plan_id;
44401
+ if (filter.task_list_id)
44402
+ query["task_list_id"] = filter.task_list_id;
44035
44403
  if (filter.assigned_to)
44036
44404
  query["assigned_to"] = filter.assigned_to;
44037
44405
  if (filter.agent_id)
@@ -44081,10 +44449,19 @@ async function cloudListProjects(client) {
44081
44449
  }
44082
44450
  async function cloudAddComment(client, taskId, input) {
44083
44451
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/comments`, input);
44084
- if (raw && typeof raw === "object" && "comment" in raw) {
44085
- return raw.comment;
44086
- }
44087
- return raw;
44452
+ const comment = raw && typeof raw === "object" && "comment" in raw ? raw.comment : raw;
44453
+ if (!isTaskComment(comment))
44454
+ throw new Error("Invalid cloud comment response");
44455
+ return redactComment3(comment);
44456
+ }
44457
+ function isTaskComment(value) {
44458
+ if (!value || typeof value !== "object" || Array.isArray(value))
44459
+ return false;
44460
+ const comment = value;
44461
+ 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";
44462
+ }
44463
+ function redactComment3(comment) {
44464
+ return { ...comment, content: redactEvidenceText(comment.content) };
44088
44465
  }
44089
44466
  async function cloudCountTasks(client, filter = {}) {
44090
44467
  const { limit: _drop, offset: _o, ...rest } = filter;
@@ -44117,6 +44494,7 @@ async function cloudReleaseAgent(client, idOrName, sessionId) {
44117
44494
  var _cache;
44118
44495
  var init_cloud_router = __esm(() => {
44119
44496
  init_storage();
44497
+ init_redaction();
44120
44498
  });
44121
44499
 
44122
44500
  // src/mcp/tools/task-crud.ts
@@ -69455,7 +69833,7 @@ var require_to_json_schema = __commonJS((exports) => {
69455
69833
  if (val === undefined) {
69456
69834
  if (this.unrepresentable === "throw") {
69457
69835
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
69458
- } else {}
69836
+ }
69459
69837
  } else if (typeof val === "bigint") {
69460
69838
  if (this.unrepresentable === "throw") {
69461
69839
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -89936,7 +90314,7 @@ function captureEnvironmentSnapshot(input = {}) {
89936
90314
  }
89937
90315
  function writeEnvironmentSnapshot(snapshot, outputPath) {
89938
90316
  const path = outputPath ? resolve16(outputPath) : join14(defaultSnapshotDir(), `${snapshot.id}.json`);
89939
- ensureDir2(dirname9(path));
90317
+ ensureDir(dirname9(path));
89940
90318
  writeJsonFile(path, snapshot);
89941
90319
  return path;
89942
90320
  }
@@ -91123,11 +91501,19 @@ function printHelp2() {
91123
91501
 
91124
91502
  Start the @hasna/todos dashboard server.
91125
91503
 
91504
+ Commands:
91505
+ migrate Apply idempotent schema migrations
91506
+ redact-comments Preview historical comment redaction (dry-run by default)
91507
+
91126
91508
  Options:
91127
91509
  --port <port> HTTP port to bind. Defaults to ${DEFAULT_PORT}
91128
91510
  --host <host> Hostname to bind. Defaults to 127.0.0.1
91129
91511
  --api-key <key> Require this API key for dashboard/API requests
91130
91512
  --no-open Do not open the dashboard in a browser
91513
+ --batch-size <n> redact-comments batch size, 1-500 (default: 100)
91514
+ --apply Apply redact-comments changes (default is dry-run)
91515
+ --confirm <value> Explicit confirmation required with --apply
91516
+ --json Emit redact-comments aggregate JSON
91131
91517
  -V, --version output the version number
91132
91518
  -h, --help display help for command
91133
91519
 
@@ -91166,7 +91552,14 @@ async function findFreePort(start) {
91166
91552
  return start;
91167
91553
  }
91168
91554
  async function runMigrate() {
91169
- const { ensureCloudSchema: ensureCloudSchema2, normalizeCloudPayloads: normalizeCloudPayloads2, pingCloud: pingCloud2, resolveCloudDatabaseUrl: resolveCloudDatabaseUrl2, closeCloud: closeCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
91555
+ const {
91556
+ ensureCloudSchema: ensureCloudSchema2,
91557
+ ensureCloudCommentCursorIndex: ensureCloudCommentCursorIndex2,
91558
+ normalizeCloudPayloads: normalizeCloudPayloads2,
91559
+ pingCloud: pingCloud2,
91560
+ resolveCloudDatabaseUrl: resolveCloudDatabaseUrl2,
91561
+ closeCloud: closeCloud2
91562
+ } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
91170
91563
  if (!resolveCloudDatabaseUrl2()) {
91171
91564
  console.error("migrate: no database URL (HASNA_TODOS_DATABASE_URL / TODOS_DATABASE_URL / DATABASE_URL)");
91172
91565
  process.exit(2);
@@ -91178,15 +91571,55 @@ async function runMigrate() {
91178
91571
  console.log("migrate: normalizing legacy double-encoded jsonb payloads\u2026");
91179
91572
  const normalized = await normalizeCloudPayloads2();
91180
91573
  console.log(`migrate: normalized ${normalized} payload row(s)`);
91574
+ console.log("migrate: prebuilding comment cursor index concurrently\u2026");
91575
+ await ensureCloudCommentCursorIndex2();
91181
91576
  console.log("migrate: done");
91182
91577
  await closeCloud2();
91183
91578
  process.exit(0);
91184
91579
  }
91185
- async function main2() {
91186
- if (process.argv.includes("migrate")) {
91187
- await runMigrate();
91188
- return;
91580
+ async function runCommentRedactionBackfill() {
91581
+ const {
91582
+ backfillCloudCommentRedaction: backfillCloudCommentRedaction2,
91583
+ resolveCloudDatabaseUrl: resolveCloudDatabaseUrl2,
91584
+ closeCloud: closeCloud2
91585
+ } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
91586
+ const {
91587
+ COMMENT_REDACTION_BACKFILL_CONFIRMATION: COMMENT_REDACTION_BACKFILL_CONFIRMATION2,
91588
+ isCommentRedactionBackfillComplete: isCommentRedactionBackfillComplete2
91589
+ } = await Promise.resolve().then(() => (init_comment_redaction_backfill(), exports_comment_redaction_backfill));
91590
+ if (!resolveCloudDatabaseUrl2()) {
91591
+ console.error("redact-comments: no database URL (HASNA_TODOS_DATABASE_URL / TODOS_DATABASE_URL / DATABASE_URL)");
91592
+ process.exit(2);
91189
91593
  }
91594
+ const apply = process.argv.includes("--apply");
91595
+ const rawBatchSize = parseStringArg("--batch-size");
91596
+ try {
91597
+ const report = await backfillCloudCommentRedaction2({
91598
+ apply,
91599
+ confirmation: parseStringArg("--confirm"),
91600
+ batchSize: rawBatchSize === undefined ? 100 : Number(rawBatchSize)
91601
+ });
91602
+ if (process.argv.includes("--json")) {
91603
+ console.log(JSON.stringify(report));
91604
+ } else {
91605
+ console.log(`redact-comments: ${report.dry_run ? "dry-run" : "applied"}; scanned=${report.scanned} candidates=${report.candidates} updated=${report.updated} conflicts=${report.conflicts} remaining=${report.remaining_candidates} batches=${report.batches}`);
91606
+ if (report.dry_run && report.candidates > 0) {
91607
+ console.log(`redact-comments: obtain approval before using --apply --confirm=${COMMENT_REDACTION_BACKFILL_CONFIRMATION2}`);
91608
+ }
91609
+ }
91610
+ if (apply && !isCommentRedactionBackfillComplete2(report)) {
91611
+ console.error("redact-comments: incomplete apply; resolve conflicts and rerun until conflicts=0 and remaining=0");
91612
+ process.exitCode = 1;
91613
+ }
91614
+ } catch (error3) {
91615
+ const message = error3.message.replace(/postgres(?:ql)?:\/\/[^@\s]+@/gi, "postgresql://[REDACTED]@");
91616
+ console.error(`redact-comments: failed: ${message}`);
91617
+ process.exitCode = 1;
91618
+ } finally {
91619
+ await closeCloud2();
91620
+ }
91621
+ }
91622
+ async function main2() {
91190
91623
  if (hasVersionFlag2()) {
91191
91624
  console.log(getPackageVersion());
91192
91625
  return;
@@ -91195,6 +91628,14 @@ async function main2() {
91195
91628
  printHelp2();
91196
91629
  return;
91197
91630
  }
91631
+ if (process.argv.includes("migrate")) {
91632
+ await runMigrate();
91633
+ return;
91634
+ }
91635
+ if (process.argv.includes("redact-comments")) {
91636
+ await runCommentRedactionBackfill();
91637
+ return;
91638
+ }
91198
91639
  const explicitPortArg = process.argv.some((a) => a === "--port" || a.startsWith("--port="));
91199
91640
  const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
91200
91641
  const requestedPort = explicitPortArg ? parsePort() : envPort ?? parsePort();