@hasna/todos 0.11.85 → 0.11.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cloud-router.d.ts +55 -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 +1106 -417
- package/dist/contracts.js +1 -1
- package/dist/db/comments.d.ts.map +1 -1
- package/dist/index.js +180 -5
- package/dist/mcp/index.js +483 -39
- 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 +900 -384
- 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 +23 -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 +184 -5
- 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) {
|
|
@@ -945,7 +1270,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
945
1270
|
getActiveWork: (filters) => getActiveWork(filters, store),
|
|
946
1271
|
getChangedSince: (since, filters) => getChangedSince(since, filters, store),
|
|
947
1272
|
lock: (id, agentId) => lockTask(id, agentId, store),
|
|
948
|
-
unlock: (id, agentId) => unlockTask(id, agentId, store)
|
|
1273
|
+
unlock: (id, agentId) => unlockTask(id, agentId, store),
|
|
1274
|
+
getByFingerprint: (fingerprint) => store.getTaskByFingerprint(fingerprint)
|
|
949
1275
|
},
|
|
950
1276
|
dependencies: {
|
|
951
1277
|
add: (taskId, dependsOn, context) => addDependency(taskId, dependsOn, store, context),
|
|
@@ -1013,7 +1339,24 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
1013
1339
|
audit: {
|
|
1014
1340
|
logTaskChange: (taskId, action, field, oldValue, newValue, agentId, context) => logTaskChange(taskId, action, field, oldValue, newValue, agentId, store, context),
|
|
1015
1341
|
addComment: (input, context) => addComment(input, store, context),
|
|
1016
|
-
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
|
+
},
|
|
1017
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)),
|
|
1018
1361
|
getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
|
|
1019
1362
|
},
|
|
@@ -1063,6 +1406,27 @@ class PostgresJsonRecordStore {
|
|
|
1063
1406
|
async list(type) {
|
|
1064
1407
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
1065
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
|
+
}
|
|
1066
1430
|
async listRecords(type) {
|
|
1067
1431
|
await this.ensureSchema();
|
|
1068
1432
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
|
|
@@ -1134,6 +1498,17 @@ class PostgresJsonRecordStore {
|
|
|
1134
1498
|
const result = await this.options.client.query(sql, params);
|
|
1135
1499
|
return result.rows.map((row) => payloadRecord2(row.payload));
|
|
1136
1500
|
}
|
|
1501
|
+
async getTaskByFingerprint(fingerprint) {
|
|
1502
|
+
await this.ensureSchema();
|
|
1503
|
+
const sql = `/* todos:task-by-fingerprint */ SELECT payload FROM ${this.tableName}
|
|
1504
|
+
WHERE service = $1 AND object_type = $2 AND deleted_at IS NULL
|
|
1505
|
+
AND payload->'metadata'->>'fingerprint' = $3
|
|
1506
|
+
ORDER BY payload->>'created_at' ASC
|
|
1507
|
+
LIMIT 1`;
|
|
1508
|
+
const result = await this.options.client.query(sql, [this.service, "tasks", fingerprint]);
|
|
1509
|
+
const row = result.rows[0];
|
|
1510
|
+
return row ? payloadRecord2(row.payload) : null;
|
|
1511
|
+
}
|
|
1137
1512
|
async countTasks(filter) {
|
|
1138
1513
|
await this.ensureSchema();
|
|
1139
1514
|
const { where, params } = this.buildTaskFilterSql(filter);
|
|
@@ -1445,7 +1820,7 @@ async function lockTask(id, agentId, store) {
|
|
|
1445
1820
|
async function unlockTask(id, agentId, store) {
|
|
1446
1821
|
const task = await requireRecord("tasks", id, store);
|
|
1447
1822
|
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
1448
|
-
throw new
|
|
1823
|
+
throw new LockError(id, task.locked_by);
|
|
1449
1824
|
}
|
|
1450
1825
|
await patchTask(task, { locked_by: null, locked_at: null }, store);
|
|
1451
1826
|
return true;
|
|
@@ -1810,13 +2185,16 @@ async function addComment(input, store, context) {
|
|
|
1810
2185
|
task_id: input.task_id,
|
|
1811
2186
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
1812
2187
|
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
1813
|
-
content: input.content,
|
|
2188
|
+
content: redactEvidenceText(input.content),
|
|
1814
2189
|
type: input.type ?? "comment",
|
|
1815
2190
|
progress_pct: input.progress_pct ?? null,
|
|
1816
2191
|
created_at: new Date().toISOString()
|
|
1817
2192
|
};
|
|
1818
2193
|
return store.upsert("comments", comment, context);
|
|
1819
2194
|
}
|
|
2195
|
+
function redactComment(comment) {
|
|
2196
|
+
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
2197
|
+
}
|
|
1820
2198
|
async function exportSnapshot(store) {
|
|
1821
2199
|
return {
|
|
1822
2200
|
exportedAt: new Date().toISOString(),
|
|
@@ -1962,7 +2340,115 @@ function numberValue2(value) {
|
|
|
1962
2340
|
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
1963
2341
|
}
|
|
1964
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";
|
|
1965
|
-
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
|
+
});
|
|
1966
2452
|
|
|
1967
2453
|
// src/server/cloud.ts
|
|
1968
2454
|
var exports_cloud = {};
|
|
@@ -1976,7 +2462,9 @@ __export(exports_cloud, {
|
|
|
1976
2462
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
1977
2463
|
getApiKeyStore: () => getApiKeyStore,
|
|
1978
2464
|
ensureCloudSchema: () => ensureCloudSchema,
|
|
2465
|
+
ensureCloudCommentCursorIndex: () => ensureCloudCommentCursorIndex,
|
|
1979
2466
|
closeCloud: () => closeCloud,
|
|
2467
|
+
backfillCloudCommentRedaction: () => backfillCloudCommentRedaction,
|
|
1980
2468
|
TODOS_APP_SLUG: () => TODOS_APP_SLUG
|
|
1981
2469
|
});
|
|
1982
2470
|
function resolveCloudDatabaseUrl(env = process.env) {
|
|
@@ -2054,6 +2542,9 @@ async function ensureCloudSchema() {
|
|
|
2054
2542
|
})();
|
|
2055
2543
|
return schemaEnsured;
|
|
2056
2544
|
}
|
|
2545
|
+
async function ensureCloudCommentCursorIndex() {
|
|
2546
|
+
await getClient().query(postgresTodosCommentCursorIndexSql());
|
|
2547
|
+
}
|
|
2057
2548
|
async function normalizeCloudPayloads() {
|
|
2058
2549
|
const client = getClient();
|
|
2059
2550
|
const res = await client.query(`UPDATE todos_sync_records
|
|
@@ -2062,6 +2553,9 @@ async function normalizeCloudPayloads() {
|
|
|
2062
2553
|
RETURNING object_id AS id`);
|
|
2063
2554
|
return res.rows.length;
|
|
2064
2555
|
}
|
|
2556
|
+
function backfillCloudCommentRedaction(options = {}) {
|
|
2557
|
+
return backfillPostgresCommentRedaction(getClient(), { ...options, service: TODOS_APP_SLUG });
|
|
2558
|
+
}
|
|
2065
2559
|
async function pingCloud() {
|
|
2066
2560
|
const client = getClient();
|
|
2067
2561
|
const res = await client.query("select 1 as ok");
|
|
@@ -2082,6 +2576,7 @@ var init_cloud = __esm(() => {
|
|
|
2082
2576
|
init_auth();
|
|
2083
2577
|
init_cloud_client();
|
|
2084
2578
|
init_postgres_adapter();
|
|
2579
|
+
init_comment_redaction_backfill();
|
|
2085
2580
|
});
|
|
2086
2581
|
|
|
2087
2582
|
// src/db/migrations.ts
|
|
@@ -4383,7 +4878,7 @@ var init_schema = __esm(() => {
|
|
|
4383
4878
|
});
|
|
4384
4879
|
|
|
4385
4880
|
// src/db/machines.ts
|
|
4386
|
-
import { existsSync as
|
|
4881
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4387
4882
|
import { hostname as osHostname, platform as osPlatform, arch as osArch } from "os";
|
|
4388
4883
|
import { resolve } from "path";
|
|
4389
4884
|
import { spawnSync } from "child_process";
|
|
@@ -4590,7 +5085,7 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
|
|
|
4590
5085
|
message: `${project.name} has ${distinctPaths.length} different machine-local paths`
|
|
4591
5086
|
});
|
|
4592
5087
|
}
|
|
4593
|
-
if (localRow && !
|
|
5088
|
+
if (localRow && !existsSync4(localRow.path)) {
|
|
4594
5089
|
pathIssues.push({
|
|
4595
5090
|
type: "path_missing",
|
|
4596
5091
|
project_id: project.id,
|
|
@@ -4601,7 +5096,7 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
|
|
|
4601
5096
|
message: `Local path does not exist on this machine: ${localRow.path}`
|
|
4602
5097
|
});
|
|
4603
5098
|
}
|
|
4604
|
-
if (!localRow && project.path && machineById.has(localMachine.id) && !
|
|
5099
|
+
if (!localRow && project.path && machineById.has(localMachine.id) && !existsSync4(project.path)) {
|
|
4605
5100
|
pathIssues.push({
|
|
4606
5101
|
type: "path_missing",
|
|
4607
5102
|
project_id: project.id,
|
|
@@ -4800,8 +5295,8 @@ __export(exports_database, {
|
|
|
4800
5295
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
4801
5296
|
});
|
|
4802
5297
|
import { Database } from "bun:sqlite";
|
|
4803
|
-
import { existsSync as
|
|
4804
|
-
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";
|
|
4805
5300
|
function isInMemoryDb(path) {
|
|
4806
5301
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
4807
5302
|
}
|
|
@@ -4810,12 +5305,12 @@ function findNearestProjectDb(startDir) {
|
|
|
4810
5305
|
const stopAt = gitRoot ? resolve2(gitRoot) : resolve2(startDir);
|
|
4811
5306
|
let dir = resolve2(startDir);
|
|
4812
5307
|
while (true) {
|
|
4813
|
-
const candidate =
|
|
4814
|
-
if (
|
|
5308
|
+
const candidate = join4(dir, ".hasna", "todos", "todos.db");
|
|
5309
|
+
if (existsSync5(candidate))
|
|
4815
5310
|
return candidate;
|
|
4816
5311
|
if (dir === stopAt)
|
|
4817
5312
|
break;
|
|
4818
|
-
const parent =
|
|
5313
|
+
const parent = dirname3(dir);
|
|
4819
5314
|
if (parent === dir)
|
|
4820
5315
|
break;
|
|
4821
5316
|
dir = parent;
|
|
@@ -4825,9 +5320,9 @@ function findNearestProjectDb(startDir) {
|
|
|
4825
5320
|
function findGitRoot(startDir) {
|
|
4826
5321
|
let dir = resolve2(startDir);
|
|
4827
5322
|
while (true) {
|
|
4828
|
-
if (
|
|
5323
|
+
if (existsSync5(join4(dir, ".git")))
|
|
4829
5324
|
return dir;
|
|
4830
|
-
const parent =
|
|
5325
|
+
const parent = dirname3(dir);
|
|
4831
5326
|
if (parent === dir)
|
|
4832
5327
|
break;
|
|
4833
5328
|
dir = parent;
|
|
@@ -4848,25 +5343,25 @@ function getDbPath() {
|
|
|
4848
5343
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4849
5344
|
const gitRoot = findGitRoot(cwd);
|
|
4850
5345
|
if (gitRoot) {
|
|
4851
|
-
return
|
|
5346
|
+
return join4(gitRoot, ".hasna", "todos", "todos.db");
|
|
4852
5347
|
}
|
|
4853
5348
|
}
|
|
4854
5349
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4855
|
-
return
|
|
5350
|
+
return join4(home, ".hasna", "todos", "todos.db");
|
|
4856
5351
|
}
|
|
4857
5352
|
function getDatabasePath() {
|
|
4858
5353
|
return getDbPath();
|
|
4859
5354
|
}
|
|
4860
|
-
function
|
|
5355
|
+
function ensureDir2(filePath) {
|
|
4861
5356
|
if (isInMemoryDb(filePath))
|
|
4862
5357
|
return;
|
|
4863
|
-
const dir =
|
|
4864
|
-
if (!
|
|
4865
|
-
|
|
5358
|
+
const dir = dirname3(resolve2(filePath));
|
|
5359
|
+
if (!existsSync5(dir)) {
|
|
5360
|
+
mkdirSync2(dir, { recursive: true });
|
|
4866
5361
|
}
|
|
4867
5362
|
}
|
|
4868
5363
|
function openDatabase(path) {
|
|
4869
|
-
|
|
5364
|
+
ensureDir2(path);
|
|
4870
5365
|
const db = new Database(path);
|
|
4871
5366
|
db.run("PRAGMA journal_mode = WAL");
|
|
4872
5367
|
db.run("PRAGMA busy_timeout = 5000");
|
|
@@ -5041,226 +5536,6 @@ var init_api_keys = __esm(() => {
|
|
|
5041
5536
|
init_database();
|
|
5042
5537
|
});
|
|
5043
5538
|
|
|
5044
|
-
// src/types/index.ts
|
|
5045
|
-
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
5046
|
-
var init_types = __esm(() => {
|
|
5047
|
-
TASK_STATUSES = [
|
|
5048
|
-
"pending",
|
|
5049
|
-
"in_progress",
|
|
5050
|
-
"completed",
|
|
5051
|
-
"failed",
|
|
5052
|
-
"cancelled"
|
|
5053
|
-
];
|
|
5054
|
-
VersionConflictError = class VersionConflictError extends Error {
|
|
5055
|
-
taskId;
|
|
5056
|
-
expectedVersion;
|
|
5057
|
-
actualVersion;
|
|
5058
|
-
static code = "VERSION_CONFLICT";
|
|
5059
|
-
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
5060
|
-
constructor(taskId, expectedVersion, actualVersion) {
|
|
5061
|
-
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
5062
|
-
this.taskId = taskId;
|
|
5063
|
-
this.expectedVersion = expectedVersion;
|
|
5064
|
-
this.actualVersion = actualVersion;
|
|
5065
|
-
this.name = "VersionConflictError";
|
|
5066
|
-
}
|
|
5067
|
-
};
|
|
5068
|
-
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
5069
|
-
taskId;
|
|
5070
|
-
static code = "TASK_NOT_FOUND";
|
|
5071
|
-
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
5072
|
-
constructor(taskId) {
|
|
5073
|
-
super(`Task not found: ${taskId}`);
|
|
5074
|
-
this.taskId = taskId;
|
|
5075
|
-
this.name = "TaskNotFoundError";
|
|
5076
|
-
}
|
|
5077
|
-
};
|
|
5078
|
-
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
5079
|
-
projectId;
|
|
5080
|
-
static code = "PROJECT_NOT_FOUND";
|
|
5081
|
-
static suggestion = "Use list_projects to see available projects.";
|
|
5082
|
-
constructor(projectId) {
|
|
5083
|
-
super(`Project not found: ${projectId}`);
|
|
5084
|
-
this.projectId = projectId;
|
|
5085
|
-
this.name = "ProjectNotFoundError";
|
|
5086
|
-
}
|
|
5087
|
-
};
|
|
5088
|
-
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
5089
|
-
planId;
|
|
5090
|
-
static code = "PLAN_NOT_FOUND";
|
|
5091
|
-
static suggestion = "Use list_plans to see available plans.";
|
|
5092
|
-
constructor(planId) {
|
|
5093
|
-
super(`Plan not found: ${planId}`);
|
|
5094
|
-
this.planId = planId;
|
|
5095
|
-
this.name = "PlanNotFoundError";
|
|
5096
|
-
}
|
|
5097
|
-
};
|
|
5098
|
-
LockError = class LockError extends Error {
|
|
5099
|
-
taskId;
|
|
5100
|
-
lockedBy;
|
|
5101
|
-
static code = "LOCK_ERROR";
|
|
5102
|
-
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
5103
|
-
constructor(taskId, lockedBy) {
|
|
5104
|
-
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
5105
|
-
this.taskId = taskId;
|
|
5106
|
-
this.lockedBy = lockedBy;
|
|
5107
|
-
this.name = "LockError";
|
|
5108
|
-
}
|
|
5109
|
-
};
|
|
5110
|
-
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
5111
|
-
agentId;
|
|
5112
|
-
static code = "AGENT_NOT_FOUND";
|
|
5113
|
-
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
5114
|
-
constructor(agentId) {
|
|
5115
|
-
super(`Agent not found: ${agentId}`);
|
|
5116
|
-
this.agentId = agentId;
|
|
5117
|
-
this.name = "AgentNotFoundError";
|
|
5118
|
-
}
|
|
5119
|
-
};
|
|
5120
|
-
TaskListNotFoundError = class TaskListNotFoundError extends Error {
|
|
5121
|
-
taskListId;
|
|
5122
|
-
static code = "TASK_LIST_NOT_FOUND";
|
|
5123
|
-
static suggestion = "Use list_task_lists to see available lists.";
|
|
5124
|
-
constructor(taskListId) {
|
|
5125
|
-
super(`Task list not found: ${taskListId}`);
|
|
5126
|
-
this.taskListId = taskListId;
|
|
5127
|
-
this.name = "TaskListNotFoundError";
|
|
5128
|
-
}
|
|
5129
|
-
};
|
|
5130
|
-
DependencyCycleError = class DependencyCycleError extends Error {
|
|
5131
|
-
taskId;
|
|
5132
|
-
dependsOn;
|
|
5133
|
-
static code = "DEPENDENCY_CYCLE";
|
|
5134
|
-
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
5135
|
-
constructor(taskId, dependsOn) {
|
|
5136
|
-
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
5137
|
-
this.taskId = taskId;
|
|
5138
|
-
this.dependsOn = dependsOn;
|
|
5139
|
-
this.name = "DependencyCycleError";
|
|
5140
|
-
}
|
|
5141
|
-
};
|
|
5142
|
-
CompletionGuardError = class CompletionGuardError extends Error {
|
|
5143
|
-
reason;
|
|
5144
|
-
retryAfterSeconds;
|
|
5145
|
-
static code = "COMPLETION_BLOCKED";
|
|
5146
|
-
static suggestion = "Wait for the cooldown period, then retry.";
|
|
5147
|
-
constructor(reason, retryAfterSeconds) {
|
|
5148
|
-
super(reason);
|
|
5149
|
-
this.reason = reason;
|
|
5150
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
5151
|
-
this.name = "CompletionGuardError";
|
|
5152
|
-
}
|
|
5153
|
-
};
|
|
5154
|
-
DispatchNotFoundError = class DispatchNotFoundError extends Error {
|
|
5155
|
-
dispatchId;
|
|
5156
|
-
static code = "DISPATCH_NOT_FOUND";
|
|
5157
|
-
static suggestion = "Check the dispatch ID with list_dispatches.";
|
|
5158
|
-
constructor(dispatchId) {
|
|
5159
|
-
super(`Dispatch not found: ${dispatchId}`);
|
|
5160
|
-
this.dispatchId = dispatchId;
|
|
5161
|
-
this.name = "DispatchNotFoundError";
|
|
5162
|
-
}
|
|
5163
|
-
};
|
|
5164
|
-
});
|
|
5165
|
-
|
|
5166
|
-
// src/lib/sync-utils.ts
|
|
5167
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "fs";
|
|
5168
|
-
import { join as join3 } from "path";
|
|
5169
|
-
function getHomeDir() {
|
|
5170
|
-
return process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
5171
|
-
}
|
|
5172
|
-
function getTodosGlobalDir() {
|
|
5173
|
-
return join3(getHomeDir(), ".hasna", "todos");
|
|
5174
|
-
}
|
|
5175
|
-
function ensureDir2(dir) {
|
|
5176
|
-
if (!existsSync4(dir))
|
|
5177
|
-
mkdirSync2(dir, { recursive: true });
|
|
5178
|
-
}
|
|
5179
|
-
function readJsonFile(path) {
|
|
5180
|
-
try {
|
|
5181
|
-
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
5182
|
-
} catch {
|
|
5183
|
-
return null;
|
|
5184
|
-
}
|
|
5185
|
-
}
|
|
5186
|
-
function writeJsonFile(path, data) {
|
|
5187
|
-
writeFileSync(path, JSON.stringify(data, null, 2) + `
|
|
5188
|
-
`);
|
|
5189
|
-
}
|
|
5190
|
-
function appendSyncConflict(metadata, conflict, limit = 5) {
|
|
5191
|
-
const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
|
|
5192
|
-
const next = [conflict, ...current].slice(0, limit);
|
|
5193
|
-
return { ...metadata, sync_conflicts: next };
|
|
5194
|
-
}
|
|
5195
|
-
var HOME;
|
|
5196
|
-
var init_sync_utils = __esm(() => {
|
|
5197
|
-
HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
5198
|
-
});
|
|
5199
|
-
|
|
5200
|
-
// src/lib/config.ts
|
|
5201
|
-
import { existsSync as existsSync5 } from "fs";
|
|
5202
|
-
import { dirname as dirname3, join as join4 } from "path";
|
|
5203
|
-
function getConfigPath() {
|
|
5204
|
-
return join4(getTodosGlobalDir(), "config.json");
|
|
5205
|
-
}
|
|
5206
|
-
function loadConfig() {
|
|
5207
|
-
if (cached)
|
|
5208
|
-
return cached;
|
|
5209
|
-
if (!existsSync5(getConfigPath())) {
|
|
5210
|
-
cached = {};
|
|
5211
|
-
return cached;
|
|
5212
|
-
}
|
|
5213
|
-
const config = readJsonFile(getConfigPath()) || {};
|
|
5214
|
-
if (typeof config.sync_agents === "string") {
|
|
5215
|
-
config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
5216
|
-
}
|
|
5217
|
-
cached = config;
|
|
5218
|
-
return cached;
|
|
5219
|
-
}
|
|
5220
|
-
function saveConfig(config) {
|
|
5221
|
-
const configPath = getConfigPath();
|
|
5222
|
-
ensureDir2(dirname3(configPath));
|
|
5223
|
-
writeJsonFile(configPath, config);
|
|
5224
|
-
cached = config;
|
|
5225
|
-
return config;
|
|
5226
|
-
}
|
|
5227
|
-
function getAgentPoolForProject(workingDir) {
|
|
5228
|
-
const config = loadConfig();
|
|
5229
|
-
if (workingDir && config.project_pools) {
|
|
5230
|
-
let bestKey = null;
|
|
5231
|
-
let bestLen = 0;
|
|
5232
|
-
for (const key of Object.keys(config.project_pools)) {
|
|
5233
|
-
if (workingDir.startsWith(key) && key.length > bestLen) {
|
|
5234
|
-
bestKey = key;
|
|
5235
|
-
bestLen = key.length;
|
|
5236
|
-
}
|
|
5237
|
-
}
|
|
5238
|
-
if (bestKey && config.project_pools[bestKey]) {
|
|
5239
|
-
return config.project_pools[bestKey];
|
|
5240
|
-
}
|
|
5241
|
-
}
|
|
5242
|
-
return config.agent_pool || null;
|
|
5243
|
-
}
|
|
5244
|
-
function getCompletionGuardConfig(projectPath) {
|
|
5245
|
-
const config = loadConfig();
|
|
5246
|
-
const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
|
|
5247
|
-
if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
|
|
5248
|
-
return { ...global, ...config.project_overrides[projectPath].completion_guard };
|
|
5249
|
-
}
|
|
5250
|
-
return global;
|
|
5251
|
-
}
|
|
5252
|
-
var cached = null, GUARD_DEFAULTS;
|
|
5253
|
-
var init_config2 = __esm(() => {
|
|
5254
|
-
init_sync_utils();
|
|
5255
|
-
GUARD_DEFAULTS = {
|
|
5256
|
-
enabled: false,
|
|
5257
|
-
min_work_seconds: 30,
|
|
5258
|
-
max_completions_per_window: 5,
|
|
5259
|
-
window_minutes: 10,
|
|
5260
|
-
cooldown_seconds: 60
|
|
5261
|
-
};
|
|
5262
|
-
});
|
|
5263
|
-
|
|
5264
5539
|
// src/db/storage-tombstones.ts
|
|
5265
5540
|
function recordStorageTombstone(input, db) {
|
|
5266
5541
|
const d = db ?? getDatabase();
|
|
@@ -5697,105 +5972,6 @@ var init_event_emission_safety = __esm(() => {
|
|
|
5697
5972
|
init_sync_utils();
|
|
5698
5973
|
});
|
|
5699
5974
|
|
|
5700
|
-
// src/lib/redaction.ts
|
|
5701
|
-
function unique(values) {
|
|
5702
|
-
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
5703
|
-
}
|
|
5704
|
-
function cloneRegex(regex) {
|
|
5705
|
-
return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
|
|
5706
|
-
}
|
|
5707
|
-
function customPatterns() {
|
|
5708
|
-
return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
|
|
5709
|
-
try {
|
|
5710
|
-
return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
|
|
5711
|
-
} catch {
|
|
5712
|
-
return [];
|
|
5713
|
-
}
|
|
5714
|
-
});
|
|
5715
|
-
}
|
|
5716
|
-
function secretPatterns() {
|
|
5717
|
-
return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
|
|
5718
|
-
}
|
|
5719
|
-
function isSecretKey(key) {
|
|
5720
|
-
if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
|
|
5721
|
-
return false;
|
|
5722
|
-
if (DEFAULT_SECRET_KEY_PATTERN.test(key))
|
|
5723
|
-
return true;
|
|
5724
|
-
return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
|
|
5725
|
-
}
|
|
5726
|
-
function redactEvidenceText(value) {
|
|
5727
|
-
let redacted = value;
|
|
5728
|
-
for (const pattern of secretPatterns()) {
|
|
5729
|
-
const regex = cloneRegex(pattern.regex);
|
|
5730
|
-
const replacement = pattern.replacement ?? "[REDACTED]";
|
|
5731
|
-
redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
|
|
5732
|
-
}
|
|
5733
|
-
return redacted;
|
|
5734
|
-
}
|
|
5735
|
-
function redactValue(value) {
|
|
5736
|
-
if (typeof value === "string")
|
|
5737
|
-
return redactEvidenceText(value);
|
|
5738
|
-
if (Array.isArray(value))
|
|
5739
|
-
return value.map(redactValue);
|
|
5740
|
-
if (value && typeof value === "object") {
|
|
5741
|
-
const redacted = {};
|
|
5742
|
-
for (const [key, child] of Object.entries(value)) {
|
|
5743
|
-
if (isSecretKey(key)) {
|
|
5744
|
-
redacted[key] = "[REDACTED]";
|
|
5745
|
-
} else {
|
|
5746
|
-
redacted[key] = redactValue(child);
|
|
5747
|
-
}
|
|
5748
|
-
}
|
|
5749
|
-
return redacted;
|
|
5750
|
-
}
|
|
5751
|
-
return value;
|
|
5752
|
-
}
|
|
5753
|
-
function listSecretFindings(value) {
|
|
5754
|
-
const findings = [];
|
|
5755
|
-
for (const pattern of secretPatterns()) {
|
|
5756
|
-
const matches = value.match(cloneRegex(pattern.regex));
|
|
5757
|
-
if (matches?.length)
|
|
5758
|
-
findings.push({ pattern: pattern.name, count: matches.length });
|
|
5759
|
-
}
|
|
5760
|
-
return findings;
|
|
5761
|
-
}
|
|
5762
|
-
function getSecretSafetyConfig() {
|
|
5763
|
-
return {
|
|
5764
|
-
redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
|
|
5765
|
-
redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
|
|
5766
|
-
};
|
|
5767
|
-
}
|
|
5768
|
-
function upsertSecretSafetyConfig(input) {
|
|
5769
|
-
const config = loadConfig();
|
|
5770
|
-
const next = {
|
|
5771
|
-
redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
|
|
5772
|
-
redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
|
|
5773
|
-
};
|
|
5774
|
-
saveConfig({ ...config, secret_safety: next });
|
|
5775
|
-
return next;
|
|
5776
|
-
}
|
|
5777
|
-
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
|
|
5778
|
-
var init_redaction = __esm(() => {
|
|
5779
|
-
init_config2();
|
|
5780
|
-
DEFAULT_SECRET_PATTERNS = [
|
|
5781
|
-
{ name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
|
|
5782
|
-
{ name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
|
|
5783
|
-
{ name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
5784
|
-
{ 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]" },
|
|
5785
|
-
{ name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
|
|
5786
|
-
];
|
|
5787
|
-
DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
|
|
5788
|
-
NON_SECRET_USAGE_KEYS = new Set([
|
|
5789
|
-
"tokens",
|
|
5790
|
-
"total_tokens",
|
|
5791
|
-
"token_count",
|
|
5792
|
-
"input_tokens",
|
|
5793
|
-
"output_tokens",
|
|
5794
|
-
"prompt_tokens",
|
|
5795
|
-
"completion_tokens"
|
|
5796
|
-
]);
|
|
5797
|
-
});
|
|
5798
|
-
|
|
5799
5975
|
// src/lib/workspace-trust.ts
|
|
5800
5976
|
import { relative, resolve as resolve4 } from "path";
|
|
5801
5977
|
function normalizePath(path) {
|
|
@@ -11232,7 +11408,7 @@ function getComment(id, db) {
|
|
|
11232
11408
|
}
|
|
11233
11409
|
function listComments(taskId, db) {
|
|
11234
11410
|
const d = db || getDatabase();
|
|
11235
|
-
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);
|
|
11236
11412
|
}
|
|
11237
11413
|
function updateComment(id, input, db) {
|
|
11238
11414
|
const d = db || getDatabase();
|
|
@@ -14966,6 +15142,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
14966
15142
|
list: (filter = {}) => listTasks2(filter, database()),
|
|
14967
15143
|
count: (filter = {}) => countTasks(filter, database()),
|
|
14968
15144
|
update: (id, input) => updateTask2(id, input, database()),
|
|
15145
|
+
unlock: (id, agentId) => {
|
|
15146
|
+
unlockTask2(id, agentId, database());
|
|
15147
|
+
return true;
|
|
15148
|
+
},
|
|
14969
15149
|
delete: (id) => deleteTask(id, database()),
|
|
14970
15150
|
start: (id, agentId) => startTask2(id, agentId, database()),
|
|
14971
15151
|
complete: (id, agentId, options2) => completeTask2(id, agentId, database(), options2),
|
|
@@ -15017,6 +15197,20 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
15017
15197
|
logTaskChange: (taskId, action, field, oldValue, newValue, agentId) => logTaskChange2(taskId, action, field, oldValue, newValue, agentId, database()),
|
|
15018
15198
|
addComment: (input) => addComment2(input, database()),
|
|
15019
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
|
+
},
|
|
15020
15214
|
getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
|
|
15021
15215
|
getRecentActivity: (limit) => getRecentActivity(limit, database())
|
|
15022
15216
|
},
|
|
@@ -15404,6 +15598,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
15404
15598
|
schemas: {
|
|
15405
15599
|
Task: taskSchema,
|
|
15406
15600
|
Project: projectSchema,
|
|
15601
|
+
TaskComment: taskCommentSchema,
|
|
15407
15602
|
CreateTaskInput: {
|
|
15408
15603
|
type: "object",
|
|
15409
15604
|
required: ["title"],
|
|
@@ -15438,6 +15633,17 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
15438
15633
|
description: { type: "string" },
|
|
15439
15634
|
task_prefix: { type: "string" }
|
|
15440
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
|
+
}
|
|
15441
15647
|
}
|
|
15442
15648
|
}
|
|
15443
15649
|
},
|
|
@@ -15536,6 +15742,61 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
15536
15742
|
}
|
|
15537
15743
|
}
|
|
15538
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
|
+
},
|
|
15539
15800
|
"/v1/tasks/{id}/start": {
|
|
15540
15801
|
post: {
|
|
15541
15802
|
operationId: "startTask",
|
|
@@ -15654,7 +15915,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
15654
15915
|
}
|
|
15655
15916
|
};
|
|
15656
15917
|
}
|
|
15657
|
-
var taskSchema, projectSchema;
|
|
15918
|
+
var taskSchema, projectSchema, taskCommentSchema;
|
|
15658
15919
|
var init_openapi = __esm(() => {
|
|
15659
15920
|
init_package_version();
|
|
15660
15921
|
taskSchema = {
|
|
@@ -15685,6 +15946,20 @@ var init_openapi = __esm(() => {
|
|
|
15685
15946
|
updated_at: { type: "string" }
|
|
15686
15947
|
}
|
|
15687
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
|
+
};
|
|
15688
15963
|
});
|
|
15689
15964
|
|
|
15690
15965
|
// src/server/v1.ts
|
|
@@ -15714,6 +15989,29 @@ function contextFromPrincipal(principal, body) {
|
|
|
15714
15989
|
const agentId = body?.agent_id || principal.agent || undefined;
|
|
15715
15990
|
return agentId ? { agentId } : {};
|
|
15716
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
|
+
}
|
|
15717
16015
|
function normalizeImportSnapshot(raw) {
|
|
15718
16016
|
const body = raw && typeof raw === "object" ? raw : {};
|
|
15719
16017
|
const arr = (v) => Array.isArray(v) ? v : [];
|
|
@@ -15734,7 +16032,7 @@ function normalizeImportSnapshot(raw) {
|
|
|
15734
16032
|
function countSnapshotRecords(s) {
|
|
15735
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);
|
|
15736
16034
|
}
|
|
15737
|
-
async function handleV1Request(req, url) {
|
|
16035
|
+
async function handleV1Request(req, url, dependencies = {}) {
|
|
15738
16036
|
const path = url.pathname;
|
|
15739
16037
|
if (path !== "/v1" && !path.startsWith("/v1/"))
|
|
15740
16038
|
return null;
|
|
@@ -15743,7 +16041,7 @@ async function handleV1Request(req, url) {
|
|
|
15743
16041
|
const requiredScopes = [isWrite ? "todos:write" : "todos:read"];
|
|
15744
16042
|
let verifier;
|
|
15745
16043
|
try {
|
|
15746
|
-
verifier = getCloudVerifier();
|
|
16044
|
+
verifier = (dependencies.getVerifier ?? getCloudVerifier)();
|
|
15747
16045
|
} catch (e) {
|
|
15748
16046
|
return error(503, e.message);
|
|
15749
16047
|
}
|
|
@@ -15752,8 +16050,8 @@ async function handleV1Request(req, url) {
|
|
|
15752
16050
|
return error(decision.status, decision.message, { reason: decision.reason });
|
|
15753
16051
|
}
|
|
15754
16052
|
const principal = decision.principal;
|
|
15755
|
-
await ensureCloudSchema();
|
|
15756
|
-
const store = getCloudStorageAdapter();
|
|
16053
|
+
await (dependencies.ensureSchema ?? ensureCloudSchema)();
|
|
16054
|
+
const store = (dependencies.getStorageAdapter ?? getCloudStorageAdapter)();
|
|
15757
16055
|
const segments = path.split("/").filter(Boolean);
|
|
15758
16056
|
const resource = segments[1];
|
|
15759
16057
|
const id = segments[2];
|
|
@@ -15781,13 +16079,74 @@ async function handleV1Request(req, url) {
|
|
|
15781
16079
|
missing
|
|
15782
16080
|
});
|
|
15783
16081
|
}
|
|
16082
|
+
if (id === "upsert" && !action) {
|
|
16083
|
+
if (method !== "POST")
|
|
16084
|
+
return error(405, `method ${method} not allowed on /v1/tasks/upsert`);
|
|
16085
|
+
if (typeof store.tasks.getByFingerprint !== "function") {
|
|
16086
|
+
return error(501, "fingerprint upsert is not supported by this storage backend");
|
|
16087
|
+
}
|
|
16088
|
+
const body = await readJson(req) ?? {};
|
|
16089
|
+
const fingerprint = typeof body.fingerprint === "string" ? body.fingerprint.trim() : "";
|
|
16090
|
+
if (!fingerprint)
|
|
16091
|
+
return error(400, "fingerprint is required");
|
|
16092
|
+
if (typeof body.title !== "string" || !body.title.trim())
|
|
16093
|
+
return error(400, "title is required");
|
|
16094
|
+
const existing = await store.tasks.getByFingerprint(fingerprint);
|
|
16095
|
+
const metadata = {
|
|
16096
|
+
...existing?.metadata ?? {},
|
|
16097
|
+
...body.metadata ?? {},
|
|
16098
|
+
fingerprint
|
|
16099
|
+
};
|
|
16100
|
+
const fields = { metadata };
|
|
16101
|
+
for (const key of [
|
|
16102
|
+
"title",
|
|
16103
|
+
"description",
|
|
16104
|
+
"priority",
|
|
16105
|
+
"status",
|
|
16106
|
+
"project_id",
|
|
16107
|
+
"assigned_to",
|
|
16108
|
+
"working_dir",
|
|
16109
|
+
"plan_id",
|
|
16110
|
+
"task_list_id",
|
|
16111
|
+
"tags",
|
|
16112
|
+
"due_at",
|
|
16113
|
+
"estimated_minutes",
|
|
16114
|
+
"sla_minutes",
|
|
16115
|
+
"requires_approval",
|
|
16116
|
+
"recurrence_rule",
|
|
16117
|
+
"task_type"
|
|
16118
|
+
]) {
|
|
16119
|
+
const bag = body;
|
|
16120
|
+
if (bag[key] !== undefined)
|
|
16121
|
+
fields[key] = bag[key];
|
|
16122
|
+
}
|
|
16123
|
+
if (!existing) {
|
|
16124
|
+
const task = await store.tasks.create({ ...fields, title: body.title }, contextFromPrincipal(principal, body));
|
|
16125
|
+
return json2({ task, created: true }, 201);
|
|
16126
|
+
}
|
|
16127
|
+
try {
|
|
16128
|
+
const task = await store.tasks.update(existing.id, { ...fields, version: existing.version }, contextFromPrincipal(principal, body));
|
|
16129
|
+
return json2({ task, created: false });
|
|
16130
|
+
} catch (e) {
|
|
16131
|
+
const msg = e.message || "";
|
|
16132
|
+
if (msg.includes("version conflict"))
|
|
16133
|
+
return error(409, msg);
|
|
16134
|
+
throw e;
|
|
16135
|
+
}
|
|
16136
|
+
}
|
|
15784
16137
|
if (!id) {
|
|
15785
16138
|
if (method === "GET") {
|
|
15786
16139
|
const filter = {
|
|
15787
|
-
...url.searchParams.get("status") ? {
|
|
15788
|
-
|
|
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
|
+
} : {},
|
|
15789
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 } : {},
|
|
15790
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") } : {},
|
|
15791
16150
|
...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
|
|
15792
16151
|
...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
|
|
15793
16152
|
...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
|
|
@@ -15811,8 +16170,47 @@ async function handleV1Request(req, url) {
|
|
|
15811
16170
|
if (action) {
|
|
15812
16171
|
if (action === "comments") {
|
|
15813
16172
|
if (method === "GET") {
|
|
15814
|
-
|
|
15815
|
-
|
|
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
|
+
});
|
|
15816
16214
|
}
|
|
15817
16215
|
if (method === "POST") {
|
|
15818
16216
|
const body2 = await readJson(req) ?? {};
|
|
@@ -15830,24 +16228,45 @@ async function handleV1Request(req, url) {
|
|
|
15830
16228
|
type: body2.type,
|
|
15831
16229
|
progress_pct: body2.progress_pct
|
|
15832
16230
|
}, contextFromPrincipal(principal, body2));
|
|
15833
|
-
return json2({ comment }, 201);
|
|
16231
|
+
return json2({ comment: redactComment2(comment) }, 201);
|
|
15834
16232
|
}
|
|
15835
16233
|
return error(405, `method ${method} not allowed on /v1/tasks/:id/comments`);
|
|
15836
16234
|
}
|
|
16235
|
+
if (action === "history") {
|
|
16236
|
+
if (method !== "GET")
|
|
16237
|
+
return error(405, `method ${method} not allowed on /v1/tasks/:id/history`);
|
|
16238
|
+
if (!await store.tasks.get(id))
|
|
16239
|
+
return error(404, "task not found");
|
|
16240
|
+
const history = await store.audit.getTaskHistory(id);
|
|
16241
|
+
return json2({ history, count: history.length });
|
|
16242
|
+
}
|
|
15837
16243
|
if (action === "lock" || action === "unlock") {
|
|
15838
16244
|
if (method !== "POST")
|
|
15839
16245
|
return error(405, `method ${method} not allowed on /v1/tasks/:id/${action}`);
|
|
15840
|
-
if (typeof store.tasks.lock !== "function" || typeof store.tasks.unlock !== "function") {
|
|
15841
|
-
return error(501, "task locking is not supported by this storage backend");
|
|
15842
|
-
}
|
|
15843
16246
|
const body2 = await readJson(req) ?? {};
|
|
15844
16247
|
if (!await store.tasks.get(id))
|
|
15845
16248
|
return error(404, "task not found");
|
|
15846
16249
|
if (action === "lock") {
|
|
15847
|
-
|
|
15848
|
-
|
|
15849
|
-
|
|
15850
|
-
|
|
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);
|
|
15851
16270
|
return json2({ success: released });
|
|
15852
16271
|
}
|
|
15853
16272
|
if (action === "dependencies") {
|
|
@@ -16130,12 +16549,28 @@ async function handleV1Request(req, url) {
|
|
|
16130
16549
|
const activity = await store.audit.getRecentActivity(limit);
|
|
16131
16550
|
return json2({ activity, count: activity.length });
|
|
16132
16551
|
}
|
|
16133
|
-
if (resource === "task-lists"
|
|
16134
|
-
if (method
|
|
16135
|
-
|
|
16136
|
-
|
|
16137
|
-
|
|
16138
|
-
|
|
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" : ""}`);
|
|
16139
16574
|
}
|
|
16140
16575
|
if (resource === "dependencies" && !id) {
|
|
16141
16576
|
if (method !== "GET")
|
|
@@ -16143,8 +16578,8 @@ async function handleV1Request(req, url) {
|
|
|
16143
16578
|
if (typeof store.dependencies?.listAll !== "function") {
|
|
16144
16579
|
return error(501, "dependency edge listing is not supported by this storage backend");
|
|
16145
16580
|
}
|
|
16146
|
-
const
|
|
16147
|
-
return json2({ dependencies, count:
|
|
16581
|
+
const dependencies2 = await store.dependencies.listAll();
|
|
16582
|
+
return json2({ dependencies: dependencies2, count: dependencies2.length });
|
|
16148
16583
|
}
|
|
16149
16584
|
if (resource === "commits" && id) {
|
|
16150
16585
|
if (method !== "GET")
|
|
@@ -16201,12 +16636,16 @@ async function handleV1Request(req, url) {
|
|
|
16201
16636
|
}
|
|
16202
16637
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
16203
16638
|
} catch (e) {
|
|
16639
|
+
if (e instanceof LockError)
|
|
16640
|
+
return error(409, e.message, { code: LockError.code });
|
|
16204
16641
|
return error(500, e.message || "internal error");
|
|
16205
16642
|
}
|
|
16206
16643
|
}
|
|
16207
|
-
var JSON_HEADERS;
|
|
16644
|
+
var JSON_HEADERS, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500;
|
|
16208
16645
|
var init_v1 = __esm(() => {
|
|
16646
|
+
init_types();
|
|
16209
16647
|
init_cloud();
|
|
16648
|
+
init_redaction();
|
|
16210
16649
|
JSON_HEADERS = { "Content-Type": "application/json" };
|
|
16211
16650
|
});
|
|
16212
16651
|
|
|
@@ -19583,7 +20022,7 @@ class JSONSchemaGenerator {
|
|
|
19583
20022
|
if (val === undefined) {
|
|
19584
20023
|
if (this.unrepresentable === "throw") {
|
|
19585
20024
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
19586
|
-
}
|
|
20025
|
+
}
|
|
19587
20026
|
} else if (typeof val === "bigint") {
|
|
19588
20027
|
if (this.unrepresentable === "throw") {
|
|
19589
20028
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -43949,14 +44388,18 @@ function unwrapTask(raw) {
|
|
|
43949
44388
|
}
|
|
43950
44389
|
function toListQuery(filter = {}) {
|
|
43951
44390
|
const query = {};
|
|
43952
|
-
if (
|
|
43953
|
-
query["status"] = filter.status;
|
|
43954
|
-
if (
|
|
43955
|
-
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;
|
|
43956
44395
|
if (filter.project_id)
|
|
43957
44396
|
query["project_id"] = filter.project_id;
|
|
44397
|
+
if (filter.parent_id !== undefined)
|
|
44398
|
+
query["parent_id"] = filter.parent_id ?? "";
|
|
43958
44399
|
if (filter.plan_id)
|
|
43959
44400
|
query["plan_id"] = filter.plan_id;
|
|
44401
|
+
if (filter.task_list_id)
|
|
44402
|
+
query["task_list_id"] = filter.task_list_id;
|
|
43960
44403
|
if (filter.assigned_to)
|
|
43961
44404
|
query["assigned_to"] = filter.assigned_to;
|
|
43962
44405
|
if (filter.agent_id)
|
|
@@ -44006,10 +44449,19 @@ async function cloudListProjects(client) {
|
|
|
44006
44449
|
}
|
|
44007
44450
|
async function cloudAddComment(client, taskId, input) {
|
|
44008
44451
|
const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/comments`, input);
|
|
44009
|
-
|
|
44010
|
-
|
|
44011
|
-
|
|
44012
|
-
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) };
|
|
44013
44465
|
}
|
|
44014
44466
|
async function cloudCountTasks(client, filter = {}) {
|
|
44015
44467
|
const { limit: _drop, offset: _o, ...rest } = filter;
|
|
@@ -44042,6 +44494,7 @@ async function cloudReleaseAgent(client, idOrName, sessionId) {
|
|
|
44042
44494
|
var _cache;
|
|
44043
44495
|
var init_cloud_router = __esm(() => {
|
|
44044
44496
|
init_storage();
|
|
44497
|
+
init_redaction();
|
|
44045
44498
|
});
|
|
44046
44499
|
|
|
44047
44500
|
// src/mcp/tools/task-crud.ts
|
|
@@ -69380,7 +69833,7 @@ var require_to_json_schema = __commonJS((exports) => {
|
|
|
69380
69833
|
if (val === undefined) {
|
|
69381
69834
|
if (this.unrepresentable === "throw") {
|
|
69382
69835
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
69383
|
-
}
|
|
69836
|
+
}
|
|
69384
69837
|
} else if (typeof val === "bigint") {
|
|
69385
69838
|
if (this.unrepresentable === "throw") {
|
|
69386
69839
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -89861,7 +90314,7 @@ function captureEnvironmentSnapshot(input = {}) {
|
|
|
89861
90314
|
}
|
|
89862
90315
|
function writeEnvironmentSnapshot(snapshot, outputPath) {
|
|
89863
90316
|
const path = outputPath ? resolve16(outputPath) : join14(defaultSnapshotDir(), `${snapshot.id}.json`);
|
|
89864
|
-
|
|
90317
|
+
ensureDir(dirname9(path));
|
|
89865
90318
|
writeJsonFile(path, snapshot);
|
|
89866
90319
|
return path;
|
|
89867
90320
|
}
|
|
@@ -91048,11 +91501,19 @@ function printHelp2() {
|
|
|
91048
91501
|
|
|
91049
91502
|
Start the @hasna/todos dashboard server.
|
|
91050
91503
|
|
|
91504
|
+
Commands:
|
|
91505
|
+
migrate Apply idempotent schema migrations
|
|
91506
|
+
redact-comments Preview historical comment redaction (dry-run by default)
|
|
91507
|
+
|
|
91051
91508
|
Options:
|
|
91052
91509
|
--port <port> HTTP port to bind. Defaults to ${DEFAULT_PORT}
|
|
91053
91510
|
--host <host> Hostname to bind. Defaults to 127.0.0.1
|
|
91054
91511
|
--api-key <key> Require this API key for dashboard/API requests
|
|
91055
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
|
|
91056
91517
|
-V, --version output the version number
|
|
91057
91518
|
-h, --help display help for command
|
|
91058
91519
|
|
|
@@ -91091,7 +91552,14 @@ async function findFreePort(start) {
|
|
|
91091
91552
|
return start;
|
|
91092
91553
|
}
|
|
91093
91554
|
async function runMigrate() {
|
|
91094
|
-
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));
|
|
91095
91563
|
if (!resolveCloudDatabaseUrl2()) {
|
|
91096
91564
|
console.error("migrate: no database URL (HASNA_TODOS_DATABASE_URL / TODOS_DATABASE_URL / DATABASE_URL)");
|
|
91097
91565
|
process.exit(2);
|
|
@@ -91103,15 +91571,55 @@ async function runMigrate() {
|
|
|
91103
91571
|
console.log("migrate: normalizing legacy double-encoded jsonb payloads\u2026");
|
|
91104
91572
|
const normalized = await normalizeCloudPayloads2();
|
|
91105
91573
|
console.log(`migrate: normalized ${normalized} payload row(s)`);
|
|
91574
|
+
console.log("migrate: prebuilding comment cursor index concurrently\u2026");
|
|
91575
|
+
await ensureCloudCommentCursorIndex2();
|
|
91106
91576
|
console.log("migrate: done");
|
|
91107
91577
|
await closeCloud2();
|
|
91108
91578
|
process.exit(0);
|
|
91109
91579
|
}
|
|
91110
|
-
async function
|
|
91111
|
-
|
|
91112
|
-
|
|
91113
|
-
|
|
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);
|
|
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();
|
|
91114
91620
|
}
|
|
91621
|
+
}
|
|
91622
|
+
async function main2() {
|
|
91115
91623
|
if (hasVersionFlag2()) {
|
|
91116
91624
|
console.log(getPackageVersion());
|
|
91117
91625
|
return;
|
|
@@ -91120,6 +91628,14 @@ async function main2() {
|
|
|
91120
91628
|
printHelp2();
|
|
91121
91629
|
return;
|
|
91122
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
|
+
}
|
|
91123
91639
|
const explicitPortArg = process.argv.some((a) => a === "--port" || a.startsWith("--port="));
|
|
91124
91640
|
const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
91125
91641
|
const requestedPort = explicitPortArg ? parsePort() : envPort ?? parsePort();
|