@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.
Files changed (47) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/commands/agent-commands.d.ts +39 -0
  3. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/dispatch.d.ts.map +1 -1
  6. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  7. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  8. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  9. package/dist/cli/helpers.d.ts +64 -2
  10. package/dist/cli/helpers.d.ts.map +1 -1
  11. package/dist/cli/index.js +624 -98
  12. package/dist/cli/stage-a.d.ts.map +1 -1
  13. package/dist/contracts.js +123 -116
  14. package/dist/db/database.d.ts.map +1 -1
  15. package/dist/db/task-crud.d.ts.map +1 -1
  16. package/dist/index.js +135 -127
  17. package/dist/lib/bulk-tags.d.ts +57 -0
  18. package/dist/lib/bulk-tags.d.ts.map +1 -0
  19. package/dist/lib/enum-vocabulary.d.ts +72 -0
  20. package/dist/lib/enum-vocabulary.d.ts.map +1 -0
  21. package/dist/lib/run-records.d.ts.map +1 -1
  22. package/dist/lib/sandbox-profiles.d.ts.map +1 -1
  23. package/dist/lib/saved-search-views.d.ts +12 -1
  24. package/dist/lib/saved-search-views.d.ts.map +1 -1
  25. package/dist/lib/sync-utils.d.ts +1 -1
  26. package/dist/lib/sync-utils.d.ts.map +1 -1
  27. package/dist/mcp/index.d.ts.map +1 -1
  28. package/dist/mcp/index.js +289 -82
  29. package/dist/mcp/tools/agents.d.ts.map +1 -1
  30. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
  31. package/dist/mcp/tools/task-crud.d.ts +1 -0
  32. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  33. package/dist/mcp.js +1 -1
  34. package/dist/registry.js +123 -116
  35. package/dist/release-provenance.json +5 -5
  36. package/dist/sdk/index.js +3 -2
  37. package/dist/sdk/v1.generated.d.ts +2 -2
  38. package/dist/sdk/v1.generated.d.ts.map +1 -1
  39. package/dist/server/index.js +245 -38
  40. package/dist/server/openapi.d.ts +58 -0
  41. package/dist/server/openapi.d.ts.map +1 -1
  42. package/dist/server/routes.d.ts.map +1 -1
  43. package/dist/server/v1.d.ts.map +1 -1
  44. package/dist/storage.js +124 -115
  45. package/dist/types/index.d.ts +11 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/package.json +1 -1
@@ -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.13.12",
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
- var TASK_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
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 = process.env["HOME"] || process.env["USERPROFILE"] || "~";
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
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
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(homedir(), ".hasna", "events");
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: completedNow ? null : task.locked_by,
14327
- locked_at: completedNow ? null : task.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 status = url.searchParams.get("status") || undefined;
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 status = url.searchParams.get("status") || undefined;
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({ status, project_id: projectId, limit: 1e4 });
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
- { name: "status", in: "query", schema: { type: "string" } },
21372
- { name: "priority", in: "query", schema: { type: "string" } },
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
- ...url.searchParams.get("status") ? {
22574
- status: url.searchParams.get("status").includes(",") ? url.searchParams.get("status").split(",") : url.searchParams.get("status")
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
- const refs = await store.gitRefs.find(id);
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 homedir2 } from "os";
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(homedir2(), ".hasna", "identities", "hasna-seats.roster.json");
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
- if (scope === "all" || scope === "tasks" || scope === "projects" || scope === "plans" || scope === "runs" || scope === "comments") {
63605
- return scope;
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
- agentKeyByAlias.set(String(agent.id).toLowerCase(), agent.id);
67296
+ indexAgentAlias(String(agent.id), agent.id);
67099
67297
  if (agent.name)
67100
- agentKeyByAlias.set(String(agent.name).toLowerCase(), agent.id);
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
- const tasksResult = db.run("UPDATE tasks SET assigned_to = ? WHERE LOWER(assigned_to) = LOWER(?)", [new_name, oldName]);
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);
@@ -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":"AA8JA,wBAAgB,sBAAsB,CAAC,OAAO,SAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+sCnE"}
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/server/openapi.ts"],"names":[],"mappings":"AA+JA,wBAAgB,sBAAsB,CAAC,OAAO,SAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgvCnE"}