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