@hasna/todos 0.13.12 → 0.15.1
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 +2 -0
- package/dist/cli/commands/agent-commands.d.ts +39 -0
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/dispatch.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts +64 -2
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +624 -98
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +123 -116
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.js +135 -127
- package/dist/lib/bulk-tags.d.ts +57 -0
- package/dist/lib/bulk-tags.d.ts.map +1 -0
- package/dist/lib/enum-vocabulary.d.ts +72 -0
- package/dist/lib/enum-vocabulary.d.ts.map +1 -0
- package/dist/lib/run-records.d.ts.map +1 -1
- package/dist/lib/sandbox-profiles.d.ts.map +1 -1
- package/dist/lib/saved-search-views.d.ts +12 -1
- package/dist/lib/saved-search-views.d.ts.map +1 -1
- package/dist/lib/sync-utils.d.ts +1 -1
- package/dist/lib/sync-utils.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +289 -82
- package/dist/mcp/tools/agents.d.ts.map +1 -1
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts +1 -0
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/registry.js +123 -116
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +3 -2
- package/dist/sdk/v1.generated.d.ts +2 -2
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +245 -38
- package/dist/server/openapi.d.ts +58 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +124 -115
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -70,7 +70,7 @@ var package_default;
|
|
|
70
70
|
var init_package = __esm(() => {
|
|
71
71
|
package_default = {
|
|
72
72
|
name: "@hasna/todos",
|
|
73
|
-
version: "0.
|
|
73
|
+
version: "0.15.1",
|
|
74
74
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
75
75
|
type: "module",
|
|
76
76
|
main: "dist/index.js",
|
|
@@ -850,7 +850,10 @@ var init_cloud_client = __esm(() => {
|
|
|
850
850
|
function isBlockingDependencyStatus(status) {
|
|
851
851
|
return status !== "completed" && status !== "cancelled";
|
|
852
852
|
}
|
|
853
|
-
|
|
853
|
+
function isTerminalStatus(status) {
|
|
854
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
855
|
+
}
|
|
856
|
+
var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
854
857
|
var init_types = __esm(() => {
|
|
855
858
|
TASK_STATUSES = [
|
|
856
859
|
"pending",
|
|
@@ -859,6 +862,12 @@ var init_types = __esm(() => {
|
|
|
859
862
|
"failed",
|
|
860
863
|
"cancelled"
|
|
861
864
|
];
|
|
865
|
+
TASK_PRIORITIES = [
|
|
866
|
+
"low",
|
|
867
|
+
"medium",
|
|
868
|
+
"high",
|
|
869
|
+
"critical"
|
|
870
|
+
];
|
|
862
871
|
VersionConflictError = class VersionConflictError extends Error {
|
|
863
872
|
taskId;
|
|
864
873
|
expectedVersion;
|
|
@@ -1032,9 +1041,10 @@ function normalizeAgentNameInput(name) {
|
|
|
1032
1041
|
|
|
1033
1042
|
// src/lib/sync-utils.ts
|
|
1034
1043
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
1044
|
+
import { homedir } from "os";
|
|
1035
1045
|
import { join } from "path";
|
|
1036
1046
|
function getHomeDir() {
|
|
1037
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
1047
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
1038
1048
|
}
|
|
1039
1049
|
function getTodosGlobalDir() {
|
|
1040
1050
|
return join(getHomeDir(), ".hasna", "todos");
|
|
@@ -1061,7 +1071,7 @@ function appendSyncConflict(metadata, conflict, limit = 5) {
|
|
|
1061
1071
|
}
|
|
1062
1072
|
var HOME;
|
|
1063
1073
|
var init_sync_utils = __esm(() => {
|
|
1064
|
-
HOME =
|
|
1074
|
+
HOME = getHomeDir();
|
|
1065
1075
|
});
|
|
1066
1076
|
|
|
1067
1077
|
// src/lib/creator-identity.ts
|
|
@@ -2770,9 +2780,11 @@ async function updateTask(id, input, store) {
|
|
|
2770
2780
|
throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
|
|
2771
2781
|
}
|
|
2772
2782
|
const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
|
|
2783
|
+
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
2773
2784
|
const task = {
|
|
2774
2785
|
...existing,
|
|
2775
2786
|
...definedPatch(input),
|
|
2787
|
+
...terminalNow ? { locked_by: null, locked_at: null } : {},
|
|
2776
2788
|
version: existing.version + 1,
|
|
2777
2789
|
updated_at: new Date().toISOString(),
|
|
2778
2790
|
tags: input.tags ?? existing.tags,
|
|
@@ -9361,8 +9373,7 @@ function findGitRoot(startDir) {
|
|
|
9361
9373
|
return null;
|
|
9362
9374
|
}
|
|
9363
9375
|
function getGlobalDbPath() {
|
|
9364
|
-
|
|
9365
|
-
return join4(home, ".hasna", "todos", "todos.db");
|
|
9376
|
+
return join4(getHomeDir(), ".hasna", "todos", "todos.db");
|
|
9366
9377
|
}
|
|
9367
9378
|
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
9368
9379
|
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
@@ -9584,6 +9595,7 @@ var init_database = __esm(() => {
|
|
|
9584
9595
|
init_machines();
|
|
9585
9596
|
init_identity_mapping();
|
|
9586
9597
|
init_types();
|
|
9598
|
+
init_sync_utils();
|
|
9587
9599
|
ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
|
|
9588
9600
|
});
|
|
9589
9601
|
|
|
@@ -10863,7 +10875,7 @@ var init_event_hooks = __esm(() => {
|
|
|
10863
10875
|
// node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
|
|
10864
10876
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
10865
10877
|
import { existsSync as existsSync6 } from "fs";
|
|
10866
|
-
import { homedir } from "os";
|
|
10878
|
+
import { homedir as homedir2 } from "os";
|
|
10867
10879
|
import { join as join5 } from "path";
|
|
10868
10880
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
10869
10881
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -10965,7 +10977,7 @@ function channelMatchesEvent(channel, event) {
|
|
|
10965
10977
|
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
10966
10978
|
}
|
|
10967
10979
|
function getEventsDataDir(override) {
|
|
10968
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(
|
|
10980
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir2(), ".hasna", "events");
|
|
10969
10981
|
}
|
|
10970
10982
|
|
|
10971
10983
|
class JsonEventsStore {
|
|
@@ -14188,11 +14200,13 @@ function updateTask2(id, input, db) {
|
|
|
14188
14200
|
}
|
|
14189
14201
|
sets.push("status = ?");
|
|
14190
14202
|
params.push(input.status);
|
|
14203
|
+
if (isTerminalStatus(input.status)) {
|
|
14204
|
+
sets.push("locked_by = NULL");
|
|
14205
|
+
sets.push("locked_at = NULL");
|
|
14206
|
+
}
|
|
14191
14207
|
if (input.status === "completed") {
|
|
14192
14208
|
sets.push("completed_at = ?");
|
|
14193
14209
|
params.push(completionTimestamp);
|
|
14194
|
-
sets.push("locked_by = NULL");
|
|
14195
|
-
sets.push("locked_at = NULL");
|
|
14196
14210
|
} else if (task.status === "completed" && input.completed_at === undefined) {
|
|
14197
14211
|
sets.push("completed_at = NULL");
|
|
14198
14212
|
}
|
|
@@ -14316,6 +14330,7 @@ function updateTask2(id, input, db) {
|
|
|
14316
14330
|
logTaskChange2(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
14317
14331
|
const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
|
|
14318
14332
|
const completedNow = input.status === "completed";
|
|
14333
|
+
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
14319
14334
|
const updatedTask = {
|
|
14320
14335
|
...task,
|
|
14321
14336
|
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
@@ -14323,8 +14338,8 @@ function updateTask2(id, input, db) {
|
|
|
14323
14338
|
metadata: input.metadata ?? task.metadata,
|
|
14324
14339
|
version: task.version + 1,
|
|
14325
14340
|
updated_at: timestamp2,
|
|
14326
|
-
locked_by:
|
|
14327
|
-
locked_at:
|
|
14341
|
+
locked_by: terminalNow ? null : task.locked_by,
|
|
14342
|
+
locked_at: terminalNow ? null : task.locked_at,
|
|
14328
14343
|
completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
14329
14344
|
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
14330
14345
|
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
@@ -17245,6 +17260,103 @@ var init_tasks = __esm(() => {
|
|
|
17245
17260
|
init_calendar();
|
|
17246
17261
|
});
|
|
17247
17262
|
|
|
17263
|
+
// src/lib/enum-vocabulary.ts
|
|
17264
|
+
function editDistance(a, b) {
|
|
17265
|
+
if (a === b)
|
|
17266
|
+
return 0;
|
|
17267
|
+
if (!a.length)
|
|
17268
|
+
return b.length;
|
|
17269
|
+
if (!b.length)
|
|
17270
|
+
return a.length;
|
|
17271
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
17272
|
+
for (let i = 1;i <= a.length; i += 1) {
|
|
17273
|
+
const current = [i];
|
|
17274
|
+
for (let j = 1;j <= b.length; j += 1) {
|
|
17275
|
+
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
17276
|
+
}
|
|
17277
|
+
previous = current;
|
|
17278
|
+
}
|
|
17279
|
+
return previous[b.length];
|
|
17280
|
+
}
|
|
17281
|
+
function suggestVocabularyMatches(value, vocabulary, limit = 3) {
|
|
17282
|
+
const needle = value.trim().toLowerCase();
|
|
17283
|
+
if (!needle)
|
|
17284
|
+
return [];
|
|
17285
|
+
const scored = [];
|
|
17286
|
+
for (const member of vocabulary) {
|
|
17287
|
+
const candidate = member.toLowerCase();
|
|
17288
|
+
if (candidate.startsWith(needle) || needle.startsWith(candidate)) {
|
|
17289
|
+
scored.push({ member, score: 0 });
|
|
17290
|
+
continue;
|
|
17291
|
+
}
|
|
17292
|
+
if (candidate.includes(needle) || needle.includes(candidate)) {
|
|
17293
|
+
scored.push({ member, score: 1 });
|
|
17294
|
+
continue;
|
|
17295
|
+
}
|
|
17296
|
+
const distance = editDistance(needle, candidate);
|
|
17297
|
+
if (distance <= Math.max(1, Math.floor(candidate.length / 3))) {
|
|
17298
|
+
scored.push({ member, score: 1 + distance });
|
|
17299
|
+
}
|
|
17300
|
+
}
|
|
17301
|
+
return scored.sort((a, b) => a.score - b.score || a.member.localeCompare(b.member)).slice(0, limit).map((entry2) => entry2.member);
|
|
17302
|
+
}
|
|
17303
|
+
function resolveEnumVocabulary(raw, spec) {
|
|
17304
|
+
const allowList = spec.allowList !== false;
|
|
17305
|
+
const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim());
|
|
17306
|
+
if (rawElements.every((element) => element.length === 0)) {
|
|
17307
|
+
return {
|
|
17308
|
+
ok: false,
|
|
17309
|
+
message: `${spec.name} requires a value. Allowed values: ${spec.vocabulary.join(", ")}.`,
|
|
17310
|
+
invalid: []
|
|
17311
|
+
};
|
|
17312
|
+
}
|
|
17313
|
+
if (rawElements.some((element) => element.length === 0)) {
|
|
17314
|
+
return {
|
|
17315
|
+
ok: false,
|
|
17316
|
+
message: `${spec.name} has an empty value in "${raw}" \u2014 remove the stray comma. ` + `Allowed values: ${spec.vocabulary.join(", ")}.`,
|
|
17317
|
+
invalid: []
|
|
17318
|
+
};
|
|
17319
|
+
}
|
|
17320
|
+
const normalized = rawElements.map((element) => spec.normalize ? spec.normalize(element) : element);
|
|
17321
|
+
const allowed = new Set(spec.vocabulary);
|
|
17322
|
+
const invalid = [];
|
|
17323
|
+
for (let i = 0;i < normalized.length; i += 1) {
|
|
17324
|
+
if (!allowed.has(normalized[i]))
|
|
17325
|
+
invalid.push(rawElements[i]);
|
|
17326
|
+
}
|
|
17327
|
+
if (invalid.length === 0) {
|
|
17328
|
+
return { ok: true, values: [...new Set(normalized)] };
|
|
17329
|
+
}
|
|
17330
|
+
const label = invalid.length === 1 ? "value" : "values";
|
|
17331
|
+
const parts = [
|
|
17332
|
+
`Invalid ${spec.name} ${label}: ${invalid.join(", ")}.`,
|
|
17333
|
+
`Allowed values: ${spec.vocabulary.join(", ")}.`
|
|
17334
|
+
];
|
|
17335
|
+
const hints = new Set;
|
|
17336
|
+
const suggestions = new Set;
|
|
17337
|
+
for (let i = 0;i < normalized.length; i += 1) {
|
|
17338
|
+
const canonical = normalized[i];
|
|
17339
|
+
if (allowed.has(canonical))
|
|
17340
|
+
continue;
|
|
17341
|
+
const hint = spec.hints?.[canonical.toLowerCase()];
|
|
17342
|
+
if (hint) {
|
|
17343
|
+
hints.add(hint);
|
|
17344
|
+
continue;
|
|
17345
|
+
}
|
|
17346
|
+
for (const match of suggestVocabularyMatches(canonical, spec.vocabulary)) {
|
|
17347
|
+
suggestions.add(match);
|
|
17348
|
+
}
|
|
17349
|
+
}
|
|
17350
|
+
if (suggestions.size > 0)
|
|
17351
|
+
parts.push(`Did you mean ${[...suggestions].join(", ")}?`);
|
|
17352
|
+
for (const hint of hints)
|
|
17353
|
+
parts.push(hint);
|
|
17354
|
+
return { ok: false, message: parts.join(" "), invalid };
|
|
17355
|
+
}
|
|
17356
|
+
function collapseEnumValues(values) {
|
|
17357
|
+
return values.length === 1 ? values[0] : values;
|
|
17358
|
+
}
|
|
17359
|
+
|
|
17248
17360
|
// src/db/agent-names.ts
|
|
17249
17361
|
function hasGeneratedNumericSuffix(name) {
|
|
17250
17362
|
return NUMERIC_SUFFIX_RE.test(normalizeAgentNameInput(name));
|
|
@@ -18672,8 +18784,19 @@ function handleStats(_ctx, json2) {
|
|
|
18672
18784
|
recurring_tasks: countRecurringTasks()
|
|
18673
18785
|
});
|
|
18674
18786
|
}
|
|
18787
|
+
function taskStatusQueryParam(url) {
|
|
18788
|
+
const raw = url.searchParams.get("status");
|
|
18789
|
+
if (!raw)
|
|
18790
|
+
return { ok: true, value: undefined };
|
|
18791
|
+
const result = resolveEnumVocabulary(raw, { name: "status", vocabulary: TASK_STATUSES });
|
|
18792
|
+
if (!result.ok)
|
|
18793
|
+
return { ok: false, message: result.message };
|
|
18794
|
+
return { ok: true, value: collapseEnumValues(result.values) };
|
|
18795
|
+
}
|
|
18675
18796
|
async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
|
|
18676
|
-
const
|
|
18797
|
+
const statusParam = taskStatusQueryParam(url);
|
|
18798
|
+
if (!statusParam.ok)
|
|
18799
|
+
return json2({ error: statusParam.message }, 400);
|
|
18677
18800
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
18678
18801
|
const sessionId = url.searchParams.get("session_id") || undefined;
|
|
18679
18802
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
@@ -18681,7 +18804,7 @@ async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
|
|
|
18681
18804
|
const offsetParam = url.searchParams.get("offset");
|
|
18682
18805
|
const fields = parseFieldsParam(url);
|
|
18683
18806
|
const tasks = listTasks2({
|
|
18684
|
-
status,
|
|
18807
|
+
status: statusParam.value,
|
|
18685
18808
|
project_id: projectId,
|
|
18686
18809
|
session_id: sessionId,
|
|
18687
18810
|
agent_id: agentId,
|
|
@@ -18746,9 +18869,19 @@ async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
|
|
|
18746
18869
|
}
|
|
18747
18870
|
function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
18748
18871
|
const format = url.searchParams.get("format") || "json";
|
|
18749
|
-
const
|
|
18872
|
+
const statusParam = taskStatusQueryParam(url);
|
|
18873
|
+
if (!statusParam.ok) {
|
|
18874
|
+
return new Response(JSON.stringify({ error: statusParam.message }), {
|
|
18875
|
+
status: 400,
|
|
18876
|
+
headers: { "Content-Type": "application/json" }
|
|
18877
|
+
});
|
|
18878
|
+
}
|
|
18750
18879
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
18751
|
-
const tasks = listTasks2({
|
|
18880
|
+
const tasks = listTasks2({
|
|
18881
|
+
status: statusParam.value,
|
|
18882
|
+
project_id: projectId,
|
|
18883
|
+
limit: 1e4
|
|
18884
|
+
});
|
|
18752
18885
|
const summaries = tasks.map((t) => taskToSummary2(t));
|
|
18753
18886
|
if (format === "csv") {
|
|
18754
18887
|
const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
|
|
@@ -19330,6 +19463,7 @@ var init_routes = __esm(() => {
|
|
|
19330
19463
|
init_tasks();
|
|
19331
19464
|
init_database();
|
|
19332
19465
|
init_types();
|
|
19466
|
+
init_types();
|
|
19333
19467
|
init_projects();
|
|
19334
19468
|
init_agents();
|
|
19335
19469
|
init_plans();
|
|
@@ -21368,8 +21502,38 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
21368
21502
|
operationId: "listTasks",
|
|
21369
21503
|
summary: "List tasks",
|
|
21370
21504
|
parameters: [
|
|
21371
|
-
{
|
|
21372
|
-
|
|
21505
|
+
{
|
|
21506
|
+
name: "status",
|
|
21507
|
+
in: "query",
|
|
21508
|
+
description: `Task status, or a comma-separated list of statuses. Allowed values: ${TASK_STATUSES.join(", ")}.`,
|
|
21509
|
+
style: "form",
|
|
21510
|
+
explode: false,
|
|
21511
|
+
schema: {
|
|
21512
|
+
oneOf: [
|
|
21513
|
+
{ type: "string", enum: [...TASK_STATUSES] },
|
|
21514
|
+
{
|
|
21515
|
+
type: "array",
|
|
21516
|
+
items: { type: "string", enum: [...TASK_STATUSES] }
|
|
21517
|
+
}
|
|
21518
|
+
]
|
|
21519
|
+
}
|
|
21520
|
+
},
|
|
21521
|
+
{
|
|
21522
|
+
name: "priority",
|
|
21523
|
+
in: "query",
|
|
21524
|
+
description: `Task priority, or a comma-separated list of priorities. Allowed values: ${TASK_PRIORITIES.join(", ")}.`,
|
|
21525
|
+
style: "form",
|
|
21526
|
+
explode: false,
|
|
21527
|
+
schema: {
|
|
21528
|
+
oneOf: [
|
|
21529
|
+
{ type: "string", enum: [...TASK_PRIORITIES] },
|
|
21530
|
+
{
|
|
21531
|
+
type: "array",
|
|
21532
|
+
items: { type: "string", enum: [...TASK_PRIORITIES] }
|
|
21533
|
+
}
|
|
21534
|
+
]
|
|
21535
|
+
}
|
|
21536
|
+
},
|
|
21373
21537
|
{ name: "project_id", in: "query", schema: { type: "string" } },
|
|
21374
21538
|
{ name: "parent_id", in: "query", schema: { type: "string", nullable: true } },
|
|
21375
21539
|
{ name: "include_subtasks", in: "query", schema: { type: "boolean" } },
|
|
@@ -21881,6 +22045,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
21881
22045
|
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
21882
22046
|
var init_openapi = __esm(() => {
|
|
21883
22047
|
init_package_version();
|
|
22048
|
+
init_types();
|
|
21884
22049
|
taskSchema = {
|
|
21885
22050
|
type: "object",
|
|
21886
22051
|
properties: {
|
|
@@ -22148,6 +22313,15 @@ function json3(body, status = 200) {
|
|
|
22148
22313
|
function error(status, message, extra) {
|
|
22149
22314
|
return json3({ error: message, ...extra ?? {} }, status);
|
|
22150
22315
|
}
|
|
22316
|
+
function enumQueryParam(url, name, vocabulary) {
|
|
22317
|
+
const raw = url.searchParams.get(name);
|
|
22318
|
+
if (raw === null || raw === "")
|
|
22319
|
+
return { ok: true, value: undefined };
|
|
22320
|
+
const result = resolveEnumVocabulary(raw, { name, vocabulary });
|
|
22321
|
+
if (!result.ok)
|
|
22322
|
+
return { ok: false, response: error(400, result.message) };
|
|
22323
|
+
return { ok: true, value: collapseEnumValues(result.values) };
|
|
22324
|
+
}
|
|
22151
22325
|
function validateTaskCompletion(value) {
|
|
22152
22326
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
22153
22327
|
return { ok: false, message: "completion body must be an object" };
|
|
@@ -22568,14 +22742,16 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
22568
22742
|
return error(400, "include_subtasks must be true or false");
|
|
22569
22743
|
}
|
|
22570
22744
|
const hasParentFilter = url.searchParams.has("parent_id");
|
|
22745
|
+
const statusParam = enumQueryParam(url, "status", TASK_STATUSES);
|
|
22746
|
+
if (!statusParam.ok)
|
|
22747
|
+
return statusParam.response;
|
|
22748
|
+
const priorityParam = enumQueryParam(url, "priority", TASK_PRIORITIES);
|
|
22749
|
+
if (!priorityParam.ok)
|
|
22750
|
+
return priorityParam.response;
|
|
22571
22751
|
const filter = {
|
|
22572
22752
|
...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
|
|
22573
|
-
...
|
|
22574
|
-
|
|
22575
|
-
} : {},
|
|
22576
|
-
...url.searchParams.get("priority") ? {
|
|
22577
|
-
priority: url.searchParams.get("priority").includes(",") ? url.searchParams.get("priority").split(",") : url.searchParams.get("priority")
|
|
22578
|
-
} : {},
|
|
22753
|
+
...statusParam.value !== undefined ? { status: statusParam.value } : {},
|
|
22754
|
+
...priorityParam.value !== undefined ? { priority: priorityParam.value } : {},
|
|
22579
22755
|
...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
|
|
22580
22756
|
...hasParentFilter ? { parent_id: url.searchParams.get("parent_id") || null, include_subtasks: true } : includeSubtasks !== null ? { include_subtasks: includeSubtasks === "true" } : {},
|
|
22581
22757
|
...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {},
|
|
@@ -23209,7 +23385,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
23209
23385
|
return error(405, `method ${method} not allowed on /v1/refs/:ref`);
|
|
23210
23386
|
if (!store.gitRefs)
|
|
23211
23387
|
return error(501, "git ref links are not supported by this storage backend");
|
|
23212
|
-
|
|
23388
|
+
let decodedRef;
|
|
23389
|
+
try {
|
|
23390
|
+
decodedRef = decodeURIComponent(id);
|
|
23391
|
+
} catch {
|
|
23392
|
+
return error(400, "ref path segment has invalid percent encoding");
|
|
23393
|
+
}
|
|
23394
|
+
const refs = await store.gitRefs.find(decodedRef);
|
|
23213
23395
|
return json3({ refs, count: refs.length });
|
|
23214
23396
|
}
|
|
23215
23397
|
if (resource === "next" && !id) {
|
|
@@ -44776,10 +44958,10 @@ var init_token_utils = __esm(() => {
|
|
|
44776
44958
|
|
|
44777
44959
|
// src/lib/assignee-validation.ts
|
|
44778
44960
|
import { readFileSync as readFileSync3 } from "fs";
|
|
44779
|
-
import { homedir as
|
|
44961
|
+
import { homedir as homedir3 } from "os";
|
|
44780
44962
|
import { join as join9 } from "path";
|
|
44781
44963
|
function defaultSeatRosterPath() {
|
|
44782
|
-
return process.env["TODOS_SEAT_ROSTER_PATH"] || join9(
|
|
44964
|
+
return process.env["TODOS_SEAT_ROSTER_PATH"] || join9(homedir3(), ".hasna", "identities", "hasna-seats.roster.json");
|
|
44783
44965
|
}
|
|
44784
44966
|
function loadSeatSlugs(path = defaultSeatRosterPath()) {
|
|
44785
44967
|
try {
|
|
@@ -58512,7 +58694,7 @@ async function validateMcpAssignee(value, allowSeat) {
|
|
|
58512
58694
|
return verdict.assignee;
|
|
58513
58695
|
}
|
|
58514
58696
|
function registerTaskCrudTools(server, ctx) {
|
|
58515
|
-
const { shouldRegisterTool, resolveId, formatError: formatError2, formatTask } = ctx;
|
|
58697
|
+
const { shouldRegisterTool, resolveId, formatError: formatError2, formatTask, applyFocus } = ctx;
|
|
58516
58698
|
function mutationTaskResponse(task) {
|
|
58517
58699
|
const compact = compactTask(task, 240);
|
|
58518
58700
|
compact["version"] = task.version;
|
|
@@ -58588,6 +58770,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
58588
58770
|
payload.max_retries = retry_count;
|
|
58589
58771
|
if (deadline)
|
|
58590
58772
|
payload.due_at = deadline;
|
|
58773
|
+
applyFocus(payload, router.agent_id || undefined);
|
|
58591
58774
|
const created = await cloudCreateTask(cloud, payload);
|
|
58592
58775
|
return { content: [{ type: "text", text: mutationTaskResponse(created) }] };
|
|
58593
58776
|
}
|
|
@@ -58614,6 +58797,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
58614
58797
|
resolved.max_retries = retry_count;
|
|
58615
58798
|
if (deadline)
|
|
58616
58799
|
resolved.due_at = deadline;
|
|
58800
|
+
applyFocus(resolved, router.agent_id || undefined);
|
|
58617
58801
|
const task = createTask2(resolved);
|
|
58618
58802
|
return { content: [{ type: "text", text: mutationTaskResponse(task) }] };
|
|
58619
58803
|
} catch (e) {
|
|
@@ -63581,7 +63765,8 @@ __export(exports_saved_search_views, {
|
|
|
63581
63765
|
normalizeScope: () => normalizeScope,
|
|
63582
63766
|
listSearchViews: () => listSearchViews,
|
|
63583
63767
|
getSearchView: () => getSearchView,
|
|
63584
|
-
deleteSearchView: () => deleteSearchView
|
|
63768
|
+
deleteSearchView: () => deleteSearchView,
|
|
63769
|
+
SAVED_SEARCH_SCOPES: () => SAVED_SEARCH_SCOPES
|
|
63585
63770
|
});
|
|
63586
63771
|
function parseFilters(value) {
|
|
63587
63772
|
if (!value)
|
|
@@ -63601,10 +63786,8 @@ function rowToSavedSearchView(row) {
|
|
|
63601
63786
|
};
|
|
63602
63787
|
}
|
|
63603
63788
|
function normalizeScope(scope) {
|
|
63604
|
-
|
|
63605
|
-
|
|
63606
|
-
}
|
|
63607
|
-
return "tasks";
|
|
63789
|
+
const candidate = (scope ?? "").trim().toLowerCase();
|
|
63790
|
+
return SAVED_SEARCH_SCOPES.includes(candidate) ? candidate : "tasks";
|
|
63608
63791
|
}
|
|
63609
63792
|
function normalizeName4(name) {
|
|
63610
63793
|
const normalized = name.trim();
|
|
@@ -63907,10 +64090,12 @@ function runSearchView(idOrName, db) {
|
|
|
63907
64090
|
throw new Error(`Saved search view not found: ${idOrName}`);
|
|
63908
64091
|
return { ...runSavedSearch(view.filters, view.scope, d), view };
|
|
63909
64092
|
}
|
|
64093
|
+
var SAVED_SEARCH_SCOPES;
|
|
63910
64094
|
var init_saved_search_views = __esm(() => {
|
|
63911
64095
|
init_database();
|
|
63912
64096
|
init_local_fields();
|
|
63913
64097
|
init_search();
|
|
64098
|
+
SAVED_SEARCH_SCOPES = ["all", "tasks", "projects", "plans", "runs", "comments"];
|
|
63914
64099
|
});
|
|
63915
64100
|
|
|
63916
64101
|
// src/mcp/tools/task-project-tools.ts
|
|
@@ -67094,10 +67279,23 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
67094
67279
|
return { content: [{ type: "text", text: "No active agents available for rebalancing." }] };
|
|
67095
67280
|
const activeTasks = listTasks4({ project_id: resolvedProjectId, status: ["pending", "in_progress"], limit: 1000 }, undefined);
|
|
67096
67281
|
const agentKeyByAlias = new Map;
|
|
67282
|
+
const ambiguousAgentAliases = new Set;
|
|
67283
|
+
const indexAgentAlias = (alias, agentId) => {
|
|
67284
|
+
const normalizedAlias2 = alias.toLowerCase();
|
|
67285
|
+
if (ambiguousAgentAliases.has(normalizedAlias2))
|
|
67286
|
+
return;
|
|
67287
|
+
const existingAgentId = agentKeyByAlias.get(normalizedAlias2);
|
|
67288
|
+
if (existingAgentId !== undefined && existingAgentId !== agentId) {
|
|
67289
|
+
agentKeyByAlias.delete(normalizedAlias2);
|
|
67290
|
+
ambiguousAgentAliases.add(normalizedAlias2);
|
|
67291
|
+
return;
|
|
67292
|
+
}
|
|
67293
|
+
agentKeyByAlias.set(normalizedAlias2, agentId);
|
|
67294
|
+
};
|
|
67097
67295
|
for (const agent of agents) {
|
|
67098
|
-
|
|
67296
|
+
indexAgentAlias(String(agent.id), agent.id);
|
|
67099
67297
|
if (agent.name)
|
|
67100
|
-
|
|
67298
|
+
indexAgentAlias(String(agent.name), agent.id);
|
|
67101
67299
|
}
|
|
67102
67300
|
const canonicalAgentKey = (assignedTo) => assignedTo ? agentKeyByAlias.get(assignedTo.toLowerCase()) : undefined;
|
|
67103
67301
|
const load = new Map(agents.map((agent) => [agent.id, activeTasks.filter((t) => canonicalAgentKey(t.assigned_to) === agent.id).length]));
|
|
@@ -103403,9 +103601,16 @@ ${text2}` }] };
|
|
|
103403
103601
|
return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
|
|
103404
103602
|
}
|
|
103405
103603
|
const oldName = agent.name;
|
|
103406
|
-
const updated = updateAgent2(agent.id, { name: new_name });
|
|
103407
103604
|
const db = getDatabase();
|
|
103408
|
-
|
|
103605
|
+
let oldNameUniquelyIdentifiesAgent = false;
|
|
103606
|
+
try {
|
|
103607
|
+
oldNameUniquelyIdentifiesAgent = resolvePartialId(db, "agents", oldName) === agent.id;
|
|
103608
|
+
} catch (error3) {
|
|
103609
|
+
if (!(error3 instanceof IdentityAliasAmbiguousError))
|
|
103610
|
+
throw error3;
|
|
103611
|
+
}
|
|
103612
|
+
const updated = updateAgent2(agent.id, { name: new_name });
|
|
103613
|
+
const tasksResult = db.run(oldNameUniquelyIdentifiesAgent ? "UPDATE tasks SET assigned_to = ? WHERE LOWER(assigned_to) = LOWER(?)" : "UPDATE tasks SET assigned_to = ? WHERE assigned_to = ?", [new_name, oldName]);
|
|
103409
103614
|
const taskNote = tasksResult.changes > 0 ? `
|
|
103410
103615
|
Updated assigned_to on ${tasksResult.changes} task(s).` : "";
|
|
103411
103616
|
return {
|
|
@@ -103589,6 +103794,7 @@ var init_agents2 = __esm(() => {
|
|
|
103589
103794
|
init_agents();
|
|
103590
103795
|
init_config2();
|
|
103591
103796
|
init_database();
|
|
103797
|
+
init_types();
|
|
103592
103798
|
init_cloud_router();
|
|
103593
103799
|
});
|
|
103594
103800
|
|
|
@@ -104870,7 +105076,8 @@ function buildServer() {
|
|
|
104870
105076
|
formatError: formatError2,
|
|
104871
105077
|
formatTask,
|
|
104872
105078
|
formatTaskDetail,
|
|
104873
|
-
getAgentFocus
|
|
105079
|
+
getAgentFocus,
|
|
105080
|
+
applyFocus
|
|
104874
105081
|
};
|
|
104875
105082
|
registerTaskCrudTools(server, toolContext);
|
|
104876
105083
|
registerTaskProjectTools(server, toolContext);
|
package/dist/server/openapi.d.ts
CHANGED
|
@@ -1901,41 +1901,99 @@ export declare function buildV1OpenApiDocument(version?: string): {
|
|
|
1901
1901
|
operationId: string;
|
|
1902
1902
|
summary: string;
|
|
1903
1903
|
parameters: ({
|
|
1904
|
+
name: string;
|
|
1905
|
+
in: string;
|
|
1906
|
+
description: string;
|
|
1907
|
+
style: string;
|
|
1908
|
+
explode: boolean;
|
|
1909
|
+
schema: {
|
|
1910
|
+
oneOf: ({
|
|
1911
|
+
type: string;
|
|
1912
|
+
enum: ("pending" | "in_progress" | "completed" | "failed" | "cancelled")[];
|
|
1913
|
+
items?: undefined;
|
|
1914
|
+
} | {
|
|
1915
|
+
type: string;
|
|
1916
|
+
items: {
|
|
1917
|
+
type: string;
|
|
1918
|
+
enum: ("pending" | "in_progress" | "completed" | "failed" | "cancelled")[];
|
|
1919
|
+
};
|
|
1920
|
+
enum?: undefined;
|
|
1921
|
+
})[];
|
|
1922
|
+
type?: undefined;
|
|
1923
|
+
nullable?: undefined;
|
|
1924
|
+
minimum?: undefined;
|
|
1925
|
+
};
|
|
1926
|
+
} | {
|
|
1927
|
+
name: string;
|
|
1928
|
+
in: string;
|
|
1929
|
+
description: string;
|
|
1930
|
+
style: string;
|
|
1931
|
+
explode: boolean;
|
|
1932
|
+
schema: {
|
|
1933
|
+
oneOf: ({
|
|
1934
|
+
type: string;
|
|
1935
|
+
enum: ("low" | "medium" | "high" | "critical")[];
|
|
1936
|
+
items?: undefined;
|
|
1937
|
+
} | {
|
|
1938
|
+
type: string;
|
|
1939
|
+
items: {
|
|
1940
|
+
type: string;
|
|
1941
|
+
enum: ("low" | "medium" | "high" | "critical")[];
|
|
1942
|
+
};
|
|
1943
|
+
enum?: undefined;
|
|
1944
|
+
})[];
|
|
1945
|
+
type?: undefined;
|
|
1946
|
+
nullable?: undefined;
|
|
1947
|
+
minimum?: undefined;
|
|
1948
|
+
};
|
|
1949
|
+
} | {
|
|
1904
1950
|
name: string;
|
|
1905
1951
|
in: string;
|
|
1906
1952
|
schema: {
|
|
1907
1953
|
type: string;
|
|
1954
|
+
oneOf?: undefined;
|
|
1908
1955
|
nullable?: undefined;
|
|
1909
1956
|
minimum?: undefined;
|
|
1910
1957
|
};
|
|
1911
1958
|
description?: undefined;
|
|
1959
|
+
style?: undefined;
|
|
1960
|
+
explode?: undefined;
|
|
1912
1961
|
} | {
|
|
1913
1962
|
name: string;
|
|
1914
1963
|
in: string;
|
|
1915
1964
|
schema: {
|
|
1916
1965
|
type: string;
|
|
1917
1966
|
nullable: boolean;
|
|
1967
|
+
oneOf?: undefined;
|
|
1918
1968
|
minimum?: undefined;
|
|
1919
1969
|
};
|
|
1920
1970
|
description?: undefined;
|
|
1971
|
+
style?: undefined;
|
|
1972
|
+
explode?: undefined;
|
|
1921
1973
|
} | {
|
|
1922
1974
|
name: string;
|
|
1923
1975
|
in: string;
|
|
1924
1976
|
schema: {
|
|
1925
1977
|
type: string;
|
|
1978
|
+
oneOf?: undefined;
|
|
1926
1979
|
nullable?: undefined;
|
|
1927
1980
|
minimum?: undefined;
|
|
1928
1981
|
};
|
|
1929
1982
|
description: string;
|
|
1983
|
+
style?: undefined;
|
|
1984
|
+
explode?: undefined;
|
|
1930
1985
|
} | {
|
|
1931
1986
|
name: string;
|
|
1932
1987
|
in: string;
|
|
1933
1988
|
schema: {
|
|
1934
1989
|
type: string;
|
|
1935
1990
|
minimum: number;
|
|
1991
|
+
oneOf?: undefined;
|
|
1936
1992
|
nullable?: undefined;
|
|
1937
1993
|
};
|
|
1938
1994
|
description?: undefined;
|
|
1995
|
+
style?: undefined;
|
|
1996
|
+
explode?: undefined;
|
|
1939
1997
|
})[];
|
|
1940
1998
|
responses: {
|
|
1941
1999
|
"200": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/server/openapi.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/server/openapi.ts"],"names":[],"mappings":"AA+JA,wBAAgB,sBAAsB,CAAC,OAAO,SAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgvCnE"}
|