@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
package/dist/cli/index.js CHANGED
@@ -1011,7 +1011,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1011
1011
  this._exitCallback = (err) => {
1012
1012
  if (err.code !== "commander.executeSubCommandAsync") {
1013
1013
  throw err;
1014
- } else {}
1014
+ }
1015
1015
  };
1016
1016
  }
1017
1017
  return this;
@@ -8478,7 +8478,312 @@ var init_storage = __esm(() => {
8478
8478
  IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
8479
8479
  });
8480
8480
 
8481
+ // src/lib/sync-utils.ts
8482
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "fs";
8483
+ import { join as join2 } from "path";
8484
+ function getHomeDir() {
8485
+ return process.env["HOME"] || process.env["USERPROFILE"] || "~";
8486
+ }
8487
+ function getTodosGlobalDir() {
8488
+ return join2(getHomeDir(), ".hasna", "todos");
8489
+ }
8490
+ function ensureDir(dir) {
8491
+ if (!existsSync2(dir))
8492
+ mkdirSync(dir, { recursive: true });
8493
+ }
8494
+ function listJsonFiles(dir) {
8495
+ if (!existsSync2(dir))
8496
+ return [];
8497
+ return readdirSync(dir).filter((f) => f.endsWith(".json"));
8498
+ }
8499
+ function readJsonFile(path) {
8500
+ try {
8501
+ return JSON.parse(readFileSync2(path, "utf-8"));
8502
+ } catch {
8503
+ return null;
8504
+ }
8505
+ }
8506
+ function writeJsonFile(path, data) {
8507
+ writeFileSync(path, JSON.stringify(data, null, 2) + `
8508
+ `);
8509
+ }
8510
+ function readHighWaterMark(dir) {
8511
+ const path = join2(dir, ".highwatermark");
8512
+ if (!existsSync2(path))
8513
+ return 1;
8514
+ const val = parseInt(readFileSync2(path, "utf-8").trim(), 10);
8515
+ return isNaN(val) ? 1 : val;
8516
+ }
8517
+ function writeHighWaterMark(dir, value) {
8518
+ writeFileSync(join2(dir, ".highwatermark"), String(value));
8519
+ }
8520
+ function getFileMtimeMs(path) {
8521
+ try {
8522
+ return statSync(path).mtimeMs;
8523
+ } catch {
8524
+ return null;
8525
+ }
8526
+ }
8527
+ function parseTimestamp(value) {
8528
+ if (typeof value !== "string")
8529
+ return null;
8530
+ const parsed = Date.parse(value);
8531
+ return Number.isNaN(parsed) ? null : parsed;
8532
+ }
8533
+ function appendSyncConflict(metadata, conflict, limit = 5) {
8534
+ const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
8535
+ const next = [conflict, ...current].slice(0, limit);
8536
+ return { ...metadata, sync_conflicts: next };
8537
+ }
8538
+ var HOME;
8539
+ var init_sync_utils = __esm(() => {
8540
+ HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
8541
+ });
8542
+
8543
+ // src/lib/config.ts
8544
+ var exports_config = {};
8545
+ __export(exports_config, {
8546
+ updateConfig: () => updateConfig,
8547
+ saveConfig: () => saveConfig,
8548
+ resetConfig: () => resetConfig,
8549
+ normalizeApiUrl: () => normalizeApiUrl,
8550
+ loadConfig: () => loadConfig,
8551
+ getTaskPrefixConfig: () => getTaskPrefixConfig,
8552
+ getSyncAgentsFromConfig: () => getSyncAgentsFromConfig,
8553
+ getLocalApiConfig: () => getLocalApiConfig,
8554
+ getConfigPath: () => getConfigPath,
8555
+ getCompletionGuardConfig: () => getCompletionGuardConfig,
8556
+ getAgentTasksDir: () => getAgentTasksDir,
8557
+ getAgentTaskListId: () => getAgentTaskListId,
8558
+ getAgentPoolForProject: () => getAgentPoolForProject
8559
+ });
8560
+ import { existsSync as existsSync3 } from "fs";
8561
+ import { dirname as dirname2, join as join3 } from "path";
8562
+ function getConfigPath() {
8563
+ return join3(getTodosGlobalDir(), "config.json");
8564
+ }
8565
+ function resetConfig() {
8566
+ cached = null;
8567
+ }
8568
+ function normalizeAgent(agent) {
8569
+ return agent.trim().toLowerCase();
8570
+ }
8571
+ function loadConfig() {
8572
+ if (cached)
8573
+ return cached;
8574
+ if (!existsSync3(getConfigPath())) {
8575
+ cached = {};
8576
+ return cached;
8577
+ }
8578
+ const config = readJsonFile(getConfigPath()) || {};
8579
+ if (typeof config.sync_agents === "string") {
8580
+ config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
8581
+ }
8582
+ cached = config;
8583
+ return cached;
8584
+ }
8585
+ function saveConfig(config) {
8586
+ const configPath = getConfigPath();
8587
+ ensureDir(dirname2(configPath));
8588
+ writeJsonFile(configPath, config);
8589
+ cached = config;
8590
+ return config;
8591
+ }
8592
+ function updateConfig(patch) {
8593
+ return saveConfig({ ...loadConfig(), ...patch });
8594
+ }
8595
+ function normalizeApiUrl(value) {
8596
+ const trimmed = value?.trim();
8597
+ if (!trimmed)
8598
+ return null;
8599
+ return trimmed.replace(/\/+$/, "");
8600
+ }
8601
+ function getLocalApiConfig(env = process.env) {
8602
+ const config = loadConfig();
8603
+ const envApiUrl = normalizeApiUrl(env["TODOS_URL"]);
8604
+ const configApiUrl = normalizeApiUrl(config.apiUrl);
8605
+ const apiUrl = envApiUrl ?? configApiUrl;
8606
+ const apiKey = env["TODOS_API_KEY"] || config.apiKey || null;
8607
+ return {
8608
+ apiUrl,
8609
+ apiKey,
8610
+ source: {
8611
+ apiUrl: envApiUrl ? "TODOS_URL" : configApiUrl ? "config" : "none",
8612
+ apiKey: env["TODOS_API_KEY"] ? "TODOS_API_KEY" : config.apiKey ? "config" : "none"
8613
+ }
8614
+ };
8615
+ }
8616
+ function getSyncAgentsFromConfig() {
8617
+ const config = loadConfig();
8618
+ const agents = config.sync_agents;
8619
+ if (Array.isArray(agents) && agents.length > 0)
8620
+ return agents.map(normalizeAgent);
8621
+ return null;
8622
+ }
8623
+ function getAgentTaskListId(agent) {
8624
+ const config = loadConfig();
8625
+ const key = normalizeAgent(agent);
8626
+ return config.agents?.[key]?.task_list_id || config.task_list_id || null;
8627
+ }
8628
+ function getAgentTasksDir(agent) {
8629
+ const config = loadConfig();
8630
+ const key = normalizeAgent(agent);
8631
+ return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
8632
+ }
8633
+ function getTaskPrefixConfig() {
8634
+ const config = loadConfig();
8635
+ return config.task_prefix || null;
8636
+ }
8637
+ function getAgentPoolForProject(workingDir) {
8638
+ const config = loadConfig();
8639
+ if (workingDir && config.project_pools) {
8640
+ let bestKey = null;
8641
+ let bestLen = 0;
8642
+ for (const key of Object.keys(config.project_pools)) {
8643
+ if (workingDir.startsWith(key) && key.length > bestLen) {
8644
+ bestKey = key;
8645
+ bestLen = key.length;
8646
+ }
8647
+ }
8648
+ if (bestKey && config.project_pools[bestKey]) {
8649
+ return config.project_pools[bestKey];
8650
+ }
8651
+ }
8652
+ return config.agent_pool || null;
8653
+ }
8654
+ function getCompletionGuardConfig(projectPath) {
8655
+ const config = loadConfig();
8656
+ const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
8657
+ if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
8658
+ return { ...global, ...config.project_overrides[projectPath].completion_guard };
8659
+ }
8660
+ return global;
8661
+ }
8662
+ var cached = null, GUARD_DEFAULTS;
8663
+ var init_config = __esm(() => {
8664
+ init_sync_utils();
8665
+ GUARD_DEFAULTS = {
8666
+ enabled: false,
8667
+ min_work_seconds: 30,
8668
+ max_completions_per_window: 5,
8669
+ window_minutes: 10,
8670
+ cooldown_seconds: 60
8671
+ };
8672
+ });
8673
+
8674
+ // src/lib/redaction.ts
8675
+ var exports_redaction = {};
8676
+ __export(exports_redaction, {
8677
+ upsertSecretSafetyConfig: () => upsertSecretSafetyConfig,
8678
+ redactValue: () => redactValue,
8679
+ redactEvidenceText: () => redactEvidenceText,
8680
+ listSecretFindings: () => listSecretFindings,
8681
+ hasSecretFindings: () => hasSecretFindings,
8682
+ getSecretSafetyConfig: () => getSecretSafetyConfig
8683
+ });
8684
+ function unique(values) {
8685
+ return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
8686
+ }
8687
+ function cloneRegex(regex) {
8688
+ return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
8689
+ }
8690
+ function customPatterns() {
8691
+ return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
8692
+ try {
8693
+ return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
8694
+ } catch {
8695
+ return [];
8696
+ }
8697
+ });
8698
+ }
8699
+ function secretPatterns() {
8700
+ return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
8701
+ }
8702
+ function isSecretKey(key) {
8703
+ if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
8704
+ return false;
8705
+ if (DEFAULT_SECRET_KEY_PATTERN.test(key))
8706
+ return true;
8707
+ return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
8708
+ }
8709
+ function redactEvidenceText(value) {
8710
+ let redacted = value;
8711
+ for (const pattern of secretPatterns()) {
8712
+ const regex = cloneRegex(pattern.regex);
8713
+ const replacement = pattern.replacement ?? "[REDACTED]";
8714
+ redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
8715
+ }
8716
+ return redacted;
8717
+ }
8718
+ function redactValue(value) {
8719
+ if (typeof value === "string")
8720
+ return redactEvidenceText(value);
8721
+ if (Array.isArray(value))
8722
+ return value.map(redactValue);
8723
+ if (value && typeof value === "object") {
8724
+ const redacted = {};
8725
+ for (const [key, child] of Object.entries(value)) {
8726
+ if (isSecretKey(key)) {
8727
+ redacted[key] = "[REDACTED]";
8728
+ } else {
8729
+ redacted[key] = redactValue(child);
8730
+ }
8731
+ }
8732
+ return redacted;
8733
+ }
8734
+ return value;
8735
+ }
8736
+ function listSecretFindings(value) {
8737
+ const findings = [];
8738
+ for (const pattern of secretPatterns()) {
8739
+ const matches = value.match(cloneRegex(pattern.regex));
8740
+ if (matches?.length)
8741
+ findings.push({ pattern: pattern.name, count: matches.length });
8742
+ }
8743
+ return findings;
8744
+ }
8745
+ function hasSecretFindings(value) {
8746
+ return listSecretFindings(value).length > 0;
8747
+ }
8748
+ function getSecretSafetyConfig() {
8749
+ return {
8750
+ redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
8751
+ redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
8752
+ };
8753
+ }
8754
+ function upsertSecretSafetyConfig(input) {
8755
+ const config = loadConfig();
8756
+ const next = {
8757
+ redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
8758
+ redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
8759
+ };
8760
+ saveConfig({ ...config, secret_safety: next });
8761
+ return next;
8762
+ }
8763
+ var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
8764
+ var init_redaction = __esm(() => {
8765
+ init_config();
8766
+ DEFAULT_SECRET_PATTERNS = [
8767
+ { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
8768
+ { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
8769
+ { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
8770
+ { 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]" },
8771
+ { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
8772
+ ];
8773
+ DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
8774
+ NON_SECRET_USAGE_KEYS = new Set([
8775
+ "tokens",
8776
+ "total_tokens",
8777
+ "token_count",
8778
+ "input_tokens",
8779
+ "output_tokens",
8780
+ "prompt_tokens",
8781
+ "completion_tokens"
8782
+ ]);
8783
+ });
8784
+
8481
8785
  // src/cli/cloud-router.ts
8786
+ import { resolve as resolvePath } from "path";
8482
8787
  function getTodosCloudClient(env = process.env) {
8483
8788
  if (_cache !== undefined)
8484
8789
  return _cache.client;
@@ -8503,14 +8808,18 @@ function unwrapTask(raw) {
8503
8808
  }
8504
8809
  function toListQuery(filter = {}) {
8505
8810
  const query = {};
8506
- if (typeof filter.status === "string")
8507
- query["status"] = filter.status;
8508
- if (typeof filter.priority === "string")
8509
- query["priority"] = filter.priority;
8811
+ if (filter.status)
8812
+ query["status"] = Array.isArray(filter.status) ? filter.status.join(",") : filter.status;
8813
+ if (filter.priority)
8814
+ query["priority"] = Array.isArray(filter.priority) ? filter.priority.join(",") : filter.priority;
8510
8815
  if (filter.project_id)
8511
8816
  query["project_id"] = filter.project_id;
8817
+ if (filter.parent_id !== undefined)
8818
+ query["parent_id"] = filter.parent_id ?? "";
8512
8819
  if (filter.plan_id)
8513
8820
  query["plan_id"] = filter.plan_id;
8821
+ if (filter.task_list_id)
8822
+ query["task_list_id"] = filter.task_list_id;
8514
8823
  if (filter.assigned_to)
8515
8824
  query["assigned_to"] = filter.assigned_to;
8516
8825
  if (filter.agent_id)
@@ -8558,6 +8867,37 @@ async function cloudListProjects(client) {
8558
8867
  const envelope = res.raw;
8559
8868
  return Array.isArray(envelope?.projects) ? envelope.projects : res.items;
8560
8869
  }
8870
+ function cloudProjectSlug(value) {
8871
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
8872
+ }
8873
+ function cloudProjectPathBasename(value) {
8874
+ return value.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? value;
8875
+ }
8876
+ function uniqueProjectMatches(projects, predicate) {
8877
+ return [...new Map(projects.filter(predicate).map((project) => [project.id, project])).values()];
8878
+ }
8879
+ async function cloudResolveProjectRef(client, ref) {
8880
+ const input = ref.trim();
8881
+ const projects = await cloudListProjects(client);
8882
+ const normalizedRef = input.toLowerCase();
8883
+ const pathLike = input.startsWith(".") || input.includes("/") || input.includes("\\");
8884
+ const normalizedPath = pathLike ? resolvePath(input) : undefined;
8885
+ const slug = cloudProjectSlug(pathLike ? cloudProjectPathBasename(input) : input);
8886
+ const matchGroups = [
8887
+ uniqueProjectMatches(projects, (project) => project.id.toLowerCase() === normalizedRef),
8888
+ uniqueProjectMatches(projects, (project) => project.path === input || normalizedPath !== undefined && resolvePath(project.path) === normalizedPath),
8889
+ uniqueProjectMatches(projects, (project) => project.name.toLowerCase() === normalizedRef),
8890
+ uniqueProjectMatches(projects, (project) => project.task_list_id === input || cloudProjectSlug(project.name) === slug || cloudProjectSlug(cloudProjectPathBasename(project.path)) === slug),
8891
+ uniqueProjectMatches(projects, (project) => project.id.toLowerCase().startsWith(normalizedRef))
8892
+ ];
8893
+ for (const matches of matchGroups) {
8894
+ if (matches.length === 1)
8895
+ return matches[0].id;
8896
+ if (matches.length > 1)
8897
+ throw new Error(`Project reference is ambiguous: "${input}"`);
8898
+ }
8899
+ throw new Error(`Project not found: "${input}"`);
8900
+ }
8561
8901
  async function cloudListPlans(client, projectId) {
8562
8902
  const query = projectId ? { project_id: projectId } : {};
8563
8903
  const res = await client.list("plans", { query });
@@ -8566,10 +8906,82 @@ async function cloudListPlans(client, projectId) {
8566
8906
  }
8567
8907
  async function cloudAddComment(client, taskId, input) {
8568
8908
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/comments`, input);
8569
- if (raw && typeof raw === "object" && "comment" in raw) {
8570
- return raw.comment;
8909
+ const comment = raw && typeof raw === "object" && "comment" in raw ? raw.comment : raw;
8910
+ if (!isTaskComment(comment))
8911
+ throw new Error("Invalid cloud comment response");
8912
+ return redactComment(comment);
8913
+ }
8914
+ async function cloudListComments(client, taskId, options = {}) {
8915
+ const limit = options.limit ?? 100;
8916
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
8917
+ throw new Error("Cloud comment limit must be an integer between 1 and 500");
8571
8918
  }
8572
- return raw;
8919
+ if (options.cursor !== undefined && (typeof options.cursor !== "string" || !options.cursor || options.cursor.length > 1024)) {
8920
+ throw new Error("Cloud comment cursor must be a non-empty string");
8921
+ }
8922
+ let raw;
8923
+ try {
8924
+ raw = await client.transport.get(`/tasks/${encodeURIComponent(taskId)}/comments`, {
8925
+ query: { limit, ...options.cursor ? { cursor: options.cursor } : {} }
8926
+ });
8927
+ } catch (error) {
8928
+ const status = error && typeof error === "object" ? error.status : undefined;
8929
+ if (status === 404 || status === 405) {
8930
+ throw new Error("Cloud task comments require a compatible @hasna/todos server; deploy the server endpoint before this CLI.", { cause: error });
8931
+ }
8932
+ throw error;
8933
+ }
8934
+ const envelope = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
8935
+ const candidate = Array.isArray(raw) ? raw : envelope?.comments;
8936
+ if (!Array.isArray(candidate) || !candidate.every(isTaskComment)) {
8937
+ throw new Error("Invalid cloud comments response");
8938
+ }
8939
+ if (envelope?.count !== undefined && (!Number.isSafeInteger(envelope.count) || envelope.count < 0 || envelope.count !== candidate.length)) {
8940
+ throw new Error("Invalid cloud comments response count");
8941
+ }
8942
+ const hasHasMore = envelope ? Object.prototype.hasOwnProperty.call(envelope, "has_more") : false;
8943
+ const hasNextCursor = envelope ? Object.prototype.hasOwnProperty.call(envelope, "next_cursor") : false;
8944
+ if (hasHasMore !== hasNextCursor)
8945
+ throw new Error("Invalid cloud comments pagination response");
8946
+ const paginationSupported = hasHasMore && hasNextCursor;
8947
+ if (!paginationSupported) {
8948
+ const comments = candidate.slice(-limit).map(redactComment);
8949
+ return {
8950
+ comments,
8951
+ count: comments.length,
8952
+ has_more: candidate.length > limit,
8953
+ next_cursor: null,
8954
+ limit,
8955
+ pagination_supported: false
8956
+ };
8957
+ }
8958
+ if (candidate.length > limit)
8959
+ throw new Error("Invalid cloud comments response: page exceeds requested limit");
8960
+ const hasMore = envelope.has_more;
8961
+ const nextCursor = envelope.next_cursor;
8962
+ if (typeof hasMore !== "boolean" || nextCursor !== null && (typeof nextCursor !== "string" || !nextCursor)) {
8963
+ throw new Error("Invalid cloud comments pagination response");
8964
+ }
8965
+ if (hasMore && nextCursor === null || !hasMore && nextCursor !== null) {
8966
+ throw new Error("Invalid cloud comments pagination response");
8967
+ }
8968
+ return {
8969
+ comments: candidate.map(redactComment),
8970
+ count: candidate.length,
8971
+ has_more: hasMore,
8972
+ next_cursor: nextCursor,
8973
+ limit,
8974
+ pagination_supported: true
8975
+ };
8976
+ }
8977
+ function isTaskComment(value) {
8978
+ if (!value || typeof value !== "object" || Array.isArray(value))
8979
+ return false;
8980
+ const comment = value;
8981
+ 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";
8982
+ }
8983
+ function redactComment(comment) {
8984
+ return { ...comment, content: redactEvidenceText(comment.content) };
8573
8985
  }
8574
8986
  async function cloudTaskHistory(client, taskId) {
8575
8987
  const raw = await client.transport.get(`/tasks/${encodeURIComponent(taskId)}/history`);
@@ -8657,8 +9069,8 @@ async function cloudLockTask(client, id, agentId) {
8657
9069
  }
8658
9070
  return raw ?? { success: true };
8659
9071
  }
8660
- async function cloudUnlockTask(client, id, agentId) {
8661
- const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/unlock`, agentId ? { agent_id: agentId } : {});
9072
+ async function cloudUnlockTask(client, id, agentId, force = false) {
9073
+ const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/unlock`, { ...agentId ? { agent_id: agentId } : {}, ...force ? { force: true } : {} });
8662
9074
  if (raw && typeof raw === "object" && "success" in raw) {
8663
9075
  return Boolean(raw.success);
8664
9076
  }
@@ -8787,6 +9199,43 @@ async function cloudListTaskLists(client, projectId) {
8787
9199
  return envelope.taskLists;
8788
9200
  return Array.isArray(raw) ? raw : [];
8789
9201
  }
9202
+ async function cloudResolveTaskListRef(client, ref, projectId) {
9203
+ const input = ref.trim();
9204
+ const normalizedIdRef = input.toLowerCase();
9205
+ if (UUID_RE.test(input) && !projectId)
9206
+ return normalizedIdRef;
9207
+ const lists = await cloudListTaskLists(client, projectId);
9208
+ const exactIds = lists.filter((list) => list.id.toLowerCase() === normalizedIdRef);
9209
+ if (exactIds.length === 1)
9210
+ return exactIds[0].id;
9211
+ if (exactIds.length > 1) {
9212
+ throw new Error(`Task list reference is ambiguous: "${input}"`);
9213
+ }
9214
+ const slugs = lists.filter((list) => list.slug === input);
9215
+ if (slugs.length === 1)
9216
+ return slugs[0].id;
9217
+ if (slugs.length > 1) {
9218
+ throw new Error(`Task list reference is ambiguous: "${input}"`);
9219
+ }
9220
+ const prefixes = lists.filter((list) => list.id.toLowerCase().startsWith(normalizedIdRef));
9221
+ if (prefixes.length === 1)
9222
+ return prefixes[0].id;
9223
+ if (prefixes.length > 1) {
9224
+ throw new Error(`Task list reference is ambiguous: "${input}"`);
9225
+ }
9226
+ throw new Error(`Task list not found: "${input}"`);
9227
+ }
9228
+ async function cloudCreateTaskList(client, input) {
9229
+ const raw = await client.transport.post("/task-lists", input);
9230
+ if (raw && typeof raw === "object" && "task_list" in raw) {
9231
+ return raw.task_list;
9232
+ }
9233
+ return raw;
9234
+ }
9235
+ async function cloudDeleteTaskList(client, id) {
9236
+ await client.delete("task-lists", id);
9237
+ return true;
9238
+ }
8790
9239
  async function cloudNextTask(client, agent, filters) {
8791
9240
  const query = {};
8792
9241
  if (agent)
@@ -8811,11 +9260,11 @@ async function cloudAllDependencies(client) {
8811
9260
  return Array.isArray(raw) ? raw : [];
8812
9261
  }
8813
9262
  async function cloudGetTasksByIds(client, ids) {
8814
- const unique = Array.from(new Set(ids));
9263
+ const unique2 = Array.from(new Set(ids));
8815
9264
  const map = new Map;
8816
9265
  const CONCURRENCY = 8;
8817
- for (let i = 0;i < unique.length; i += CONCURRENCY) {
8818
- const batch = unique.slice(i, i + CONCURRENCY);
9266
+ for (let i = 0;i < unique2.length; i += CONCURRENCY) {
9267
+ const batch = unique2.slice(i, i + CONCURRENCY);
8819
9268
  const tasks = await Promise.all(batch.map((id) => cloudGetTask(client, id)));
8820
9269
  for (const task of tasks)
8821
9270
  if (task && task.id)
@@ -8910,9 +9359,11 @@ async function cloudTimeline(client, options = {}) {
8910
9359
  const limit = options.limit ?? 50;
8911
9360
  return { entries: entries.slice(offset, offset + limit), total, limit, offset };
8912
9361
  }
8913
- var _cache, PRIORITY_RANK;
9362
+ var UUID_RE, _cache, PRIORITY_RANK;
8914
9363
  var init_cloud_router = __esm(() => {
8915
9364
  init_storage();
9365
+ init_redaction();
9366
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8916
9367
  PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
8917
9368
  });
8918
9369
 
@@ -11215,7 +11666,7 @@ var init_schema = __esm(() => {
11215
11666
  });
11216
11667
 
11217
11668
  // src/db/machines.ts
11218
- import { existsSync as existsSync2 } from "fs";
11669
+ import { existsSync as existsSync4 } from "fs";
11219
11670
  import { hostname as osHostname, platform as osPlatform, arch as osArch } from "os";
11220
11671
  import { resolve } from "path";
11221
11672
  import { spawnSync } from "child_process";
@@ -11422,7 +11873,7 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
11422
11873
  message: `${project.name} has ${distinctPaths.length} different machine-local paths`
11423
11874
  });
11424
11875
  }
11425
- if (localRow && !existsSync2(localRow.path)) {
11876
+ if (localRow && !existsSync4(localRow.path)) {
11426
11877
  pathIssues.push({
11427
11878
  type: "path_missing",
11428
11879
  project_id: project.id,
@@ -11433,7 +11884,7 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
11433
11884
  message: `Local path does not exist on this machine: ${localRow.path}`
11434
11885
  });
11435
11886
  }
11436
- if (!localRow && project.path && machineById.has(localMachine.id) && !existsSync2(project.path)) {
11887
+ if (!localRow && project.path && machineById.has(localMachine.id) && !existsSync4(project.path)) {
11437
11888
  pathIssues.push({
11438
11889
  type: "path_missing",
11439
11890
  project_id: project.id,
@@ -11541,8 +11992,8 @@ var init_machines = __esm(() => {
11541
11992
  });
11542
11993
 
11543
11994
  // src/storage/config.ts
11544
- var exports_config = {};
11545
- __export(exports_config, {
11995
+ var exports_config2 = {};
11996
+ __export(exports_config2, {
11546
11997
  parseStorageMode: () => parseStorageMode,
11547
11998
  loadTodosStorageConfig: () => loadTodosStorageConfig,
11548
11999
  loadStorageConfig: () => loadStorageConfig,
@@ -11705,7 +12156,7 @@ function parsePositiveInteger(value, fallback) {
11705
12156
  return parsed;
11706
12157
  }
11707
12158
  var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER = "hasna-xyz-infra-apps-prod-postgres", CANONICAL_TODOS_RDS_DATABASE = "todos", CANONICAL_TODOS_RDS_RUNTIME_PATH = "hasna/xyz/opensource/todos/prod/rds";
11708
- var init_config = __esm(() => {
12159
+ var init_config2 = __esm(() => {
11709
12160
  TODOS_STORAGE_TABLES = [
11710
12161
  "todos_sync_records",
11711
12162
  "todos_sync_cursors"
@@ -11844,8 +12295,8 @@ __export(exports_database, {
11844
12295
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
11845
12296
  });
11846
12297
  import { Database } from "bun:sqlite";
11847
- import { existsSync as existsSync3, mkdirSync } from "fs";
11848
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
12298
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
12299
+ import { dirname as dirname3, join as join4, resolve as resolve2 } from "path";
11849
12300
  function isInMemoryDb(path) {
11850
12301
  return path === ":memory:" || path.startsWith("file::memory:");
11851
12302
  }
@@ -11854,12 +12305,12 @@ function findNearestProjectDb(startDir) {
11854
12305
  const stopAt = gitRoot ? resolve2(gitRoot) : resolve2(startDir);
11855
12306
  let dir = resolve2(startDir);
11856
12307
  while (true) {
11857
- const candidate = join2(dir, ".hasna", "todos", "todos.db");
11858
- if (existsSync3(candidate))
12308
+ const candidate = join4(dir, ".hasna", "todos", "todos.db");
12309
+ if (existsSync5(candidate))
11859
12310
  return candidate;
11860
12311
  if (dir === stopAt)
11861
12312
  break;
11862
- const parent = dirname2(dir);
12313
+ const parent = dirname3(dir);
11863
12314
  if (parent === dir)
11864
12315
  break;
11865
12316
  dir = parent;
@@ -11869,9 +12320,9 @@ function findNearestProjectDb(startDir) {
11869
12320
  function findGitRoot(startDir) {
11870
12321
  let dir = resolve2(startDir);
11871
12322
  while (true) {
11872
- if (existsSync3(join2(dir, ".git")))
12323
+ if (existsSync5(join4(dir, ".git")))
11873
12324
  return dir;
11874
- const parent = dirname2(dir);
12325
+ const parent = dirname3(dir);
11875
12326
  if (parent === dir)
11876
12327
  break;
11877
12328
  dir = parent;
@@ -11892,25 +12343,25 @@ function getDbPath() {
11892
12343
  if (process.env["TODOS_DB_SCOPE"] === "project") {
11893
12344
  const gitRoot = findGitRoot(cwd);
11894
12345
  if (gitRoot) {
11895
- return join2(gitRoot, ".hasna", "todos", "todos.db");
12346
+ return join4(gitRoot, ".hasna", "todos", "todos.db");
11896
12347
  }
11897
12348
  }
11898
12349
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
11899
- return join2(home, ".hasna", "todos", "todos.db");
12350
+ return join4(home, ".hasna", "todos", "todos.db");
11900
12351
  }
11901
12352
  function getDatabasePath() {
11902
12353
  return getDbPath();
11903
12354
  }
11904
- function ensureDir(filePath) {
12355
+ function ensureDir2(filePath) {
11905
12356
  if (isInMemoryDb(filePath))
11906
12357
  return;
11907
- const dir = dirname2(resolve2(filePath));
11908
- if (!existsSync3(dir)) {
11909
- mkdirSync(dir, { recursive: true });
12358
+ const dir = dirname3(resolve2(filePath));
12359
+ if (!existsSync5(dir)) {
12360
+ mkdirSync2(dir, { recursive: true });
11910
12361
  }
11911
12362
  }
11912
12363
  function openDatabase(path) {
11913
- ensureDir(path);
12364
+ ensureDir2(path);
11914
12365
  const db = new Database(path);
11915
12366
  db.run("PRAGMA journal_mode = WAL");
11916
12367
  db.run("PRAGMA busy_timeout = 5000");
@@ -11923,7 +12374,7 @@ function openDatabase(path) {
11923
12374
  }
11924
12375
  function maybeInstallShadowCapture(db) {
11925
12376
  try {
11926
- const { isTodosShadowEnabled: isTodosShadowEnabled2 } = (init_config(), __toCommonJS(exports_config));
12377
+ const { isTodosShadowEnabled: isTodosShadowEnabled2 } = (init_config2(), __toCommonJS(exports_config2));
11927
12378
  if (!isTodosShadowEnabled2())
11928
12379
  return;
11929
12380
  const { installShadowOutboxSchema: installShadowOutboxSchema2 } = (init_shadow_outbox_schema(), __toCommonJS(exports_shadow_outbox_schema));
@@ -12704,199 +13155,6 @@ var init_helpers = __esm(() => {
12704
13155
  };
12705
13156
  });
12706
13157
 
12707
- // src/lib/sync-utils.ts
12708
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "fs";
12709
- import { join as join3 } from "path";
12710
- function getHomeDir() {
12711
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
12712
- }
12713
- function getTodosGlobalDir() {
12714
- return join3(getHomeDir(), ".hasna", "todos");
12715
- }
12716
- function ensureDir2(dir) {
12717
- if (!existsSync4(dir))
12718
- mkdirSync2(dir, { recursive: true });
12719
- }
12720
- function listJsonFiles(dir) {
12721
- if (!existsSync4(dir))
12722
- return [];
12723
- return readdirSync(dir).filter((f) => f.endsWith(".json"));
12724
- }
12725
- function readJsonFile(path) {
12726
- try {
12727
- return JSON.parse(readFileSync2(path, "utf-8"));
12728
- } catch {
12729
- return null;
12730
- }
12731
- }
12732
- function writeJsonFile(path, data) {
12733
- writeFileSync(path, JSON.stringify(data, null, 2) + `
12734
- `);
12735
- }
12736
- function readHighWaterMark(dir) {
12737
- const path = join3(dir, ".highwatermark");
12738
- if (!existsSync4(path))
12739
- return 1;
12740
- const val = parseInt(readFileSync2(path, "utf-8").trim(), 10);
12741
- return isNaN(val) ? 1 : val;
12742
- }
12743
- function writeHighWaterMark(dir, value) {
12744
- writeFileSync(join3(dir, ".highwatermark"), String(value));
12745
- }
12746
- function getFileMtimeMs(path) {
12747
- try {
12748
- return statSync(path).mtimeMs;
12749
- } catch {
12750
- return null;
12751
- }
12752
- }
12753
- function parseTimestamp(value) {
12754
- if (typeof value !== "string")
12755
- return null;
12756
- const parsed = Date.parse(value);
12757
- return Number.isNaN(parsed) ? null : parsed;
12758
- }
12759
- function appendSyncConflict(metadata, conflict, limit = 5) {
12760
- const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
12761
- const next = [conflict, ...current].slice(0, limit);
12762
- return { ...metadata, sync_conflicts: next };
12763
- }
12764
- var HOME;
12765
- var init_sync_utils = __esm(() => {
12766
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
12767
- });
12768
-
12769
- // src/lib/config.ts
12770
- var exports_config2 = {};
12771
- __export(exports_config2, {
12772
- updateConfig: () => updateConfig,
12773
- saveConfig: () => saveConfig,
12774
- resetConfig: () => resetConfig,
12775
- normalizeApiUrl: () => normalizeApiUrl,
12776
- loadConfig: () => loadConfig,
12777
- getTaskPrefixConfig: () => getTaskPrefixConfig,
12778
- getSyncAgentsFromConfig: () => getSyncAgentsFromConfig,
12779
- getLocalApiConfig: () => getLocalApiConfig,
12780
- getConfigPath: () => getConfigPath,
12781
- getCompletionGuardConfig: () => getCompletionGuardConfig,
12782
- getAgentTasksDir: () => getAgentTasksDir,
12783
- getAgentTaskListId: () => getAgentTaskListId,
12784
- getAgentPoolForProject: () => getAgentPoolForProject
12785
- });
12786
- import { existsSync as existsSync5 } from "fs";
12787
- import { dirname as dirname3, join as join4 } from "path";
12788
- function getConfigPath() {
12789
- return join4(getTodosGlobalDir(), "config.json");
12790
- }
12791
- function resetConfig() {
12792
- cached = null;
12793
- }
12794
- function normalizeAgent(agent) {
12795
- return agent.trim().toLowerCase();
12796
- }
12797
- function loadConfig() {
12798
- if (cached)
12799
- return cached;
12800
- if (!existsSync5(getConfigPath())) {
12801
- cached = {};
12802
- return cached;
12803
- }
12804
- const config = readJsonFile(getConfigPath()) || {};
12805
- if (typeof config.sync_agents === "string") {
12806
- config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
12807
- }
12808
- cached = config;
12809
- return cached;
12810
- }
12811
- function saveConfig(config) {
12812
- const configPath = getConfigPath();
12813
- ensureDir2(dirname3(configPath));
12814
- writeJsonFile(configPath, config);
12815
- cached = config;
12816
- return config;
12817
- }
12818
- function updateConfig(patch) {
12819
- return saveConfig({ ...loadConfig(), ...patch });
12820
- }
12821
- function normalizeApiUrl(value) {
12822
- const trimmed = value?.trim();
12823
- if (!trimmed)
12824
- return null;
12825
- return trimmed.replace(/\/+$/, "");
12826
- }
12827
- function getLocalApiConfig(env = process.env) {
12828
- const config = loadConfig();
12829
- const envApiUrl = normalizeApiUrl(env["TODOS_URL"]);
12830
- const configApiUrl = normalizeApiUrl(config.apiUrl);
12831
- const apiUrl = envApiUrl ?? configApiUrl;
12832
- const apiKey = env["TODOS_API_KEY"] || config.apiKey || null;
12833
- return {
12834
- apiUrl,
12835
- apiKey,
12836
- source: {
12837
- apiUrl: envApiUrl ? "TODOS_URL" : configApiUrl ? "config" : "none",
12838
- apiKey: env["TODOS_API_KEY"] ? "TODOS_API_KEY" : config.apiKey ? "config" : "none"
12839
- }
12840
- };
12841
- }
12842
- function getSyncAgentsFromConfig() {
12843
- const config = loadConfig();
12844
- const agents = config.sync_agents;
12845
- if (Array.isArray(agents) && agents.length > 0)
12846
- return agents.map(normalizeAgent);
12847
- return null;
12848
- }
12849
- function getAgentTaskListId(agent) {
12850
- const config = loadConfig();
12851
- const key = normalizeAgent(agent);
12852
- return config.agents?.[key]?.task_list_id || config.task_list_id || null;
12853
- }
12854
- function getAgentTasksDir(agent) {
12855
- const config = loadConfig();
12856
- const key = normalizeAgent(agent);
12857
- return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
12858
- }
12859
- function getTaskPrefixConfig() {
12860
- const config = loadConfig();
12861
- return config.task_prefix || null;
12862
- }
12863
- function getAgentPoolForProject(workingDir) {
12864
- const config = loadConfig();
12865
- if (workingDir && config.project_pools) {
12866
- let bestKey = null;
12867
- let bestLen = 0;
12868
- for (const key of Object.keys(config.project_pools)) {
12869
- if (workingDir.startsWith(key) && key.length > bestLen) {
12870
- bestKey = key;
12871
- bestLen = key.length;
12872
- }
12873
- }
12874
- if (bestKey && config.project_pools[bestKey]) {
12875
- return config.project_pools[bestKey];
12876
- }
12877
- }
12878
- return config.agent_pool || null;
12879
- }
12880
- function getCompletionGuardConfig(projectPath) {
12881
- const config = loadConfig();
12882
- const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
12883
- if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
12884
- return { ...global, ...config.project_overrides[projectPath].completion_guard };
12885
- }
12886
- return global;
12887
- }
12888
- var cached = null, GUARD_DEFAULTS;
12889
- var init_config2 = __esm(() => {
12890
- init_sync_utils();
12891
- GUARD_DEFAULTS = {
12892
- enabled: false,
12893
- min_work_seconds: 30,
12894
- max_completions_per_window: 5,
12895
- window_minutes: 10,
12896
- cooldown_seconds: 60
12897
- };
12898
- });
12899
-
12900
13158
  // src/lib/completion-guard.ts
12901
13159
  function checkCompletionGuard(task, agentId, db, configOverride) {
12902
13160
  let config;
@@ -12942,7 +13200,7 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
12942
13200
  }
12943
13201
  var init_completion_guard = __esm(() => {
12944
13202
  init_types();
12945
- init_config2();
13203
+ init_config();
12946
13204
  init_projects();
12947
13205
  });
12948
13206
 
@@ -12997,117 +13255,6 @@ var init_event_emission_safety = __esm(() => {
12997
13255
  init_sync_utils();
12998
13256
  });
12999
13257
 
13000
- // src/lib/redaction.ts
13001
- var exports_redaction = {};
13002
- __export(exports_redaction, {
13003
- upsertSecretSafetyConfig: () => upsertSecretSafetyConfig,
13004
- redactValue: () => redactValue,
13005
- redactEvidenceText: () => redactEvidenceText,
13006
- listSecretFindings: () => listSecretFindings,
13007
- hasSecretFindings: () => hasSecretFindings,
13008
- getSecretSafetyConfig: () => getSecretSafetyConfig
13009
- });
13010
- function unique(values) {
13011
- return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
13012
- }
13013
- function cloneRegex(regex) {
13014
- return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
13015
- }
13016
- function customPatterns() {
13017
- return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
13018
- try {
13019
- return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
13020
- } catch {
13021
- return [];
13022
- }
13023
- });
13024
- }
13025
- function secretPatterns() {
13026
- return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
13027
- }
13028
- function isSecretKey(key) {
13029
- if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
13030
- return false;
13031
- if (DEFAULT_SECRET_KEY_PATTERN.test(key))
13032
- return true;
13033
- return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
13034
- }
13035
- function redactEvidenceText(value) {
13036
- let redacted = value;
13037
- for (const pattern of secretPatterns()) {
13038
- const regex = cloneRegex(pattern.regex);
13039
- const replacement = pattern.replacement ?? "[REDACTED]";
13040
- redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
13041
- }
13042
- return redacted;
13043
- }
13044
- function redactValue(value) {
13045
- if (typeof value === "string")
13046
- return redactEvidenceText(value);
13047
- if (Array.isArray(value))
13048
- return value.map(redactValue);
13049
- if (value && typeof value === "object") {
13050
- const redacted = {};
13051
- for (const [key, child] of Object.entries(value)) {
13052
- if (isSecretKey(key)) {
13053
- redacted[key] = "[REDACTED]";
13054
- } else {
13055
- redacted[key] = redactValue(child);
13056
- }
13057
- }
13058
- return redacted;
13059
- }
13060
- return value;
13061
- }
13062
- function listSecretFindings(value) {
13063
- const findings = [];
13064
- for (const pattern of secretPatterns()) {
13065
- const matches = value.match(cloneRegex(pattern.regex));
13066
- if (matches?.length)
13067
- findings.push({ pattern: pattern.name, count: matches.length });
13068
- }
13069
- return findings;
13070
- }
13071
- function hasSecretFindings(value) {
13072
- return listSecretFindings(value).length > 0;
13073
- }
13074
- function getSecretSafetyConfig() {
13075
- return {
13076
- redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
13077
- redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
13078
- };
13079
- }
13080
- function upsertSecretSafetyConfig(input) {
13081
- const config = loadConfig();
13082
- const next = {
13083
- redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
13084
- redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
13085
- };
13086
- saveConfig({ ...config, secret_safety: next });
13087
- return next;
13088
- }
13089
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
13090
- var init_redaction = __esm(() => {
13091
- init_config2();
13092
- DEFAULT_SECRET_PATTERNS = [
13093
- { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
13094
- { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
13095
- { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
13096
- { 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]" },
13097
- { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
13098
- ];
13099
- DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
13100
- NON_SECRET_USAGE_KEYS = new Set([
13101
- "tokens",
13102
- "total_tokens",
13103
- "token_count",
13104
- "input_tokens",
13105
- "output_tokens",
13106
- "prompt_tokens",
13107
- "completion_tokens"
13108
- ]);
13109
- });
13110
-
13111
13258
  // src/lib/workspace-trust.ts
13112
13259
  var exports_workspace_trust = {};
13113
13260
  __export(exports_workspace_trust, {
@@ -13253,7 +13400,7 @@ function checkWorkspacePermission(input = {}) {
13253
13400
  }
13254
13401
  var DEFAULT_DENYLIST, DEFAULT_ENV_REDACTIONS, PRESET_DEFAULTS;
13255
13402
  var init_workspace_trust = __esm(() => {
13256
- init_config2();
13403
+ init_config();
13257
13404
  DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
13258
13405
  DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
13259
13406
  PRESET_DEFAULTS = {
@@ -13496,7 +13643,7 @@ function explainRunnerSandbox(input = {}) {
13496
13643
  }
13497
13644
  var DEFAULT_COMMAND_DENYLIST, DEFAULT_ENV_REDACTIONS2;
13498
13645
  var init_runner_sandbox = __esm(() => {
13499
- init_config2();
13646
+ init_config();
13500
13647
  init_workspace_trust();
13501
13648
  DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
13502
13649
  DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
@@ -13743,7 +13890,7 @@ var LOCAL_EVENT_TYPES, VALID_TARGETS;
13743
13890
  var init_event_hooks = __esm(() => {
13744
13891
  init_redaction();
13745
13892
  init_runner_sandbox();
13746
- init_config2();
13893
+ init_config();
13747
13894
  init_event_emission_safety();
13748
13895
  LOCAL_EVENT_TYPES = [
13749
13896
  "task.created",
@@ -18628,7 +18775,7 @@ function getComment(id, db) {
18628
18775
  }
18629
18776
  function listComments(taskId, db) {
18630
18777
  const d = db || getDatabase();
18631
- return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
18778
+ return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at, rowid").all(taskId);
18632
18779
  }
18633
18780
  function updateComment(id, input, db) {
18634
18781
  const d = db || getDatabase();
@@ -20256,10 +20403,27 @@ var init_task_routing = __esm(() => {
20256
20403
  // src/cli/commands/task-commands.ts
20257
20404
  var exports_task_commands = {};
20258
20405
  __export(exports_task_commands, {
20259
- registerTaskCommands: () => registerTaskCommands
20406
+ registerTaskCommands: () => registerTaskCommands,
20407
+ escapeTerminalControls: () => escapeTerminalControls
20260
20408
  });
20261
20409
  import chalk2 from "chalk";
20262
20410
  import { basename as basename3, resolve as resolve9 } from "path";
20411
+ function escapeTerminalControls(value) {
20412
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, (character) => {
20413
+ const code = character.charCodeAt(0);
20414
+ if (code === 10)
20415
+ return "\\n";
20416
+ if (code === 13)
20417
+ return "\\r";
20418
+ if (code === 9)
20419
+ return "\\t";
20420
+ return `\\x${code.toString(16).padStart(2, "0")}`;
20421
+ });
20422
+ }
20423
+ function formatHumanComment(comment) {
20424
+ const agent = comment.agent_id ? chalk2.cyan(`[${escapeTerminalControls(comment.agent_id)}] `) : "";
20425
+ return ` ${agent}${chalk2.dim(escapeTerminalControls(comment.created_at))}: ${escapeTerminalControls(comment.content)}`;
20426
+ }
20263
20427
  function resolveProjectIdOrSlug(input) {
20264
20428
  const db = getDatabase();
20265
20429
  if (isPathLike(input)) {
@@ -20416,18 +20580,24 @@ function registerTaskCommands(program2) {
20416
20580
  if (cloud) {
20417
20581
  let task3;
20418
20582
  try {
20583
+ const cloudProjectId = opts.project || globalOpts.project;
20584
+ const cloudTaskListId = opts.list ? await cloudResolveTaskListRef(cloud, opts.list, cloudProjectId) : undefined;
20585
+ if (opts.list && !cloudTaskListId) {
20586
+ throw new Error(`Could not resolve task list ID or slug: ${opts.list}`);
20587
+ }
20419
20588
  task3 = await cloudCreateTask(cloud, {
20420
20589
  title,
20421
20590
  description: opts.description,
20422
20591
  priority: parsePriority(opts.priority),
20592
+ parent_id: opts.parent,
20423
20593
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
20424
20594
  plan_id: opts.plan,
20425
20595
  assigned_to: opts.assign,
20426
20596
  status: parseStatus(opts.status),
20427
- task_list_id: opts.list,
20597
+ task_list_id: cloudTaskListId,
20428
20598
  agent_id: globalOpts.agent,
20429
20599
  session_id: globalOpts.session,
20430
- project_id: opts.project || globalOpts.project,
20600
+ project_id: cloudProjectId,
20431
20601
  estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
20432
20602
  sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
20433
20603
  requires_approval: opts.approval || undefined,
@@ -20624,12 +20794,12 @@ function registerTaskCommands(program2) {
20624
20794
  console.log(` ${chalk2.dim("Manifest:")} ${state.pointers.latest_manifest_path}`);
20625
20795
  }
20626
20796
  });
20627
- program2.command("list").description("List tasks").option("-s, --status <status>", "Filter by status").option("-p, --priority <priority>", "Filter by priority").option("--assigned <agent>", "Filter by assigned agent").option("--tags <tags>", "Filter by tags (comma-separated)").option("--tag <tags>", "Filter by tags (alias for --tags)").option("-a, --all", "Show all tasks (including completed/cancelled)").option("--list <id>", "Filter by task list ID").option("--task-list <id>", "Filter by task list ID (alias for --list)").option("--project-name <name>", "Filter by project name").option("--agent-name <name>", "Filter by agent name/assigned").option("--sort <field>", "Sort by: updated, created, priority, status").option("--format <fmt>", "Output format: table (default), compact, csv, json").option("--due-today", "Only tasks due today or earlier").option("--overdue", "Only overdue tasks (past due_at)").option("--recurring", "Only recurring tasks").option("--limit <n>", "Max tasks to return").action(async (opts) => {
20797
+ program2.command("list").description("List tasks").option("-s, --status <status>", "Filter by status").option("-p, --priority <priority>", "Filter by priority").option("--assigned <agent>", "Filter by assigned agent").option("--tags <tags>", "Filter by tags (comma-separated)").option("--tag <tags>", "Filter by tags (alias for --tags)").option("-a, --all", "Show all tasks (including completed/cancelled)").option("--list <ref>", "Filter by task list UUID, unique UUID prefix, or project-scoped slug").option("--task-list <ref>", "Filter by task list UUID, unique UUID prefix, or project-scoped slug (alias for --list)").option("--project-name <name>", "Filter by project name").option("--agent-name <name>", "Filter by agent name/assigned").option("--sort <field>", "Sort by: updated, created, priority, status").option("--format <fmt>", "Output format: table (default), compact, csv, json").option("--due-today", "Only tasks due today or earlier").option("--overdue", "Only overdue tasks (past due_at)").option("--recurring", "Only recurring tasks").option("--limit <n>", "Max tasks to return").action(async (opts) => {
20628
20798
  const globalOpts = program2.opts();
20629
20799
  opts.tags = opts.tags || opts.tag;
20630
20800
  opts.list = opts.list || opts.taskList;
20631
20801
  const cloud = getTodosCloudClient();
20632
- const projectId = cloud ? undefined : autoProject(globalOpts);
20802
+ const projectId = cloud && globalOpts.project ? await cloudResolveProjectRef(cloud, globalOpts.project) : cloud ? undefined : autoProject(globalOpts);
20633
20803
  const hasAssignedFilter = Boolean(opts.assigned || opts.agentName);
20634
20804
  const hasExplicitProjectFilter = Boolean(globalOpts.project || opts.projectName);
20635
20805
  const allowedSortFields = new Set(["updated", "created", "priority", "status"]);
@@ -20647,7 +20817,7 @@ function registerTaskCommands(program2) {
20647
20817
  filter["project_id"] = projectId;
20648
20818
  }
20649
20819
  if (opts.list && cloud) {
20650
- filter["task_list_id"] = opts.list;
20820
+ filter["task_list_id"] = await cloudResolveTaskListRef(cloud, opts.list, projectId);
20651
20821
  } else if (opts.list) {
20652
20822
  const db = getDatabase();
20653
20823
  const listId = resolvePartialId(db, "task_lists", opts.list);
@@ -20786,7 +20956,22 @@ function registerTaskCommands(program2) {
20786
20956
  let task2;
20787
20957
  if (cloud) {
20788
20958
  const remote = await cloudGetTask(cloud, resolveTaskId(id));
20789
- task2 = remote ? { subtasks: [], dependencies: [], blocked_by: [], comments: [], ...remote, tags: remote.tags ?? [] } : null;
20959
+ const commentPage = remote ? await cloudListComments(cloud, remote.id) : null;
20960
+ task2 = remote ? {
20961
+ subtasks: [],
20962
+ dependencies: [],
20963
+ blocked_by: [],
20964
+ ...remote,
20965
+ tags: remote.tags ?? [],
20966
+ comments: commentPage.comments,
20967
+ comments_page: {
20968
+ count: commentPage.count,
20969
+ limit: commentPage.limit,
20970
+ has_more: commentPage.has_more,
20971
+ next_cursor: commentPage.next_cursor,
20972
+ pagination_supported: commentPage.pagination_supported
20973
+ }
20974
+ } : null;
20790
20975
  } else {
20791
20976
  const resolvedId = resolveTaskId(id);
20792
20977
  task2 = getTaskWithRelations(resolvedId);
@@ -20870,11 +21055,11 @@ function registerTaskCommands(program2) {
20870
21055
  }
20871
21056
  }
20872
21057
  if (task2.comments.length > 0) {
21058
+ const suffix = task2.comments_page?.has_more ? task2.comments_page.pagination_supported ? ", newer page shown; older comments available" : ", newer comments shown; older comments omitted until the server is upgraded" : "";
20873
21059
  console.log(chalk2.bold(`
20874
- Comments (${task2.comments.length}):`));
21060
+ Comments (${task2.comments.length}${suffix}):`));
20875
21061
  for (const c of task2.comments) {
20876
- const agent = c.agent_id ? chalk2.cyan(`[${c.agent_id}] `) : "";
20877
- console.log(` ${agent}${chalk2.dim(c.created_at)}: ${c.content}`);
21062
+ console.log(formatHumanComment(c));
20878
21063
  }
20879
21064
  }
20880
21065
  });
@@ -20900,7 +21085,23 @@ function registerTaskCommands(program2) {
20900
21085
  let task2;
20901
21086
  if (cloud) {
20902
21087
  const remote = await cloudGetTask(cloud, resolvedId);
20903
- task2 = remote ? { subtasks: [], dependencies: [], blocked_by: [], comments: [], checklist: [], ...remote, tags: remote.tags ?? [] } : null;
21088
+ const commentPage = remote ? await cloudListComments(cloud, remote.id) : null;
21089
+ task2 = remote ? {
21090
+ subtasks: [],
21091
+ dependencies: [],
21092
+ blocked_by: [],
21093
+ checklist: [],
21094
+ ...remote,
21095
+ tags: remote.tags ?? [],
21096
+ comments: commentPage.comments,
21097
+ comments_page: {
21098
+ count: commentPage.count,
21099
+ limit: commentPage.limit,
21100
+ has_more: commentPage.has_more,
21101
+ next_cursor: commentPage.next_cursor,
21102
+ pagination_supported: commentPage.pagination_supported
21103
+ }
21104
+ } : null;
20904
21105
  } else {
20905
21106
  task2 = getTaskWithRelations(resolvedId);
20906
21107
  }
@@ -21007,11 +21208,11 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21007
21208
  console.error(chalk2.dim(`Warning: could not load task commits: ${e instanceof Error ? e.message : String(e)}`));
21008
21209
  }
21009
21210
  if (task2.comments.length > 0) {
21211
+ const suffix = task2.comments_page?.has_more ? task2.comments_page.pagination_supported ? ", newer page shown; older comments available" : ", newer comments shown; older comments omitted until the server is upgraded" : "";
21010
21212
  console.log(chalk2.bold(`
21011
- Comments (${task2.comments.length}):`));
21213
+ Comments (${task2.comments.length}${suffix}):`));
21012
21214
  for (const c of task2.comments) {
21013
- const agent = c.agent_id ? chalk2.cyan(`[${c.agent_id}] `) : "";
21014
- console.log(` ${agent}${chalk2.dim(c.created_at)}: ${c.content}`);
21215
+ console.log(formatHumanComment(c));
21015
21216
  }
21016
21217
  }
21017
21218
  if (task2.checklist && task2.checklist.length > 0) {
@@ -21307,7 +21508,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21307
21508
  const resolvedId = resolveTaskId(id);
21308
21509
  try {
21309
21510
  if (cloud)
21310
- await cloudUnlockTask(cloud, resolvedId, globalOpts.agent);
21511
+ await cloudUnlockTask(cloud, resolvedId, globalOpts.agent, !globalOpts.agent);
21311
21512
  else
21312
21513
  unlockTask(resolvedId, globalOpts.agent);
21313
21514
  } catch (e) {
@@ -23262,7 +23463,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
23262
23463
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
23263
23464
  const dir = getTaskListDir(taskListId);
23264
23465
  if (!existsSync10(dir))
23265
- ensureDir2(dir);
23466
+ ensureDir(dir);
23266
23467
  const filter = {};
23267
23468
  if (projectId)
23268
23469
  filter["project_id"] = projectId;
@@ -23445,7 +23646,7 @@ function syncClaudeTaskList(taskListId, projectId, options = {}) {
23445
23646
  }
23446
23647
  var init_claude_tasks = __esm(() => {
23447
23648
  init_tasks();
23448
- init_config2();
23649
+ init_config();
23449
23650
  init_sync_utils();
23450
23651
  });
23451
23652
 
@@ -23489,7 +23690,7 @@ function metadataKey(agent) {
23489
23690
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
23490
23691
  const dir = getTaskListDir2(agent, taskListId);
23491
23692
  if (!existsSync11(dir))
23492
- ensureDir2(dir);
23693
+ ensureDir(dir);
23493
23694
  const filter = {};
23494
23695
  if (projectId)
23495
23696
  filter["project_id"] = projectId;
@@ -23662,7 +23863,7 @@ function syncAgentTaskList(agent, taskListId, projectId, options = {}) {
23662
23863
  var init_agent_tasks = __esm(() => {
23663
23864
  init_tasks();
23664
23865
  init_sync_utils();
23665
- init_config2();
23866
+ init_config();
23666
23867
  });
23667
23868
 
23668
23869
  // src/lib/sync.ts
@@ -23734,7 +23935,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
23734
23935
  var init_sync = __esm(() => {
23735
23936
  init_claude_tasks();
23736
23937
  init_agent_tasks();
23737
- init_config2();
23938
+ init_config();
23738
23939
  });
23739
23940
 
23740
23941
  // src/lib/project-bootstrap.ts
@@ -31447,7 +31648,7 @@ function applyExportProfile(data, options = {}) {
31447
31648
  }
31448
31649
  var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value", TODOS_ENCRYPTED_BRIDGE_KIND = "hasna.todos.encrypted-bridge", TODOS_ENCRYPTION_SCHEMA_VERSION = 1, DEFAULT_ENCRYPTION_PROFILE = "default", DEFAULT_ENCRYPTION_KEY_ENV = "TODOS_ENCRYPTION_KEY", EncryptionKeyUnavailableError, EncryptedPayloadError;
31449
31650
  var init_local_encryption = __esm(() => {
31450
- init_config2();
31651
+ init_config();
31451
31652
  init_redaction();
31452
31653
  EncryptionKeyUnavailableError = class EncryptionKeyUnavailableError extends Error {
31453
31654
  keyEnv;
@@ -32626,7 +32827,7 @@ var init_project_commands = __esm(() => {
32626
32827
  init_cloud_router();
32627
32828
  init_saved_search_views();
32628
32829
  init_sync();
32629
- init_config2();
32830
+ init_config();
32630
32831
  init_helpers();
32631
32832
  });
32632
32833
 
@@ -33626,9 +33827,11 @@ ${isOnline ? chalk5.green("\u25CF") : chalk5.dim("\u25CB")} ${chalk5.bold(agent.
33626
33827
  program2.command("lists").aliases(["task-lists", "tl"]).description("List and manage task lists").option("--add <name>", "Create a task list").option("--slug <slug>", "Custom slug (with --add)").option("-d, --description <text>", "Description (with --add)").option("--delete <id>", "Delete a task list").action(async (opts) => {
33627
33828
  try {
33628
33829
  const globalOpts = program2.opts();
33629
- const projectId = autoProject(globalOpts);
33830
+ const cloud = getTodosCloudClient();
33831
+ const projectId = cloud ? globalOpts.project : autoProject(globalOpts);
33630
33832
  if (opts.add) {
33631
- const list = createTaskList({ name: opts.add, slug: opts.slug, description: opts.description, project_id: projectId });
33833
+ const input = { name: opts.add, slug: opts.slug, description: opts.description, project_id: projectId };
33834
+ const list = cloud ? await cloudCreateTaskList(cloud, input) : createTaskList(input);
33632
33835
  if (globalOpts.json) {
33633
33836
  output(list, true);
33634
33837
  return;
@@ -33640,6 +33843,14 @@ ${isOnline ? chalk5.green("\u25CF") : chalk5.dim("\u25CB")} ${chalk5.bold(agent.
33640
33843
  return;
33641
33844
  }
33642
33845
  if (opts.delete) {
33846
+ if (cloud) {
33847
+ const resolved2 = await cloudResolveTaskListRef(cloud, opts.delete, projectId ?? undefined);
33848
+ if (!resolved2)
33849
+ throw new Error(`Task list not found or ambiguous: ${opts.delete}`);
33850
+ await cloudDeleteTaskList(cloud, resolved2);
33851
+ console.log(chalk5.green("Task list deleted."));
33852
+ return;
33853
+ }
33643
33854
  const db = getDatabase();
33644
33855
  const resolved = resolvePartialId(db, "task_lists", opts.delete);
33645
33856
  if (!resolved) {
@@ -33650,7 +33861,6 @@ ${isOnline ? chalk5.green("\u25CF") : chalk5.dim("\u25CB")} ${chalk5.bold(agent.
33650
33861
  console.log(chalk5.green("Task list deleted."));
33651
33862
  return;
33652
33863
  }
33653
- const cloud = getTodosCloudClient();
33654
33864
  const lists = cloud ? await cloudListTaskLists(cloud, projectId ?? undefined) : listTaskLists(projectId);
33655
33865
  if (globalOpts.json) {
33656
33866
  output(lists, true);
@@ -35065,7 +35275,7 @@ function renderExtensionSummary(record) {
35065
35275
  }
35066
35276
  var BUILTIN_CLI_COMMANDS;
35067
35277
  var init_local_extensions = __esm(() => {
35068
- init_config2();
35278
+ init_config();
35069
35279
  init_mcp();
35070
35280
  init_package_version();
35071
35281
  init_redaction();
@@ -35497,7 +35707,7 @@ var init_policy_packs = __esm(() => {
35497
35707
  init_database();
35498
35708
  init_tasks();
35499
35709
  init_task_runs();
35500
- init_config2();
35710
+ init_config();
35501
35711
  });
35502
35712
 
35503
35713
  // src/db/checkpoints.ts
@@ -36020,7 +36230,7 @@ function describeTerminalNotificationRule(rule) {
36020
36230
  }
36021
36231
  var SEVERITY_ORDER, EVENT_SEVERITY, VALID_SEVERITIES, VALID_FORMATS;
36022
36232
  var init_terminal_notifications = __esm(() => {
36023
- init_config2();
36233
+ init_config();
36024
36234
  init_event_hooks();
36025
36235
  init_redaction();
36026
36236
  SEVERITY_ORDER = {
@@ -37523,7 +37733,7 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
37523
37733
  return createTodosCloudQueryClient(url, options);
37524
37734
  }
37525
37735
  var init_cloud_client = __esm(() => {
37526
- init_config();
37736
+ init_config2();
37527
37737
  });
37528
37738
 
37529
37739
  // src/storage/sqlite-snapshot.ts
@@ -37905,6 +38115,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
37905
38115
  list: (filter = {}) => listTasks(filter, database()),
37906
38116
  count: (filter = {}) => countTasks(filter, database()),
37907
38117
  update: (id, input) => updateTask(id, input, database()),
38118
+ unlock: (id, agentId) => {
38119
+ unlockTask(id, agentId, database());
38120
+ return true;
38121
+ },
37908
38122
  delete: (id) => deleteTask(id, database()),
37909
38123
  start: (id, agentId) => startTask(id, agentId, database()),
37910
38124
  complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
@@ -37956,6 +38170,20 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
37956
38170
  logTaskChange: (taskId, action, field, oldValue, newValue, agentId) => logTaskChange(taskId, action, field, oldValue, newValue, agentId, database()),
37957
38171
  addComment: (input) => addComment(input, database()),
37958
38172
  getComments: (taskId) => listComments(taskId, database()),
38173
+ getCommentsPage: (taskId, options2) => {
38174
+ if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
38175
+ throw new Error("Comment limit must be an integer between 1 and 1001");
38176
+ }
38177
+ let comments = listComments(taskId, database());
38178
+ comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
38179
+ if (options2?.before) {
38180
+ const before = options2.before;
38181
+ comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
38182
+ }
38183
+ if (options2?.limit !== undefined)
38184
+ comments = comments.slice(-options2.limit);
38185
+ return comments;
38186
+ },
37959
38187
  getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
37960
38188
  getRecentActivity: (limit) => getRecentActivity(limit, database())
37961
38189
  },
@@ -38013,6 +38241,12 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
38013
38241
  )`
38014
38242
  ];
38015
38243
  }
38244
+ function postgresTodosCommentCursorIndexSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
38245
+ assertSafeIdentifier(tableName);
38246
+ return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_comment_task_created_idx
38247
+ ON ${tableName} (service, (payload->>'task_id'), (payload->>'created_at'), object_id)
38248
+ WHERE object_type = 'comments' AND deleted_at IS NULL`;
38249
+ }
38016
38250
 
38017
38251
  class PostgresTodosSyncStore {
38018
38252
  client;
@@ -38538,7 +38772,7 @@ function __resetRuntimeShadowForTests() {
38538
38772
  }
38539
38773
  var _capturedDb = null, _outbox = null, _cloud = null, _exitRegistered = false;
38540
38774
  var init_shadow_runtime = __esm(() => {
38541
- init_config();
38775
+ init_config2();
38542
38776
  init_cloud_client();
38543
38777
  init_shadow_outbox();
38544
38778
  });
@@ -38994,7 +39228,24 @@ function createPostgresTodosStorageAdapter(options) {
38994
39228
  audit: {
38995
39229
  logTaskChange: (taskId, action, field, oldValue, newValue, agentId, context) => logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context),
38996
39230
  addComment: (input, context) => addComment2(input, store, context),
38997
- getComments: async (taskId) => (await store.list("comments")).filter((comment) => comment.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
39231
+ getComments: async (taskId) => {
39232
+ const pages = [];
39233
+ let before;
39234
+ while (true) {
39235
+ const page = await store.listComments(taskId, { limit: 1000, ...before ? { before } : {} });
39236
+ if (page.length === 0)
39237
+ break;
39238
+ pages.unshift(page);
39239
+ if (page.length < 1000)
39240
+ break;
39241
+ const oldest = page[0];
39242
+ before = { created_at: oldest.created_at, id: oldest.id };
39243
+ }
39244
+ return pages.flat().map(redactComment2).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
39245
+ },
39246
+ getCommentsPage: async (taskId, options2) => {
39247
+ return (await store.listComments(taskId, options2)).map(redactComment2).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
39248
+ },
38998
39249
  getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
38999
39250
  getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
39000
39251
  },
@@ -39044,6 +39295,27 @@ class PostgresJsonRecordStore {
39044
39295
  async list(type) {
39045
39296
  return (await this.listRecords(type)).map((record) => record.payload);
39046
39297
  }
39298
+ async listComments(taskId, options = {}) {
39299
+ await this.ensureSchema();
39300
+ const limit = options.limit ?? 100;
39301
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
39302
+ throw new Error("Postgres comment limit must be an integer between 1 and 1001");
39303
+ }
39304
+ const params = [this.service, taskId];
39305
+ let cursorPredicate = "";
39306
+ if (options.before) {
39307
+ params.push(options.before.created_at, options.before.id);
39308
+ cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
39309
+ }
39310
+ params.push(limit);
39311
+ const result = await this.options.client.query(`/* todos:list-comments */ SELECT payload FROM ${this.tableName}
39312
+ WHERE service = $1 AND object_type = 'comments' AND deleted_at IS NULL
39313
+ AND payload->>'task_id' = $2
39314
+ ${cursorPredicate}
39315
+ ORDER BY payload->>'created_at' DESC, object_id DESC
39316
+ LIMIT $${params.length}`, params);
39317
+ return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
39318
+ }
39047
39319
  async listRecords(type) {
39048
39320
  await this.ensureSchema();
39049
39321
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
@@ -39437,7 +39709,7 @@ async function lockTask2(id, agentId, store) {
39437
39709
  async function unlockTask2(id, agentId, store) {
39438
39710
  const task = await requireRecord("tasks", id, store);
39439
39711
  if (agentId && task.locked_by && task.locked_by !== agentId) {
39440
- throw new Error(`Task ${id} is locked by ${task.locked_by}, not ${agentId}`);
39712
+ throw new LockError(id, task.locked_by);
39441
39713
  }
39442
39714
  await patchTask(task, { locked_by: null, locked_at: null }, store);
39443
39715
  return true;
@@ -39802,13 +40074,16 @@ async function addComment2(input, store, context) {
39802
40074
  task_id: input.task_id,
39803
40075
  agent_id: input.agent_id ?? context?.agentId ?? null,
39804
40076
  session_id: input.session_id ?? context?.sessionId ?? null,
39805
- content: input.content,
40077
+ content: redactEvidenceText(input.content),
39806
40078
  type: input.type ?? "comment",
39807
40079
  progress_pct: input.progress_pct ?? null,
39808
40080
  created_at: new Date().toISOString()
39809
40081
  };
39810
40082
  return store.upsert("comments", comment, context);
39811
40083
  }
40084
+ function redactComment2(comment) {
40085
+ return { ...comment, content: redactEvidenceText(comment.content) };
40086
+ }
39812
40087
  async function exportSnapshot(store) {
39813
40088
  return {
39814
40089
  exportedAt: new Date().toISOString(),
@@ -39954,7 +40229,109 @@ function numberValue2(value) {
39954
40229
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
39955
40230
  }
39956
40231
  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";
39957
- var init_postgres_adapter = () => {};
40232
+ var init_postgres_adapter = __esm(() => {
40233
+ init_types();
40234
+ init_redaction();
40235
+ });
40236
+
40237
+ // src/storage/comment-redaction-backfill.ts
40238
+ function assertSafeIdentifier2(value) {
40239
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
40240
+ throw new Error(`Unsafe Postgres identifier: ${value}`);
40241
+ }
40242
+ }
40243
+ function payloadObject(value) {
40244
+ if (value && typeof value === "object" && !Array.isArray(value))
40245
+ return value;
40246
+ if (typeof value !== "string")
40247
+ return null;
40248
+ try {
40249
+ const parsed = JSON.parse(value);
40250
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
40251
+ } catch {
40252
+ return null;
40253
+ }
40254
+ }
40255
+ async function backfillPostgresCommentRedaction(client, options = {}) {
40256
+ const apply = options.apply === true;
40257
+ if (apply && options.confirmation !== COMMENT_REDACTION_BACKFILL_CONFIRMATION) {
40258
+ throw new Error(`Applying the comment redaction backfill requires confirmation ${COMMENT_REDACTION_BACKFILL_CONFIRMATION}`);
40259
+ }
40260
+ const tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
40261
+ assertSafeIdentifier2(tableName);
40262
+ const service = options.service ?? "todos";
40263
+ const batchSize = options.batchSize ?? 100;
40264
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 500) {
40265
+ throw new Error("Comment redaction backfill batchSize must be an integer between 1 and 500");
40266
+ }
40267
+ const result = {
40268
+ dry_run: !apply,
40269
+ scanned: 0,
40270
+ candidates: 0,
40271
+ updated: 0,
40272
+ conflicts: 0,
40273
+ batches: 0,
40274
+ remaining_candidates: 0
40275
+ };
40276
+ let afterId = "";
40277
+ while (true) {
40278
+ const page = await client.query(`/* todos:comment-redaction-backfill-scan */
40279
+ SELECT object_id, payload
40280
+ FROM ${tableName}
40281
+ WHERE service = $1 AND object_type = 'comments'
40282
+ AND object_id > $2
40283
+ ORDER BY object_id ASC
40284
+ LIMIT $3`, [service, afterId, batchSize]);
40285
+ if (page.rows.length === 0)
40286
+ break;
40287
+ result.batches += 1;
40288
+ for (const row of page.rows) {
40289
+ afterId = row.object_id;
40290
+ result.scanned += 1;
40291
+ const payload = payloadObject(row.payload);
40292
+ const original = payload?.["content"];
40293
+ if (typeof original !== "string")
40294
+ continue;
40295
+ const redacted = redactEvidenceText(original);
40296
+ if (redacted === original)
40297
+ continue;
40298
+ result.candidates += 1;
40299
+ if (!apply)
40300
+ continue;
40301
+ const nextPayload = { ...payload, content: redacted };
40302
+ const update = await client.query(`/* todos:comment-redaction-backfill-apply */
40303
+ UPDATE ${tableName}
40304
+ SET payload = $3::jsonb
40305
+ WHERE service = $1 AND object_type = 'comments' AND object_id = $2
40306
+ AND payload = $4::jsonb
40307
+ RETURNING object_id`, [service, row.object_id, nextPayload, row.payload]);
40308
+ if (update.rows.length === 1)
40309
+ result.updated += 1;
40310
+ else
40311
+ result.conflicts += 1;
40312
+ }
40313
+ if (page.rows.length < batchSize)
40314
+ break;
40315
+ }
40316
+ if (!apply) {
40317
+ result.remaining_candidates = result.candidates;
40318
+ return result;
40319
+ }
40320
+ const verification = await backfillPostgresCommentRedaction(client, {
40321
+ ...options,
40322
+ apply: false,
40323
+ confirmation: undefined
40324
+ });
40325
+ result.remaining_candidates = verification.candidates;
40326
+ return result;
40327
+ }
40328
+ function isCommentRedactionBackfillComplete(result) {
40329
+ return !result.dry_run && result.conflicts === 0 && result.remaining_candidates === 0;
40330
+ }
40331
+ var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
40332
+ var init_comment_redaction_backfill = __esm(() => {
40333
+ init_redaction();
40334
+ });
39958
40335
 
39959
40336
  // src/server/cloud.ts
39960
40337
  var exports_cloud = {};
@@ -39968,7 +40345,9 @@ __export(exports_cloud, {
39968
40345
  getCloudStorageAdapter: () => getCloudStorageAdapter,
39969
40346
  getApiKeyStore: () => getApiKeyStore,
39970
40347
  ensureCloudSchema: () => ensureCloudSchema,
40348
+ ensureCloudCommentCursorIndex: () => ensureCloudCommentCursorIndex,
39971
40349
  closeCloud: () => closeCloud,
40350
+ backfillCloudCommentRedaction: () => backfillCloudCommentRedaction,
39972
40351
  TODOS_APP_SLUG: () => TODOS_APP_SLUG
39973
40352
  });
39974
40353
  function resolveCloudDatabaseUrl(env = process.env) {
@@ -40046,6 +40425,9 @@ async function ensureCloudSchema() {
40046
40425
  })();
40047
40426
  return schemaEnsured;
40048
40427
  }
40428
+ async function ensureCloudCommentCursorIndex() {
40429
+ await getClient().query(postgresTodosCommentCursorIndexSql());
40430
+ }
40049
40431
  async function normalizeCloudPayloads() {
40050
40432
  const client = getClient();
40051
40433
  const res = await client.query(`UPDATE todos_sync_records
@@ -40054,6 +40436,9 @@ async function normalizeCloudPayloads() {
40054
40436
  RETURNING object_id AS id`);
40055
40437
  return res.rows.length;
40056
40438
  }
40439
+ function backfillCloudCommentRedaction(options = {}) {
40440
+ return backfillPostgresCommentRedaction(getClient(), { ...options, service: TODOS_APP_SLUG });
40441
+ }
40057
40442
  async function pingCloud() {
40058
40443
  const client = getClient();
40059
40444
  const res = await client.query("select 1 as ok");
@@ -40074,6 +40459,7 @@ var init_cloud = __esm(() => {
40074
40459
  init_auth();
40075
40460
  init_cloud_client();
40076
40461
  init_postgres_adapter();
40462
+ init_comment_redaction_backfill();
40077
40463
  });
40078
40464
 
40079
40465
  // src/server/openapi.ts
@@ -40097,6 +40483,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40097
40483
  schemas: {
40098
40484
  Task: taskSchema,
40099
40485
  Project: projectSchema,
40486
+ TaskComment: taskCommentSchema,
40100
40487
  CreateTaskInput: {
40101
40488
  type: "object",
40102
40489
  required: ["title"],
@@ -40131,6 +40518,17 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40131
40518
  description: { type: "string" },
40132
40519
  task_prefix: { type: "string" }
40133
40520
  }
40521
+ },
40522
+ CreateTaskCommentInput: {
40523
+ type: "object",
40524
+ required: ["content"],
40525
+ properties: {
40526
+ content: { type: "string", minLength: 1 },
40527
+ agent_id: { type: "string" },
40528
+ session_id: { type: "string" },
40529
+ type: { type: "string", enum: ["comment", "progress", "note"] },
40530
+ progress_pct: { type: "number" }
40531
+ }
40134
40532
  }
40135
40533
  }
40136
40534
  },
@@ -40229,6 +40627,61 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40229
40627
  }
40230
40628
  }
40231
40629
  },
40630
+ "/v1/tasks/{id}/comments": {
40631
+ get: {
40632
+ operationId: "listTaskComments",
40633
+ summary: "List a bounded page of task comments",
40634
+ 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.",
40635
+ parameters: [
40636
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
40637
+ { name: "limit", in: "query", required: true, schema: { type: "integer", minimum: 1, maximum: 500, default: 100 } },
40638
+ { name: "cursor", in: "query", schema: { type: "string" } }
40639
+ ],
40640
+ responses: {
40641
+ "200": {
40642
+ content: {
40643
+ "application/json": {
40644
+ schema: {
40645
+ type: "object",
40646
+ required: ["comments", "count", "has_more", "next_cursor"],
40647
+ properties: {
40648
+ comments: { type: "array", maxItems: 500, items: { $ref: "#/components/schemas/TaskComment" } },
40649
+ count: { type: "integer", minimum: 0, maximum: 500 },
40650
+ has_more: { type: "boolean" },
40651
+ next_cursor: { type: "string", nullable: true }
40652
+ }
40653
+ }
40654
+ }
40655
+ }
40656
+ },
40657
+ "426": {
40658
+ 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."
40659
+ }
40660
+ }
40661
+ },
40662
+ post: {
40663
+ operationId: "createTaskComment",
40664
+ summary: "Create a task comment",
40665
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
40666
+ requestBody: {
40667
+ required: true,
40668
+ content: { "application/json": { schema: { $ref: "#/components/schemas/CreateTaskCommentInput" } } }
40669
+ },
40670
+ responses: {
40671
+ "201": {
40672
+ content: {
40673
+ "application/json": {
40674
+ schema: {
40675
+ type: "object",
40676
+ required: ["comment"],
40677
+ properties: { comment: { $ref: "#/components/schemas/TaskComment" } }
40678
+ }
40679
+ }
40680
+ }
40681
+ }
40682
+ }
40683
+ }
40684
+ },
40232
40685
  "/v1/tasks/{id}/start": {
40233
40686
  post: {
40234
40687
  operationId: "startTask",
@@ -40347,7 +40800,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40347
40800
  }
40348
40801
  };
40349
40802
  }
40350
- var taskSchema, projectSchema;
40803
+ var taskSchema, projectSchema, taskCommentSchema;
40351
40804
  var init_openapi = __esm(() => {
40352
40805
  init_package_version();
40353
40806
  taskSchema = {
@@ -40378,6 +40831,20 @@ var init_openapi = __esm(() => {
40378
40831
  updated_at: { type: "string" }
40379
40832
  }
40380
40833
  };
40834
+ taskCommentSchema = {
40835
+ type: "object",
40836
+ required: ["id", "task_id", "agent_id", "session_id", "content", "type", "progress_pct", "created_at"],
40837
+ properties: {
40838
+ id: { type: "string" },
40839
+ task_id: { type: "string" },
40840
+ agent_id: { type: "string", nullable: true },
40841
+ session_id: { type: "string", nullable: true },
40842
+ content: { type: "string" },
40843
+ type: { type: "string", enum: ["comment", "progress", "note"] },
40844
+ progress_pct: { type: "number", nullable: true },
40845
+ created_at: { type: "string", format: "date-time" }
40846
+ }
40847
+ };
40381
40848
  });
40382
40849
 
40383
40850
  // src/server/v1.ts
@@ -40407,6 +40874,29 @@ function contextFromPrincipal(principal, body) {
40407
40874
  const agentId = body?.agent_id || principal.agent || undefined;
40408
40875
  return agentId ? { agentId } : {};
40409
40876
  }
40877
+ function redactComment3(comment) {
40878
+ return { ...comment, content: redactEvidenceText(comment.content) };
40879
+ }
40880
+ function encodeCommentCursor(comment) {
40881
+ return Buffer.from(JSON.stringify({ created_at: comment.created_at, id: comment.id }), "utf8").toString("base64url");
40882
+ }
40883
+ function decodeCommentCursor(value) {
40884
+ if (value.length > 1024)
40885
+ throw new Error("invalid comment cursor");
40886
+ let parsed;
40887
+ try {
40888
+ parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
40889
+ } catch {
40890
+ throw new Error("invalid comment cursor");
40891
+ }
40892
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
40893
+ throw new Error("invalid comment cursor");
40894
+ const cursor = parsed;
40895
+ 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) {
40896
+ throw new Error("invalid comment cursor");
40897
+ }
40898
+ return { created_at: cursor["created_at"], id: cursor["id"] };
40899
+ }
40410
40900
  function normalizeImportSnapshot(raw) {
40411
40901
  const body = raw && typeof raw === "object" ? raw : {};
40412
40902
  const arr = (v) => Array.isArray(v) ? v : [];
@@ -40427,7 +40917,7 @@ function normalizeImportSnapshot(raw) {
40427
40917
  function countSnapshotRecords(s) {
40428
40918
  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);
40429
40919
  }
40430
- async function handleV1Request(req, url) {
40920
+ async function handleV1Request(req, url, dependencies = {}) {
40431
40921
  const path = url.pathname;
40432
40922
  if (path !== "/v1" && !path.startsWith("/v1/"))
40433
40923
  return null;
@@ -40436,7 +40926,7 @@ async function handleV1Request(req, url) {
40436
40926
  const requiredScopes = [isWrite ? "todos:write" : "todos:read"];
40437
40927
  let verifier;
40438
40928
  try {
40439
- verifier = getCloudVerifier();
40929
+ verifier = (dependencies.getVerifier ?? getCloudVerifier)();
40440
40930
  } catch (e) {
40441
40931
  return error(503, e.message);
40442
40932
  }
@@ -40445,8 +40935,8 @@ async function handleV1Request(req, url) {
40445
40935
  return error(decision.status, decision.message, { reason: decision.reason });
40446
40936
  }
40447
40937
  const principal = decision.principal;
40448
- await ensureCloudSchema();
40449
- const store = getCloudStorageAdapter();
40938
+ await (dependencies.ensureSchema ?? ensureCloudSchema)();
40939
+ const store = (dependencies.getStorageAdapter ?? getCloudStorageAdapter)();
40450
40940
  const segments = path.split("/").filter(Boolean);
40451
40941
  const resource = segments[1];
40452
40942
  const id = segments[2];
@@ -40532,10 +41022,16 @@ async function handleV1Request(req, url) {
40532
41022
  if (!id) {
40533
41023
  if (method === "GET") {
40534
41024
  const filter = {
40535
- ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
40536
- ...url.searchParams.get("priority") ? { priority: url.searchParams.get("priority") } : {},
41025
+ ...url.searchParams.get("status") ? {
41026
+ status: url.searchParams.get("status").includes(",") ? url.searchParams.get("status").split(",") : url.searchParams.get("status")
41027
+ } : {},
41028
+ ...url.searchParams.get("priority") ? {
41029
+ priority: url.searchParams.get("priority").includes(",") ? url.searchParams.get("priority").split(",") : url.searchParams.get("priority")
41030
+ } : {},
40537
41031
  ...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
41032
+ ...url.searchParams.has("parent_id") ? { parent_id: url.searchParams.get("parent_id") || null, include_subtasks: true } : {},
40538
41033
  ...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {},
41034
+ ...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
40539
41035
  ...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
40540
41036
  ...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
40541
41037
  ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
@@ -40559,8 +41055,47 @@ async function handleV1Request(req, url) {
40559
41055
  if (action) {
40560
41056
  if (action === "comments") {
40561
41057
  if (method === "GET") {
40562
- const comments = await store.audit.getComments(id);
40563
- return json2({ comments, count: comments.length });
41058
+ if (!await store.tasks.get(id))
41059
+ return error(404, "task not found");
41060
+ const rawLimit = url.searchParams.get("limit");
41061
+ const cursor = url.searchParams.get("cursor");
41062
+ if (rawLimit === null && cursor === null) {
41063
+ const storageContext = contextFromPrincipal(principal);
41064
+ const legacyPage = (await (store.audit.getCommentsPage ? store.audit.getCommentsPage(id, { limit: LEGACY_COMMENT_RESPONSE_LIMIT + 1 }, storageContext) : store.audit.getComments(id, storageContext))).map(redactComment3);
41065
+ if (legacyPage.length > LEGACY_COMMENT_RESPONSE_LIMIT) {
41066
+ return error(426, "task has too many comments for this client; upgrade @hasna/todos to use cursor pagination");
41067
+ }
41068
+ return json2({
41069
+ comments: legacyPage,
41070
+ count: legacyPage.length,
41071
+ has_more: false,
41072
+ next_cursor: null
41073
+ });
41074
+ }
41075
+ const limit = rawLimit === null ? DEFAULT_COMMENT_PAGE_SIZE : Number(rawLimit);
41076
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_COMMENT_PAGE_SIZE) {
41077
+ return error(400, `limit must be an integer between 1 and ${MAX_COMMENT_PAGE_SIZE}`);
41078
+ }
41079
+ let before;
41080
+ if (cursor) {
41081
+ try {
41082
+ before = decodeCommentCursor(cursor);
41083
+ } catch {
41084
+ return error(400, "invalid comment cursor");
41085
+ }
41086
+ }
41087
+ if (!store.audit.getCommentsPage) {
41088
+ return error(426, "storage adapter must be upgraded to support cursor-paginated comments");
41089
+ }
41090
+ const page = (await store.audit.getCommentsPage(id, { limit: limit + 1, ...before ? { before } : {} }, contextFromPrincipal(principal))).map(redactComment3);
41091
+ const hasMore = page.length > limit;
41092
+ const comments = hasMore ? page.slice(1) : page;
41093
+ return json2({
41094
+ comments,
41095
+ count: comments.length,
41096
+ has_more: hasMore,
41097
+ next_cursor: hasMore && comments[0] ? encodeCommentCursor(comments[0]) : null
41098
+ });
40564
41099
  }
40565
41100
  if (method === "POST") {
40566
41101
  const body2 = await readJson(req) ?? {};
@@ -40578,7 +41113,7 @@ async function handleV1Request(req, url) {
40578
41113
  type: body2.type,
40579
41114
  progress_pct: body2.progress_pct
40580
41115
  }, contextFromPrincipal(principal, body2));
40581
- return json2({ comment }, 201);
41116
+ return json2({ comment: redactComment3(comment) }, 201);
40582
41117
  }
40583
41118
  return error(405, `method ${method} not allowed on /v1/tasks/:id/comments`);
40584
41119
  }
@@ -40593,17 +41128,30 @@ async function handleV1Request(req, url) {
40593
41128
  if (action === "lock" || action === "unlock") {
40594
41129
  if (method !== "POST")
40595
41130
  return error(405, `method ${method} not allowed on /v1/tasks/:id/${action}`);
40596
- if (typeof store.tasks.lock !== "function" || typeof store.tasks.unlock !== "function") {
40597
- return error(501, "task locking is not supported by this storage backend");
40598
- }
40599
41131
  const body2 = await readJson(req) ?? {};
40600
41132
  if (!await store.tasks.get(id))
40601
41133
  return error(404, "task not found");
40602
41134
  if (action === "lock") {
40603
- const agentId2 = body2.agent_id || principal.agent || "todos-serve";
40604
- return json2({ result: await store.tasks.lock(id, agentId2) });
41135
+ if (typeof store.tasks.lock !== "function")
41136
+ return error(501, "task locking is not supported by this storage backend");
41137
+ const agentId3 = body2.agent_id || principal.agent || "todos-serve";
41138
+ return json2({ result: await store.tasks.lock(id, agentId3) });
40605
41139
  }
40606
- const released = await store.tasks.unlock(id, body2.agent_id || principal.agent || undefined);
41140
+ if (typeof store.tasks.unlock !== "function")
41141
+ return error(501, "task unlocking is not supported by this storage backend");
41142
+ if (body2.force === true) {
41143
+ if (!principal.scopes.includes("todos:*"))
41144
+ return error(403, "force unlock requires todos:* scope");
41145
+ const released2 = await store.tasks.unlock(id);
41146
+ return json2({ success: released2 });
41147
+ }
41148
+ if (body2.agent_id && principal.agent && body2.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
41149
+ return error(403, "unlock agent_id must match the authenticated agent");
41150
+ }
41151
+ const agentId2 = principal.agent || body2.agent_id;
41152
+ if (!agentId2)
41153
+ return error(403, "unlock requires an agent-bound key or force=true");
41154
+ const released = await store.tasks.unlock(id, agentId2);
40607
41155
  return json2({ success: released });
40608
41156
  }
40609
41157
  if (action === "dependencies") {
@@ -40886,12 +41434,28 @@ async function handleV1Request(req, url) {
40886
41434
  const activity = await store.audit.getRecentActivity(limit);
40887
41435
  return json2({ activity, count: activity.length });
40888
41436
  }
40889
- if (resource === "task-lists" && !id) {
40890
- if (method !== "GET")
40891
- return error(405, `method ${method} not allowed on /v1/task-lists`);
40892
- const projectId = url.searchParams.get("project_id") ?? undefined;
40893
- const taskLists = await store.taskLists.list(projectId);
40894
- return json2({ task_lists: taskLists, count: taskLists.length });
41437
+ if (resource === "task-lists") {
41438
+ if (!id && method === "GET") {
41439
+ const projectId = url.searchParams.get("project_id") ?? undefined;
41440
+ const taskLists = await store.taskLists.list(projectId);
41441
+ return json2({ task_lists: taskLists, count: taskLists.length });
41442
+ }
41443
+ if (!id && method === "POST") {
41444
+ const body = await readJson(req);
41445
+ if (!body || typeof body.name !== "string" || !body.name.trim())
41446
+ return error(400, "name is required");
41447
+ const taskList = await store.taskLists.create(body, contextFromPrincipal(principal));
41448
+ return json2({ task_list: taskList }, 201);
41449
+ }
41450
+ if (id && method === "GET") {
41451
+ const taskList = await store.taskLists.get(id);
41452
+ return taskList ? json2({ task_list: taskList }) : error(404, "task list not found");
41453
+ }
41454
+ if (id && method === "DELETE") {
41455
+ const deleted = await store.taskLists.delete(id, contextFromPrincipal(principal));
41456
+ return deleted ? json2({ deleted: true, id }) : error(404, "task list not found");
41457
+ }
41458
+ return error(405, `method ${method} not allowed on /v1/task-lists${id ? "/:id" : ""}`);
40895
41459
  }
40896
41460
  if (resource === "dependencies" && !id) {
40897
41461
  if (method !== "GET")
@@ -40899,8 +41463,8 @@ async function handleV1Request(req, url) {
40899
41463
  if (typeof store.dependencies?.listAll !== "function") {
40900
41464
  return error(501, "dependency edge listing is not supported by this storage backend");
40901
41465
  }
40902
- const dependencies = await store.dependencies.listAll();
40903
- return json2({ dependencies, count: dependencies.length });
41466
+ const dependencies2 = await store.dependencies.listAll();
41467
+ return json2({ dependencies: dependencies2, count: dependencies2.length });
40904
41468
  }
40905
41469
  if (resource === "commits" && id) {
40906
41470
  if (method !== "GET")
@@ -40957,12 +41521,16 @@ async function handleV1Request(req, url) {
40957
41521
  }
40958
41522
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
40959
41523
  } catch (e) {
41524
+ if (e instanceof LockError)
41525
+ return error(409, e.message, { code: LockError.code });
40960
41526
  return error(500, e.message || "internal error");
40961
41527
  }
40962
41528
  }
40963
- var JSON_HEADERS;
41529
+ var JSON_HEADERS, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500;
40964
41530
  var init_v1 = __esm(() => {
41531
+ init_types();
40965
41532
  init_cloud();
41533
+ init_redaction();
40966
41534
  JSON_HEADERS = { "Content-Type": "application/json" };
40967
41535
  });
40968
41536
 
@@ -47197,7 +47765,7 @@ var init_workflow_states = __esm(() => {
47197
47765
  init_tasks();
47198
47766
  init_types();
47199
47767
  init_database();
47200
- init_config2();
47768
+ init_config();
47201
47769
  init_local_fields();
47202
47770
  DEFAULT_WORKFLOW_STATES = [
47203
47771
  { name: "pending", canonical_status: "pending", aliases: ["todo", "backlog"], transitions: null, terminal: false },
@@ -48362,7 +48930,7 @@ var init_roadmaps = __esm(() => {
48362
48930
  init_tasks();
48363
48931
  init_plans();
48364
48932
  init_task_runs();
48365
- init_config2();
48933
+ init_config();
48366
48934
  });
48367
48935
 
48368
48936
  // src/lib/capacity-forecasts.ts
@@ -48587,7 +49155,7 @@ var LOCAL_CAPACITY_SCHEMA_VERSION = 1;
48587
49155
  var init_capacity_forecasts = __esm(() => {
48588
49156
  init_tasks();
48589
49157
  init_task_relations();
48590
- init_config2();
49158
+ init_config();
48591
49159
  });
48592
49160
 
48593
49161
  // src/lib/audit-ledger.ts
@@ -48879,7 +49447,7 @@ var LOCAL_AUDIT_LEDGER_SCHEMA_VERSION = 1, LOCAL_AUDIT_LEDGER_HASH_ALGORITHM = "
48879
49447
  var init_audit_ledger = __esm(() => {
48880
49448
  init_database();
48881
49449
  init_task_runs();
48882
- init_config2();
49450
+ init_config();
48883
49451
  init_redaction();
48884
49452
  LOCAL_AUDIT_LEDGER_INITIAL_HASH = "0".repeat(64);
48885
49453
  });
@@ -54760,7 +55328,7 @@ var init_agent_run_dispatcher = __esm(() => {
54760
55328
  init_database();
54761
55329
  init_redaction();
54762
55330
  init_runner_sandbox();
54763
- init_config2();
55331
+ init_config();
54764
55332
  });
54765
55333
 
54766
55334
  // src/lib/verification-providers.ts
@@ -55105,7 +55673,7 @@ var init_verification_providers = __esm(() => {
55105
55673
  init_task_commits();
55106
55674
  init_database();
55107
55675
  init_tasks();
55108
- init_config2();
55676
+ init_config();
55109
55677
  init_redaction();
55110
55678
  DEFAULT_RETRY = {
55111
55679
  attempts: 1,
@@ -61931,7 +62499,7 @@ var init_review_queues = __esm(() => {
61931
62499
  init_audit();
61932
62500
  init_database();
61933
62501
  init_tasks();
61934
- init_config2();
62502
+ init_config();
61935
62503
  init_event_emission_safety();
61936
62504
  init_event_hooks();
61937
62505
  init_task_contracts();
@@ -63818,7 +64386,7 @@ ID: ${updated.id}${taskNote}`
63818
64386
  var init_agents2 = __esm(() => {
63819
64387
  init_zod();
63820
64388
  init_agents();
63821
- init_config2();
64389
+ init_config();
63822
64390
  init_database();
63823
64391
  init_cloud_router();
63824
64392
  });
@@ -64252,7 +64820,7 @@ function captureEnvironmentSnapshot(input = {}) {
64252
64820
  }
64253
64821
  function writeEnvironmentSnapshot(snapshot, outputPath) {
64254
64822
  const path = outputPath ? resolve20(outputPath) : join18(defaultSnapshotDir(), `${snapshot.id}.json`);
64255
- ensureDir2(dirname9(path));
64823
+ ensureDir(dirname9(path));
64256
64824
  writeJsonFile(path, snapshot);
64257
64825
  return path;
64258
64826
  }
@@ -67895,7 +68463,7 @@ Commit Links (${commitRows.length}):`));
67895
68463
  var init_config_serve_commands = __esm(() => {
67896
68464
  init_database();
67897
68465
  init_tasks();
67898
- init_config2();
68466
+ init_config();
67899
68467
  init_sync_utils();
67900
68468
  init_helpers();
67901
68469
  });
@@ -70101,7 +70669,7 @@ Findings`));
70101
70669
  checks.push({ name: "Migrations", ok: false, message: "Could not read migration version" });
70102
70670
  }
70103
70671
  try {
70104
- const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), exports_config2));
70672
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
70105
70673
  loadConfig2();
70106
70674
  checks.push({ name: "Config", ok: true, message: "Loaded successfully" });
70107
70675
  } catch (e) {
@@ -79532,7 +80100,7 @@ var init_factory = __esm(() => {
79532
80100
  init_postgres_adapter();
79533
80101
  init_shadow();
79534
80102
  init_cloud_client();
79535
- init_config();
80103
+ init_config2();
79536
80104
  });
79537
80105
 
79538
80106
  // src/storage/s3-artifacts.ts
@@ -79974,6 +80542,7 @@ __export(exports_storage, {
79974
80542
  signAwsV4Request: () => signAwsV4Request,
79975
80543
  registerShadowExitFlush: () => registerShadowExitFlush,
79976
80544
  postgresTodosSyncSchemaSql: () => postgresTodosSyncSchemaSql,
80545
+ postgresTodosCommentCursorIndexSql: () => postgresTodosCommentCursorIndexSql,
79977
80546
  planRunArtifactsS3Sync: () => planRunArtifactsS3Sync,
79978
80547
  parseStorageMode: () => parseStorageMode,
79979
80548
  maybeInstallShadowCapture: () => maybeInstallShadowCapture2,
@@ -79981,6 +80550,7 @@ __export(exports_storage, {
79981
80550
  loadStorageConfig: () => loadStorageConfig,
79982
80551
  isTodosShadowEnabled: () => isTodosShadowEnabled,
79983
80552
  isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
80553
+ isCommentRedactionBackfillComplete: () => isCommentRedactionBackfillComplete,
79984
80554
  installShadowOutboxSchema: () => installShadowOutboxSchema,
79985
80555
  importSqliteTodosStorageSnapshot: () => importSqliteTodosStorageSnapshot,
79986
80556
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
@@ -80008,6 +80578,7 @@ __export(exports_storage, {
80008
80578
  closeRuntimeShadowCloud: () => closeRuntimeShadowCloud,
80009
80579
  buildS3ObjectUrl: () => buildS3ObjectUrl,
80010
80580
  buildS3ObjectKey: () => buildS3ObjectKey,
80581
+ backfillPostgresCommentRedaction: () => backfillPostgresCommentRedaction,
80011
80582
  assertTodosShadowConfig: () => assertTodosShadowConfig,
80012
80583
  assertTodosRemoteStorageConfig: () => assertTodosRemoteStorageConfig,
80013
80584
  TodosShadowOutbox: () => TodosShadowOutbox,
@@ -80020,12 +80591,13 @@ __export(exports_storage, {
80020
80591
  PostgresTodosSyncStore: () => PostgresTodosSyncStore,
80021
80592
  DEFAULT_TODOS_POSTGRES_SYNC_TABLE: () => DEFAULT_TODOS_POSTGRES_SYNC_TABLE,
80022
80593
  DEFAULT_TODOS_POSTGRES_CURSOR_TABLE: () => DEFAULT_TODOS_POSTGRES_CURSOR_TABLE,
80594
+ COMMENT_REDACTION_BACKFILL_CONFIRMATION: () => COMMENT_REDACTION_BACKFILL_CONFIRMATION,
80023
80595
  CANONICAL_TODOS_RDS_RUNTIME_PATH: () => CANONICAL_TODOS_RDS_RUNTIME_PATH,
80024
80596
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
80025
80597
  CANONICAL_TODOS_RDS_CLUSTER: () => CANONICAL_TODOS_RDS_CLUSTER
80026
80598
  });
80027
80599
  var init_storage2 = __esm(() => {
80028
- init_config();
80600
+ init_config2();
80029
80601
  init_factory();
80030
80602
  init_shadow();
80031
80603
  init_shadow_outbox();
@@ -80034,6 +80606,7 @@ var init_storage2 = __esm(() => {
80034
80606
  init_hybrid();
80035
80607
  init_local_sqlite();
80036
80608
  init_sqlite_snapshot();
80609
+ init_comment_redaction_backfill();
80037
80610
  init_postgres_adapter();
80038
80611
  init_s3_artifacts();
80039
80612
  init_s3_artifact_sync();