@hasna/todos 0.13.1 → 0.13.3
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/README.md +80 -15
- package/dist/cli/cloud-router.d.ts +78 -12
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/doctor-integrity.d.ts +106 -0
- package/dist/cli/doctor-integrity.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts +2 -2
- package/dist/cli/index.js +1748 -1012
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +137 -39
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/findings.d.ts +1 -0
- package/dist/db/findings.d.ts.map +1 -1
- package/dist/db/integrity.d.ts +20 -0
- package/dist/db/integrity.d.ts.map +1 -0
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +592 -147
- package/dist/lib/context-packs.d.ts +1 -0
- package/dist/lib/context-packs.d.ts.map +1 -1
- package/dist/lib/dependency-graph.d.ts +19 -11
- package/dist/lib/dependency-graph.d.ts.map +1 -1
- package/dist/lib/doctor.d.ts +20 -0
- package/dist/lib/doctor.d.ts.map +1 -1
- package/dist/lib/integrity.d.ts +215 -0
- package/dist/lib/integrity.d.ts.map +1 -0
- package/dist/lib/native-storage-status.d.ts +2 -2
- package/dist/lib/native-storage-status.d.ts.map +1 -1
- package/dist/lib/redaction.d.ts.map +1 -1
- package/dist/lib/release-compatibility.d.ts.map +1 -1
- package/dist/lib/task-dedupe.d.ts +15 -0
- package/dist/lib/task-dedupe.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +544 -97
- package/dist/mcp.js +8 -4
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +144 -39
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/types.d.ts +42 -0
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +1 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +8 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +585 -118
- package/dist/server/openapi.d.ts +12 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/port.d.ts +52 -0
- package/dist/server/port.d.ts.map +1 -0
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/config.d.ts +23 -5
- package/dist/storage/config.d.ts.map +1 -1
- package/dist/storage/factory.d.ts +9 -3
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/hybrid.d.ts +7 -0
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/index.d.ts +3 -3
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +43 -8
- 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.d.ts +3 -3
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +619 -256
- package/dist/testing.d.ts +79 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +107 -0
- package/dist/types/index.d.ts +27 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +8 -4
package/dist/mcp/index.js
CHANGED
|
@@ -41,7 +41,10 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
41
41
|
var __require = import.meta.require;
|
|
42
42
|
|
|
43
43
|
// src/types/index.ts
|
|
44
|
-
|
|
44
|
+
function isBlockingDependencyStatus(status) {
|
|
45
|
+
return status !== "completed" && status !== "cancelled";
|
|
46
|
+
}
|
|
47
|
+
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
45
48
|
var init_types = __esm(() => {
|
|
46
49
|
TASK_STATUSES = [
|
|
47
50
|
"pending",
|
|
@@ -74,6 +77,22 @@ var init_types = __esm(() => {
|
|
|
74
77
|
this.name = "TaskNotFoundError";
|
|
75
78
|
}
|
|
76
79
|
};
|
|
80
|
+
TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
|
|
81
|
+
reference;
|
|
82
|
+
static code = "TASK_REFERENCE_AMBIGUOUS";
|
|
83
|
+
candidateTaskIds;
|
|
84
|
+
candidateProjectIds;
|
|
85
|
+
constructor(reference, candidates) {
|
|
86
|
+
const candidateTaskIds = [...new Set(candidates.map((candidate) => candidate.task_id))].sort();
|
|
87
|
+
const candidateProjectIds = [...new Set(candidates.map((candidate) => candidate.project_id).filter((projectId) => typeof projectId === "string" && projectId.length > 0))].sort();
|
|
88
|
+
const projectList = candidateProjectIds.length > 0 ? candidateProjectIds.join(", ") : "(unscoped)";
|
|
89
|
+
super(`Task reference is ambiguous: "${reference}". Candidate project IDs: ${projectList}. ` + "Use a full task UUID.");
|
|
90
|
+
this.reference = reference;
|
|
91
|
+
this.name = "TaskReferenceAmbiguousError";
|
|
92
|
+
this.candidateTaskIds = candidateTaskIds;
|
|
93
|
+
this.candidateProjectIds = candidateProjectIds;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
77
96
|
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
78
97
|
projectId;
|
|
79
98
|
static code = "PROJECT_NOT_FOUND";
|
|
@@ -3763,15 +3782,18 @@ var init_identity_mapping = __esm(() => {
|
|
|
3763
3782
|
var exports_config = {};
|
|
3764
3783
|
__export(exports_config, {
|
|
3765
3784
|
parseStorageMode: () => parseStorageMode,
|
|
3785
|
+
parseStorageBackend: () => parseStorageBackend,
|
|
3766
3786
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
3767
3787
|
loadStorageConfig: () => loadStorageConfig,
|
|
3768
3788
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
3769
3789
|
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
3790
|
+
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
3770
3791
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
3771
3792
|
getTodosStorageMode: () => getTodosStorageMode,
|
|
3772
3793
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
3773
3794
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
3774
3795
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
3796
|
+
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
3775
3797
|
getStorageMode: () => getStorageMode,
|
|
3776
3798
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
3777
3799
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
@@ -3796,7 +3818,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
|
|
|
3796
3818
|
};
|
|
3797
3819
|
}
|
|
3798
3820
|
function loadTodosStorageConfig(env = process.env) {
|
|
3799
|
-
const mode =
|
|
3821
|
+
const mode = getTodosStorageBackend(env);
|
|
3800
3822
|
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
3801
3823
|
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
3802
3824
|
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
@@ -3833,29 +3855,41 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
3833
3855
|
function loadStorageConfig(env = process.env) {
|
|
3834
3856
|
return loadTodosStorageConfig(env);
|
|
3835
3857
|
}
|
|
3858
|
+
function isTodosPostgresBackend(config) {
|
|
3859
|
+
return config.mode === "postgres";
|
|
3860
|
+
}
|
|
3836
3861
|
function isTodosRemoteStorageEnabled(config) {
|
|
3837
|
-
return config
|
|
3862
|
+
return isTodosPostgresBackend(config);
|
|
3838
3863
|
}
|
|
3839
3864
|
function assertTodosRemoteStorageConfig(config) {
|
|
3840
|
-
if (!
|
|
3865
|
+
if (!isTodosPostgresBackend(config))
|
|
3841
3866
|
return;
|
|
3842
3867
|
if (!config.database?.url) {
|
|
3843
3868
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
3844
3869
|
}
|
|
3845
3870
|
}
|
|
3846
|
-
function
|
|
3871
|
+
function parseStorageBackend(value) {
|
|
3847
3872
|
const normalized = clean(value)?.toLowerCase();
|
|
3848
3873
|
if (!normalized)
|
|
3849
|
-
return "
|
|
3850
|
-
if (normalized === "
|
|
3874
|
+
return "sqlite";
|
|
3875
|
+
if (normalized === "sqlite" || normalized === "postgres")
|
|
3851
3876
|
return normalized;
|
|
3852
|
-
|
|
3877
|
+
const legacy = LEGACY_BACKEND_TOKENS[normalized];
|
|
3878
|
+
if (legacy)
|
|
3879
|
+
return legacy;
|
|
3880
|
+
throw new Error(`${TODOS_STORAGE_ENV.mode} must be sqlite or postgres (legacy values local and remote are accepted)`);
|
|
3881
|
+
}
|
|
3882
|
+
function parseStorageMode(value) {
|
|
3883
|
+
return parseStorageBackend(value);
|
|
3884
|
+
}
|
|
3885
|
+
function getTodosStorageBackend(env = process.env) {
|
|
3886
|
+
return parseStorageBackend(readStorageEnv(env, "mode").value);
|
|
3853
3887
|
}
|
|
3854
3888
|
function getTodosStorageMode(env = process.env) {
|
|
3855
|
-
return
|
|
3889
|
+
return getTodosStorageBackend(env);
|
|
3856
3890
|
}
|
|
3857
3891
|
function getStorageMode(env = process.env) {
|
|
3858
|
-
return
|
|
3892
|
+
return getTodosStorageBackend(env);
|
|
3859
3893
|
}
|
|
3860
3894
|
function isTodosShadowEnabled(env = process.env) {
|
|
3861
3895
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
@@ -3866,8 +3900,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
|
|
|
3866
3900
|
function assertTodosShadowConfig(config, env = process.env) {
|
|
3867
3901
|
if (!isTodosShadowEnabled(env))
|
|
3868
3902
|
return;
|
|
3869
|
-
if (config.mode !== "
|
|
3870
|
-
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires
|
|
3903
|
+
if (config.mode !== "sqlite") {
|
|
3904
|
+
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
|
|
3871
3905
|
}
|
|
3872
3906
|
if (!config.database?.url) {
|
|
3873
3907
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
|
|
@@ -3923,7 +3957,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
3923
3957
|
}
|
|
3924
3958
|
return parsed;
|
|
3925
3959
|
}
|
|
3926
|
-
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
|
|
3960
|
+
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos", LEGACY_BACKEND_TOKENS;
|
|
3927
3961
|
var init_config = __esm(() => {
|
|
3928
3962
|
TODOS_STORAGE_TABLES = [
|
|
3929
3963
|
"todos_sync_records",
|
|
@@ -3964,6 +3998,14 @@ var init_config = __esm(() => {
|
|
|
3964
3998
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
3965
3999
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
3966
4000
|
};
|
|
4001
|
+
LEGACY_BACKEND_TOKENS = {
|
|
4002
|
+
local: "sqlite",
|
|
4003
|
+
remote: "postgres",
|
|
4004
|
+
postgresql: "postgres",
|
|
4005
|
+
hybrid: "postgres",
|
|
4006
|
+
self_hosted: "postgres",
|
|
4007
|
+
cloud: "postgres"
|
|
4008
|
+
};
|
|
3967
4009
|
});
|
|
3968
4010
|
|
|
3969
4011
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -4097,6 +4139,30 @@ function findGitRoot(startDir) {
|
|
|
4097
4139
|
}
|
|
4098
4140
|
return null;
|
|
4099
4141
|
}
|
|
4142
|
+
function getGlobalDbPath() {
|
|
4143
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4144
|
+
return join(home, ".hasna", "todos", "todos.db");
|
|
4145
|
+
}
|
|
4146
|
+
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
4147
|
+
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
4148
|
+
}
|
|
4149
|
+
function getCliCommand(args = process.argv.slice(2)) {
|
|
4150
|
+
const globalOptionsWithValues = new Set(["--project", "--agent", "--session"]);
|
|
4151
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
4152
|
+
const arg = args[index];
|
|
4153
|
+
if (globalOptionsWithValues.has(arg)) {
|
|
4154
|
+
index += 1;
|
|
4155
|
+
continue;
|
|
4156
|
+
}
|
|
4157
|
+
if (arg.startsWith("-"))
|
|
4158
|
+
continue;
|
|
4159
|
+
return arg;
|
|
4160
|
+
}
|
|
4161
|
+
return null;
|
|
4162
|
+
}
|
|
4163
|
+
function canCreateScopedProjectDb(args = process.argv.slice(2)) {
|
|
4164
|
+
return getCliCommand(args) === "project-bootstrap" && !args.some((arg) => arg === "--dry-run" || arg.startsWith("--dry-run="));
|
|
4165
|
+
}
|
|
4100
4166
|
function getDbPath() {
|
|
4101
4167
|
if (process.env["HASNA_TODOS_DB_PATH"]) {
|
|
4102
4168
|
return process.env["HASNA_TODOS_DB_PATH"];
|
|
@@ -4104,18 +4170,19 @@ function getDbPath() {
|
|
|
4104
4170
|
if (process.env["TODOS_DB_PATH"]) {
|
|
4105
4171
|
return process.env["TODOS_DB_PATH"];
|
|
4106
4172
|
}
|
|
4173
|
+
if (hasExplicitProjectArg())
|
|
4174
|
+
return getGlobalDbPath();
|
|
4107
4175
|
const cwd = process.cwd();
|
|
4108
4176
|
const nearest = findNearestProjectDb(cwd);
|
|
4109
4177
|
if (nearest)
|
|
4110
4178
|
return nearest;
|
|
4111
4179
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4112
4180
|
const gitRoot = findGitRoot(cwd);
|
|
4113
|
-
if (gitRoot) {
|
|
4181
|
+
if (gitRoot && canCreateScopedProjectDb()) {
|
|
4114
4182
|
return join(gitRoot, ".hasna", "todos", "todos.db");
|
|
4115
4183
|
}
|
|
4116
4184
|
}
|
|
4117
|
-
|
|
4118
|
-
return join(home, ".hasna", "todos", "todos.db");
|
|
4185
|
+
return getGlobalDbPath();
|
|
4119
4186
|
}
|
|
4120
4187
|
function getDatabasePath() {
|
|
4121
4188
|
return getDbPath();
|
|
@@ -4131,8 +4198,8 @@ function ensureDir(filePath) {
|
|
|
4131
4198
|
function openDatabase(path) {
|
|
4132
4199
|
ensureDir(path);
|
|
4133
4200
|
const db = new Database(path);
|
|
4134
|
-
db.run("PRAGMA journal_mode = WAL");
|
|
4135
4201
|
db.run("PRAGMA busy_timeout = 5000");
|
|
4202
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
4136
4203
|
db.run("PRAGMA foreign_keys = ON");
|
|
4137
4204
|
runMigrations(db);
|
|
4138
4205
|
ensureAgentIdentitySchema(db);
|
|
@@ -4218,10 +4285,13 @@ function resolvePartialId(db, table, partialId) {
|
|
|
4218
4285
|
return null;
|
|
4219
4286
|
}
|
|
4220
4287
|
if (table === "tasks") {
|
|
4221
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
|
|
4288
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = LOWER(?) ORDER BY project_id, id").all(partialId);
|
|
4222
4289
|
if (shortIdRows.length === 1) {
|
|
4223
4290
|
return shortIdRows[0].id;
|
|
4224
4291
|
}
|
|
4292
|
+
if (shortIdRows.length > 1) {
|
|
4293
|
+
throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
4294
|
+
}
|
|
4225
4295
|
}
|
|
4226
4296
|
if (table === "task_lists") {
|
|
4227
4297
|
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
@@ -9107,11 +9177,15 @@ function secretPatterns() {
|
|
|
9107
9177
|
return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
|
|
9108
9178
|
}
|
|
9109
9179
|
function isRedactionPlaceholderMatch(match) {
|
|
9110
|
-
const placeholder = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
9111
9180
|
const trimmed = match.trim();
|
|
9112
|
-
return new RegExp(`^${
|
|
9181
|
+
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(trimmed) || new RegExp(`=\\s*['"]?${REDACTION_PLACEHOLDER}['"]?$`).test(trimmed);
|
|
9182
|
+
}
|
|
9183
|
+
function isRedactionPlaceholderKey(key) {
|
|
9184
|
+
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(key.trim());
|
|
9113
9185
|
}
|
|
9114
9186
|
function isSecretKey(key) {
|
|
9187
|
+
if (isRedactionPlaceholderKey(key))
|
|
9188
|
+
return false;
|
|
9115
9189
|
if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
|
|
9116
9190
|
return false;
|
|
9117
9191
|
if (DEFAULT_SECRET_KEY_PATTERN.test(key))
|
|
@@ -9169,7 +9243,7 @@ function upsertSecretSafetyConfig(input) {
|
|
|
9169
9243
|
saveConfig({ ...config, secret_safety: next });
|
|
9170
9244
|
return next;
|
|
9171
9245
|
}
|
|
9172
|
-
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
|
|
9246
|
+
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
9173
9247
|
var init_redaction = __esm(() => {
|
|
9174
9248
|
init_config2();
|
|
9175
9249
|
DEFAULT_SECRET_PATTERNS = [
|
|
@@ -9193,6 +9267,7 @@ var init_redaction = __esm(() => {
|
|
|
9193
9267
|
"completion_tokens",
|
|
9194
9268
|
"cost_tokens"
|
|
9195
9269
|
]);
|
|
9270
|
+
REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
9196
9271
|
});
|
|
9197
9272
|
|
|
9198
9273
|
// src/lib/secret-redaction.ts
|
|
@@ -9228,7 +9303,7 @@ function scanTextForSecrets(text, options = {}) {
|
|
|
9228
9303
|
};
|
|
9229
9304
|
}
|
|
9230
9305
|
function redactText(text, options = {}) {
|
|
9231
|
-
const placeholder = options.placeholder ??
|
|
9306
|
+
const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
|
|
9232
9307
|
let out = text;
|
|
9233
9308
|
for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
|
|
9234
9309
|
out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
|
|
@@ -9255,7 +9330,7 @@ function redactExportRecord(record) {
|
|
|
9255
9330
|
}
|
|
9256
9331
|
return base;
|
|
9257
9332
|
}
|
|
9258
|
-
var SECRET_REDACTION_SCHEMA,
|
|
9333
|
+
var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
|
|
9259
9334
|
var init_secret_redaction = __esm(() => {
|
|
9260
9335
|
init_redaction();
|
|
9261
9336
|
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
@@ -13420,10 +13495,11 @@ function getTaskWithRelations(id, db) {
|
|
|
13420
13495
|
JOIN task_dependencies td ON td.depends_on = t.id
|
|
13421
13496
|
WHERE td.task_id = ?`).all(id);
|
|
13422
13497
|
const dependencies = depRows.map(rowToTask);
|
|
13423
|
-
const
|
|
13498
|
+
const blocked_by = dependencies.filter((dep) => isBlockingDependencyStatus(dep.status));
|
|
13499
|
+
const blocksRows = d.query(`SELECT t.* FROM tasks t
|
|
13424
13500
|
JOIN task_dependencies td ON td.task_id = t.id
|
|
13425
13501
|
WHERE td.depends_on = ?`).all(id);
|
|
13426
|
-
const
|
|
13502
|
+
const blocks = blocksRows.map(rowToTask);
|
|
13427
13503
|
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
13428
13504
|
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
13429
13505
|
const checklist = getChecklist(id, d);
|
|
@@ -13432,6 +13508,7 @@ function getTaskWithRelations(id, db) {
|
|
|
13432
13508
|
subtasks,
|
|
13433
13509
|
dependencies,
|
|
13434
13510
|
blocked_by,
|
|
13511
|
+
blocks,
|
|
13435
13512
|
comments,
|
|
13436
13513
|
parent,
|
|
13437
13514
|
checklist
|
|
@@ -20012,9 +20089,6 @@ function cleanMode(value) {
|
|
|
20012
20089
|
const normalized = value?.trim().toLowerCase();
|
|
20013
20090
|
return normalized || null;
|
|
20014
20091
|
}
|
|
20015
|
-
function modeRole(mode) {
|
|
20016
|
-
return mode === "local" ? "local" : "remote";
|
|
20017
|
-
}
|
|
20018
20092
|
function resolveTodosCliStorageMode(env = process.env) {
|
|
20019
20093
|
for (const source of ["HASNA_TODOS_STORAGE_MODE", "TODOS_STORAGE_MODE"]) {
|
|
20020
20094
|
if (env[source] !== undefined && env[source].trim() === "") {
|
|
@@ -20027,22 +20101,25 @@ function resolveTodosCliStorageMode(env = process.env) {
|
|
|
20027
20101
|
["HASNA_TODOS_STORAGE_MODE", canonical],
|
|
20028
20102
|
["TODOS_STORAGE_MODE", fallback]
|
|
20029
20103
|
]) {
|
|
20030
|
-
if (value && !
|
|
20031
|
-
throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source}=${value} must be local
|
|
20104
|
+
if (value && !(value in TRANSPORT_TOKENS)) {
|
|
20105
|
+
throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source}=${value} must be sqlite (local file) or http (hosted /v1 authority); ` + "legacy values local and remote are accepted; " + "local SQLite fallback is disabled for invalid routing state");
|
|
20032
20106
|
}
|
|
20033
20107
|
}
|
|
20034
|
-
|
|
20108
|
+
const canonicalTransport = canonical ? TRANSPORT_TOKENS[canonical] : null;
|
|
20109
|
+
const fallbackTransport = fallback ? TRANSPORT_TOKENS[fallback] : null;
|
|
20110
|
+
if (canonicalTransport && fallbackTransport && canonicalTransport !== fallbackTransport) {
|
|
20035
20111
|
throw new Error(`REMOTE_STORAGE_MODE_CONFLICT: HASNA_TODOS_STORAGE_MODE=${canonical} conflicts with ` + `TODOS_STORAGE_MODE=${fallback}; local SQLite fallback is disabled`);
|
|
20036
20112
|
}
|
|
20037
|
-
const
|
|
20113
|
+
const transport = canonicalTransport ?? fallbackTransport ?? "sqlite";
|
|
20038
20114
|
return {
|
|
20039
|
-
mode,
|
|
20040
|
-
|
|
20115
|
+
mode: transport,
|
|
20116
|
+
transport,
|
|
20117
|
+
selected: transport === "http",
|
|
20041
20118
|
source: canonical ? "HASNA_TODOS_STORAGE_MODE" : fallback ? "TODOS_STORAGE_MODE" : "default"
|
|
20042
20119
|
};
|
|
20043
20120
|
}
|
|
20044
|
-
function
|
|
20045
|
-
return resolveTodosCliStorageMode(env).
|
|
20121
|
+
function requestedTransport(env) {
|
|
20122
|
+
return resolveTodosCliStorageMode(env).transport;
|
|
20046
20123
|
}
|
|
20047
20124
|
function normalizeRemoteAuthorityUrl(value) {
|
|
20048
20125
|
const trimmed = value?.trim();
|
|
@@ -20095,7 +20172,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
20095
20172
|
return {
|
|
20096
20173
|
selected: false,
|
|
20097
20174
|
ok: true,
|
|
20098
|
-
mode
|
|
20175
|
+
mode,
|
|
20099
20176
|
api_url_configured: false,
|
|
20100
20177
|
api_key_configured: false,
|
|
20101
20178
|
v1_base_url: null,
|
|
@@ -20207,8 +20284,7 @@ async function requiredRemoteRoute(client, route, request) {
|
|
|
20207
20284
|
}
|
|
20208
20285
|
}
|
|
20209
20286
|
function getTodosCloudClient(env = process.env) {
|
|
20210
|
-
|
|
20211
|
-
if (!CLOUD_MODES.has(mode))
|
|
20287
|
+
if (requestedTransport(env) !== "http")
|
|
20212
20288
|
return null;
|
|
20213
20289
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
20214
20290
|
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
@@ -20243,13 +20319,38 @@ function toListQuery(filter = {}) {
|
|
|
20243
20319
|
query["assigned_to"] = filter.assigned_to;
|
|
20244
20320
|
if (filter.agent_id)
|
|
20245
20321
|
query["agent_id"] = filter.agent_id;
|
|
20322
|
+
if (filter.tags?.length)
|
|
20323
|
+
query["tags"] = filter.tags.join(",");
|
|
20246
20324
|
if (typeof filter.limit === "number")
|
|
20247
20325
|
query["limit"] = filter.limit;
|
|
20248
20326
|
if (typeof filter.offset === "number")
|
|
20249
20327
|
query["offset"] = filter.offset;
|
|
20250
20328
|
return query;
|
|
20251
20329
|
}
|
|
20330
|
+
async function fetchListTagsCapability(client) {
|
|
20331
|
+
const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
|
|
20332
|
+
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
20333
|
+
return false;
|
|
20334
|
+
const paths = document["paths"];
|
|
20335
|
+
const tasksPath = paths && typeof paths === "object" && !Array.isArray(paths) ? paths["/v1/tasks"] : undefined;
|
|
20336
|
+
const get = tasksPath && typeof tasksPath === "object" && !Array.isArray(tasksPath) ? tasksPath["get"] : undefined;
|
|
20337
|
+
const parameters = get && typeof get === "object" && !Array.isArray(get) ? get["parameters"] : undefined;
|
|
20338
|
+
return Array.isArray(parameters) && parameters.some((parameter) => parameter && typeof parameter === "object" && parameter["name"] === "tags");
|
|
20339
|
+
}
|
|
20340
|
+
async function requireTagsFilterCapability(client) {
|
|
20341
|
+
const authority = remoteAuthorityBase(client);
|
|
20342
|
+
let capability = listTagsCapabilityCache.get(authority);
|
|
20343
|
+
if (!capability) {
|
|
20344
|
+
capability = fetchListTagsCapability(client);
|
|
20345
|
+
listTagsCapabilityCache.set(authority, capability);
|
|
20346
|
+
}
|
|
20347
|
+
if (!await capability) {
|
|
20348
|
+
throw new Error(`REMOTE_TAGS_FILTER_UNSUPPORTED: configured Todos authority ${authority} does not advertise the tags ` + "query param on GET /v1/tasks; deploy the current @hasna/todos /v1 server to filter by tag; " + "no unfiltered task read was issued");
|
|
20349
|
+
}
|
|
20350
|
+
}
|
|
20252
20351
|
async function cloudListTasks(client, filter = {}) {
|
|
20352
|
+
if (filter.tags?.length)
|
|
20353
|
+
await requireTagsFilterCapability(client);
|
|
20253
20354
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
|
|
20254
20355
|
const envelope = res.raw;
|
|
20255
20356
|
return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
|
|
@@ -20357,6 +20458,8 @@ function redactComment(comment) {
|
|
|
20357
20458
|
}
|
|
20358
20459
|
async function cloudCountTasks(client, filter = {}) {
|
|
20359
20460
|
const { limit: _drop, offset: _o, ...rest } = filter;
|
|
20461
|
+
if (rest.tags?.length)
|
|
20462
|
+
await requireTagsFilterCapability(client);
|
|
20360
20463
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: { ...toListQuery(rest), limit: 1 } }));
|
|
20361
20464
|
const envelope = res.raw;
|
|
20362
20465
|
if (typeof envelope?.total === "number")
|
|
@@ -20419,14 +20522,23 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
20419
20522
|
}
|
|
20420
20523
|
throw new Error(`Task list not found: "${input}"`);
|
|
20421
20524
|
}
|
|
20422
|
-
var UUID_RE,
|
|
20525
|
+
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, listTagsCapabilityCache;
|
|
20423
20526
|
var init_cloud_router = __esm(() => {
|
|
20527
|
+
init_types();
|
|
20424
20528
|
init_redaction();
|
|
20425
20529
|
init_http_client();
|
|
20426
20530
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20427
|
-
|
|
20428
|
-
|
|
20531
|
+
TRANSPORT_TOKENS = {
|
|
20532
|
+
sqlite: "sqlite",
|
|
20533
|
+
http: "http",
|
|
20534
|
+
local: "sqlite",
|
|
20535
|
+
remote: "http",
|
|
20536
|
+
self_hosted: "http",
|
|
20537
|
+
cloud: "http",
|
|
20538
|
+
hybrid: "http"
|
|
20539
|
+
};
|
|
20429
20540
|
completionCapabilityCache = new Map;
|
|
20541
|
+
listTagsCapabilityCache = new Map;
|
|
20430
20542
|
});
|
|
20431
20543
|
|
|
20432
20544
|
// src/mcp/tools/task-crud.ts
|
|
@@ -24012,12 +24124,11 @@ ${task.description || ""}`.split(/\r?\n/);
|
|
|
24012
24124
|
function normalizeStackLocation(value) {
|
|
24013
24125
|
return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
|
|
24014
24126
|
}
|
|
24015
|
-
function
|
|
24127
|
+
function fingerprintContent(task) {
|
|
24016
24128
|
const body = task.description || "";
|
|
24017
24129
|
const text = `${task.title}
|
|
24018
24130
|
${body}`;
|
|
24019
24131
|
return {
|
|
24020
|
-
task,
|
|
24021
24132
|
title: normalizeText(task.title),
|
|
24022
24133
|
body: normalizeText(body),
|
|
24023
24134
|
text: normalizeText(text),
|
|
@@ -24026,6 +24137,9 @@ ${body}`;
|
|
|
24026
24137
|
stackKeys: stackKeysFor(task)
|
|
24027
24138
|
};
|
|
24028
24139
|
}
|
|
24140
|
+
function fingerprint(task) {
|
|
24141
|
+
return { task, ...fingerprintContent(task) };
|
|
24142
|
+
}
|
|
24029
24143
|
function intersects(left, right) {
|
|
24030
24144
|
for (const value of left)
|
|
24031
24145
|
if (right.has(value))
|
|
@@ -25362,7 +25476,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
25362
25476
|
"todos --help",
|
|
25363
25477
|
"todos-mcp --help",
|
|
25364
25478
|
"todos-serve --help",
|
|
25365
|
-
"todos doctor"
|
|
25479
|
+
"todos doctor --no-fail-on-findings"
|
|
25366
25480
|
],
|
|
25367
25481
|
smoke_tests: [
|
|
25368
25482
|
"command -v todos",
|
|
@@ -25370,7 +25484,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
25370
25484
|
"command -v todos-serve",
|
|
25371
25485
|
"todos --version",
|
|
25372
25486
|
"todos --help",
|
|
25373
|
-
"todos doctor"
|
|
25487
|
+
"todos doctor --no-fail-on-findings"
|
|
25374
25488
|
],
|
|
25375
25489
|
rollback: [
|
|
25376
25490
|
"npm view @hasna/todos versions --json",
|
|
@@ -29016,6 +29130,211 @@ var init_auto_assign = __esm(() => {
|
|
|
29016
29130
|
init_agents();
|
|
29017
29131
|
});
|
|
29018
29132
|
|
|
29133
|
+
// src/lib/integrity.ts
|
|
29134
|
+
function resolveIntegritySeverity(spec, measurement) {
|
|
29135
|
+
if (spec.kind === "dangling")
|
|
29136
|
+
return "error";
|
|
29137
|
+
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
29138
|
+
return "error";
|
|
29139
|
+
return spec.base_severity;
|
|
29140
|
+
}
|
|
29141
|
+
function plural(entity, count) {
|
|
29142
|
+
const [one, many] = ENTITY_LABEL[entity];
|
|
29143
|
+
return count === 1 ? one : many;
|
|
29144
|
+
}
|
|
29145
|
+
function formatIntegrityMessage(spec, measurement) {
|
|
29146
|
+
const noun = plural(spec.entity, measurement.count);
|
|
29147
|
+
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
29148
|
+
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
29149
|
+
return spec.kind === "missing" ? `${measurement.count} ${noun} ${verb[0]} no ${spec.field}${open}` : `${measurement.count} ${noun} ${verb[1]} a ${spec.target.replace("_", " ")} that is not registered${open}`;
|
|
29150
|
+
}
|
|
29151
|
+
function measuredCondition(spec, measurement, source) {
|
|
29152
|
+
return {
|
|
29153
|
+
id: spec.id,
|
|
29154
|
+
entity: spec.entity,
|
|
29155
|
+
field: spec.field,
|
|
29156
|
+
kind: spec.kind,
|
|
29157
|
+
count: measurement.count,
|
|
29158
|
+
open_count: measurement.open_count,
|
|
29159
|
+
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
29160
|
+
verified: true,
|
|
29161
|
+
source,
|
|
29162
|
+
message: formatIntegrityMessage(spec, measurement),
|
|
29163
|
+
impact: spec.impact
|
|
29164
|
+
};
|
|
29165
|
+
}
|
|
29166
|
+
function unverifiedCondition(spec, reason) {
|
|
29167
|
+
return {
|
|
29168
|
+
id: spec.id,
|
|
29169
|
+
entity: spec.entity,
|
|
29170
|
+
field: spec.field,
|
|
29171
|
+
kind: spec.kind,
|
|
29172
|
+
count: null,
|
|
29173
|
+
open_count: null,
|
|
29174
|
+
severity: null,
|
|
29175
|
+
verified: false,
|
|
29176
|
+
source: "unverified",
|
|
29177
|
+
unverified_reason: reason,
|
|
29178
|
+
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
29179
|
+
impact: spec.impact
|
|
29180
|
+
};
|
|
29181
|
+
}
|
|
29182
|
+
function summarizeIntegrity(conditions) {
|
|
29183
|
+
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
29184
|
+
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
29185
|
+
const unverified = conditions.length - measured.length;
|
|
29186
|
+
return {
|
|
29187
|
+
ok: unverified === 0 && findings.length === 0,
|
|
29188
|
+
findings: findings.length,
|
|
29189
|
+
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
29190
|
+
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
29191
|
+
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
29192
|
+
unverified,
|
|
29193
|
+
complete: unverified === 0
|
|
29194
|
+
};
|
|
29195
|
+
}
|
|
29196
|
+
function buildIntegrityReport(conditions, generatedAt) {
|
|
29197
|
+
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
|
|
29198
|
+
return {
|
|
29199
|
+
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
29200
|
+
generated_at: generatedAt,
|
|
29201
|
+
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
29202
|
+
conditions,
|
|
29203
|
+
summary: summarizeIntegrity(conditions)
|
|
29204
|
+
};
|
|
29205
|
+
}
|
|
29206
|
+
function sqliteOpenStatusList() {
|
|
29207
|
+
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
29208
|
+
}
|
|
29209
|
+
function buildSqliteIntegritySql(spec) {
|
|
29210
|
+
const table = SQLITE_TABLE[spec.entity];
|
|
29211
|
+
const target = SQLITE_TABLE[spec.target];
|
|
29212
|
+
const column = `t."${spec.field}"`;
|
|
29213
|
+
const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' ` + `AND NOT EXISTS (SELECT 1 FROM "${target}" r WHERE r."id" = ${column})`;
|
|
29214
|
+
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
29215
|
+
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
29216
|
+
}
|
|
29217
|
+
function buildPostgresIntegritySql(spec, options) {
|
|
29218
|
+
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
29219
|
+
const p = (value) => {
|
|
29220
|
+
params.push(value);
|
|
29221
|
+
return `$${params.length}`;
|
|
29222
|
+
};
|
|
29223
|
+
const column = `t.payload->>'${spec.field}'`;
|
|
29224
|
+
const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' AND NOT EXISTS (` + `SELECT 1 FROM ${options.table} r WHERE r.service = $1 AND r.object_type = ${p(RECORD_TYPE[spec.target])} ` + `AND r.deleted_at IS NULL AND r.object_id = ${column})`;
|
|
29225
|
+
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
29226
|
+
const sql = `/* todos:integrity-${spec.id} */ SELECT COUNT(*)::int AS count, ${openExpr} AS open_count ` + `FROM ${options.table} t WHERE t.service = $1 AND t.object_type = $2 AND t.deleted_at IS NULL AND ${predicate}`;
|
|
29227
|
+
return { sql, params };
|
|
29228
|
+
}
|
|
29229
|
+
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1", TERMINAL_TASK_STATUSES, OPEN_TASK_STATUSES, INTEGRITY_CONDITIONS, ENTITY_LABEL, SQLITE_TABLE, RECORD_TYPE;
|
|
29230
|
+
var init_integrity = __esm(() => {
|
|
29231
|
+
init_types();
|
|
29232
|
+
TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
29233
|
+
OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
29234
|
+
INTEGRITY_CONDITIONS = [
|
|
29235
|
+
{
|
|
29236
|
+
id: "tasks_without_project",
|
|
29237
|
+
entity: "task",
|
|
29238
|
+
field: "project_id",
|
|
29239
|
+
target: "project",
|
|
29240
|
+
kind: "missing",
|
|
29241
|
+
base_severity: "warn",
|
|
29242
|
+
escalate_when_open: true,
|
|
29243
|
+
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
29244
|
+
},
|
|
29245
|
+
{
|
|
29246
|
+
id: "tasks_without_task_list",
|
|
29247
|
+
entity: "task",
|
|
29248
|
+
field: "task_list_id",
|
|
29249
|
+
target: "task_list",
|
|
29250
|
+
kind: "missing",
|
|
29251
|
+
base_severity: "warn",
|
|
29252
|
+
escalate_when_open: true,
|
|
29253
|
+
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
29254
|
+
},
|
|
29255
|
+
{
|
|
29256
|
+
id: "tasks_with_unregistered_project",
|
|
29257
|
+
entity: "task",
|
|
29258
|
+
field: "project_id",
|
|
29259
|
+
target: "project",
|
|
29260
|
+
kind: "dangling",
|
|
29261
|
+
base_severity: "error",
|
|
29262
|
+
escalate_when_open: false,
|
|
29263
|
+
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
29264
|
+
},
|
|
29265
|
+
{
|
|
29266
|
+
id: "tasks_with_unregistered_task_list",
|
|
29267
|
+
entity: "task",
|
|
29268
|
+
field: "task_list_id",
|
|
29269
|
+
target: "task_list",
|
|
29270
|
+
kind: "dangling",
|
|
29271
|
+
base_severity: "error",
|
|
29272
|
+
escalate_when_open: false,
|
|
29273
|
+
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
29274
|
+
},
|
|
29275
|
+
{
|
|
29276
|
+
id: "task_lists_without_project",
|
|
29277
|
+
entity: "task_list",
|
|
29278
|
+
field: "project_id",
|
|
29279
|
+
target: "project",
|
|
29280
|
+
kind: "missing",
|
|
29281
|
+
base_severity: "warn",
|
|
29282
|
+
escalate_when_open: false,
|
|
29283
|
+
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
29284
|
+
},
|
|
29285
|
+
{
|
|
29286
|
+
id: "task_lists_with_unregistered_project",
|
|
29287
|
+
entity: "task_list",
|
|
29288
|
+
field: "project_id",
|
|
29289
|
+
target: "project",
|
|
29290
|
+
kind: "dangling",
|
|
29291
|
+
base_severity: "error",
|
|
29292
|
+
escalate_when_open: false,
|
|
29293
|
+
impact: "points at a project id that does not exist; the list can never be reached"
|
|
29294
|
+
}
|
|
29295
|
+
];
|
|
29296
|
+
ENTITY_LABEL = {
|
|
29297
|
+
task: ["task", "tasks"],
|
|
29298
|
+
task_list: ["task list", "task lists"]
|
|
29299
|
+
};
|
|
29300
|
+
SQLITE_TABLE = {
|
|
29301
|
+
task: "tasks",
|
|
29302
|
+
task_list: "task_lists",
|
|
29303
|
+
project: "projects"
|
|
29304
|
+
};
|
|
29305
|
+
RECORD_TYPE = {
|
|
29306
|
+
task: "tasks",
|
|
29307
|
+
task_list: "task_lists",
|
|
29308
|
+
project: "projects"
|
|
29309
|
+
};
|
|
29310
|
+
});
|
|
29311
|
+
|
|
29312
|
+
// src/db/integrity.ts
|
|
29313
|
+
function tableExists(db, table) {
|
|
29314
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
29315
|
+
}
|
|
29316
|
+
function scanSqliteIntegrity(db = getDatabase()) {
|
|
29317
|
+
const missing = Object.keys(REQUIRED_TABLES2).filter((table) => !tableExists(db, table));
|
|
29318
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
29319
|
+
if (missing.length > 0) {
|
|
29320
|
+
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
29321
|
+
}
|
|
29322
|
+
try {
|
|
29323
|
+
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
29324
|
+
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
29325
|
+
} catch (error) {
|
|
29326
|
+
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
29327
|
+
}
|
|
29328
|
+
});
|
|
29329
|
+
return buildIntegrityReport(conditions, now());
|
|
29330
|
+
}
|
|
29331
|
+
var REQUIRED_TABLES2;
|
|
29332
|
+
var init_integrity2 = __esm(() => {
|
|
29333
|
+
init_database();
|
|
29334
|
+
init_integrity();
|
|
29335
|
+
REQUIRED_TABLES2 = { tasks: true, task_lists: true, projects: true };
|
|
29336
|
+
});
|
|
29337
|
+
|
|
29019
29338
|
// src/lib/doctor.ts
|
|
29020
29339
|
var exports_doctor = {};
|
|
29021
29340
|
__export(exports_doctor, {
|
|
@@ -29023,7 +29342,7 @@ __export(exports_doctor, {
|
|
|
29023
29342
|
});
|
|
29024
29343
|
import { chmodSync, copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
|
|
29025
29344
|
import { basename as basename4, dirname as dirname6, join as join8 } from "path";
|
|
29026
|
-
function
|
|
29345
|
+
function tableExists2(db, table) {
|
|
29027
29346
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
29028
29347
|
}
|
|
29029
29348
|
function quoteIdent(identifier) {
|
|
@@ -29107,7 +29426,7 @@ function findDuplicateIndexes(db) {
|
|
|
29107
29426
|
return duplicates;
|
|
29108
29427
|
}
|
|
29109
29428
|
function findMissingProjectRoots(db) {
|
|
29110
|
-
if (!
|
|
29429
|
+
if (!tableExists2(db, "projects"))
|
|
29111
29430
|
return 0;
|
|
29112
29431
|
let missing = 0;
|
|
29113
29432
|
const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
@@ -29122,7 +29441,7 @@ function findMissingProjectRoots(db) {
|
|
|
29122
29441
|
return missing;
|
|
29123
29442
|
}
|
|
29124
29443
|
function addTaskStateChecks(db, checks) {
|
|
29125
|
-
if (!
|
|
29444
|
+
if (!tableExists2(db, "tasks"))
|
|
29126
29445
|
return;
|
|
29127
29446
|
const columns = getTableColumns(db, "tasks");
|
|
29128
29447
|
const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
@@ -29194,15 +29513,42 @@ function createBackup(dbPath) {
|
|
|
29194
29513
|
function pushRepair(repairs, type, message, applied, count) {
|
|
29195
29514
|
repairs.push({ type, message, applied, count });
|
|
29196
29515
|
}
|
|
29197
|
-
function summarize2(checks, repairs) {
|
|
29516
|
+
function summarize2(checks, repairs, integrity) {
|
|
29198
29517
|
return {
|
|
29199
29518
|
errors: checks.filter((check) => check.severity === "error").length,
|
|
29200
29519
|
warnings: checks.filter((check) => check.severity === "warn").length,
|
|
29201
29520
|
infos: checks.filter((check) => check.severity === "info").length,
|
|
29202
29521
|
repairable: checks.filter((check) => check.repairable).length,
|
|
29203
|
-
applied: repairs.filter((repair) => repair.applied).length
|
|
29522
|
+
applied: repairs.filter((repair) => repair.applied).length,
|
|
29523
|
+
integrity_findings: integrity.summary.findings,
|
|
29524
|
+
integrity_rows: integrity.summary.rows,
|
|
29525
|
+
integrity_unverified: integrity.summary.unverified
|
|
29204
29526
|
};
|
|
29205
29527
|
}
|
|
29528
|
+
function addIntegrityChecks(integrity, checks) {
|
|
29529
|
+
for (const condition of integrity.conditions) {
|
|
29530
|
+
if (!condition.verified) {
|
|
29531
|
+
addCheck(checks, {
|
|
29532
|
+
severity: "warn",
|
|
29533
|
+
type: `${condition.id}_unverified`,
|
|
29534
|
+
message: condition.message,
|
|
29535
|
+
repairable: false,
|
|
29536
|
+
integrity: true
|
|
29537
|
+
});
|
|
29538
|
+
continue;
|
|
29539
|
+
}
|
|
29540
|
+
if ((condition.count ?? 0) === 0)
|
|
29541
|
+
continue;
|
|
29542
|
+
addCheck(checks, {
|
|
29543
|
+
severity: condition.severity ?? "warn",
|
|
29544
|
+
type: condition.id,
|
|
29545
|
+
message: `${condition.message} \u2014 ${condition.impact}`,
|
|
29546
|
+
count: condition.count ?? undefined,
|
|
29547
|
+
repairable: false,
|
|
29548
|
+
integrity: true
|
|
29549
|
+
});
|
|
29550
|
+
}
|
|
29551
|
+
}
|
|
29206
29552
|
function deleteOrphans(db, table, where) {
|
|
29207
29553
|
const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
29208
29554
|
if (before > 0)
|
|
@@ -29231,7 +29577,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29231
29577
|
message: `Schema at migration ${migrationCurrent}`
|
|
29232
29578
|
});
|
|
29233
29579
|
}
|
|
29234
|
-
const missingTables =
|
|
29580
|
+
const missingTables = REQUIRED_TABLES3.filter((table) => !tableExists2(db, table));
|
|
29235
29581
|
if (missingTables.length > 0) {
|
|
29236
29582
|
addCheck(checks, {
|
|
29237
29583
|
severity: "error",
|
|
@@ -29241,7 +29587,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29241
29587
|
repairable: true
|
|
29242
29588
|
});
|
|
29243
29589
|
}
|
|
29244
|
-
const orphanedParents =
|
|
29590
|
+
const orphanedParents = tableExists2(db, "tasks") ? countQuery(db, "SELECT COUNT(*) as count FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)") : 0;
|
|
29245
29591
|
if (orphanedParents > 0) {
|
|
29246
29592
|
addCheck(checks, {
|
|
29247
29593
|
severity: "error",
|
|
@@ -29251,7 +29597,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29251
29597
|
repairable: true
|
|
29252
29598
|
});
|
|
29253
29599
|
}
|
|
29254
|
-
const orphanedDependencies =
|
|
29600
|
+
const orphanedDependencies = tableExists2(db, "task_dependencies") && tableExists2(db, "tasks") ? countQuery(db, "SELECT COUNT(*) as count FROM task_dependencies d WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.depends_on)") : 0;
|
|
29255
29601
|
if (orphanedDependencies > 0) {
|
|
29256
29602
|
addCheck(checks, {
|
|
29257
29603
|
severity: "error",
|
|
@@ -29270,7 +29616,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29270
29616
|
];
|
|
29271
29617
|
let orphanedRows = 0;
|
|
29272
29618
|
for (const [table, where] of orphanTables) {
|
|
29273
|
-
if (!
|
|
29619
|
+
if (!tableExists2(db, table))
|
|
29274
29620
|
continue;
|
|
29275
29621
|
orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
29276
29622
|
}
|
|
@@ -29284,6 +29630,8 @@ function runTodosDoctor(options = {}) {
|
|
|
29284
29630
|
});
|
|
29285
29631
|
}
|
|
29286
29632
|
addTaskStateChecks(db, checks);
|
|
29633
|
+
const integrity = scanSqliteIntegrity(db);
|
|
29634
|
+
addIntegrityChecks(integrity, checks);
|
|
29287
29635
|
const corruptJson = findCorruptJsonMetadata(db);
|
|
29288
29636
|
if (corruptJson.length > 0) {
|
|
29289
29637
|
addCheck(checks, {
|
|
@@ -29342,12 +29690,12 @@ function runTodosDoctor(options = {}) {
|
|
|
29342
29690
|
db.run("UPDATE tasks SET parent_id = NULL WHERE parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = tasks.parent_id)");
|
|
29343
29691
|
pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
|
|
29344
29692
|
}
|
|
29345
|
-
if (orphanedDependencies > 0 &&
|
|
29693
|
+
if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
|
|
29346
29694
|
const count = deleteOrphans(db, "task_dependencies", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.depends_on)");
|
|
29347
29695
|
pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
|
|
29348
29696
|
}
|
|
29349
29697
|
for (const [table, where] of orphanTables) {
|
|
29350
|
-
if (!
|
|
29698
|
+
if (!tableExists2(db, table))
|
|
29351
29699
|
continue;
|
|
29352
29700
|
const count = deleteOrphans(db, table, where);
|
|
29353
29701
|
if (count > 0)
|
|
@@ -29376,26 +29724,30 @@ function runTodosDoctor(options = {}) {
|
|
|
29376
29724
|
}
|
|
29377
29725
|
}
|
|
29378
29726
|
}
|
|
29379
|
-
const
|
|
29380
|
-
const
|
|
29727
|
+
const reread = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }) : undefined;
|
|
29728
|
+
const finalChecks = reread?.checks ?? checks;
|
|
29729
|
+
const finalIntegrity = reread?.integrity ?? integrity;
|
|
29730
|
+
const summary = summarize2(finalChecks, repairs, finalIntegrity);
|
|
29381
29731
|
return {
|
|
29382
|
-
ok: !finalChecks.some((check) => check.severity === "error"),
|
|
29732
|
+
ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
|
|
29383
29733
|
dry_run: !apply,
|
|
29384
29734
|
database_path: dbPath,
|
|
29385
29735
|
migration: { current: getMigrationLevel(db), expected: migrationExpected },
|
|
29386
29736
|
backup,
|
|
29387
29737
|
checks: finalChecks,
|
|
29388
29738
|
repairs,
|
|
29739
|
+
integrity: finalIntegrity,
|
|
29389
29740
|
summary
|
|
29390
29741
|
};
|
|
29391
29742
|
}
|
|
29392
|
-
var
|
|
29743
|
+
var REQUIRED_TABLES3;
|
|
29393
29744
|
var init_doctor = __esm(() => {
|
|
29394
29745
|
init_database();
|
|
29746
|
+
init_integrity2();
|
|
29395
29747
|
init_migrations();
|
|
29396
29748
|
init_schema();
|
|
29397
29749
|
init_recurrence();
|
|
29398
|
-
|
|
29750
|
+
REQUIRED_TABLES3 = [
|
|
29399
29751
|
"_migrations",
|
|
29400
29752
|
"projects",
|
|
29401
29753
|
"tasks",
|
|
@@ -29739,7 +30091,7 @@ function limits(input) {
|
|
|
29739
30091
|
verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
|
|
29740
30092
|
run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
|
|
29741
30093
|
dependency_limit: clamp(input.dependency_limit, DEFAULT_LIMITS.dependency_limit, 100),
|
|
29742
|
-
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit,
|
|
30094
|
+
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit, MAX_CONTEXT_TASKS),
|
|
29743
30095
|
max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
|
|
29744
30096
|
summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
|
|
29745
30097
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
@@ -29762,6 +30114,13 @@ function taskSummary(task) {
|
|
|
29762
30114
|
priority: task.priority
|
|
29763
30115
|
};
|
|
29764
30116
|
}
|
|
30117
|
+
function latestPlanTasks(planId, limit, db) {
|
|
30118
|
+
const rows = db.query(`SELECT * FROM tasks
|
|
30119
|
+
WHERE plan_id = ? AND archived_at IS NULL
|
|
30120
|
+
ORDER BY updated_at DESC, created_at DESC, id ASC
|
|
30121
|
+
LIMIT ?`).all(planId, limit);
|
|
30122
|
+
return rows.map(rowToTask);
|
|
30123
|
+
}
|
|
29765
30124
|
function acceptanceCriteria(task, maxText) {
|
|
29766
30125
|
const metadata = task.metadata || {};
|
|
29767
30126
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
@@ -29859,7 +30218,7 @@ function summarizeSection(pack, section, maxChars) {
|
|
|
29859
30218
|
return "No plan was attached.";
|
|
29860
30219
|
return summarizeStrings([
|
|
29861
30220
|
`Plan ${pack.plan.name} is ${pack.plan.status}`,
|
|
29862
|
-
`${pack.plan.tasks.length} listed plan
|
|
30221
|
+
`${pack.plan.tasks.length} of ${pack.plan.total_tasks} plan tasks listed; ${pack.plan.omitted_tasks} omitted`,
|
|
29863
30222
|
...pack.plan.tasks.slice(0, 4).map((task) => `${task.status} ${task.short_id || task.id.slice(0, 8)} ${task.title}`)
|
|
29864
30223
|
], maxChars);
|
|
29865
30224
|
}
|
|
@@ -30009,7 +30368,8 @@ function createAgentContextPack(input, db) {
|
|
|
30009
30368
|
const limit = limits(input);
|
|
30010
30369
|
const project = task.project_id ? getProject(task.project_id, d) : null;
|
|
30011
30370
|
const plan = task.plan_id ? getPlan(task.plan_id, d) : null;
|
|
30012
|
-
const planTasks = task.plan_id ?
|
|
30371
|
+
const planTasks = task.plan_id ? latestPlanTasks(task.plan_id, limit.plan_task_limit, d) : [];
|
|
30372
|
+
const totalPlanTasks = task.plan_id ? countTasks({ plan_id: task.plan_id }, d) : 0;
|
|
30013
30373
|
const upstream = getTaskDependencies(task.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
|
|
30014
30374
|
const downstream = getTaskDependents(task.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
|
|
30015
30375
|
const comments = listComments(task.id, d);
|
|
@@ -30052,6 +30412,9 @@ function createAgentContextPack(input, db) {
|
|
|
30052
30412
|
warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
|
|
30053
30413
|
if (fileMap.size > relevantFiles.length)
|
|
30054
30414
|
warnings.push(`${fileMap.size - relevantFiles.length} relevant files omitted`);
|
|
30415
|
+
if (totalPlanTasks > planTasks.length && plan) {
|
|
30416
|
+
warnings.push(`${totalPlanTasks - planTasks.length} plan tasks omitted (${totalPlanTasks} total); load more on demand with "todos plans --show ${plan.id}" and inspect task details with "todos show <task-id>" or "todos context-pack <task-id>"`);
|
|
30417
|
+
}
|
|
30055
30418
|
const contextTask = {
|
|
30056
30419
|
id: task.id,
|
|
30057
30420
|
short_id: task.short_id,
|
|
@@ -30083,8 +30446,9 @@ function createAgentContextPack(input, db) {
|
|
|
30083
30446
|
description: truncate2(plan.description, limit.max_text_chars),
|
|
30084
30447
|
status: plan.status,
|
|
30085
30448
|
agent_id: plan.agent_id,
|
|
30086
|
-
tasks: planTasks.
|
|
30087
|
-
|
|
30449
|
+
tasks: planTasks.map(taskSummary).filter((item) => Boolean(item)),
|
|
30450
|
+
total_tasks: totalPlanTasks,
|
|
30451
|
+
omitted_tasks: Math.max(0, totalPlanTasks - planTasks.length)
|
|
30088
30452
|
} : null,
|
|
30089
30453
|
acceptance_criteria: acceptanceCriteria(task, limit.max_text_chars),
|
|
30090
30454
|
dependencies: {
|
|
@@ -30194,6 +30558,8 @@ ${pack.task.description}` : null,
|
|
|
30194
30558
|
"## Project And Plan",
|
|
30195
30559
|
pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
|
|
30196
30560
|
pack.plan ? `- Plan: ${pack.plan.name} (${pack.plan.status})` : "- Plan: none",
|
|
30561
|
+
pack.plan ? `- Plan tasks: ${pack.plan.tasks.length} shown of ${pack.plan.total_tasks} total (${pack.plan.omitted_tasks} omitted)` : null,
|
|
30562
|
+
pack.plan && pack.plan.omitted_tasks > 0 ? `- Load more on demand: \`todos plans --show ${pack.plan.id}\`; inspect one task with \`todos show <task-id>\` or \`todos context-pack <task-id>\`.` : null,
|
|
30197
30563
|
pack.plan && pack.plan.tasks.length > 0 ? bullet(pack.plan.tasks.map((task) => `${task.status} ${task.short_id || task.id.slice(0, 8)} ${task.title}`)) : null,
|
|
30198
30564
|
"",
|
|
30199
30565
|
"## Acceptance Criteria",
|
|
@@ -30274,7 +30640,7 @@ function renderAgentContextPack(pack, format, compact2 = false) {
|
|
|
30274
30640
|
return compact2 ? renderAgentContextPackCompactMarkdown(pack) : renderAgentContextPackMarkdown(pack);
|
|
30275
30641
|
return compact2 ? JSON.stringify(pack) : JSON.stringify(pack, null, 2);
|
|
30276
30642
|
}
|
|
30277
|
-
var DEFAULT_LIMITS, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
30643
|
+
var DEFAULT_LIMITS, MAX_CONTEXT_TASKS = 24, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
30278
30644
|
var init_context_packs = __esm(() => {
|
|
30279
30645
|
init_comments();
|
|
30280
30646
|
init_database();
|
|
@@ -30293,7 +30659,7 @@ var init_context_packs = __esm(() => {
|
|
|
30293
30659
|
verification_limit: 10,
|
|
30294
30660
|
run_limit: 3,
|
|
30295
30661
|
dependency_limit: 12,
|
|
30296
|
-
plan_task_limit:
|
|
30662
|
+
plan_task_limit: 24,
|
|
30297
30663
|
max_text_chars: 6000,
|
|
30298
30664
|
summary_char_limit: 480,
|
|
30299
30665
|
stale_after_hours: 72
|
|
@@ -31342,7 +31708,7 @@ function parseObject2(value) {
|
|
|
31342
31708
|
function normalizeKey(value) {
|
|
31343
31709
|
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
31344
31710
|
}
|
|
31345
|
-
function
|
|
31711
|
+
function normalizeTaskFindingFingerprint(value) {
|
|
31346
31712
|
const normalized = normalizeKey(value);
|
|
31347
31713
|
if (!normalized)
|
|
31348
31714
|
throw new Error("finding fingerprint is required");
|
|
@@ -31451,7 +31817,7 @@ function assertTask(taskId, db) {
|
|
|
31451
31817
|
throw new TaskNotFoundError(taskId);
|
|
31452
31818
|
}
|
|
31453
31819
|
function nextFinding(input, db) {
|
|
31454
|
-
const fingerprint2 =
|
|
31820
|
+
const fingerprint2 = normalizeTaskFindingFingerprint(input.fingerprint);
|
|
31455
31821
|
const title = redactOptional(input.title, 300);
|
|
31456
31822
|
if (!title)
|
|
31457
31823
|
throw new Error("finding title is required");
|
|
@@ -31599,7 +31965,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
31599
31965
|
const timestamp4 = input.resolved_at || now();
|
|
31600
31966
|
const status = normalizeResolutionStatus(input.status);
|
|
31601
31967
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
31602
|
-
const present = new Set(input.fingerprints.map(
|
|
31968
|
+
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
31603
31969
|
const warnings = [];
|
|
31604
31970
|
const conditions = ["task_id = ?", "status = 'open'"];
|
|
31605
31971
|
const params = [input.task_id];
|
|
@@ -34187,7 +34553,7 @@ var package_default;
|
|
|
34187
34553
|
var init_package = __esm(() => {
|
|
34188
34554
|
package_default = {
|
|
34189
34555
|
name: "@hasna/todos",
|
|
34190
|
-
version: "0.13.
|
|
34556
|
+
version: "0.13.3",
|
|
34191
34557
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
34192
34558
|
type: "module",
|
|
34193
34559
|
main: "dist/index.js",
|
|
@@ -34221,6 +34587,10 @@ var init_package = __esm(() => {
|
|
|
34221
34587
|
"./storage": {
|
|
34222
34588
|
types: "./dist/storage.d.ts",
|
|
34223
34589
|
import: "./dist/storage.js"
|
|
34590
|
+
},
|
|
34591
|
+
"./testing": {
|
|
34592
|
+
types: "./dist/testing.d.ts",
|
|
34593
|
+
import: "./dist/testing.js"
|
|
34224
34594
|
}
|
|
34225
34595
|
},
|
|
34226
34596
|
workspaces: [
|
|
@@ -34233,13 +34603,13 @@ var init_package = __esm(() => {
|
|
|
34233
34603
|
"README.md"
|
|
34234
34604
|
],
|
|
34235
34605
|
scripts: {
|
|
34236
|
-
build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
34237
|
-
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
34606
|
+
build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
34607
|
+
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
34238
34608
|
migrate: "bun run src/server/index.ts migrate",
|
|
34239
34609
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
34240
34610
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
34241
34611
|
"build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
|
|
34242
|
-
typecheck: "tsc --noEmit",
|
|
34612
|
+
typecheck: "tsc --noEmit -p tsconfig.typecheck.json",
|
|
34243
34613
|
test: "bun test",
|
|
34244
34614
|
"test:no-cloud": "bun test src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
|
|
34245
34615
|
"dev:cli": "bun run src/cli/index.tsx",
|
|
@@ -43940,15 +44310,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
43940
44310
|
return null;
|
|
43941
44311
|
if (TASK_UUID_RE.test(raw))
|
|
43942
44312
|
return getTask(raw, db);
|
|
43943
|
-
const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
44313
|
+
const prefixRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' ORDER BY project_id, id LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
43944
44314
|
if (prefixRows.length > 1) {
|
|
43945
|
-
throw new
|
|
44315
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
43946
44316
|
}
|
|
43947
44317
|
if (prefixRows.length === 1)
|
|
43948
44318
|
return getTask(prefixRows[0].id, db);
|
|
43949
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
|
|
44319
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
|
|
43950
44320
|
if (shortIdRows.length > 1) {
|
|
43951
|
-
throw new
|
|
44321
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
43952
44322
|
}
|
|
43953
44323
|
if (shortIdRows.length === 1)
|
|
43954
44324
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -44097,6 +44467,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
44097
44467
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
44098
44468
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
44099
44469
|
},
|
|
44470
|
+
integrity: {
|
|
44471
|
+
report: () => scanSqliteIntegrity(database())
|
|
44472
|
+
},
|
|
44100
44473
|
transaction: (fn) => {
|
|
44101
44474
|
const tx = database().transaction(() => fn(adapter));
|
|
44102
44475
|
return tx();
|
|
@@ -44106,6 +44479,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
44106
44479
|
}
|
|
44107
44480
|
var TASK_UUID_RE;
|
|
44108
44481
|
var init_local_sqlite = __esm(() => {
|
|
44482
|
+
init_types();
|
|
44109
44483
|
init_search();
|
|
44110
44484
|
init_tasks();
|
|
44111
44485
|
init_projects();
|
|
@@ -44116,6 +44490,7 @@ var init_local_sqlite = __esm(() => {
|
|
|
44116
44490
|
init_audit();
|
|
44117
44491
|
init_comments();
|
|
44118
44492
|
init_database();
|
|
44493
|
+
init_integrity2();
|
|
44119
44494
|
init_sqlite_snapshot();
|
|
44120
44495
|
TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
44121
44496
|
});
|
|
@@ -45040,6 +45415,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
45040
45415
|
exportSnapshot: () => exportSnapshot(store),
|
|
45041
45416
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
45042
45417
|
},
|
|
45418
|
+
integrity: {
|
|
45419
|
+
report: () => store.integrityReport()
|
|
45420
|
+
},
|
|
45043
45421
|
transaction: (fn) => fn(adapter)
|
|
45044
45422
|
};
|
|
45045
45423
|
return adapter;
|
|
@@ -45146,8 +45524,9 @@ class PostgresJsonRecordStore {
|
|
|
45146
45524
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
45147
45525
|
if (filter.session_id !== undefined)
|
|
45148
45526
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
45149
|
-
if (filter.tags?.length)
|
|
45150
|
-
conds.push(`payload->'tags'
|
|
45527
|
+
if (filter.tags?.length) {
|
|
45528
|
+
conds.push(`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(payload->'tags', '[]'::jsonb)) AS task_tags(tag) ` + `WHERE ${inClause("task_tags.tag", filter.tags)})`);
|
|
45529
|
+
}
|
|
45151
45530
|
if (filter.has_recurrence !== undefined) {
|
|
45152
45531
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
45153
45532
|
}
|
|
@@ -45208,7 +45587,8 @@ class PostgresJsonRecordStore {
|
|
|
45208
45587
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
45209
45588
|
LIMIT 2`, [this.service, raw, upper]);
|
|
45210
45589
|
if (prefixResult.rows.length > 1) {
|
|
45211
|
-
|
|
45590
|
+
const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
|
|
45591
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
|
|
45212
45592
|
}
|
|
45213
45593
|
if (prefixResult.rows.length === 1) {
|
|
45214
45594
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -45219,7 +45599,8 @@ class PostgresJsonRecordStore {
|
|
|
45219
45599
|
AND LOWER(payload->>'short_id') = $2
|
|
45220
45600
|
LIMIT 2`, [this.service, raw]);
|
|
45221
45601
|
if (shortIdResult.rows.length > 1) {
|
|
45222
|
-
|
|
45602
|
+
const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
|
|
45603
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
|
|
45223
45604
|
}
|
|
45224
45605
|
if (shortIdResult.rows.length === 1) {
|
|
45225
45606
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -45233,6 +45614,24 @@ class PostgresJsonRecordStore {
|
|
|
45233
45614
|
const result = await this.options.client.query(sql, params);
|
|
45234
45615
|
return Number(result.rows[0]?.count ?? 0);
|
|
45235
45616
|
}
|
|
45617
|
+
async integrityReport() {
|
|
45618
|
+
await this.ensureSchema();
|
|
45619
|
+
const conditions = [];
|
|
45620
|
+
for (const spec of INTEGRITY_CONDITIONS) {
|
|
45621
|
+
const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
|
|
45622
|
+
try {
|
|
45623
|
+
const result = await this.options.client.query(sql, params);
|
|
45624
|
+
const row = result.rows[0];
|
|
45625
|
+
conditions.push(measuredCondition(spec, {
|
|
45626
|
+
count: Number(row?.count ?? 0),
|
|
45627
|
+
open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
|
|
45628
|
+
}, "postgres"));
|
|
45629
|
+
} catch (error) {
|
|
45630
|
+
conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
|
|
45631
|
+
}
|
|
45632
|
+
}
|
|
45633
|
+
return buildIntegrityReport(conditions, new Date().toISOString());
|
|
45634
|
+
}
|
|
45236
45635
|
async listTombstones() {
|
|
45237
45636
|
await this.ensureSchema();
|
|
45238
45637
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -45708,7 +46107,7 @@ async function startTask2(id, agentId, store) {
|
|
|
45708
46107
|
if (task2.status !== "pending" && task2.status !== "in_progress") {
|
|
45709
46108
|
throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
|
|
45710
46109
|
}
|
|
45711
|
-
|
|
46110
|
+
const started = await patchTask(task2, {
|
|
45712
46111
|
status: "in_progress",
|
|
45713
46112
|
assigned_to: task2.assigned_to ?? agentId,
|
|
45714
46113
|
agent_id: task2.agent_id ?? agentId,
|
|
@@ -45716,6 +46115,8 @@ async function startTask2(id, agentId, store) {
|
|
|
45716
46115
|
locked_at: new Date().toISOString(),
|
|
45717
46116
|
started_at: task2.started_at ?? new Date().toISOString()
|
|
45718
46117
|
}, store);
|
|
46118
|
+
await logTaskChange2(task2.id, "start", "status", task2.status, "in_progress", agentId, store);
|
|
46119
|
+
return started;
|
|
45719
46120
|
}
|
|
45720
46121
|
async function completeTask2(id, agentId, options, store) {
|
|
45721
46122
|
const task2 = await store.completeTask(id, agentId, options);
|
|
@@ -45832,9 +46233,11 @@ async function removeDependency2(taskId, dependsOn, store) {
|
|
|
45832
46233
|
}
|
|
45833
46234
|
async function listDependencies(taskId, store) {
|
|
45834
46235
|
const edges = await store.list("dependencies");
|
|
46236
|
+
const incoming = edges.filter((edge) => edge.depends_on === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on }));
|
|
45835
46237
|
return {
|
|
45836
46238
|
dependencies: edges.filter((edge) => edge.task_id === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on })),
|
|
45837
|
-
|
|
46239
|
+
blocks: incoming,
|
|
46240
|
+
blocked_by: incoming
|
|
45838
46241
|
};
|
|
45839
46242
|
}
|
|
45840
46243
|
async function addVerification(input, store, context) {
|
|
@@ -46380,6 +46783,7 @@ var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'prior
|
|
|
46380
46783
|
var init_postgres_adapter = __esm(() => {
|
|
46381
46784
|
init_types();
|
|
46382
46785
|
init_postgres_sync();
|
|
46786
|
+
init_integrity();
|
|
46383
46787
|
init_redaction();
|
|
46384
46788
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
46385
46789
|
});
|
|
@@ -46968,6 +47372,7 @@ __export(exports_cloud, {
|
|
|
46968
47372
|
resolveCloudDatabaseUrl: () => resolveCloudDatabaseUrl,
|
|
46969
47373
|
pingCloud: () => pingCloud,
|
|
46970
47374
|
normalizeCloudPayloads: () => normalizeCloudPayloads,
|
|
47375
|
+
isPostgresBackendConfigured: () => isPostgresBackendConfigured,
|
|
46971
47376
|
isCloudModeEnabled: () => isCloudModeEnabled,
|
|
46972
47377
|
getCloudVerifier: () => getCloudVerifier,
|
|
46973
47378
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
@@ -46990,9 +47395,12 @@ function resolveCloudDatabaseUrl(env = process.env) {
|
|
|
46990
47395
|
function resolveSigningSecret(env = process.env) {
|
|
46991
47396
|
return env.HASNA_TODOS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
|
|
46992
47397
|
}
|
|
46993
|
-
function
|
|
47398
|
+
function isPostgresBackendConfigured(env = process.env) {
|
|
46994
47399
|
return Boolean(resolveCloudDatabaseUrl(env));
|
|
46995
47400
|
}
|
|
47401
|
+
function isCloudModeEnabled(env = process.env) {
|
|
47402
|
+
return isPostgresBackendConfigured(env);
|
|
47403
|
+
}
|
|
46996
47404
|
function getClient() {
|
|
46997
47405
|
if (cachedClient)
|
|
46998
47406
|
return cachedClient;
|
|
@@ -47172,7 +47580,7 @@ function authNotConfiguredMessage(host) {
|
|
|
47172
47580
|
`);
|
|
47173
47581
|
}
|
|
47174
47582
|
function resolveAuthPosture(input) {
|
|
47175
|
-
const hosted = input.hosted ??
|
|
47583
|
+
const hosted = input.hosted ?? isPostgresBackendConfigured();
|
|
47176
47584
|
const hasCredentialSource = Boolean(input.apiKey) || input.hasGeneratedKeys;
|
|
47177
47585
|
if (hasCredentialSource) {
|
|
47178
47586
|
return {
|
|
@@ -49133,6 +49541,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49133
49541
|
{ name: "task_list_id", in: "query", schema: { type: "string" } },
|
|
49134
49542
|
{ name: "assigned_to", in: "query", schema: { type: "string" } },
|
|
49135
49543
|
{ name: "agent_id", in: "query", schema: { type: "string" } },
|
|
49544
|
+
{ name: "tags", in: "query", schema: { type: "string" }, description: "Comma-separated tags; matches tasks carrying any of them" },
|
|
49136
49545
|
{ name: "limit", in: "query", schema: { type: "integer", minimum: 1 } },
|
|
49137
49546
|
{ name: "offset", in: "query", schema: { type: "integer", minimum: 0 } }
|
|
49138
49547
|
],
|
|
@@ -50337,6 +50746,9 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50337
50746
|
...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
|
|
50338
50747
|
...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
|
|
50339
50748
|
...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
|
|
50749
|
+
...url.searchParams.get("tags") ? {
|
|
50750
|
+
tags: url.searchParams.get("tags").split(",").map((tag) => tag.trim()).filter(Boolean)
|
|
50751
|
+
} : {},
|
|
50340
50752
|
...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
|
|
50341
50753
|
...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
|
|
50342
50754
|
};
|
|
@@ -50618,6 +51030,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50618
51030
|
try {
|
|
50619
51031
|
task2 = await store.tasks.resolveRef(id);
|
|
50620
51032
|
} catch (e) {
|
|
51033
|
+
if (e instanceof TaskReferenceAmbiguousError) {
|
|
51034
|
+
return error(409, e.message, {
|
|
51035
|
+
code: TaskReferenceAmbiguousError.code,
|
|
51036
|
+
candidate_project_ids: e.candidateProjectIds,
|
|
51037
|
+
candidate_task_ids: e.candidateTaskIds
|
|
51038
|
+
});
|
|
51039
|
+
}
|
|
50621
51040
|
const msg = e.message || "";
|
|
50622
51041
|
if (/ambiguous/i.test(msg))
|
|
50623
51042
|
return error(409, msg);
|
|
@@ -50975,6 +51394,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50975
51394
|
]);
|
|
50976
51395
|
return json3({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
|
|
50977
51396
|
}
|
|
51397
|
+
if (resource === "integrity" && !id) {
|
|
51398
|
+
if (method !== "GET")
|
|
51399
|
+
return error(405, `method ${method} not allowed on /v1/integrity`);
|
|
51400
|
+
if (typeof store.integrity?.report !== "function") {
|
|
51401
|
+
return error(501, "referential-integrity reporting is not supported by this storage backend");
|
|
51402
|
+
}
|
|
51403
|
+
const integrity = await store.integrity.report();
|
|
51404
|
+
return json3({ integrity });
|
|
51405
|
+
}
|
|
50978
51406
|
if (resource === "import") {
|
|
50979
51407
|
if (method !== "POST")
|
|
50980
51408
|
return error(405, `method ${method} not allowed on /v1/import`);
|
|
@@ -50994,6 +51422,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50994
51422
|
}
|
|
50995
51423
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
50996
51424
|
} catch (e) {
|
|
51425
|
+
if (e instanceof TaskReferenceAmbiguousError) {
|
|
51426
|
+
return error(409, e.message, {
|
|
51427
|
+
code: TaskReferenceAmbiguousError.code,
|
|
51428
|
+
candidate_project_ids: e.candidateProjectIds,
|
|
51429
|
+
candidate_task_ids: e.candidateTaskIds
|
|
51430
|
+
});
|
|
51431
|
+
}
|
|
50997
51432
|
if (e instanceof LockError)
|
|
50998
51433
|
return error(409, e.message, { code: LockError.code });
|
|
50999
51434
|
if (e instanceof ResourceConflictError)
|
|
@@ -51489,7 +51924,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
51489
51924
|
const path = url.pathname;
|
|
51490
51925
|
const method = req.method;
|
|
51491
51926
|
const reqOrigin = req.headers.get("origin") || undefined;
|
|
51492
|
-
const corsHeaders = reqOrigin &&
|
|
51927
|
+
const corsHeaders = reqOrigin && reqOrigin === `http://localhost:${ctx.port}` ? {
|
|
51493
51928
|
"Access-Control-Allow-Origin": reqOrigin,
|
|
51494
51929
|
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
51495
51930
|
"Access-Control-Allow-Headers": "Content-Type, X-API-Key, Authorization",
|
|
@@ -51514,23 +51949,24 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
51514
51949
|
}
|
|
51515
51950
|
if ((path === "/health" || path === "/ready" || path === "/version") && method === "GET") {
|
|
51516
51951
|
const { getPackageVersion: getPackageVersion2 } = await Promise.resolve().then(() => (init_package_version(), exports_package_version));
|
|
51517
|
-
const {
|
|
51518
|
-
const
|
|
51952
|
+
const { isPostgresBackendConfigured: isPostgresBackendConfigured2, pingCloud: pingCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
51953
|
+
const backend = isPostgresBackendConfigured2() ? "postgresql" : "sqlite";
|
|
51954
|
+
const mode = backend === "postgresql" ? "remote" : "local";
|
|
51519
51955
|
const version = getPackageVersion2();
|
|
51520
51956
|
if (path === "/version") {
|
|
51521
|
-
return Response.json({ status: "ok", version, mode, name: "todos" });
|
|
51957
|
+
return Response.json({ status: "ok", version, mode, backend, name: "todos" });
|
|
51522
51958
|
}
|
|
51523
51959
|
if (path === "/ready") {
|
|
51524
|
-
if (
|
|
51960
|
+
if (backend === "postgresql") {
|
|
51525
51961
|
try {
|
|
51526
51962
|
await pingCloud2();
|
|
51527
51963
|
} catch (e) {
|
|
51528
|
-
return Response.json({ status: "unavailable", version, mode, error: e.message }, { status: 503 });
|
|
51964
|
+
return Response.json({ status: "unavailable", version, mode, backend, error: e.message }, { status: 503 });
|
|
51529
51965
|
}
|
|
51530
51966
|
}
|
|
51531
|
-
return Response.json({ status: "ready", version, mode });
|
|
51967
|
+
return Response.json({ status: "ready", version, mode, backend });
|
|
51532
51968
|
}
|
|
51533
|
-
return Response.json({ status: "ok", version, mode, name: "todos" });
|
|
51969
|
+
return Response.json({ status: "ok", version, mode, backend, name: "todos" });
|
|
51534
51970
|
}
|
|
51535
51971
|
if ((path === "/openapi.json" || path === "/v1/openapi.json") && method === "GET") {
|
|
51536
51972
|
const { buildV1OpenApiDocument: buildV1OpenApiDocument2 } = await Promise.resolve().then(() => (init_openapi(), exports_openapi));
|
|
@@ -51777,7 +52213,9 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
51777
52213
|
};
|
|
51778
52214
|
process.on("SIGINT", shutdown);
|
|
51779
52215
|
process.on("SIGTERM", shutdown);
|
|
51780
|
-
const
|
|
52216
|
+
const boundPort = server.port ?? port;
|
|
52217
|
+
ctx.port = boundPort;
|
|
52218
|
+
const serverUrl = `http://localhost:${boundPort}`;
|
|
51781
52219
|
console.log(`Todos Dashboard running at ${serverUrl}`);
|
|
51782
52220
|
if (shouldOpen) {
|
|
51783
52221
|
try {
|
|
@@ -51884,6 +52322,15 @@ function formatError(error2) {
|
|
|
51884
52322
|
if (error2 instanceof TaskNotFoundError) {
|
|
51885
52323
|
return JSON.stringify({ code: TaskNotFoundError.code, message: error2.message, suggestion: TaskNotFoundError.suggestion });
|
|
51886
52324
|
}
|
|
52325
|
+
if (error2 instanceof TaskReferenceAmbiguousError) {
|
|
52326
|
+
return JSON.stringify({
|
|
52327
|
+
code: TaskReferenceAmbiguousError.code,
|
|
52328
|
+
message: error2.message,
|
|
52329
|
+
candidate_project_ids: error2.candidateProjectIds,
|
|
52330
|
+
candidate_task_ids: error2.candidateTaskIds,
|
|
52331
|
+
suggestion: "Use a full task UUID."
|
|
52332
|
+
});
|
|
52333
|
+
}
|
|
51887
52334
|
if (error2 instanceof ProjectNotFoundError) {
|
|
51888
52335
|
return JSON.stringify({ code: ProjectNotFoundError.code, message: error2.message, suggestion: ProjectNotFoundError.suggestion });
|
|
51889
52336
|
}
|