@hasna/todos 0.13.2 → 0.13.4
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 +78 -14
- package/dist/cli/cloud-router.d.ts +100 -7
- 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/query-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 +1509 -786
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +118 -30
- package/dist/db/agent-names.d.ts +2 -1
- package/dist/db/agent-names.d.ts.map +1 -1
- 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/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +584 -140
- package/dist/lib/agent-name-normalize.d.ts +17 -0
- package/dist/lib/agent-name-normalize.d.ts.map +1 -0
- 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 +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/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 +554 -92
- package/dist/mcp.js +7 -3
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +125 -30
- 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 +560 -98
- package/dist/server/openapi.d.ts +12 -0
- package/dist/server/openapi.d.ts.map +1 -1
- 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 +29 -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 +623 -258
- 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 +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -3
package/dist/mcp/index.js
CHANGED
|
@@ -44,7 +44,7 @@ var __require = import.meta.require;
|
|
|
44
44
|
function isBlockingDependencyStatus(status) {
|
|
45
45
|
return status !== "completed" && status !== "cancelled";
|
|
46
46
|
}
|
|
47
|
-
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
47
|
+
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
48
48
|
var init_types = __esm(() => {
|
|
49
49
|
TASK_STATUSES = [
|
|
50
50
|
"pending",
|
|
@@ -77,6 +77,22 @@ var init_types = __esm(() => {
|
|
|
77
77
|
this.name = "TaskNotFoundError";
|
|
78
78
|
}
|
|
79
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
|
+
};
|
|
80
96
|
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
81
97
|
projectId;
|
|
82
98
|
static code = "PROJECT_NOT_FOUND";
|
|
@@ -3766,15 +3782,18 @@ var init_identity_mapping = __esm(() => {
|
|
|
3766
3782
|
var exports_config = {};
|
|
3767
3783
|
__export(exports_config, {
|
|
3768
3784
|
parseStorageMode: () => parseStorageMode,
|
|
3785
|
+
parseStorageBackend: () => parseStorageBackend,
|
|
3769
3786
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
3770
3787
|
loadStorageConfig: () => loadStorageConfig,
|
|
3771
3788
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
3772
3789
|
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
3790
|
+
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
3773
3791
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
3774
3792
|
getTodosStorageMode: () => getTodosStorageMode,
|
|
3775
3793
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
3776
3794
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
3777
3795
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
3796
|
+
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
3778
3797
|
getStorageMode: () => getStorageMode,
|
|
3779
3798
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
3780
3799
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
@@ -3799,7 +3818,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
|
|
|
3799
3818
|
};
|
|
3800
3819
|
}
|
|
3801
3820
|
function loadTodosStorageConfig(env = process.env) {
|
|
3802
|
-
const mode =
|
|
3821
|
+
const mode = getTodosStorageBackend(env);
|
|
3803
3822
|
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
3804
3823
|
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
3805
3824
|
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
@@ -3836,29 +3855,41 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
3836
3855
|
function loadStorageConfig(env = process.env) {
|
|
3837
3856
|
return loadTodosStorageConfig(env);
|
|
3838
3857
|
}
|
|
3858
|
+
function isTodosPostgresBackend(config) {
|
|
3859
|
+
return config.mode === "postgres";
|
|
3860
|
+
}
|
|
3839
3861
|
function isTodosRemoteStorageEnabled(config) {
|
|
3840
|
-
return config
|
|
3862
|
+
return isTodosPostgresBackend(config);
|
|
3841
3863
|
}
|
|
3842
3864
|
function assertTodosRemoteStorageConfig(config) {
|
|
3843
|
-
if (!
|
|
3865
|
+
if (!isTodosPostgresBackend(config))
|
|
3844
3866
|
return;
|
|
3845
3867
|
if (!config.database?.url) {
|
|
3846
3868
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
3847
3869
|
}
|
|
3848
3870
|
}
|
|
3849
|
-
function
|
|
3871
|
+
function parseStorageBackend(value) {
|
|
3850
3872
|
const normalized = clean(value)?.toLowerCase();
|
|
3851
3873
|
if (!normalized)
|
|
3852
|
-
return "
|
|
3853
|
-
if (normalized === "
|
|
3874
|
+
return "sqlite";
|
|
3875
|
+
if (normalized === "sqlite" || normalized === "postgres")
|
|
3854
3876
|
return normalized;
|
|
3855
|
-
|
|
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);
|
|
3856
3887
|
}
|
|
3857
3888
|
function getTodosStorageMode(env = process.env) {
|
|
3858
|
-
return
|
|
3889
|
+
return getTodosStorageBackend(env);
|
|
3859
3890
|
}
|
|
3860
3891
|
function getStorageMode(env = process.env) {
|
|
3861
|
-
return
|
|
3892
|
+
return getTodosStorageBackend(env);
|
|
3862
3893
|
}
|
|
3863
3894
|
function isTodosShadowEnabled(env = process.env) {
|
|
3864
3895
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
@@ -3869,8 +3900,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
|
|
|
3869
3900
|
function assertTodosShadowConfig(config, env = process.env) {
|
|
3870
3901
|
if (!isTodosShadowEnabled(env))
|
|
3871
3902
|
return;
|
|
3872
|
-
if (config.mode !== "
|
|
3873
|
-
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})`);
|
|
3874
3905
|
}
|
|
3875
3906
|
if (!config.database?.url) {
|
|
3876
3907
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
|
|
@@ -3926,7 +3957,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
3926
3957
|
}
|
|
3927
3958
|
return parsed;
|
|
3928
3959
|
}
|
|
3929
|
-
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;
|
|
3930
3961
|
var init_config = __esm(() => {
|
|
3931
3962
|
TODOS_STORAGE_TABLES = [
|
|
3932
3963
|
"todos_sync_records",
|
|
@@ -3967,6 +3998,14 @@ var init_config = __esm(() => {
|
|
|
3967
3998
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
3968
3999
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
3969
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
|
+
};
|
|
3970
4009
|
});
|
|
3971
4010
|
|
|
3972
4011
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -4100,6 +4139,30 @@ function findGitRoot(startDir) {
|
|
|
4100
4139
|
}
|
|
4101
4140
|
return null;
|
|
4102
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
|
+
}
|
|
4103
4166
|
function getDbPath() {
|
|
4104
4167
|
if (process.env["HASNA_TODOS_DB_PATH"]) {
|
|
4105
4168
|
return process.env["HASNA_TODOS_DB_PATH"];
|
|
@@ -4107,18 +4170,19 @@ function getDbPath() {
|
|
|
4107
4170
|
if (process.env["TODOS_DB_PATH"]) {
|
|
4108
4171
|
return process.env["TODOS_DB_PATH"];
|
|
4109
4172
|
}
|
|
4173
|
+
if (hasExplicitProjectArg())
|
|
4174
|
+
return getGlobalDbPath();
|
|
4110
4175
|
const cwd = process.cwd();
|
|
4111
4176
|
const nearest = findNearestProjectDb(cwd);
|
|
4112
4177
|
if (nearest)
|
|
4113
4178
|
return nearest;
|
|
4114
4179
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4115
4180
|
const gitRoot = findGitRoot(cwd);
|
|
4116
|
-
if (gitRoot) {
|
|
4181
|
+
if (gitRoot && canCreateScopedProjectDb()) {
|
|
4117
4182
|
return join(gitRoot, ".hasna", "todos", "todos.db");
|
|
4118
4183
|
}
|
|
4119
4184
|
}
|
|
4120
|
-
|
|
4121
|
-
return join(home, ".hasna", "todos", "todos.db");
|
|
4185
|
+
return getGlobalDbPath();
|
|
4122
4186
|
}
|
|
4123
4187
|
function getDatabasePath() {
|
|
4124
4188
|
return getDbPath();
|
|
@@ -4221,10 +4285,13 @@ function resolvePartialId(db, table, partialId) {
|
|
|
4221
4285
|
return null;
|
|
4222
4286
|
}
|
|
4223
4287
|
if (table === "tasks") {
|
|
4224
|
-
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);
|
|
4225
4289
|
if (shortIdRows.length === 1) {
|
|
4226
4290
|
return shortIdRows[0].id;
|
|
4227
4291
|
}
|
|
4292
|
+
if (shortIdRows.length > 1) {
|
|
4293
|
+
throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
4294
|
+
}
|
|
4228
4295
|
}
|
|
4229
4296
|
if (table === "task_lists") {
|
|
4230
4297
|
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
@@ -4346,10 +4413,12 @@ var init_storage_tombstones = __esm(() => {
|
|
|
4346
4413
|
init_machines();
|
|
4347
4414
|
});
|
|
4348
4415
|
|
|
4349
|
-
// src/
|
|
4416
|
+
// src/lib/agent-name-normalize.ts
|
|
4350
4417
|
function normalizeAgentNameInput(name) {
|
|
4351
4418
|
return name.trim().toLowerCase();
|
|
4352
4419
|
}
|
|
4420
|
+
|
|
4421
|
+
// src/db/agent-names.ts
|
|
4353
4422
|
function hasGeneratedNumericSuffix(name) {
|
|
4354
4423
|
return NUMERIC_SUFFIX_RE.test(normalizeAgentNameInput(name));
|
|
4355
4424
|
}
|
|
@@ -20017,14 +20086,12 @@ var init_http_client = __esm(() => {
|
|
|
20017
20086
|
|
|
20018
20087
|
// src/cli/cloud-router.ts
|
|
20019
20088
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
20089
|
+
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
20020
20090
|
import { resolve as resolvePath } from "path";
|
|
20021
20091
|
function cleanMode(value) {
|
|
20022
20092
|
const normalized = value?.trim().toLowerCase();
|
|
20023
20093
|
return normalized || null;
|
|
20024
20094
|
}
|
|
20025
|
-
function modeRole(mode) {
|
|
20026
|
-
return mode === "local" ? "local" : "remote";
|
|
20027
|
-
}
|
|
20028
20095
|
function resolveTodosCliStorageMode(env = process.env) {
|
|
20029
20096
|
for (const source of ["HASNA_TODOS_STORAGE_MODE", "TODOS_STORAGE_MODE"]) {
|
|
20030
20097
|
if (env[source] !== undefined && env[source].trim() === "") {
|
|
@@ -20037,22 +20104,25 @@ function resolveTodosCliStorageMode(env = process.env) {
|
|
|
20037
20104
|
["HASNA_TODOS_STORAGE_MODE", canonical],
|
|
20038
20105
|
["TODOS_STORAGE_MODE", fallback]
|
|
20039
20106
|
]) {
|
|
20040
|
-
if (value && !
|
|
20041
|
-
throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source}=${value} must be local
|
|
20107
|
+
if (value && !(value in TRANSPORT_TOKENS)) {
|
|
20108
|
+
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");
|
|
20042
20109
|
}
|
|
20043
20110
|
}
|
|
20044
|
-
|
|
20111
|
+
const canonicalTransport = canonical ? TRANSPORT_TOKENS[canonical] : null;
|
|
20112
|
+
const fallbackTransport = fallback ? TRANSPORT_TOKENS[fallback] : null;
|
|
20113
|
+
if (canonicalTransport && fallbackTransport && canonicalTransport !== fallbackTransport) {
|
|
20045
20114
|
throw new Error(`REMOTE_STORAGE_MODE_CONFLICT: HASNA_TODOS_STORAGE_MODE=${canonical} conflicts with ` + `TODOS_STORAGE_MODE=${fallback}; local SQLite fallback is disabled`);
|
|
20046
20115
|
}
|
|
20047
|
-
const
|
|
20116
|
+
const transport = canonicalTransport ?? fallbackTransport ?? "sqlite";
|
|
20048
20117
|
return {
|
|
20049
|
-
mode,
|
|
20050
|
-
|
|
20118
|
+
mode: transport,
|
|
20119
|
+
transport,
|
|
20120
|
+
selected: transport === "http",
|
|
20051
20121
|
source: canonical ? "HASNA_TODOS_STORAGE_MODE" : fallback ? "TODOS_STORAGE_MODE" : "default"
|
|
20052
20122
|
};
|
|
20053
20123
|
}
|
|
20054
|
-
function
|
|
20055
|
-
return resolveTodosCliStorageMode(env).
|
|
20124
|
+
function requestedTransport(env) {
|
|
20125
|
+
return resolveTodosCliStorageMode(env).transport;
|
|
20056
20126
|
}
|
|
20057
20127
|
function normalizeRemoteAuthorityUrl(value) {
|
|
20058
20128
|
const trimmed = value?.trim();
|
|
@@ -20105,7 +20175,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
20105
20175
|
return {
|
|
20106
20176
|
selected: false,
|
|
20107
20177
|
ok: true,
|
|
20108
|
-
mode
|
|
20178
|
+
mode,
|
|
20109
20179
|
api_url_configured: false,
|
|
20110
20180
|
api_key_configured: false,
|
|
20111
20181
|
v1_base_url: null,
|
|
@@ -20138,13 +20208,27 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
20138
20208
|
local_fallback: false
|
|
20139
20209
|
};
|
|
20140
20210
|
}
|
|
20211
|
+
function serverStorageMode(normalize = normalizeStorageMode) {
|
|
20212
|
+
const useCache = normalize === normalizeStorageMode;
|
|
20213
|
+
if (useCache && cachedServerMode !== null)
|
|
20214
|
+
return cachedServerMode;
|
|
20215
|
+
for (const candidate of SERVER_MODE_CANDIDATES) {
|
|
20216
|
+
try {
|
|
20217
|
+
normalize(candidate);
|
|
20218
|
+
if (useCache)
|
|
20219
|
+
cachedServerMode = candidate;
|
|
20220
|
+
return candidate;
|
|
20221
|
+
} catch {}
|
|
20222
|
+
}
|
|
20223
|
+
throw new Error(`REMOTE_STORAGE_MODE_UNSUPPORTED: no known server storage mode is accepted by the installed ` + `@hasna/contracts (tried ${SERVER_MODE_CANDIDATES.join(", ")}); the storage-mode enum has changed. ` + `Add the new server token to SERVER_MODE_CANDIDATES in src/cli/cloud-router.ts; ` + `local SQLite fallback is disabled.`);
|
|
20224
|
+
}
|
|
20141
20225
|
function requireTodosRemoteAuthorityEnv(env) {
|
|
20142
20226
|
const status = getTodosRemoteAuthorityConfigStatus(env);
|
|
20143
20227
|
if (!status.ok)
|
|
20144
20228
|
throw new Error(status.issues[0]);
|
|
20145
20229
|
return {
|
|
20146
20230
|
...env,
|
|
20147
|
-
HASNA_TODOS_STORAGE_MODE:
|
|
20231
|
+
HASNA_TODOS_STORAGE_MODE: serverStorageMode(),
|
|
20148
20232
|
HASNA_TODOS_API_URL: status.v1_base_url.replace(/\/v1$/, ""),
|
|
20149
20233
|
HASNA_TODOS_API_KEY: env.HASNA_TODOS_API_KEY.trim()
|
|
20150
20234
|
};
|
|
@@ -20217,8 +20301,7 @@ async function requiredRemoteRoute(client, route, request) {
|
|
|
20217
20301
|
}
|
|
20218
20302
|
}
|
|
20219
20303
|
function getTodosCloudClient(env = process.env) {
|
|
20220
|
-
|
|
20221
|
-
if (!CLOUD_MODES.has(mode))
|
|
20304
|
+
if (requestedTransport(env) !== "http")
|
|
20222
20305
|
return null;
|
|
20223
20306
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
20224
20307
|
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
@@ -20253,13 +20336,38 @@ function toListQuery(filter = {}) {
|
|
|
20253
20336
|
query["assigned_to"] = filter.assigned_to;
|
|
20254
20337
|
if (filter.agent_id)
|
|
20255
20338
|
query["agent_id"] = filter.agent_id;
|
|
20339
|
+
if (filter.tags?.length)
|
|
20340
|
+
query["tags"] = filter.tags.join(",");
|
|
20256
20341
|
if (typeof filter.limit === "number")
|
|
20257
20342
|
query["limit"] = filter.limit;
|
|
20258
20343
|
if (typeof filter.offset === "number")
|
|
20259
20344
|
query["offset"] = filter.offset;
|
|
20260
20345
|
return query;
|
|
20261
20346
|
}
|
|
20347
|
+
async function fetchListTagsCapability(client) {
|
|
20348
|
+
const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
|
|
20349
|
+
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
20350
|
+
return false;
|
|
20351
|
+
const paths = document["paths"];
|
|
20352
|
+
const tasksPath = paths && typeof paths === "object" && !Array.isArray(paths) ? paths["/v1/tasks"] : undefined;
|
|
20353
|
+
const get = tasksPath && typeof tasksPath === "object" && !Array.isArray(tasksPath) ? tasksPath["get"] : undefined;
|
|
20354
|
+
const parameters = get && typeof get === "object" && !Array.isArray(get) ? get["parameters"] : undefined;
|
|
20355
|
+
return Array.isArray(parameters) && parameters.some((parameter) => parameter && typeof parameter === "object" && parameter["name"] === "tags");
|
|
20356
|
+
}
|
|
20357
|
+
async function requireTagsFilterCapability(client) {
|
|
20358
|
+
const authority = remoteAuthorityBase(client);
|
|
20359
|
+
let capability = listTagsCapabilityCache.get(authority);
|
|
20360
|
+
if (!capability) {
|
|
20361
|
+
capability = fetchListTagsCapability(client);
|
|
20362
|
+
listTagsCapabilityCache.set(authority, capability);
|
|
20363
|
+
}
|
|
20364
|
+
if (!await capability) {
|
|
20365
|
+
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");
|
|
20366
|
+
}
|
|
20367
|
+
}
|
|
20262
20368
|
async function cloudListTasks(client, filter = {}) {
|
|
20369
|
+
if (filter.tags?.length)
|
|
20370
|
+
await requireTagsFilterCapability(client);
|
|
20263
20371
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
|
|
20264
20372
|
const envelope = res.raw;
|
|
20265
20373
|
return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
|
|
@@ -20367,6 +20475,8 @@ function redactComment(comment) {
|
|
|
20367
20475
|
}
|
|
20368
20476
|
async function cloudCountTasks(client, filter = {}) {
|
|
20369
20477
|
const { limit: _drop, offset: _o, ...rest } = filter;
|
|
20478
|
+
if (rest.tags?.length)
|
|
20479
|
+
await requireTagsFilterCapability(client);
|
|
20370
20480
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: { ...toListQuery(rest), limit: 1 } }));
|
|
20371
20481
|
const envelope = res.raw;
|
|
20372
20482
|
if (typeof envelope?.total === "number")
|
|
@@ -20429,15 +20539,24 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
20429
20539
|
}
|
|
20430
20540
|
throw new Error(`Task list not found: "${input}"`);
|
|
20431
20541
|
}
|
|
20432
|
-
var UUID_RE,
|
|
20542
|
+
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, listTagsCapabilityCache;
|
|
20433
20543
|
var init_cloud_router = __esm(() => {
|
|
20434
20544
|
init_types();
|
|
20435
20545
|
init_redaction();
|
|
20436
20546
|
init_http_client();
|
|
20437
20547
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20438
|
-
|
|
20439
|
-
|
|
20548
|
+
TRANSPORT_TOKENS = {
|
|
20549
|
+
sqlite: "sqlite",
|
|
20550
|
+
http: "http",
|
|
20551
|
+
local: "sqlite",
|
|
20552
|
+
remote: "http",
|
|
20553
|
+
self_hosted: "http",
|
|
20554
|
+
cloud: "http",
|
|
20555
|
+
hybrid: "http"
|
|
20556
|
+
};
|
|
20440
20557
|
completionCapabilityCache = new Map;
|
|
20558
|
+
SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
|
|
20559
|
+
listTagsCapabilityCache = new Map;
|
|
20441
20560
|
});
|
|
20442
20561
|
|
|
20443
20562
|
// src/mcp/tools/task-crud.ts
|
|
@@ -24023,12 +24142,11 @@ ${task.description || ""}`.split(/\r?\n/);
|
|
|
24023
24142
|
function normalizeStackLocation(value) {
|
|
24024
24143
|
return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
|
|
24025
24144
|
}
|
|
24026
|
-
function
|
|
24145
|
+
function fingerprintContent(task) {
|
|
24027
24146
|
const body = task.description || "";
|
|
24028
24147
|
const text = `${task.title}
|
|
24029
24148
|
${body}`;
|
|
24030
24149
|
return {
|
|
24031
|
-
task,
|
|
24032
24150
|
title: normalizeText(task.title),
|
|
24033
24151
|
body: normalizeText(body),
|
|
24034
24152
|
text: normalizeText(text),
|
|
@@ -24037,6 +24155,9 @@ ${body}`;
|
|
|
24037
24155
|
stackKeys: stackKeysFor(task)
|
|
24038
24156
|
};
|
|
24039
24157
|
}
|
|
24158
|
+
function fingerprint(task) {
|
|
24159
|
+
return { task, ...fingerprintContent(task) };
|
|
24160
|
+
}
|
|
24040
24161
|
function intersects(left, right) {
|
|
24041
24162
|
for (const value of left)
|
|
24042
24163
|
if (right.has(value))
|
|
@@ -25373,7 +25494,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
25373
25494
|
"todos --help",
|
|
25374
25495
|
"todos-mcp --help",
|
|
25375
25496
|
"todos-serve --help",
|
|
25376
|
-
"todos doctor"
|
|
25497
|
+
"todos doctor --no-fail-on-findings"
|
|
25377
25498
|
],
|
|
25378
25499
|
smoke_tests: [
|
|
25379
25500
|
"command -v todos",
|
|
@@ -25381,7 +25502,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
25381
25502
|
"command -v todos-serve",
|
|
25382
25503
|
"todos --version",
|
|
25383
25504
|
"todos --help",
|
|
25384
|
-
"todos doctor"
|
|
25505
|
+
"todos doctor --no-fail-on-findings"
|
|
25385
25506
|
],
|
|
25386
25507
|
rollback: [
|
|
25387
25508
|
"npm view @hasna/todos versions --json",
|
|
@@ -29027,6 +29148,211 @@ var init_auto_assign = __esm(() => {
|
|
|
29027
29148
|
init_agents();
|
|
29028
29149
|
});
|
|
29029
29150
|
|
|
29151
|
+
// src/lib/integrity.ts
|
|
29152
|
+
function resolveIntegritySeverity(spec, measurement) {
|
|
29153
|
+
if (spec.kind === "dangling")
|
|
29154
|
+
return "error";
|
|
29155
|
+
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
29156
|
+
return "error";
|
|
29157
|
+
return spec.base_severity;
|
|
29158
|
+
}
|
|
29159
|
+
function plural(entity, count) {
|
|
29160
|
+
const [one, many] = ENTITY_LABEL[entity];
|
|
29161
|
+
return count === 1 ? one : many;
|
|
29162
|
+
}
|
|
29163
|
+
function formatIntegrityMessage(spec, measurement) {
|
|
29164
|
+
const noun = plural(spec.entity, measurement.count);
|
|
29165
|
+
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
29166
|
+
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
29167
|
+
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}`;
|
|
29168
|
+
}
|
|
29169
|
+
function measuredCondition(spec, measurement, source) {
|
|
29170
|
+
return {
|
|
29171
|
+
id: spec.id,
|
|
29172
|
+
entity: spec.entity,
|
|
29173
|
+
field: spec.field,
|
|
29174
|
+
kind: spec.kind,
|
|
29175
|
+
count: measurement.count,
|
|
29176
|
+
open_count: measurement.open_count,
|
|
29177
|
+
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
29178
|
+
verified: true,
|
|
29179
|
+
source,
|
|
29180
|
+
message: formatIntegrityMessage(spec, measurement),
|
|
29181
|
+
impact: spec.impact
|
|
29182
|
+
};
|
|
29183
|
+
}
|
|
29184
|
+
function unverifiedCondition(spec, reason) {
|
|
29185
|
+
return {
|
|
29186
|
+
id: spec.id,
|
|
29187
|
+
entity: spec.entity,
|
|
29188
|
+
field: spec.field,
|
|
29189
|
+
kind: spec.kind,
|
|
29190
|
+
count: null,
|
|
29191
|
+
open_count: null,
|
|
29192
|
+
severity: null,
|
|
29193
|
+
verified: false,
|
|
29194
|
+
source: "unverified",
|
|
29195
|
+
unverified_reason: reason,
|
|
29196
|
+
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
29197
|
+
impact: spec.impact
|
|
29198
|
+
};
|
|
29199
|
+
}
|
|
29200
|
+
function summarizeIntegrity(conditions) {
|
|
29201
|
+
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
29202
|
+
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
29203
|
+
const unverified = conditions.length - measured.length;
|
|
29204
|
+
return {
|
|
29205
|
+
ok: unverified === 0 && findings.length === 0,
|
|
29206
|
+
findings: findings.length,
|
|
29207
|
+
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
29208
|
+
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
29209
|
+
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
29210
|
+
unverified,
|
|
29211
|
+
complete: unverified === 0
|
|
29212
|
+
};
|
|
29213
|
+
}
|
|
29214
|
+
function buildIntegrityReport(conditions, generatedAt) {
|
|
29215
|
+
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
|
|
29216
|
+
return {
|
|
29217
|
+
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
29218
|
+
generated_at: generatedAt,
|
|
29219
|
+
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
29220
|
+
conditions,
|
|
29221
|
+
summary: summarizeIntegrity(conditions)
|
|
29222
|
+
};
|
|
29223
|
+
}
|
|
29224
|
+
function sqliteOpenStatusList() {
|
|
29225
|
+
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
29226
|
+
}
|
|
29227
|
+
function buildSqliteIntegritySql(spec) {
|
|
29228
|
+
const table = SQLITE_TABLE[spec.entity];
|
|
29229
|
+
const target = SQLITE_TABLE[spec.target];
|
|
29230
|
+
const column = `t."${spec.field}"`;
|
|
29231
|
+
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})`;
|
|
29232
|
+
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
29233
|
+
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
29234
|
+
}
|
|
29235
|
+
function buildPostgresIntegritySql(spec, options) {
|
|
29236
|
+
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
29237
|
+
const p = (value) => {
|
|
29238
|
+
params.push(value);
|
|
29239
|
+
return `$${params.length}`;
|
|
29240
|
+
};
|
|
29241
|
+
const column = `t.payload->>'${spec.field}'`;
|
|
29242
|
+
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})`;
|
|
29243
|
+
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
29244
|
+
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}`;
|
|
29245
|
+
return { sql, params };
|
|
29246
|
+
}
|
|
29247
|
+
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1", TERMINAL_TASK_STATUSES, OPEN_TASK_STATUSES, INTEGRITY_CONDITIONS, ENTITY_LABEL, SQLITE_TABLE, RECORD_TYPE;
|
|
29248
|
+
var init_integrity = __esm(() => {
|
|
29249
|
+
init_types();
|
|
29250
|
+
TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
29251
|
+
OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
29252
|
+
INTEGRITY_CONDITIONS = [
|
|
29253
|
+
{
|
|
29254
|
+
id: "tasks_without_project",
|
|
29255
|
+
entity: "task",
|
|
29256
|
+
field: "project_id",
|
|
29257
|
+
target: "project",
|
|
29258
|
+
kind: "missing",
|
|
29259
|
+
base_severity: "warn",
|
|
29260
|
+
escalate_when_open: true,
|
|
29261
|
+
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
29262
|
+
},
|
|
29263
|
+
{
|
|
29264
|
+
id: "tasks_without_task_list",
|
|
29265
|
+
entity: "task",
|
|
29266
|
+
field: "task_list_id",
|
|
29267
|
+
target: "task_list",
|
|
29268
|
+
kind: "missing",
|
|
29269
|
+
base_severity: "warn",
|
|
29270
|
+
escalate_when_open: true,
|
|
29271
|
+
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
29272
|
+
},
|
|
29273
|
+
{
|
|
29274
|
+
id: "tasks_with_unregistered_project",
|
|
29275
|
+
entity: "task",
|
|
29276
|
+
field: "project_id",
|
|
29277
|
+
target: "project",
|
|
29278
|
+
kind: "dangling",
|
|
29279
|
+
base_severity: "error",
|
|
29280
|
+
escalate_when_open: false,
|
|
29281
|
+
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
29282
|
+
},
|
|
29283
|
+
{
|
|
29284
|
+
id: "tasks_with_unregistered_task_list",
|
|
29285
|
+
entity: "task",
|
|
29286
|
+
field: "task_list_id",
|
|
29287
|
+
target: "task_list",
|
|
29288
|
+
kind: "dangling",
|
|
29289
|
+
base_severity: "error",
|
|
29290
|
+
escalate_when_open: false,
|
|
29291
|
+
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
29292
|
+
},
|
|
29293
|
+
{
|
|
29294
|
+
id: "task_lists_without_project",
|
|
29295
|
+
entity: "task_list",
|
|
29296
|
+
field: "project_id",
|
|
29297
|
+
target: "project",
|
|
29298
|
+
kind: "missing",
|
|
29299
|
+
base_severity: "warn",
|
|
29300
|
+
escalate_when_open: false,
|
|
29301
|
+
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
29302
|
+
},
|
|
29303
|
+
{
|
|
29304
|
+
id: "task_lists_with_unregistered_project",
|
|
29305
|
+
entity: "task_list",
|
|
29306
|
+
field: "project_id",
|
|
29307
|
+
target: "project",
|
|
29308
|
+
kind: "dangling",
|
|
29309
|
+
base_severity: "error",
|
|
29310
|
+
escalate_when_open: false,
|
|
29311
|
+
impact: "points at a project id that does not exist; the list can never be reached"
|
|
29312
|
+
}
|
|
29313
|
+
];
|
|
29314
|
+
ENTITY_LABEL = {
|
|
29315
|
+
task: ["task", "tasks"],
|
|
29316
|
+
task_list: ["task list", "task lists"]
|
|
29317
|
+
};
|
|
29318
|
+
SQLITE_TABLE = {
|
|
29319
|
+
task: "tasks",
|
|
29320
|
+
task_list: "task_lists",
|
|
29321
|
+
project: "projects"
|
|
29322
|
+
};
|
|
29323
|
+
RECORD_TYPE = {
|
|
29324
|
+
task: "tasks",
|
|
29325
|
+
task_list: "task_lists",
|
|
29326
|
+
project: "projects"
|
|
29327
|
+
};
|
|
29328
|
+
});
|
|
29329
|
+
|
|
29330
|
+
// src/db/integrity.ts
|
|
29331
|
+
function tableExists(db, table) {
|
|
29332
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
29333
|
+
}
|
|
29334
|
+
function scanSqliteIntegrity(db = getDatabase()) {
|
|
29335
|
+
const missing = Object.keys(REQUIRED_TABLES2).filter((table) => !tableExists(db, table));
|
|
29336
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
29337
|
+
if (missing.length > 0) {
|
|
29338
|
+
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
29339
|
+
}
|
|
29340
|
+
try {
|
|
29341
|
+
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
29342
|
+
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
29343
|
+
} catch (error) {
|
|
29344
|
+
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
29345
|
+
}
|
|
29346
|
+
});
|
|
29347
|
+
return buildIntegrityReport(conditions, now());
|
|
29348
|
+
}
|
|
29349
|
+
var REQUIRED_TABLES2;
|
|
29350
|
+
var init_integrity2 = __esm(() => {
|
|
29351
|
+
init_database();
|
|
29352
|
+
init_integrity();
|
|
29353
|
+
REQUIRED_TABLES2 = { tasks: true, task_lists: true, projects: true };
|
|
29354
|
+
});
|
|
29355
|
+
|
|
29030
29356
|
// src/lib/doctor.ts
|
|
29031
29357
|
var exports_doctor = {};
|
|
29032
29358
|
__export(exports_doctor, {
|
|
@@ -29034,7 +29360,7 @@ __export(exports_doctor, {
|
|
|
29034
29360
|
});
|
|
29035
29361
|
import { chmodSync, copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
|
|
29036
29362
|
import { basename as basename4, dirname as dirname6, join as join8 } from "path";
|
|
29037
|
-
function
|
|
29363
|
+
function tableExists2(db, table) {
|
|
29038
29364
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
29039
29365
|
}
|
|
29040
29366
|
function quoteIdent(identifier) {
|
|
@@ -29118,7 +29444,7 @@ function findDuplicateIndexes(db) {
|
|
|
29118
29444
|
return duplicates;
|
|
29119
29445
|
}
|
|
29120
29446
|
function findMissingProjectRoots(db) {
|
|
29121
|
-
if (!
|
|
29447
|
+
if (!tableExists2(db, "projects"))
|
|
29122
29448
|
return 0;
|
|
29123
29449
|
let missing = 0;
|
|
29124
29450
|
const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
@@ -29133,7 +29459,7 @@ function findMissingProjectRoots(db) {
|
|
|
29133
29459
|
return missing;
|
|
29134
29460
|
}
|
|
29135
29461
|
function addTaskStateChecks(db, checks) {
|
|
29136
|
-
if (!
|
|
29462
|
+
if (!tableExists2(db, "tasks"))
|
|
29137
29463
|
return;
|
|
29138
29464
|
const columns = getTableColumns(db, "tasks");
|
|
29139
29465
|
const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
@@ -29205,15 +29531,42 @@ function createBackup(dbPath) {
|
|
|
29205
29531
|
function pushRepair(repairs, type, message, applied, count) {
|
|
29206
29532
|
repairs.push({ type, message, applied, count });
|
|
29207
29533
|
}
|
|
29208
|
-
function summarize2(checks, repairs) {
|
|
29534
|
+
function summarize2(checks, repairs, integrity) {
|
|
29209
29535
|
return {
|
|
29210
29536
|
errors: checks.filter((check) => check.severity === "error").length,
|
|
29211
29537
|
warnings: checks.filter((check) => check.severity === "warn").length,
|
|
29212
29538
|
infos: checks.filter((check) => check.severity === "info").length,
|
|
29213
29539
|
repairable: checks.filter((check) => check.repairable).length,
|
|
29214
|
-
applied: repairs.filter((repair) => repair.applied).length
|
|
29540
|
+
applied: repairs.filter((repair) => repair.applied).length,
|
|
29541
|
+
integrity_findings: integrity.summary.findings,
|
|
29542
|
+
integrity_rows: integrity.summary.rows,
|
|
29543
|
+
integrity_unverified: integrity.summary.unverified
|
|
29215
29544
|
};
|
|
29216
29545
|
}
|
|
29546
|
+
function addIntegrityChecks(integrity, checks) {
|
|
29547
|
+
for (const condition of integrity.conditions) {
|
|
29548
|
+
if (!condition.verified) {
|
|
29549
|
+
addCheck(checks, {
|
|
29550
|
+
severity: "warn",
|
|
29551
|
+
type: `${condition.id}_unverified`,
|
|
29552
|
+
message: condition.message,
|
|
29553
|
+
repairable: false,
|
|
29554
|
+
integrity: true
|
|
29555
|
+
});
|
|
29556
|
+
continue;
|
|
29557
|
+
}
|
|
29558
|
+
if ((condition.count ?? 0) === 0)
|
|
29559
|
+
continue;
|
|
29560
|
+
addCheck(checks, {
|
|
29561
|
+
severity: condition.severity ?? "warn",
|
|
29562
|
+
type: condition.id,
|
|
29563
|
+
message: `${condition.message} \u2014 ${condition.impact}`,
|
|
29564
|
+
count: condition.count ?? undefined,
|
|
29565
|
+
repairable: false,
|
|
29566
|
+
integrity: true
|
|
29567
|
+
});
|
|
29568
|
+
}
|
|
29569
|
+
}
|
|
29217
29570
|
function deleteOrphans(db, table, where) {
|
|
29218
29571
|
const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
29219
29572
|
if (before > 0)
|
|
@@ -29242,7 +29595,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29242
29595
|
message: `Schema at migration ${migrationCurrent}`
|
|
29243
29596
|
});
|
|
29244
29597
|
}
|
|
29245
|
-
const missingTables =
|
|
29598
|
+
const missingTables = REQUIRED_TABLES3.filter((table) => !tableExists2(db, table));
|
|
29246
29599
|
if (missingTables.length > 0) {
|
|
29247
29600
|
addCheck(checks, {
|
|
29248
29601
|
severity: "error",
|
|
@@ -29252,7 +29605,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29252
29605
|
repairable: true
|
|
29253
29606
|
});
|
|
29254
29607
|
}
|
|
29255
|
-
const orphanedParents =
|
|
29608
|
+
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;
|
|
29256
29609
|
if (orphanedParents > 0) {
|
|
29257
29610
|
addCheck(checks, {
|
|
29258
29611
|
severity: "error",
|
|
@@ -29262,7 +29615,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29262
29615
|
repairable: true
|
|
29263
29616
|
});
|
|
29264
29617
|
}
|
|
29265
|
-
const orphanedDependencies =
|
|
29618
|
+
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;
|
|
29266
29619
|
if (orphanedDependencies > 0) {
|
|
29267
29620
|
addCheck(checks, {
|
|
29268
29621
|
severity: "error",
|
|
@@ -29281,7 +29634,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29281
29634
|
];
|
|
29282
29635
|
let orphanedRows = 0;
|
|
29283
29636
|
for (const [table, where] of orphanTables) {
|
|
29284
|
-
if (!
|
|
29637
|
+
if (!tableExists2(db, table))
|
|
29285
29638
|
continue;
|
|
29286
29639
|
orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
29287
29640
|
}
|
|
@@ -29295,6 +29648,8 @@ function runTodosDoctor(options = {}) {
|
|
|
29295
29648
|
});
|
|
29296
29649
|
}
|
|
29297
29650
|
addTaskStateChecks(db, checks);
|
|
29651
|
+
const integrity = scanSqliteIntegrity(db);
|
|
29652
|
+
addIntegrityChecks(integrity, checks);
|
|
29298
29653
|
const corruptJson = findCorruptJsonMetadata(db);
|
|
29299
29654
|
if (corruptJson.length > 0) {
|
|
29300
29655
|
addCheck(checks, {
|
|
@@ -29353,12 +29708,12 @@ function runTodosDoctor(options = {}) {
|
|
|
29353
29708
|
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)");
|
|
29354
29709
|
pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
|
|
29355
29710
|
}
|
|
29356
|
-
if (orphanedDependencies > 0 &&
|
|
29711
|
+
if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
|
|
29357
29712
|
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)");
|
|
29358
29713
|
pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
|
|
29359
29714
|
}
|
|
29360
29715
|
for (const [table, where] of orphanTables) {
|
|
29361
|
-
if (!
|
|
29716
|
+
if (!tableExists2(db, table))
|
|
29362
29717
|
continue;
|
|
29363
29718
|
const count = deleteOrphans(db, table, where);
|
|
29364
29719
|
if (count > 0)
|
|
@@ -29387,26 +29742,30 @@ function runTodosDoctor(options = {}) {
|
|
|
29387
29742
|
}
|
|
29388
29743
|
}
|
|
29389
29744
|
}
|
|
29390
|
-
const
|
|
29391
|
-
const
|
|
29745
|
+
const reread = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }) : undefined;
|
|
29746
|
+
const finalChecks = reread?.checks ?? checks;
|
|
29747
|
+
const finalIntegrity = reread?.integrity ?? integrity;
|
|
29748
|
+
const summary = summarize2(finalChecks, repairs, finalIntegrity);
|
|
29392
29749
|
return {
|
|
29393
|
-
ok: !finalChecks.some((check) => check.severity === "error"),
|
|
29750
|
+
ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
|
|
29394
29751
|
dry_run: !apply,
|
|
29395
29752
|
database_path: dbPath,
|
|
29396
29753
|
migration: { current: getMigrationLevel(db), expected: migrationExpected },
|
|
29397
29754
|
backup,
|
|
29398
29755
|
checks: finalChecks,
|
|
29399
29756
|
repairs,
|
|
29757
|
+
integrity: finalIntegrity,
|
|
29400
29758
|
summary
|
|
29401
29759
|
};
|
|
29402
29760
|
}
|
|
29403
|
-
var
|
|
29761
|
+
var REQUIRED_TABLES3;
|
|
29404
29762
|
var init_doctor = __esm(() => {
|
|
29405
29763
|
init_database();
|
|
29764
|
+
init_integrity2();
|
|
29406
29765
|
init_migrations();
|
|
29407
29766
|
init_schema();
|
|
29408
29767
|
init_recurrence();
|
|
29409
|
-
|
|
29768
|
+
REQUIRED_TABLES3 = [
|
|
29410
29769
|
"_migrations",
|
|
29411
29770
|
"projects",
|
|
29412
29771
|
"tasks",
|
|
@@ -29750,7 +30109,7 @@ function limits(input) {
|
|
|
29750
30109
|
verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
|
|
29751
30110
|
run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
|
|
29752
30111
|
dependency_limit: clamp(input.dependency_limit, DEFAULT_LIMITS.dependency_limit, 100),
|
|
29753
|
-
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit,
|
|
30112
|
+
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit, MAX_CONTEXT_TASKS),
|
|
29754
30113
|
max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
|
|
29755
30114
|
summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
|
|
29756
30115
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
@@ -29773,6 +30132,13 @@ function taskSummary(task) {
|
|
|
29773
30132
|
priority: task.priority
|
|
29774
30133
|
};
|
|
29775
30134
|
}
|
|
30135
|
+
function latestPlanTasks(planId, limit, db) {
|
|
30136
|
+
const rows = db.query(`SELECT * FROM tasks
|
|
30137
|
+
WHERE plan_id = ? AND archived_at IS NULL
|
|
30138
|
+
ORDER BY updated_at DESC, created_at DESC, id ASC
|
|
30139
|
+
LIMIT ?`).all(planId, limit);
|
|
30140
|
+
return rows.map(rowToTask);
|
|
30141
|
+
}
|
|
29776
30142
|
function acceptanceCriteria(task, maxText) {
|
|
29777
30143
|
const metadata = task.metadata || {};
|
|
29778
30144
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
@@ -29870,7 +30236,7 @@ function summarizeSection(pack, section, maxChars) {
|
|
|
29870
30236
|
return "No plan was attached.";
|
|
29871
30237
|
return summarizeStrings([
|
|
29872
30238
|
`Plan ${pack.plan.name} is ${pack.plan.status}`,
|
|
29873
|
-
`${pack.plan.tasks.length} listed plan
|
|
30239
|
+
`${pack.plan.tasks.length} of ${pack.plan.total_tasks} plan tasks listed; ${pack.plan.omitted_tasks} omitted`,
|
|
29874
30240
|
...pack.plan.tasks.slice(0, 4).map((task) => `${task.status} ${task.short_id || task.id.slice(0, 8)} ${task.title}`)
|
|
29875
30241
|
], maxChars);
|
|
29876
30242
|
}
|
|
@@ -30020,7 +30386,8 @@ function createAgentContextPack(input, db) {
|
|
|
30020
30386
|
const limit = limits(input);
|
|
30021
30387
|
const project = task.project_id ? getProject(task.project_id, d) : null;
|
|
30022
30388
|
const plan = task.plan_id ? getPlan(task.plan_id, d) : null;
|
|
30023
|
-
const planTasks = task.plan_id ?
|
|
30389
|
+
const planTasks = task.plan_id ? latestPlanTasks(task.plan_id, limit.plan_task_limit, d) : [];
|
|
30390
|
+
const totalPlanTasks = task.plan_id ? countTasks({ plan_id: task.plan_id }, d) : 0;
|
|
30024
30391
|
const upstream = getTaskDependencies(task.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
|
|
30025
30392
|
const downstream = getTaskDependents(task.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
|
|
30026
30393
|
const comments = listComments(task.id, d);
|
|
@@ -30063,6 +30430,9 @@ function createAgentContextPack(input, db) {
|
|
|
30063
30430
|
warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
|
|
30064
30431
|
if (fileMap.size > relevantFiles.length)
|
|
30065
30432
|
warnings.push(`${fileMap.size - relevantFiles.length} relevant files omitted`);
|
|
30433
|
+
if (totalPlanTasks > planTasks.length && plan) {
|
|
30434
|
+
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>"`);
|
|
30435
|
+
}
|
|
30066
30436
|
const contextTask = {
|
|
30067
30437
|
id: task.id,
|
|
30068
30438
|
short_id: task.short_id,
|
|
@@ -30094,8 +30464,9 @@ function createAgentContextPack(input, db) {
|
|
|
30094
30464
|
description: truncate2(plan.description, limit.max_text_chars),
|
|
30095
30465
|
status: plan.status,
|
|
30096
30466
|
agent_id: plan.agent_id,
|
|
30097
|
-
tasks: planTasks.
|
|
30098
|
-
|
|
30467
|
+
tasks: planTasks.map(taskSummary).filter((item) => Boolean(item)),
|
|
30468
|
+
total_tasks: totalPlanTasks,
|
|
30469
|
+
omitted_tasks: Math.max(0, totalPlanTasks - planTasks.length)
|
|
30099
30470
|
} : null,
|
|
30100
30471
|
acceptance_criteria: acceptanceCriteria(task, limit.max_text_chars),
|
|
30101
30472
|
dependencies: {
|
|
@@ -30205,6 +30576,8 @@ ${pack.task.description}` : null,
|
|
|
30205
30576
|
"## Project And Plan",
|
|
30206
30577
|
pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
|
|
30207
30578
|
pack.plan ? `- Plan: ${pack.plan.name} (${pack.plan.status})` : "- Plan: none",
|
|
30579
|
+
pack.plan ? `- Plan tasks: ${pack.plan.tasks.length} shown of ${pack.plan.total_tasks} total (${pack.plan.omitted_tasks} omitted)` : null,
|
|
30580
|
+
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,
|
|
30208
30581
|
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,
|
|
30209
30582
|
"",
|
|
30210
30583
|
"## Acceptance Criteria",
|
|
@@ -30285,7 +30658,7 @@ function renderAgentContextPack(pack, format, compact2 = false) {
|
|
|
30285
30658
|
return compact2 ? renderAgentContextPackCompactMarkdown(pack) : renderAgentContextPackMarkdown(pack);
|
|
30286
30659
|
return compact2 ? JSON.stringify(pack) : JSON.stringify(pack, null, 2);
|
|
30287
30660
|
}
|
|
30288
|
-
var DEFAULT_LIMITS, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
30661
|
+
var DEFAULT_LIMITS, MAX_CONTEXT_TASKS = 24, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
30289
30662
|
var init_context_packs = __esm(() => {
|
|
30290
30663
|
init_comments();
|
|
30291
30664
|
init_database();
|
|
@@ -30304,7 +30677,7 @@ var init_context_packs = __esm(() => {
|
|
|
30304
30677
|
verification_limit: 10,
|
|
30305
30678
|
run_limit: 3,
|
|
30306
30679
|
dependency_limit: 12,
|
|
30307
|
-
plan_task_limit:
|
|
30680
|
+
plan_task_limit: 24,
|
|
30308
30681
|
max_text_chars: 6000,
|
|
30309
30682
|
summary_char_limit: 480,
|
|
30310
30683
|
stale_after_hours: 72
|
|
@@ -31353,7 +31726,7 @@ function parseObject2(value) {
|
|
|
31353
31726
|
function normalizeKey(value) {
|
|
31354
31727
|
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
31355
31728
|
}
|
|
31356
|
-
function
|
|
31729
|
+
function normalizeTaskFindingFingerprint(value) {
|
|
31357
31730
|
const normalized = normalizeKey(value);
|
|
31358
31731
|
if (!normalized)
|
|
31359
31732
|
throw new Error("finding fingerprint is required");
|
|
@@ -31462,7 +31835,7 @@ function assertTask(taskId, db) {
|
|
|
31462
31835
|
throw new TaskNotFoundError(taskId);
|
|
31463
31836
|
}
|
|
31464
31837
|
function nextFinding(input, db) {
|
|
31465
|
-
const fingerprint2 =
|
|
31838
|
+
const fingerprint2 = normalizeTaskFindingFingerprint(input.fingerprint);
|
|
31466
31839
|
const title = redactOptional(input.title, 300);
|
|
31467
31840
|
if (!title)
|
|
31468
31841
|
throw new Error("finding title is required");
|
|
@@ -31610,7 +31983,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
31610
31983
|
const timestamp4 = input.resolved_at || now();
|
|
31611
31984
|
const status = normalizeResolutionStatus(input.status);
|
|
31612
31985
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
31613
|
-
const present = new Set(input.fingerprints.map(
|
|
31986
|
+
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
31614
31987
|
const warnings = [];
|
|
31615
31988
|
const conditions = ["task_id = ?", "status = 'open'"];
|
|
31616
31989
|
const params = [input.task_id];
|
|
@@ -34198,7 +34571,7 @@ var package_default;
|
|
|
34198
34571
|
var init_package = __esm(() => {
|
|
34199
34572
|
package_default = {
|
|
34200
34573
|
name: "@hasna/todos",
|
|
34201
|
-
version: "0.13.
|
|
34574
|
+
version: "0.13.4",
|
|
34202
34575
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
34203
34576
|
type: "module",
|
|
34204
34577
|
main: "dist/index.js",
|
|
@@ -34232,6 +34605,10 @@ var init_package = __esm(() => {
|
|
|
34232
34605
|
"./storage": {
|
|
34233
34606
|
types: "./dist/storage.d.ts",
|
|
34234
34607
|
import: "./dist/storage.js"
|
|
34608
|
+
},
|
|
34609
|
+
"./testing": {
|
|
34610
|
+
types: "./dist/testing.d.ts",
|
|
34611
|
+
import: "./dist/testing.js"
|
|
34235
34612
|
}
|
|
34236
34613
|
},
|
|
34237
34614
|
workspaces: [
|
|
@@ -34244,8 +34621,8 @@ var init_package = __esm(() => {
|
|
|
34244
34621
|
"README.md"
|
|
34245
34622
|
],
|
|
34246
34623
|
scripts: {
|
|
34247
|
-
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",
|
|
34248
|
-
"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/*'",
|
|
34624
|
+
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",
|
|
34625
|
+
"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/*'",
|
|
34249
34626
|
migrate: "bun run src/server/index.ts migrate",
|
|
34250
34627
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
34251
34628
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
@@ -43951,15 +44328,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
43951
44328
|
return null;
|
|
43952
44329
|
if (TASK_UUID_RE.test(raw))
|
|
43953
44330
|
return getTask(raw, db);
|
|
43954
|
-
const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
44331
|
+
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}`)}%`);
|
|
43955
44332
|
if (prefixRows.length > 1) {
|
|
43956
|
-
throw new
|
|
44333
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
43957
44334
|
}
|
|
43958
44335
|
if (prefixRows.length === 1)
|
|
43959
44336
|
return getTask(prefixRows[0].id, db);
|
|
43960
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
|
|
44337
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
|
|
43961
44338
|
if (shortIdRows.length > 1) {
|
|
43962
|
-
throw new
|
|
44339
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
43963
44340
|
}
|
|
43964
44341
|
if (shortIdRows.length === 1)
|
|
43965
44342
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -44108,6 +44485,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
44108
44485
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
44109
44486
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
44110
44487
|
},
|
|
44488
|
+
integrity: {
|
|
44489
|
+
report: () => scanSqliteIntegrity(database())
|
|
44490
|
+
},
|
|
44111
44491
|
transaction: (fn) => {
|
|
44112
44492
|
const tx = database().transaction(() => fn(adapter));
|
|
44113
44493
|
return tx();
|
|
@@ -44117,6 +44497,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
44117
44497
|
}
|
|
44118
44498
|
var TASK_UUID_RE;
|
|
44119
44499
|
var init_local_sqlite = __esm(() => {
|
|
44500
|
+
init_types();
|
|
44120
44501
|
init_search();
|
|
44121
44502
|
init_tasks();
|
|
44122
44503
|
init_projects();
|
|
@@ -44127,6 +44508,7 @@ var init_local_sqlite = __esm(() => {
|
|
|
44127
44508
|
init_audit();
|
|
44128
44509
|
init_comments();
|
|
44129
44510
|
init_database();
|
|
44511
|
+
init_integrity2();
|
|
44130
44512
|
init_sqlite_snapshot();
|
|
44131
44513
|
TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
44132
44514
|
});
|
|
@@ -44994,7 +45376,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
44994
45376
|
agents: {
|
|
44995
45377
|
register: (input, context) => registerAgent2(input, store, context),
|
|
44996
45378
|
get: (id) => store.get("agents", id),
|
|
44997
|
-
getByName: async (name) => (await store.list("agents")
|
|
45379
|
+
getByName: async (name) => matchAgentByName(await store.list("agents"), name),
|
|
44998
45380
|
list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
|
|
44999
45381
|
update: (id, input) => updateAgent2(id, input, store),
|
|
45000
45382
|
heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
|
|
@@ -45051,6 +45433,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
45051
45433
|
exportSnapshot: () => exportSnapshot(store),
|
|
45052
45434
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
45053
45435
|
},
|
|
45436
|
+
integrity: {
|
|
45437
|
+
report: () => store.integrityReport()
|
|
45438
|
+
},
|
|
45054
45439
|
transaction: (fn) => fn(adapter)
|
|
45055
45440
|
};
|
|
45056
45441
|
return adapter;
|
|
@@ -45157,8 +45542,9 @@ class PostgresJsonRecordStore {
|
|
|
45157
45542
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
45158
45543
|
if (filter.session_id !== undefined)
|
|
45159
45544
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
45160
|
-
if (filter.tags?.length)
|
|
45161
|
-
conds.push(`payload->'tags'
|
|
45545
|
+
if (filter.tags?.length) {
|
|
45546
|
+
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)})`);
|
|
45547
|
+
}
|
|
45162
45548
|
if (filter.has_recurrence !== undefined) {
|
|
45163
45549
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
45164
45550
|
}
|
|
@@ -45219,7 +45605,8 @@ class PostgresJsonRecordStore {
|
|
|
45219
45605
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
45220
45606
|
LIMIT 2`, [this.service, raw, upper]);
|
|
45221
45607
|
if (prefixResult.rows.length > 1) {
|
|
45222
|
-
|
|
45608
|
+
const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
|
|
45609
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
|
|
45223
45610
|
}
|
|
45224
45611
|
if (prefixResult.rows.length === 1) {
|
|
45225
45612
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -45230,7 +45617,8 @@ class PostgresJsonRecordStore {
|
|
|
45230
45617
|
AND LOWER(payload->>'short_id') = $2
|
|
45231
45618
|
LIMIT 2`, [this.service, raw]);
|
|
45232
45619
|
if (shortIdResult.rows.length > 1) {
|
|
45233
|
-
|
|
45620
|
+
const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
|
|
45621
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
|
|
45234
45622
|
}
|
|
45235
45623
|
if (shortIdResult.rows.length === 1) {
|
|
45236
45624
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -45244,6 +45632,24 @@ class PostgresJsonRecordStore {
|
|
|
45244
45632
|
const result = await this.options.client.query(sql, params);
|
|
45245
45633
|
return Number(result.rows[0]?.count ?? 0);
|
|
45246
45634
|
}
|
|
45635
|
+
async integrityReport() {
|
|
45636
|
+
await this.ensureSchema();
|
|
45637
|
+
const conditions = [];
|
|
45638
|
+
for (const spec of INTEGRITY_CONDITIONS) {
|
|
45639
|
+
const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
|
|
45640
|
+
try {
|
|
45641
|
+
const result = await this.options.client.query(sql, params);
|
|
45642
|
+
const row = result.rows[0];
|
|
45643
|
+
conditions.push(measuredCondition(spec, {
|
|
45644
|
+
count: Number(row?.count ?? 0),
|
|
45645
|
+
open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
|
|
45646
|
+
}, "postgres"));
|
|
45647
|
+
} catch (error) {
|
|
45648
|
+
conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
|
|
45649
|
+
}
|
|
45650
|
+
}
|
|
45651
|
+
return buildIntegrityReport(conditions, new Date().toISOString());
|
|
45652
|
+
}
|
|
45247
45653
|
async listTombstones() {
|
|
45248
45654
|
await this.ensureSchema();
|
|
45249
45655
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -45719,7 +46125,7 @@ async function startTask2(id, agentId, store) {
|
|
|
45719
46125
|
if (task2.status !== "pending" && task2.status !== "in_progress") {
|
|
45720
46126
|
throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
|
|
45721
46127
|
}
|
|
45722
|
-
|
|
46128
|
+
const started = await patchTask(task2, {
|
|
45723
46129
|
status: "in_progress",
|
|
45724
46130
|
assigned_to: task2.assigned_to ?? agentId,
|
|
45725
46131
|
agent_id: task2.agent_id ?? agentId,
|
|
@@ -45727,6 +46133,8 @@ async function startTask2(id, agentId, store) {
|
|
|
45727
46133
|
locked_at: new Date().toISOString(),
|
|
45728
46134
|
started_at: task2.started_at ?? new Date().toISOString()
|
|
45729
46135
|
}, store);
|
|
46136
|
+
await logTaskChange2(task2.id, "start", "status", task2.status, "in_progress", agentId, store);
|
|
46137
|
+
return started;
|
|
45730
46138
|
}
|
|
45731
46139
|
async function completeTask2(id, agentId, options, store) {
|
|
45732
46140
|
const task2 = await store.completeTask(id, agentId, options);
|
|
@@ -46029,15 +46437,27 @@ async function updatePlan2(id, input, store) {
|
|
|
46029
46437
|
}
|
|
46030
46438
|
return store.upsert("plans", { ...plan, ...patch, updated_at: new Date().toISOString() });
|
|
46031
46439
|
}
|
|
46440
|
+
function matchAgentByName(agents, name, options) {
|
|
46441
|
+
const target = normalizeAgentNameInput(name);
|
|
46442
|
+
if (!target)
|
|
46443
|
+
return null;
|
|
46444
|
+
const matches = agents.filter((agent) => normalizeAgentNameInput(agent.name) === target && (options?.includeArchived !== false || agent.status !== "archived"));
|
|
46445
|
+
if (matches.length === 0)
|
|
46446
|
+
return null;
|
|
46447
|
+
return matches.reduce((freshest, candidate) => new Date(candidate.last_seen_at).getTime() > new Date(freshest.last_seen_at).getTime() ? candidate : freshest);
|
|
46448
|
+
}
|
|
46032
46449
|
async function registerAgent2(input, store, context) {
|
|
46033
|
-
const
|
|
46450
|
+
const canonicalName = normalizeAgentNameInput(input.name);
|
|
46451
|
+
const existing = matchAgentByName(await store.list("agents"), canonicalName, {
|
|
46452
|
+
includeArchived: false
|
|
46453
|
+
});
|
|
46034
46454
|
if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
|
|
46035
|
-
return { conflict: true, message: `Agent name '${
|
|
46455
|
+
return { conflict: true, message: `Agent name '${canonicalName}' is already active` };
|
|
46036
46456
|
}
|
|
46037
46457
|
const timestamp4 = new Date().toISOString();
|
|
46038
46458
|
const agent = {
|
|
46039
46459
|
id: existing?.id ?? randomUUID3().slice(0, 8),
|
|
46040
|
-
name:
|
|
46460
|
+
name: canonicalName,
|
|
46041
46461
|
description: input.description ?? existing?.description ?? null,
|
|
46042
46462
|
role: input.role ?? existing?.role ?? null,
|
|
46043
46463
|
title: input.title ?? existing?.title ?? null,
|
|
@@ -46075,7 +46495,7 @@ async function resolveAgent3(idOrName, store) {
|
|
|
46075
46495
|
const byId = await store.get("agents", idOrName);
|
|
46076
46496
|
if (byId)
|
|
46077
46497
|
return byId;
|
|
46078
|
-
return (await store.list("agents")
|
|
46498
|
+
return matchAgentByName(await store.list("agents"), idOrName);
|
|
46079
46499
|
}
|
|
46080
46500
|
async function heartbeatAgent(idOrName, store, context) {
|
|
46081
46501
|
const agent = await resolveAgent3(idOrName, store);
|
|
@@ -46393,6 +46813,7 @@ var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'prior
|
|
|
46393
46813
|
var init_postgres_adapter = __esm(() => {
|
|
46394
46814
|
init_types();
|
|
46395
46815
|
init_postgres_sync();
|
|
46816
|
+
init_integrity();
|
|
46396
46817
|
init_redaction();
|
|
46397
46818
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
46398
46819
|
});
|
|
@@ -46981,6 +47402,7 @@ __export(exports_cloud, {
|
|
|
46981
47402
|
resolveCloudDatabaseUrl: () => resolveCloudDatabaseUrl,
|
|
46982
47403
|
pingCloud: () => pingCloud,
|
|
46983
47404
|
normalizeCloudPayloads: () => normalizeCloudPayloads,
|
|
47405
|
+
isPostgresBackendConfigured: () => isPostgresBackendConfigured,
|
|
46984
47406
|
isCloudModeEnabled: () => isCloudModeEnabled,
|
|
46985
47407
|
getCloudVerifier: () => getCloudVerifier,
|
|
46986
47408
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
@@ -47003,9 +47425,12 @@ function resolveCloudDatabaseUrl(env = process.env) {
|
|
|
47003
47425
|
function resolveSigningSecret(env = process.env) {
|
|
47004
47426
|
return env.HASNA_TODOS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
|
|
47005
47427
|
}
|
|
47006
|
-
function
|
|
47428
|
+
function isPostgresBackendConfigured(env = process.env) {
|
|
47007
47429
|
return Boolean(resolveCloudDatabaseUrl(env));
|
|
47008
47430
|
}
|
|
47431
|
+
function isCloudModeEnabled(env = process.env) {
|
|
47432
|
+
return isPostgresBackendConfigured(env);
|
|
47433
|
+
}
|
|
47009
47434
|
function getClient() {
|
|
47010
47435
|
if (cachedClient)
|
|
47011
47436
|
return cachedClient;
|
|
@@ -47185,7 +47610,7 @@ function authNotConfiguredMessage(host) {
|
|
|
47185
47610
|
`);
|
|
47186
47611
|
}
|
|
47187
47612
|
function resolveAuthPosture(input) {
|
|
47188
|
-
const hosted = input.hosted ??
|
|
47613
|
+
const hosted = input.hosted ?? isPostgresBackendConfigured();
|
|
47189
47614
|
const hasCredentialSource = Boolean(input.apiKey) || input.hasGeneratedKeys;
|
|
47190
47615
|
if (hasCredentialSource) {
|
|
47191
47616
|
return {
|
|
@@ -49146,6 +49571,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49146
49571
|
{ name: "task_list_id", in: "query", schema: { type: "string" } },
|
|
49147
49572
|
{ name: "assigned_to", in: "query", schema: { type: "string" } },
|
|
49148
49573
|
{ name: "agent_id", in: "query", schema: { type: "string" } },
|
|
49574
|
+
{ name: "tags", in: "query", schema: { type: "string" }, description: "Comma-separated tags; matches tasks carrying any of them" },
|
|
49149
49575
|
{ name: "limit", in: "query", schema: { type: "integer", minimum: 1 } },
|
|
49150
49576
|
{ name: "offset", in: "query", schema: { type: "integer", minimum: 0 } }
|
|
49151
49577
|
],
|
|
@@ -50350,6 +50776,9 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50350
50776
|
...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
|
|
50351
50777
|
...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
|
|
50352
50778
|
...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
|
|
50779
|
+
...url.searchParams.get("tags") ? {
|
|
50780
|
+
tags: url.searchParams.get("tags").split(",").map((tag) => tag.trim()).filter(Boolean)
|
|
50781
|
+
} : {},
|
|
50353
50782
|
...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
|
|
50354
50783
|
...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
|
|
50355
50784
|
};
|
|
@@ -50631,6 +51060,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50631
51060
|
try {
|
|
50632
51061
|
task2 = await store.tasks.resolveRef(id);
|
|
50633
51062
|
} catch (e) {
|
|
51063
|
+
if (e instanceof TaskReferenceAmbiguousError) {
|
|
51064
|
+
return error(409, e.message, {
|
|
51065
|
+
code: TaskReferenceAmbiguousError.code,
|
|
51066
|
+
candidate_project_ids: e.candidateProjectIds,
|
|
51067
|
+
candidate_task_ids: e.candidateTaskIds
|
|
51068
|
+
});
|
|
51069
|
+
}
|
|
50634
51070
|
const msg = e.message || "";
|
|
50635
51071
|
if (/ambiguous/i.test(msg))
|
|
50636
51072
|
return error(409, msg);
|
|
@@ -50988,6 +51424,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50988
51424
|
]);
|
|
50989
51425
|
return json3({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
|
|
50990
51426
|
}
|
|
51427
|
+
if (resource === "integrity" && !id) {
|
|
51428
|
+
if (method !== "GET")
|
|
51429
|
+
return error(405, `method ${method} not allowed on /v1/integrity`);
|
|
51430
|
+
if (typeof store.integrity?.report !== "function") {
|
|
51431
|
+
return error(501, "referential-integrity reporting is not supported by this storage backend");
|
|
51432
|
+
}
|
|
51433
|
+
const integrity = await store.integrity.report();
|
|
51434
|
+
return json3({ integrity });
|
|
51435
|
+
}
|
|
50991
51436
|
if (resource === "import") {
|
|
50992
51437
|
if (method !== "POST")
|
|
50993
51438
|
return error(405, `method ${method} not allowed on /v1/import`);
|
|
@@ -51007,6 +51452,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51007
51452
|
}
|
|
51008
51453
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
51009
51454
|
} catch (e) {
|
|
51455
|
+
if (e instanceof TaskReferenceAmbiguousError) {
|
|
51456
|
+
return error(409, e.message, {
|
|
51457
|
+
code: TaskReferenceAmbiguousError.code,
|
|
51458
|
+
candidate_project_ids: e.candidateProjectIds,
|
|
51459
|
+
candidate_task_ids: e.candidateTaskIds
|
|
51460
|
+
});
|
|
51461
|
+
}
|
|
51010
51462
|
if (e instanceof LockError)
|
|
51011
51463
|
return error(409, e.message, { code: LockError.code });
|
|
51012
51464
|
if (e instanceof ResourceConflictError)
|
|
@@ -51527,23 +51979,24 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
51527
51979
|
}
|
|
51528
51980
|
if ((path === "/health" || path === "/ready" || path === "/version") && method === "GET") {
|
|
51529
51981
|
const { getPackageVersion: getPackageVersion2 } = await Promise.resolve().then(() => (init_package_version(), exports_package_version));
|
|
51530
|
-
const {
|
|
51531
|
-
const
|
|
51982
|
+
const { isPostgresBackendConfigured: isPostgresBackendConfigured2, pingCloud: pingCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
51983
|
+
const backend = isPostgresBackendConfigured2() ? "postgresql" : "sqlite";
|
|
51984
|
+
const mode = backend === "postgresql" ? "remote" : "local";
|
|
51532
51985
|
const version = getPackageVersion2();
|
|
51533
51986
|
if (path === "/version") {
|
|
51534
|
-
return Response.json({ status: "ok", version, mode, name: "todos" });
|
|
51987
|
+
return Response.json({ status: "ok", version, mode, backend, name: "todos" });
|
|
51535
51988
|
}
|
|
51536
51989
|
if (path === "/ready") {
|
|
51537
|
-
if (
|
|
51990
|
+
if (backend === "postgresql") {
|
|
51538
51991
|
try {
|
|
51539
51992
|
await pingCloud2();
|
|
51540
51993
|
} catch (e) {
|
|
51541
|
-
return Response.json({ status: "unavailable", version, mode, error: e.message }, { status: 503 });
|
|
51994
|
+
return Response.json({ status: "unavailable", version, mode, backend, error: e.message }, { status: 503 });
|
|
51542
51995
|
}
|
|
51543
51996
|
}
|
|
51544
|
-
return Response.json({ status: "ready", version, mode });
|
|
51997
|
+
return Response.json({ status: "ready", version, mode, backend });
|
|
51545
51998
|
}
|
|
51546
|
-
return Response.json({ status: "ok", version, mode, name: "todos" });
|
|
51999
|
+
return Response.json({ status: "ok", version, mode, backend, name: "todos" });
|
|
51547
52000
|
}
|
|
51548
52001
|
if ((path === "/openapi.json" || path === "/v1/openapi.json") && method === "GET") {
|
|
51549
52002
|
const { buildV1OpenApiDocument: buildV1OpenApiDocument2 } = await Promise.resolve().then(() => (init_openapi(), exports_openapi));
|
|
@@ -51899,6 +52352,15 @@ function formatError(error2) {
|
|
|
51899
52352
|
if (error2 instanceof TaskNotFoundError) {
|
|
51900
52353
|
return JSON.stringify({ code: TaskNotFoundError.code, message: error2.message, suggestion: TaskNotFoundError.suggestion });
|
|
51901
52354
|
}
|
|
52355
|
+
if (error2 instanceof TaskReferenceAmbiguousError) {
|
|
52356
|
+
return JSON.stringify({
|
|
52357
|
+
code: TaskReferenceAmbiguousError.code,
|
|
52358
|
+
message: error2.message,
|
|
52359
|
+
candidate_project_ids: error2.candidateProjectIds,
|
|
52360
|
+
candidate_task_ids: error2.candidateTaskIds,
|
|
52361
|
+
suggestion: "Use a full task UUID."
|
|
52362
|
+
});
|
|
52363
|
+
}
|
|
51902
52364
|
if (error2 instanceof ProjectNotFoundError) {
|
|
51903
52365
|
return JSON.stringify({ code: ProjectNotFoundError.code, message: error2.message, suggestion: ProjectNotFoundError.suggestion });
|
|
51904
52366
|
}
|