@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
package/dist/mcp/index.js CHANGED
@@ -44,7 +44,10 @@ 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, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
47
+ function isTerminalStatus(status) {
48
+ return status === "completed" || status === "failed" || status === "cancelled";
49
+ }
50
+ var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
48
51
  var init_types = __esm(() => {
49
52
  TASK_STATUSES = [
50
53
  "pending",
@@ -53,6 +56,12 @@ var init_types = __esm(() => {
53
56
  "failed",
54
57
  "cancelled"
55
58
  ];
59
+ TASK_PRIORITIES = [
60
+ "low",
61
+ "medium",
62
+ "high",
63
+ "critical"
64
+ ];
56
65
  VersionConflictError = class VersionConflictError extends Error {
57
66
  taskId;
58
67
  expectedVersion;
@@ -3795,6 +3804,41 @@ var init_identity_mapping = __esm(() => {
3795
3804
  });
3796
3805
  });
3797
3806
 
3807
+ // src/lib/sync-utils.ts
3808
+ import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
3809
+ import { homedir } from "os";
3810
+ import { join } from "path";
3811
+ function getHomeDir() {
3812
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
3813
+ }
3814
+ function getTodosGlobalDir() {
3815
+ return join(getHomeDir(), ".hasna", "todos");
3816
+ }
3817
+ function ensureDir(dir) {
3818
+ if (!existsSync2(dir))
3819
+ mkdirSync(dir, { recursive: true });
3820
+ }
3821
+ function readJsonFile(path) {
3822
+ try {
3823
+ return JSON.parse(readFileSync(path, "utf-8"));
3824
+ } catch {
3825
+ return null;
3826
+ }
3827
+ }
3828
+ function writeJsonFile(path, data) {
3829
+ writeFileSync(path, JSON.stringify(data, null, 2) + `
3830
+ `);
3831
+ }
3832
+ function appendSyncConflict(metadata, conflict, limit = 5) {
3833
+ const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
3834
+ const next = [conflict, ...current].slice(0, limit);
3835
+ return { ...metadata, sync_conflicts: next };
3836
+ }
3837
+ var HOME;
3838
+ var init_sync_utils = __esm(() => {
3839
+ HOME = getHomeDir();
3840
+ });
3841
+
3798
3842
  // src/storage/config.ts
3799
3843
  var exports_config = {};
3800
3844
  __export(exports_config, {
@@ -4125,8 +4169,8 @@ __export(exports_database, {
4125
4169
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
4126
4170
  });
4127
4171
  import { Database } from "bun:sqlite";
4128
- import { existsSync as existsSync2, mkdirSync } from "fs";
4129
- import { dirname, join, resolve as resolve2 } from "path";
4172
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
4173
+ import { dirname, join as join2, resolve as resolve2 } from "path";
4130
4174
  function isInMemoryDb(path) {
4131
4175
  return path === ":memory:" || path.startsWith("file::memory:");
4132
4176
  }
@@ -4135,8 +4179,8 @@ function findNearestProjectDb(startDir) {
4135
4179
  const stopAt = gitRoot ? resolve2(gitRoot) : resolve2(startDir);
4136
4180
  let dir = resolve2(startDir);
4137
4181
  while (true) {
4138
- const candidate = join(dir, ".hasna", "todos", "todos.db");
4139
- if (existsSync2(candidate))
4182
+ const candidate = join2(dir, ".hasna", "todos", "todos.db");
4183
+ if (existsSync3(candidate))
4140
4184
  return candidate;
4141
4185
  if (dir === stopAt)
4142
4186
  break;
@@ -4150,7 +4194,7 @@ function findNearestProjectDb(startDir) {
4150
4194
  function findGitRoot(startDir) {
4151
4195
  let dir = resolve2(startDir);
4152
4196
  while (true) {
4153
- if (existsSync2(join(dir, ".git")))
4197
+ if (existsSync3(join2(dir, ".git")))
4154
4198
  return dir;
4155
4199
  const parent = dirname(dir);
4156
4200
  if (parent === dir)
@@ -4160,8 +4204,7 @@ function findGitRoot(startDir) {
4160
4204
  return null;
4161
4205
  }
4162
4206
  function getGlobalDbPath() {
4163
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4164
- return join(home, ".hasna", "todos", "todos.db");
4207
+ return join2(getHomeDir(), ".hasna", "todos", "todos.db");
4165
4208
  }
4166
4209
  function hasExplicitProjectArg(args = process.argv.slice(2)) {
4167
4210
  return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
@@ -4199,7 +4242,7 @@ function getDbPath() {
4199
4242
  if (process.env["TODOS_DB_SCOPE"] === "project") {
4200
4243
  const gitRoot = findGitRoot(cwd);
4201
4244
  if (gitRoot && canCreateScopedProjectDb()) {
4202
- return join(gitRoot, ".hasna", "todos", "todos.db");
4245
+ return join2(gitRoot, ".hasna", "todos", "todos.db");
4203
4246
  }
4204
4247
  }
4205
4248
  return getGlobalDbPath();
@@ -4207,16 +4250,16 @@ function getDbPath() {
4207
4250
  function getDatabasePath() {
4208
4251
  return getDbPath();
4209
4252
  }
4210
- function ensureDir(filePath) {
4253
+ function ensureDir2(filePath) {
4211
4254
  if (isInMemoryDb(filePath))
4212
4255
  return;
4213
4256
  const dir = dirname(resolve2(filePath));
4214
- if (!existsSync2(dir)) {
4215
- mkdirSync(dir, { recursive: true });
4257
+ if (!existsSync3(dir)) {
4258
+ mkdirSync2(dir, { recursive: true });
4216
4259
  }
4217
4260
  }
4218
4261
  function openDatabase(path) {
4219
- ensureDir(path);
4262
+ ensureDir2(path);
4220
4263
  const db = new Database(path);
4221
4264
  db.run("PRAGMA busy_timeout = 5000");
4222
4265
  db.run("PRAGMA journal_mode = WAL");
@@ -4383,6 +4426,7 @@ var init_database = __esm(() => {
4383
4426
  init_machines();
4384
4427
  init_identity_mapping();
4385
4428
  init_types();
4429
+ init_sync_utils();
4386
4430
  ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
4387
4431
  });
4388
4432
 
@@ -9126,40 +9170,6 @@ var init_zod = __esm(() => {
9126
9170
  init_external();
9127
9171
  });
9128
9172
 
9129
- // src/lib/sync-utils.ts
9130
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
9131
- import { join as join2 } from "path";
9132
- function getHomeDir() {
9133
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
9134
- }
9135
- function getTodosGlobalDir() {
9136
- return join2(getHomeDir(), ".hasna", "todos");
9137
- }
9138
- function ensureDir2(dir) {
9139
- if (!existsSync3(dir))
9140
- mkdirSync2(dir, { recursive: true });
9141
- }
9142
- function readJsonFile(path) {
9143
- try {
9144
- return JSON.parse(readFileSync(path, "utf-8"));
9145
- } catch {
9146
- return null;
9147
- }
9148
- }
9149
- function writeJsonFile(path, data) {
9150
- writeFileSync(path, JSON.stringify(data, null, 2) + `
9151
- `);
9152
- }
9153
- function appendSyncConflict(metadata, conflict, limit = 5) {
9154
- const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
9155
- const next = [conflict, ...current].slice(0, limit);
9156
- return { ...metadata, sync_conflicts: next };
9157
- }
9158
- var HOME;
9159
- var init_sync_utils = __esm(() => {
9160
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
9161
- });
9162
-
9163
9173
  // src/lib/config.ts
9164
9174
  import { existsSync as existsSync4 } from "fs";
9165
9175
  import { dirname as dirname2, join as join3 } from "path";
@@ -9182,7 +9192,7 @@ function loadConfig() {
9182
9192
  }
9183
9193
  function saveConfig(config) {
9184
9194
  const configPath = getConfigPath();
9185
- ensureDir2(dirname2(configPath));
9195
+ ensureDir(dirname2(configPath));
9186
9196
  writeJsonFile(configPath, config);
9187
9197
  cached = config;
9188
9198
  return config;
@@ -10737,7 +10747,7 @@ var init_event_hooks = __esm(() => {
10737
10747
  // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
10738
10748
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
10739
10749
  import { existsSync as existsSync5 } from "fs";
10740
- import { homedir } from "os";
10750
+ import { homedir as homedir2 } from "os";
10741
10751
  import { join as join4 } from "path";
10742
10752
  import { createHmac, timingSafeEqual } from "crypto";
10743
10753
  import { randomUUID as randomUUID2 } from "crypto";
@@ -10839,7 +10849,7 @@ function channelMatchesEvent(channel, event) {
10839
10849
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
10840
10850
  }
10841
10851
  function getEventsDataDir(override) {
10842
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir(), ".hasna", "events");
10852
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir2(), ".hasna", "events");
10843
10853
  }
10844
10854
 
10845
10855
  class JsonEventsStore {
@@ -13927,11 +13937,13 @@ function updateTask(id, input, db) {
13927
13937
  }
13928
13938
  sets.push("status = ?");
13929
13939
  params.push(input.status);
13940
+ if (isTerminalStatus(input.status)) {
13941
+ sets.push("locked_by = NULL");
13942
+ sets.push("locked_at = NULL");
13943
+ }
13930
13944
  if (input.status === "completed") {
13931
13945
  sets.push("completed_at = ?");
13932
13946
  params.push(completionTimestamp);
13933
- sets.push("locked_by = NULL");
13934
- sets.push("locked_at = NULL");
13935
13947
  } else if (task.status === "completed" && input.completed_at === undefined) {
13936
13948
  sets.push("completed_at = NULL");
13937
13949
  }
@@ -14055,6 +14067,7 @@ function updateTask(id, input, db) {
14055
14067
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
14056
14068
  const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
14057
14069
  const completedNow = input.status === "completed";
14070
+ const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
14058
14071
  const updatedTask = {
14059
14072
  ...task,
14060
14073
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
@@ -14062,8 +14075,8 @@ function updateTask(id, input, db) {
14062
14075
  metadata: input.metadata ?? task.metadata,
14063
14076
  version: task.version + 1,
14064
14077
  updated_at: timestamp2,
14065
- locked_by: completedNow ? null : task.locked_by,
14066
- locked_at: completedNow ? null : task.locked_at,
14078
+ locked_by: terminalNow ? null : task.locked_by,
14079
+ locked_at: terminalNow ? null : task.locked_at,
14067
14080
  completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
14068
14081
  sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
14069
14082
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
@@ -17990,10 +18003,10 @@ var init_token_utils = __esm(() => {
17990
18003
 
17991
18004
  // src/lib/assignee-validation.ts
17992
18005
  import { readFileSync as readFileSync3 } from "fs";
17993
- import { homedir as homedir2 } from "os";
18006
+ import { homedir as homedir3 } from "os";
17994
18007
  import { join as join7 } from "path";
17995
18008
  function defaultSeatRosterPath() {
17996
- return process.env["TODOS_SEAT_ROSTER_PATH"] || join7(homedir2(), ".hasna", "identities", "hasna-seats.roster.json");
18009
+ return process.env["TODOS_SEAT_ROSTER_PATH"] || join7(homedir3(), ".hasna", "identities", "hasna-seats.roster.json");
17997
18010
  }
17998
18011
  function loadSeatSlugs(path = defaultSeatRosterPath()) {
17999
18012
  try {
@@ -20821,7 +20834,7 @@ async function validateMcpAssignee(value, allowSeat) {
20821
20834
  return verdict.assignee;
20822
20835
  }
20823
20836
  function registerTaskCrudTools(server, ctx) {
20824
- const { shouldRegisterTool, resolveId, formatError, formatTask } = ctx;
20837
+ const { shouldRegisterTool, resolveId, formatError, formatTask, applyFocus } = ctx;
20825
20838
  function mutationTaskResponse(task) {
20826
20839
  const compact = compactTask(task, 240);
20827
20840
  compact["version"] = task.version;
@@ -20897,6 +20910,7 @@ function registerTaskCrudTools(server, ctx) {
20897
20910
  payload.max_retries = retry_count;
20898
20911
  if (deadline)
20899
20912
  payload.due_at = deadline;
20913
+ applyFocus(payload, router.agent_id || undefined);
20900
20914
  const created = await cloudCreateTask(cloud, payload);
20901
20915
  return { content: [{ type: "text", text: mutationTaskResponse(created) }] };
20902
20916
  }
@@ -20923,6 +20937,7 @@ function registerTaskCrudTools(server, ctx) {
20923
20937
  resolved.max_retries = retry_count;
20924
20938
  if (deadline)
20925
20939
  resolved.due_at = deadline;
20940
+ applyFocus(resolved, router.agent_id || undefined);
20926
20941
  const task = createTask(resolved);
20927
20942
  return { content: [{ type: "text", text: mutationTaskResponse(task) }] };
20928
20943
  } catch (e) {
@@ -26049,7 +26064,8 @@ __export(exports_saved_search_views, {
26049
26064
  normalizeScope: () => normalizeScope,
26050
26065
  listSearchViews: () => listSearchViews,
26051
26066
  getSearchView: () => getSearchView,
26052
- deleteSearchView: () => deleteSearchView
26067
+ deleteSearchView: () => deleteSearchView,
26068
+ SAVED_SEARCH_SCOPES: () => SAVED_SEARCH_SCOPES
26053
26069
  });
26054
26070
  function parseFilters(value) {
26055
26071
  if (!value)
@@ -26069,10 +26085,8 @@ function rowToSavedSearchView(row) {
26069
26085
  };
26070
26086
  }
26071
26087
  function normalizeScope(scope) {
26072
- if (scope === "all" || scope === "tasks" || scope === "projects" || scope === "plans" || scope === "runs" || scope === "comments") {
26073
- return scope;
26074
- }
26075
- return "tasks";
26088
+ const candidate = (scope ?? "").trim().toLowerCase();
26089
+ return SAVED_SEARCH_SCOPES.includes(candidate) ? candidate : "tasks";
26076
26090
  }
26077
26091
  function normalizeName4(name) {
26078
26092
  const normalized = name.trim();
@@ -26375,10 +26389,12 @@ function runSearchView(idOrName, db) {
26375
26389
  throw new Error(`Saved search view not found: ${idOrName}`);
26376
26390
  return { ...runSavedSearch(view.filters, view.scope, d), view };
26377
26391
  }
26392
+ var SAVED_SEARCH_SCOPES;
26378
26393
  var init_saved_search_views = __esm(() => {
26379
26394
  init_database();
26380
26395
  init_local_fields();
26381
26396
  init_search();
26397
+ SAVED_SEARCH_SCOPES = ["all", "tasks", "projects", "plans", "runs", "comments"];
26382
26398
  });
26383
26399
 
26384
26400
  // src/mcp/tools/task-project-tools.ts
@@ -30194,10 +30210,23 @@ function registerTaskAutoTools(server, ctx) {
30194
30210
  return { content: [{ type: "text", text: "No active agents available for rebalancing." }] };
30195
30211
  const activeTasks = listTasks3({ project_id: resolvedProjectId, status: ["pending", "in_progress"], limit: 1000 }, undefined);
30196
30212
  const agentKeyByAlias = new Map;
30213
+ const ambiguousAgentAliases = new Set;
30214
+ const indexAgentAlias = (alias, agentId) => {
30215
+ const normalizedAlias2 = alias.toLowerCase();
30216
+ if (ambiguousAgentAliases.has(normalizedAlias2))
30217
+ return;
30218
+ const existingAgentId = agentKeyByAlias.get(normalizedAlias2);
30219
+ if (existingAgentId !== undefined && existingAgentId !== agentId) {
30220
+ agentKeyByAlias.delete(normalizedAlias2);
30221
+ ambiguousAgentAliases.add(normalizedAlias2);
30222
+ return;
30223
+ }
30224
+ agentKeyByAlias.set(normalizedAlias2, agentId);
30225
+ };
30197
30226
  for (const agent of agents) {
30198
- agentKeyByAlias.set(String(agent.id).toLowerCase(), agent.id);
30227
+ indexAgentAlias(String(agent.id), agent.id);
30199
30228
  if (agent.name)
30200
- agentKeyByAlias.set(String(agent.name).toLowerCase(), agent.id);
30229
+ indexAgentAlias(String(agent.name), agent.id);
30201
30230
  }
30202
30231
  const canonicalAgentKey = (assignedTo) => assignedTo ? agentKeyByAlias.get(assignedTo.toLowerCase()) : undefined;
30203
30232
  const load = new Map(agents.map((agent) => [agent.id, activeTasks.filter((t) => canonicalAgentKey(t.assigned_to) === agent.id).length]));
@@ -34886,7 +34915,7 @@ var package_default;
34886
34915
  var init_package = __esm(() => {
34887
34916
  package_default = {
34888
34917
  name: "@hasna/todos",
34889
- version: "0.13.12",
34918
+ version: "0.15.1",
34890
34919
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
34891
34920
  type: "module",
34892
34921
  main: "dist/index.js",
@@ -42865,9 +42894,16 @@ ${text2}` }] };
42865
42894
  return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
42866
42895
  }
42867
42896
  const oldName = agent.name;
42868
- const updated = updateAgent(agent.id, { name: new_name });
42869
42897
  const db = getDatabase();
42870
- const tasksResult = db.run("UPDATE tasks SET assigned_to = ? WHERE LOWER(assigned_to) = LOWER(?)", [new_name, oldName]);
42898
+ let oldNameUniquelyIdentifiesAgent = false;
42899
+ try {
42900
+ oldNameUniquelyIdentifiesAgent = resolvePartialId(db, "agents", oldName) === agent.id;
42901
+ } catch (error) {
42902
+ if (!(error instanceof IdentityAliasAmbiguousError))
42903
+ throw error;
42904
+ }
42905
+ const updated = updateAgent(agent.id, { name: new_name });
42906
+ 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]);
42871
42907
  const taskNote = tasksResult.changes > 0 ? `
42872
42908
  Updated assigned_to on ${tasksResult.changes} task(s).` : "";
42873
42909
  return {
@@ -43051,6 +43087,7 @@ var init_agents2 = __esm(() => {
43051
43087
  init_agents();
43052
43088
  init_config2();
43053
43089
  init_database();
43090
+ init_types();
43054
43091
  init_cloud_router();
43055
43092
  });
43056
43093
 
@@ -43819,7 +43856,7 @@ function captureEnvironmentSnapshot(input = {}) {
43819
43856
  }
43820
43857
  function writeEnvironmentSnapshot(snapshot, outputPath) {
43821
43858
  const path = outputPath ? resolve15(outputPath) : join14(defaultSnapshotDir(), `${snapshot.id}.json`);
43822
- ensureDir2(dirname8(path));
43859
+ ensureDir(dirname8(path));
43823
43860
  writeJsonFile(path, snapshot);
43824
43861
  return path;
43825
43862
  }
@@ -46459,9 +46496,11 @@ async function updateTask2(id, input, store) {
46459
46496
  throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
46460
46497
  }
46461
46498
  const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
46499
+ const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
46462
46500
  const task2 = {
46463
46501
  ...existing,
46464
46502
  ...definedPatch(input),
46503
+ ...terminalNow ? { locked_by: null, locked_at: null } : {},
46465
46504
  version: existing.version + 1,
46466
46505
  updated_at: new Date().toISOString(),
46467
46506
  tags: input.tags ?? existing.tags,
@@ -48034,6 +48073,103 @@ var init_auth_posture = __esm(() => {
48034
48073
  LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]", "ip6-localhost"]);
48035
48074
  });
48036
48075
 
48076
+ // src/lib/enum-vocabulary.ts
48077
+ function editDistance(a, b) {
48078
+ if (a === b)
48079
+ return 0;
48080
+ if (!a.length)
48081
+ return b.length;
48082
+ if (!b.length)
48083
+ return a.length;
48084
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
48085
+ for (let i = 1;i <= a.length; i += 1) {
48086
+ const current = [i];
48087
+ for (let j = 1;j <= b.length; j += 1) {
48088
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
48089
+ }
48090
+ previous = current;
48091
+ }
48092
+ return previous[b.length];
48093
+ }
48094
+ function suggestVocabularyMatches(value, vocabulary, limit = 3) {
48095
+ const needle = value.trim().toLowerCase();
48096
+ if (!needle)
48097
+ return [];
48098
+ const scored = [];
48099
+ for (const member of vocabulary) {
48100
+ const candidate = member.toLowerCase();
48101
+ if (candidate.startsWith(needle) || needle.startsWith(candidate)) {
48102
+ scored.push({ member, score: 0 });
48103
+ continue;
48104
+ }
48105
+ if (candidate.includes(needle) || needle.includes(candidate)) {
48106
+ scored.push({ member, score: 1 });
48107
+ continue;
48108
+ }
48109
+ const distance = editDistance(needle, candidate);
48110
+ if (distance <= Math.max(1, Math.floor(candidate.length / 3))) {
48111
+ scored.push({ member, score: 1 + distance });
48112
+ }
48113
+ }
48114
+ return scored.sort((a, b) => a.score - b.score || a.member.localeCompare(b.member)).slice(0, limit).map((entry2) => entry2.member);
48115
+ }
48116
+ function resolveEnumVocabulary(raw, spec) {
48117
+ const allowList = spec.allowList !== false;
48118
+ const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim());
48119
+ if (rawElements.every((element) => element.length === 0)) {
48120
+ return {
48121
+ ok: false,
48122
+ message: `${spec.name} requires a value. Allowed values: ${spec.vocabulary.join(", ")}.`,
48123
+ invalid: []
48124
+ };
48125
+ }
48126
+ if (rawElements.some((element) => element.length === 0)) {
48127
+ return {
48128
+ ok: false,
48129
+ message: `${spec.name} has an empty value in "${raw}" \u2014 remove the stray comma. ` + `Allowed values: ${spec.vocabulary.join(", ")}.`,
48130
+ invalid: []
48131
+ };
48132
+ }
48133
+ const normalized = rawElements.map((element) => spec.normalize ? spec.normalize(element) : element);
48134
+ const allowed = new Set(spec.vocabulary);
48135
+ const invalid2 = [];
48136
+ for (let i = 0;i < normalized.length; i += 1) {
48137
+ if (!allowed.has(normalized[i]))
48138
+ invalid2.push(rawElements[i]);
48139
+ }
48140
+ if (invalid2.length === 0) {
48141
+ return { ok: true, values: [...new Set(normalized)] };
48142
+ }
48143
+ const label = invalid2.length === 1 ? "value" : "values";
48144
+ const parts = [
48145
+ `Invalid ${spec.name} ${label}: ${invalid2.join(", ")}.`,
48146
+ `Allowed values: ${spec.vocabulary.join(", ")}.`
48147
+ ];
48148
+ const hints = new Set;
48149
+ const suggestions = new Set;
48150
+ for (let i = 0;i < normalized.length; i += 1) {
48151
+ const canonical = normalized[i];
48152
+ if (allowed.has(canonical))
48153
+ continue;
48154
+ const hint = spec.hints?.[canonical.toLowerCase()];
48155
+ if (hint) {
48156
+ hints.add(hint);
48157
+ continue;
48158
+ }
48159
+ for (const match of suggestVocabularyMatches(canonical, spec.vocabulary)) {
48160
+ suggestions.add(match);
48161
+ }
48162
+ }
48163
+ if (suggestions.size > 0)
48164
+ parts.push(`Did you mean ${[...suggestions].join(", ")}?`);
48165
+ for (const hint of hints)
48166
+ parts.push(hint);
48167
+ return { ok: false, message: parts.join(" "), invalid: invalid2 };
48168
+ }
48169
+ function collapseEnumValues(values) {
48170
+ return values.length === 1 ? values[0] : values;
48171
+ }
48172
+
48037
48173
  // src/db/orgs.ts
48038
48174
  function rowToOrg(row) {
48039
48175
  return { ...row, metadata: JSON.parse(row.metadata || "{}") };
@@ -48334,8 +48470,19 @@ function handleStats(_ctx, json2) {
48334
48470
  recurring_tasks: countRecurringTasks()
48335
48471
  });
48336
48472
  }
48473
+ function taskStatusQueryParam(url) {
48474
+ const raw = url.searchParams.get("status");
48475
+ if (!raw)
48476
+ return { ok: true, value: undefined };
48477
+ const result = resolveEnumVocabulary(raw, { name: "status", vocabulary: TASK_STATUSES });
48478
+ if (!result.ok)
48479
+ return { ok: false, message: result.message };
48480
+ return { ok: true, value: collapseEnumValues(result.values) };
48481
+ }
48337
48482
  async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
48338
- const status = url.searchParams.get("status") || undefined;
48483
+ const statusParam = taskStatusQueryParam(url);
48484
+ if (!statusParam.ok)
48485
+ return json2({ error: statusParam.message }, 400);
48339
48486
  const projectId = url.searchParams.get("project_id") || undefined;
48340
48487
  const sessionId = url.searchParams.get("session_id") || undefined;
48341
48488
  const agentId = url.searchParams.get("agent_id") || undefined;
@@ -48343,7 +48490,7 @@ async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
48343
48490
  const offsetParam = url.searchParams.get("offset");
48344
48491
  const fields = parseFieldsParam(url);
48345
48492
  const tasks = listTasks({
48346
- status,
48493
+ status: statusParam.value,
48347
48494
  project_id: projectId,
48348
48495
  session_id: sessionId,
48349
48496
  agent_id: agentId,
@@ -48408,9 +48555,19 @@ async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
48408
48555
  }
48409
48556
  function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
48410
48557
  const format = url.searchParams.get("format") || "json";
48411
- const status = url.searchParams.get("status") || undefined;
48558
+ const statusParam = taskStatusQueryParam(url);
48559
+ if (!statusParam.ok) {
48560
+ return new Response(JSON.stringify({ error: statusParam.message }), {
48561
+ status: 400,
48562
+ headers: { "Content-Type": "application/json" }
48563
+ });
48564
+ }
48412
48565
  const projectId = url.searchParams.get("project_id") || undefined;
48413
- const tasks = listTasks({ status, project_id: projectId, limit: 1e4 });
48566
+ const tasks = listTasks({
48567
+ status: statusParam.value,
48568
+ project_id: projectId,
48569
+ limit: 1e4
48570
+ });
48414
48571
  const summaries = tasks.map((t) => taskToSummary2(t));
48415
48572
  if (format === "csv") {
48416
48573
  const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
@@ -48992,6 +49149,7 @@ var init_routes = __esm(() => {
48992
49149
  init_tasks();
48993
49150
  init_database();
48994
49151
  init_types();
49152
+ init_types();
48995
49153
  init_projects();
48996
49154
  init_agents();
48997
49155
  init_plans();
@@ -49945,8 +50103,38 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
49945
50103
  operationId: "listTasks",
49946
50104
  summary: "List tasks",
49947
50105
  parameters: [
49948
- { name: "status", in: "query", schema: { type: "string" } },
49949
- { name: "priority", in: "query", schema: { type: "string" } },
50106
+ {
50107
+ name: "status",
50108
+ in: "query",
50109
+ description: `Task status, or a comma-separated list of statuses. Allowed values: ${TASK_STATUSES.join(", ")}.`,
50110
+ style: "form",
50111
+ explode: false,
50112
+ schema: {
50113
+ oneOf: [
50114
+ { type: "string", enum: [...TASK_STATUSES] },
50115
+ {
50116
+ type: "array",
50117
+ items: { type: "string", enum: [...TASK_STATUSES] }
50118
+ }
50119
+ ]
50120
+ }
50121
+ },
50122
+ {
50123
+ name: "priority",
50124
+ in: "query",
50125
+ description: `Task priority, or a comma-separated list of priorities. Allowed values: ${TASK_PRIORITIES.join(", ")}.`,
50126
+ style: "form",
50127
+ explode: false,
50128
+ schema: {
50129
+ oneOf: [
50130
+ { type: "string", enum: [...TASK_PRIORITIES] },
50131
+ {
50132
+ type: "array",
50133
+ items: { type: "string", enum: [...TASK_PRIORITIES] }
50134
+ }
50135
+ ]
50136
+ }
50137
+ },
49950
50138
  { name: "project_id", in: "query", schema: { type: "string" } },
49951
50139
  { name: "parent_id", in: "query", schema: { type: "string", nullable: true } },
49952
50140
  { name: "include_subtasks", in: "query", schema: { type: "boolean" } },
@@ -50458,6 +50646,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
50458
50646
  var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
50459
50647
  var init_openapi = __esm(() => {
50460
50648
  init_package_version();
50649
+ init_types();
50461
50650
  taskSchema = {
50462
50651
  type: "object",
50463
50652
  properties: {
@@ -50725,6 +50914,15 @@ function json3(body, status = 200) {
50725
50914
  function error(status, message, extra) {
50726
50915
  return json3({ error: message, ...extra ?? {} }, status);
50727
50916
  }
50917
+ function enumQueryParam(url, name, vocabulary) {
50918
+ const raw = url.searchParams.get(name);
50919
+ if (raw === null || raw === "")
50920
+ return { ok: true, value: undefined };
50921
+ const result = resolveEnumVocabulary(raw, { name, vocabulary });
50922
+ if (!result.ok)
50923
+ return { ok: false, response: error(400, result.message) };
50924
+ return { ok: true, value: collapseEnumValues(result.values) };
50925
+ }
50728
50926
  function validateTaskCompletion(value) {
50729
50927
  if (!value || typeof value !== "object" || Array.isArray(value))
50730
50928
  return { ok: false, message: "completion body must be an object" };
@@ -51145,14 +51343,16 @@ async function handleV1Request(req, url, dependencies = {}) {
51145
51343
  return error(400, "include_subtasks must be true or false");
51146
51344
  }
51147
51345
  const hasParentFilter = url.searchParams.has("parent_id");
51346
+ const statusParam = enumQueryParam(url, "status", TASK_STATUSES);
51347
+ if (!statusParam.ok)
51348
+ return statusParam.response;
51349
+ const priorityParam = enumQueryParam(url, "priority", TASK_PRIORITIES);
51350
+ if (!priorityParam.ok)
51351
+ return priorityParam.response;
51148
51352
  const filter = {
51149
51353
  ...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
51150
- ...url.searchParams.get("status") ? {
51151
- status: url.searchParams.get("status").includes(",") ? url.searchParams.get("status").split(",") : url.searchParams.get("status")
51152
- } : {},
51153
- ...url.searchParams.get("priority") ? {
51154
- priority: url.searchParams.get("priority").includes(",") ? url.searchParams.get("priority").split(",") : url.searchParams.get("priority")
51155
- } : {},
51354
+ ...statusParam.value !== undefined ? { status: statusParam.value } : {},
51355
+ ...priorityParam.value !== undefined ? { priority: priorityParam.value } : {},
51156
51356
  ...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
51157
51357
  ...hasParentFilter ? { parent_id: url.searchParams.get("parent_id") || null, include_subtasks: true } : includeSubtasks !== null ? { include_subtasks: includeSubtasks === "true" } : {},
51158
51358
  ...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {},
@@ -51786,7 +51986,13 @@ async function handleV1Request(req, url, dependencies = {}) {
51786
51986
  return error(405, `method ${method} not allowed on /v1/refs/:ref`);
51787
51987
  if (!store.gitRefs)
51788
51988
  return error(501, "git ref links are not supported by this storage backend");
51789
- const refs = await store.gitRefs.find(id);
51989
+ let decodedRef;
51990
+ try {
51991
+ decodedRef = decodeURIComponent(id);
51992
+ } catch {
51993
+ return error(400, "ref path segment has invalid percent encoding");
51994
+ }
51995
+ const refs = await store.gitRefs.find(decodedRef);
51790
51996
  return json3({ refs, count: refs.length });
51791
51997
  }
51792
51998
  if (resource === "next" && !id) {
@@ -52892,7 +53098,8 @@ function buildServer() {
52892
53098
  formatError,
52893
53099
  formatTask,
52894
53100
  formatTaskDetail,
52895
- getAgentFocus
53101
+ getAgentFocus,
53102
+ applyFocus
52896
53103
  };
52897
53104
  registerTaskCrudTools(server, toolContext);
52898
53105
  registerTaskProjectTools(server, toolContext);
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAOzE,UAAU,UAAU;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,KAAK,OAAO,GAAG;IACb,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;IACpC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;CAC5D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,EAAE,OAAO,GAAG,IAAI,CAqfjJ"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAQzE,UAAU,UAAU;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,KAAK,OAAO,GAAG;IACb,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;IACpC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;CAC5D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,EAAE,OAAO,GAAG,IAAI,CA4fjJ"}