@hasna/todos 0.13.12 → 0.14.0

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/dist/cli/index.js CHANGED
@@ -2123,7 +2123,7 @@ var package_default;
2123
2123
  var init_package = __esm(() => {
2124
2124
  package_default = {
2125
2125
  name: "@hasna/todos",
2126
- version: "0.13.12",
2126
+ version: "0.14.0",
2127
2127
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2128
2128
  type: "module",
2129
2129
  main: "dist/index.js",
@@ -2258,7 +2258,10 @@ var init_package_version = __esm(() => {
2258
2258
  function isBlockingDependencyStatus(status) {
2259
2259
  return status !== "completed" && status !== "cancelled";
2260
2260
  }
2261
- var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
2261
+ function isTerminalStatus(status) {
2262
+ return status === "completed" || status === "failed" || status === "cancelled";
2263
+ }
2264
+ var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
2262
2265
  var init_types = __esm(() => {
2263
2266
  TASK_STATUSES = [
2264
2267
  "pending",
@@ -2427,6 +2430,7 @@ var init_types = __esm(() => {
2427
2430
  this.name = "CompletionGuardError";
2428
2431
  }
2429
2432
  };
2433
+ DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
2430
2434
  DispatchNotFoundError = class DispatchNotFoundError extends Error {
2431
2435
  dispatchId;
2432
2436
  static code = "DISPATCH_NOT_FOUND";
@@ -11456,6 +11460,96 @@ var init_lock_display = __esm(() => {
11456
11460
  init_database();
11457
11461
  });
11458
11462
 
11463
+ // src/lib/enum-vocabulary.ts
11464
+ function editDistance2(a, b) {
11465
+ if (a === b)
11466
+ return 0;
11467
+ if (!a.length)
11468
+ return b.length;
11469
+ if (!b.length)
11470
+ return a.length;
11471
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
11472
+ for (let i = 1;i <= a.length; i += 1) {
11473
+ const current = [i];
11474
+ for (let j = 1;j <= b.length; j += 1) {
11475
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
11476
+ }
11477
+ previous = current;
11478
+ }
11479
+ return previous[b.length];
11480
+ }
11481
+ function suggestVocabularyMatches(value, vocabulary, limit = 3) {
11482
+ const needle = value.trim().toLowerCase();
11483
+ if (!needle)
11484
+ return [];
11485
+ const scored = [];
11486
+ for (const member of vocabulary) {
11487
+ const candidate = member.toLowerCase();
11488
+ if (candidate.startsWith(needle) || needle.startsWith(candidate)) {
11489
+ scored.push({ member, score: 0 });
11490
+ continue;
11491
+ }
11492
+ if (candidate.includes(needle) || needle.includes(candidate)) {
11493
+ scored.push({ member, score: 1 });
11494
+ continue;
11495
+ }
11496
+ const distance = editDistance2(needle, candidate);
11497
+ if (distance <= Math.max(1, Math.floor(candidate.length / 3))) {
11498
+ scored.push({ member, score: 1 + distance });
11499
+ }
11500
+ }
11501
+ return scored.sort((a, b) => a.score - b.score || a.member.localeCompare(b.member)).slice(0, limit).map((entry) => entry.member);
11502
+ }
11503
+ function resolveEnumVocabulary(raw, spec) {
11504
+ const allowList = spec.allowList !== false;
11505
+ const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim()).filter((element) => element.length > 0);
11506
+ if (rawElements.length === 0) {
11507
+ return {
11508
+ ok: false,
11509
+ message: `${spec.name} requires a value. Allowed values: ${spec.vocabulary.join(", ")}.`,
11510
+ invalid: []
11511
+ };
11512
+ }
11513
+ const normalized = rawElements.map((element) => spec.normalize ? spec.normalize(element) : element);
11514
+ const allowed = new Set(spec.vocabulary);
11515
+ const invalid2 = [];
11516
+ for (let i = 0;i < normalized.length; i += 1) {
11517
+ if (!allowed.has(normalized[i]))
11518
+ invalid2.push(rawElements[i]);
11519
+ }
11520
+ if (invalid2.length === 0) {
11521
+ return { ok: true, values: [...new Set(normalized)] };
11522
+ }
11523
+ const label = invalid2.length === 1 ? "value" : "values";
11524
+ const parts = [
11525
+ `Invalid ${spec.name} ${label}: ${invalid2.join(", ")}.`,
11526
+ `Allowed values: ${spec.vocabulary.join(", ")}.`
11527
+ ];
11528
+ const hints = new Set;
11529
+ const suggestions = new Set;
11530
+ for (let i = 0;i < normalized.length; i += 1) {
11531
+ const canonical = normalized[i];
11532
+ if (allowed.has(canonical))
11533
+ continue;
11534
+ const hint = spec.hints?.[canonical.toLowerCase()];
11535
+ if (hint) {
11536
+ hints.add(hint);
11537
+ continue;
11538
+ }
11539
+ for (const match of suggestVocabularyMatches(canonical, spec.vocabulary)) {
11540
+ suggestions.add(match);
11541
+ }
11542
+ }
11543
+ if (suggestions.size > 0)
11544
+ parts.push(`Did you mean ${[...suggestions].join(", ")}?`);
11545
+ for (const hint of hints)
11546
+ parts.push(hint);
11547
+ return { ok: false, message: parts.join(" "), invalid: invalid2 };
11548
+ }
11549
+ function collapseEnumValues(values) {
11550
+ return values.length === 1 ? values[0] : values;
11551
+ }
11552
+
11459
11553
  // src/cli/helpers.ts
11460
11554
  var exports_helpers = {};
11461
11555
  __export(exports_helpers, {
@@ -11466,9 +11560,13 @@ __export(exports_helpers, {
11466
11560
  resolveExplicitProject: () => resolveExplicitProject,
11467
11561
  priorityColors: () => priorityColors,
11468
11562
  printJson: () => printJson,
11563
+ parseEnumFlagList: () => parseEnumFlagList,
11564
+ parseEnumFlag: () => parseEnumFlag,
11565
+ outputRecord: () => outputRecord,
11469
11566
  output: () => output,
11470
11567
  normalizeStatusList: () => normalizeStatusList,
11471
11568
  normalizeStatus: () => normalizeStatus,
11569
+ normalizePriority: () => normalizePriority,
11472
11570
  jsonModeRequested: () => jsonModeRequested,
11473
11571
  handleError: () => handleError,
11474
11572
  getPackageVersion: () => getPackageVersion,
@@ -11476,7 +11574,10 @@ __export(exports_helpers, {
11476
11574
  detectGitRoot: () => detectGitRoot,
11477
11575
  cacheCloudTaskForIdResolution: () => cacheCloudTaskForIdResolution,
11478
11576
  autoProject: () => autoProject,
11479
- autoDetectProject: () => autoDetectProject
11577
+ autoDetectProject: () => autoDetectProject,
11578
+ TASK_STATUS_FLAG_HINTS: () => TASK_STATUS_FLAG_HINTS,
11579
+ TASK_STATUS_FLAG: () => TASK_STATUS_FLAG,
11580
+ TASK_PRIORITY_FLAG: () => TASK_PRIORITY_FLAG
11480
11581
  });
11481
11582
  import chalk from "chalk";
11482
11583
  import { execSync } from "child_process";
@@ -11486,8 +11587,19 @@ import { dirname as dirname3, join as join4, resolve as resolve3, sep } from "pa
11486
11587
  function jsonModeRequested(argv = process.argv) {
11487
11588
  return argv.some((arg) => arg === "--json" || /^-[a-z]+$/i.test(arg) && arg.includes("j"));
11488
11589
  }
11590
+ function remoteErrorDetail(e) {
11591
+ if (!e || typeof e !== "object")
11592
+ return null;
11593
+ const body = e.body;
11594
+ if (!body || typeof body !== "object" || Array.isArray(body))
11595
+ return null;
11596
+ const detail = body.error;
11597
+ return typeof detail === "string" && detail.trim() ? detail.trim() : null;
11598
+ }
11489
11599
  function handleError(e) {
11490
- const message = e instanceof Error ? e.message : String(e);
11600
+ const baseMessage = e instanceof Error ? e.message : String(e);
11601
+ const detail = remoteErrorDetail(e);
11602
+ const message = detail && !baseMessage.includes(detail) ? `${baseMessage}: ${detail}` : baseMessage;
11491
11603
  console.error(chalk.red(message));
11492
11604
  if (jsonModeRequested()) {
11493
11605
  console.log(JSON.stringify({ error: message }));
@@ -11665,7 +11777,8 @@ function autoProject(opts) {
11665
11777
  return autoDetectProject(opts)?.id;
11666
11778
  }
11667
11779
  function normalizeStatus(s) {
11668
- switch (s.toLowerCase().trim()) {
11780
+ const folded = s.toLowerCase().trim();
11781
+ switch (folded) {
11669
11782
  case "done":
11670
11783
  return "completed";
11671
11784
  case "complete":
@@ -11679,14 +11792,33 @@ function normalizeStatus(s) {
11679
11792
  case "canceled":
11680
11793
  return "cancelled";
11681
11794
  default:
11682
- return s;
11795
+ return folded;
11683
11796
  }
11684
11797
  }
11798
+ function normalizePriority(p) {
11799
+ return p.toLowerCase().trim();
11800
+ }
11685
11801
  function normalizeStatusList(statuses) {
11686
11802
  if (Array.isArray(statuses))
11687
11803
  return statuses.map(normalizeStatus);
11688
11804
  return normalizeStatus(statuses);
11689
11805
  }
11806
+ function parseEnumFlag(raw, spec) {
11807
+ if (raw === undefined || raw === null)
11808
+ return;
11809
+ const result = resolveEnumVocabulary(String(raw), spec);
11810
+ if (!result.ok)
11811
+ handleError(new Error(result.message));
11812
+ return collapseEnumValues(result.values);
11813
+ }
11814
+ function parseEnumFlagList(raw, spec) {
11815
+ if (raw === undefined || raw === null)
11816
+ return;
11817
+ const result = resolveEnumVocabulary(String(raw), spec);
11818
+ if (!result.ok)
11819
+ handleError(new Error(result.message));
11820
+ return result.values;
11821
+ }
11690
11822
  function writeStdoutSync(text) {
11691
11823
  const buffer = Buffer.from(text);
11692
11824
  let offset = 0;
@@ -11718,6 +11850,41 @@ function output(data, jsonMode) {
11718
11850
  printJson(data);
11719
11851
  }
11720
11852
  }
11853
+ function formatRecordLines(data) {
11854
+ if (data === null || data === undefined)
11855
+ return [];
11856
+ if (typeof data !== "object")
11857
+ return [String(data)];
11858
+ if (Array.isArray(data)) {
11859
+ return data.flatMap((entry, index) => [
11860
+ chalk.dim(`[${index}]`),
11861
+ ...formatRecordLines(entry).map((line) => ` ${line}`)
11862
+ ]);
11863
+ }
11864
+ const lines = [];
11865
+ for (const [key, value] of Object.entries(data)) {
11866
+ if (value === undefined)
11867
+ continue;
11868
+ const rendered = value === null ? chalk.dim("\u2014") : typeof value === "object" ? JSON.stringify(value) : String(value);
11869
+ lines.push(` ${chalk.dim(`${key}:`)} ${rendered}`);
11870
+ }
11871
+ return lines;
11872
+ }
11873
+ function outputRecord(data, jsonMode, heading) {
11874
+ if (jsonMode) {
11875
+ printJson(data);
11876
+ return;
11877
+ }
11878
+ if (heading)
11879
+ console.log(chalk.bold(heading));
11880
+ const lines = formatRecordLines(data);
11881
+ if (lines.length === 0) {
11882
+ console.log(chalk.dim("(empty)"));
11883
+ return;
11884
+ }
11885
+ for (const line of lines)
11886
+ console.log(line);
11887
+ }
11721
11888
  function formatTaskLine(t) {
11722
11889
  const statusFn = statusColors[t.status] || chalk.white;
11723
11890
  const priorityFn = priorityColors[t.priority] || chalk.white;
@@ -11728,16 +11895,33 @@ function formatTaskLine(t) {
11728
11895
  const plan = t.plan_id ? chalk.magenta(` [plan:${t.plan_id.slice(0, 8)}]`) : "";
11729
11896
  return `${chalk.dim(t.id.slice(0, 8))} ${statusFn(t.status.padEnd(11))} ${priorityFn(t.priority.padEnd(8))} ${t.title}${assigned}${lock}${tags}${plan}`;
11730
11897
  }
11731
- var stdoutRetryBuffer, stdoutRetrySignal, TASK_UUID_RE, statusColors, priorityColors;
11898
+ var stdoutRetryBuffer, stdoutRetrySignal, TASK_UUID_RE, TASK_STATUS_FLAG_HINTS, TASK_STATUS_FLAG, TASK_PRIORITY_FLAG, statusColors, priorityColors;
11732
11899
  var init_helpers = __esm(() => {
11733
11900
  init_cloud_router();
11734
11901
  init_database();
11735
11902
  init_projects();
11736
11903
  init_lock_display();
11737
11904
  init_package_version();
11905
+ init_types();
11738
11906
  stdoutRetryBuffer = new SharedArrayBuffer(4);
11739
11907
  stdoutRetrySignal = new Int32Array(stdoutRetryBuffer);
11740
11908
  TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11909
+ TASK_STATUS_FLAG_HINTS = {
11910
+ all: "To include every status, use -a/--all instead of a --status value.",
11911
+ any: "To include every status, use -a/--all instead of a --status value.",
11912
+ open: "Unstarted and running work is pending and in_progress (the default when no --status is given)."
11913
+ };
11914
+ TASK_STATUS_FLAG = {
11915
+ name: "--status",
11916
+ vocabulary: TASK_STATUSES,
11917
+ normalize: normalizeStatus,
11918
+ hints: TASK_STATUS_FLAG_HINTS
11919
+ };
11920
+ TASK_PRIORITY_FLAG = {
11921
+ name: "--priority",
11922
+ vocabulary: TASK_PRIORITIES,
11923
+ normalize: normalizePriority
11924
+ };
11741
11925
  statusColors = {
11742
11926
  pending: chalk.yellow,
11743
11927
  in_progress: chalk.blue,
@@ -15960,11 +16144,13 @@ function updateTask(id, input, db) {
15960
16144
  }
15961
16145
  sets.push("status = ?");
15962
16146
  params.push(input.status);
16147
+ if (isTerminalStatus(input.status)) {
16148
+ sets.push("locked_by = NULL");
16149
+ sets.push("locked_at = NULL");
16150
+ }
15963
16151
  if (input.status === "completed") {
15964
16152
  sets.push("completed_at = ?");
15965
16153
  params.push(completionTimestamp);
15966
- sets.push("locked_by = NULL");
15967
- sets.push("locked_at = NULL");
15968
16154
  } else if (task.status === "completed" && input.completed_at === undefined) {
15969
16155
  sets.push("completed_at = NULL");
15970
16156
  }
@@ -16088,6 +16274,7 @@ function updateTask(id, input, db) {
16088
16274
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
16089
16275
  const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
16090
16276
  const completedNow = input.status === "completed";
16277
+ const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
16091
16278
  const updatedTask = {
16092
16279
  ...task,
16093
16280
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
@@ -16095,8 +16282,8 @@ function updateTask(id, input, db) {
16095
16282
  metadata: input.metadata ?? task.metadata,
16096
16283
  version: task.version + 1,
16097
16284
  updated_at: timestamp2,
16098
- locked_by: completedNow ? null : task.locked_by,
16099
- locked_at: completedNow ? null : task.locked_at,
16285
+ locked_by: terminalNow ? null : task.locked_by,
16286
+ locked_at: terminalNow ? null : task.locked_at,
16100
16287
  completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
16101
16288
  sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
16102
16289
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
@@ -20214,11 +20401,7 @@ function resolveProjectIdOrSlug(input) {
20214
20401
  function parseStatus(value) {
20215
20402
  if (!value)
20216
20403
  return;
20217
- const normalized = normalizeStatus(value);
20218
- if (!TASK_STATUSES.includes(normalized)) {
20219
- handleError(new Error(`--status must be one of: ${TASK_STATUSES.join(", ")}`));
20220
- }
20221
- return normalized;
20404
+ return parseEnumFlagList(value, { ...TASK_STATUS_FLAG, allowList: false })?.[0];
20222
20405
  }
20223
20406
  function parseIntOption(value, flag) {
20224
20407
  if (value === undefined)
@@ -20243,10 +20426,7 @@ function resolvePlanId(input) {
20243
20426
  function parsePriority(value) {
20244
20427
  if (!value)
20245
20428
  return;
20246
- if (!TASK_PRIORITIES.includes(value)) {
20247
- handleError(new Error("--priority must be one of: low, medium, high, critical"));
20248
- }
20249
- return value;
20429
+ return parseEnumFlagList(value, { ...TASK_PRIORITY_FLAG, allowList: false })?.[0];
20250
20430
  }
20251
20431
  function parseJsonObject3(value, flag) {
20252
20432
  if (!value)
@@ -20631,12 +20811,12 @@ function registerTaskCommands(program2) {
20631
20811
  filter["task_list_id"] = listId;
20632
20812
  }
20633
20813
  if (opts.status) {
20634
- filter["status"] = opts.status.includes(",") ? opts.status.split(",").map((s) => normalizeStatus(s.trim())) : normalizeStatus(opts.status);
20814
+ filter["status"] = parseEnumFlag(opts.status, TASK_STATUS_FLAG);
20635
20815
  } else if (!opts.all) {
20636
20816
  filter["status"] = ["pending", "in_progress"];
20637
20817
  }
20638
20818
  if (opts.priority)
20639
- filter["priority"] = opts.priority;
20819
+ filter["priority"] = parseEnumFlag(opts.priority, TASK_PRIORITY_FLAG);
20640
20820
  if (opts.assigned)
20641
20821
  filter["assigned_to"] = opts.assigned;
20642
20822
  if (opts.createdBy)
@@ -21609,7 +21789,6 @@ var init_task_commands = __esm(() => {
21609
21789
  init_assignee_guard();
21610
21790
  init_helpers();
21611
21791
  init_output_redaction();
21612
- init_types();
21613
21792
  });
21614
21793
 
21615
21794
  // src/lib/plan-artifacts.ts
@@ -23441,7 +23620,8 @@ __export(exports_saved_search_views, {
23441
23620
  normalizeScope: () => normalizeScope,
23442
23621
  listSearchViews: () => listSearchViews,
23443
23622
  getSearchView: () => getSearchView,
23444
- deleteSearchView: () => deleteSearchView
23623
+ deleteSearchView: () => deleteSearchView,
23624
+ SAVED_SEARCH_SCOPES: () => SAVED_SEARCH_SCOPES
23445
23625
  });
23446
23626
  function parseFilters(value) {
23447
23627
  if (!value)
@@ -23461,10 +23641,8 @@ function rowToSavedSearchView(row) {
23461
23641
  };
23462
23642
  }
23463
23643
  function normalizeScope(scope) {
23464
- if (scope === "all" || scope === "tasks" || scope === "projects" || scope === "plans" || scope === "runs" || scope === "comments") {
23465
- return scope;
23466
- }
23467
- return "tasks";
23644
+ const candidate = (scope ?? "").trim().toLowerCase();
23645
+ return SAVED_SEARCH_SCOPES.includes(candidate) ? candidate : "tasks";
23468
23646
  }
23469
23647
  function normalizeName(name) {
23470
23648
  const normalized = name.trim();
@@ -23767,10 +23945,12 @@ function runSearchView(idOrName, db) {
23767
23945
  throw new Error(`Saved search view not found: ${idOrName}`);
23768
23946
  return { ...runSavedSearch(view.filters, view.scope, d), view };
23769
23947
  }
23948
+ var SAVED_SEARCH_SCOPES;
23770
23949
  var init_saved_search_views = __esm(() => {
23771
23950
  init_database();
23772
23951
  init_local_fields();
23773
23952
  init_search();
23953
+ SAVED_SEARCH_SCOPES = ["all", "tasks", "projects", "plans", "runs", "comments"];
23774
23954
  });
23775
23955
 
23776
23956
  // src/lib/claude-tasks.ts
@@ -26340,12 +26520,11 @@ function buildSearchFilters(query, opts, projectId) {
26340
26520
  const customFields = parseJsonObjectOption(opts.fieldCustom, "--field-custom");
26341
26521
  const labels = splitList(opts.fieldLabel);
26342
26522
  const tags = splitList(opts.tag);
26343
- const statuses = splitList(opts.status)?.map(normalizeStatus);
26344
26523
  const filters = {
26345
26524
  query: query || opts.query,
26346
26525
  project_id: opts.allProjects ? undefined : projectId,
26347
- status: statuses,
26348
- priority: splitList(opts.priority),
26526
+ status: parseEnumFlagList(opts.status, TASK_STATUS_FLAG),
26527
+ priority: parseEnumFlagList(opts.priority, TASK_PRIORITY_FLAG),
26349
26528
  assigned_to: opts.assigned,
26350
26529
  agent_id: opts.agentId,
26351
26530
  task_list_id: filterPatch?.task_list_id === undefined ? resolveTaskListFilter(opts.taskList, projectId) : undefined,
@@ -26450,7 +26629,7 @@ function registerProjectCommands(program2) {
26450
26629
  try {
26451
26630
  const cloud = getTodosCloudClient();
26452
26631
  const projectId = opts.allProjects ? undefined : cloud ? undefined : autoProject(globalOpts);
26453
- const scope = normalizeScope(opts.scope);
26632
+ const scope = parseEnumFlag(opts.scope, SEARCH_SCOPE_FLAG) ?? "tasks";
26454
26633
  const searchOpts = buildSearchFilters(query, opts, projectId);
26455
26634
  if (opts.saveAs) {
26456
26635
  const view = saveSearchView({
@@ -26542,7 +26721,7 @@ function registerProjectCommands(program2) {
26542
26721
  const view = saveSearchView({
26543
26722
  name,
26544
26723
  description: opts.description,
26545
- scope: normalizeScope(opts.scope),
26724
+ scope: parseEnumFlag(opts.scope, SEARCH_SCOPE_FLAG) ?? "tasks",
26546
26725
  filters: buildSearchFilters(opts.query, opts, projectId)
26547
26726
  });
26548
26727
  output(view, Boolean(globalOpts.json));
@@ -26555,7 +26734,7 @@ function registerProjectCommands(program2) {
26555
26734
  views.command("list").description("List local saved search views").option("--scope <scope>", "Filter by scope").action((opts) => {
26556
26735
  const globalOpts = program2.opts();
26557
26736
  try {
26558
- const rows = listSearchViews(opts.scope ? normalizeScope(opts.scope) : undefined);
26737
+ const rows = listSearchViews(parseEnumFlag(opts.scope, SEARCH_SCOPE_FLAG));
26559
26738
  output(rows, Boolean(globalOpts.json));
26560
26739
  if (!globalOpts.json) {
26561
26740
  if (rows.length === 0) {
@@ -26751,7 +26930,7 @@ function registerProjectCommands(program2) {
26751
26930
  const cloud = getTodosCloudClient();
26752
26931
  if (opts.show) {
26753
26932
  const project = cloud ? await cloudResolveProject(cloud, opts.show) : resolveExplicitProject(opts.show);
26754
- output(project, Boolean(globalOpts.json));
26933
+ outputRecord(project, Boolean(globalOpts.json), "Project:");
26755
26934
  return;
26756
26935
  }
26757
26936
  if (opts.update) {
@@ -26765,7 +26944,7 @@ function registerProjectCommands(program2) {
26765
26944
  }
26766
26945
  const current = cloud ? await cloudResolveProject(cloud, opts.update) : resolveExplicitProject(opts.update);
26767
26946
  const project = cloud ? await cloudUpdateProject(cloud, current.id, patch) : updateProject(current.id, patch);
26768
- output(project, Boolean(globalOpts.json));
26947
+ outputRecord(project, Boolean(globalOpts.json), "Project updated:");
26769
26948
  return;
26770
26949
  }
26771
26950
  if (opts.deregister) {
@@ -27073,6 +27252,7 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
27073
27252
  program2.command("export").description("Export tasks").option("-f, --format <format>", "Format: json, md, todos.md, or bridge", "json").option("-o, --output <path>", "Write export output to a file").option("--encrypt", "Encrypt bridge exports with a local encryption profile").option("--encryption-profile <name>", "Encryption profile name", "default").option("--allow-plaintext-sensitive", "Deprecated; bridge exports are redacted unless --encrypt is used").action(async (opts) => {
27074
27253
  const { listTasks: listTasks2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
27075
27254
  const globalOpts = program2.opts();
27255
+ opts.format = parseEnumFlag(opts.format, EXPORT_FORMAT_FLAG) ?? "json";
27076
27256
  const projectId = autoProject(globalOpts);
27077
27257
  const writeOutput = async (content) => {
27078
27258
  if (opts.output) {
@@ -27234,7 +27414,7 @@ function resolveTaskListId(partialId) {
27234
27414
  }
27235
27415
  return id;
27236
27416
  }
27237
- var CLOUD_GRAPH_EDGE_CONCURRENCY = 6;
27417
+ var SEARCH_SCOPE_FLAG, EXPORT_FORMATS, EXPORT_FORMAT_FLAG, CLOUD_GRAPH_EDGE_CONCURRENCY = 6;
27238
27418
  var init_project_commands = __esm(() => {
27239
27419
  init_database();
27240
27420
  init_projects();
@@ -27246,12 +27426,26 @@ var init_project_commands = __esm(() => {
27246
27426
  init_config();
27247
27427
  init_helpers();
27248
27428
  init_output_redaction();
27429
+ SEARCH_SCOPE_FLAG = {
27430
+ name: "--scope",
27431
+ vocabulary: SAVED_SEARCH_SCOPES,
27432
+ normalize: (value) => value.toLowerCase().trim(),
27433
+ allowList: false
27434
+ };
27435
+ EXPORT_FORMATS = ["json", "md", "markdown", "todos.md", "todos-md", "bridge"];
27436
+ EXPORT_FORMAT_FLAG = {
27437
+ name: "--format",
27438
+ vocabulary: EXPORT_FORMATS,
27439
+ normalize: (value) => value.toLowerCase().trim(),
27440
+ allowList: false
27441
+ };
27249
27442
  });
27250
27443
 
27251
27444
  // src/cli/commands/agent-commands.ts
27252
27445
  var exports_agent_commands = {};
27253
27446
  __export(exports_agent_commands, {
27254
- registerAgentCommands: () => registerAgentCommands
27447
+ registerAgentCommands: () => registerAgentCommands,
27448
+ findCaseVariantRows: () => findCaseVariantRows
27255
27449
  });
27256
27450
  import chalk6 from "chalk";
27257
27451
  import { execSync as execSync2 } from "child_process";
@@ -27265,6 +27459,15 @@ function resolveCloudAgentByNameOrId(agents, nameOrId) {
27265
27459
  return null;
27266
27460
  return matches.reduce((freshest, candidate) => new Date(candidate.last_seen_at).getTime() > new Date(freshest.last_seen_at).getTime() ? candidate : freshest);
27267
27461
  }
27462
+ function findCaseVariantRows(agents, requested) {
27463
+ const raw = requested.trim();
27464
+ const target = normalizeAgentNameInput(raw);
27465
+ if (!target)
27466
+ return [];
27467
+ if (agents.some((agent) => agent.name === raw))
27468
+ return [];
27469
+ return agents.filter((agent) => normalizeAgentNameInput(agent.name) === target);
27470
+ }
27268
27471
  function clearIdentityIfMine(agentId, agentName) {
27269
27472
  const persisted = readPersistedIdentity();
27270
27473
  if (!persisted)
@@ -27278,7 +27481,22 @@ function registerAgentCommands(program2) {
27278
27481
  const globalOpts = program2.opts();
27279
27482
  try {
27280
27483
  const cloud = getTodosCloudClient();
27281
- const result = cloud ? await cloudRegisterAgent(cloud, { name, description: opts.description }) : (await Promise.resolve().then(() => (init_agents(), exports_agents))).registerAgent({ name, description: opts.description });
27484
+ const registrationName = name.trim();
27485
+ if (cloud) {
27486
+ let roster = null;
27487
+ try {
27488
+ roster = await cloudListAgents(cloud);
27489
+ } catch {
27490
+ console.error(chalk6.yellow("Warning: could not read the shared roster, so the case-variant check was skipped for this registration."));
27491
+ }
27492
+ const variants = roster ? findCaseVariantRows(roster, registrationName) : [];
27493
+ if (variants.length > 0) {
27494
+ const listed = variants.map((a) => `${a.name} (${a.id})`).join(", ");
27495
+ console.error(chalk6.red(`'${name}' is a case variant of an agent that already exists: ${listed}. ` + `Agent names are ONE case-insensitive identity, so registering this spelling would create a second row ` + `and split the identity in two \u2014 tasks assigned to one spelling are invisible to the other. ` + `Register with the existing spelling instead.`));
27496
+ process.exit(1);
27497
+ }
27498
+ }
27499
+ const result = cloud ? await cloudRegisterAgent(cloud, { name: registrationName, description: opts.description }) : (await Promise.resolve().then(() => (init_agents(), exports_agents))).registerAgent({ name: registrationName, description: opts.description });
27282
27500
  const { isAgentConflict: isAgentConflict2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
27283
27501
  if (isAgentConflict2(result)) {
27284
27502
  console.error(chalk6.red("CONFLICT:"), result.message);
@@ -27623,7 +27841,7 @@ ${isOnline ? chalk6.green("\u25CF") : chalk6.dim("\u25CB")} ${chalk6.bold(agent.
27623
27841
  const list2 = cloud ? await cloudGetTaskList(cloud, resolved) : getTaskList(resolved);
27624
27842
  if (!list2)
27625
27843
  throw new Error(`Task list not found: ${ref}`);
27626
- output(list2, Boolean(globalOpts.json));
27844
+ outputRecord(list2, Boolean(globalOpts.json), "Task list:");
27627
27845
  return;
27628
27846
  }
27629
27847
  const patch = {
@@ -27634,7 +27852,7 @@ ${isOnline ? chalk6.green("\u25CF") : chalk6.dim("\u25CB")} ${chalk6.bold(agent.
27634
27852
  if (Object.keys(patch).length === 0)
27635
27853
  throw new Error("lists --update requires --name, --slug, or --description");
27636
27854
  const list = cloud ? await cloudUpdateTaskList(cloud, resolved, patch) : updateTaskList(resolved, patch);
27637
- output(list, Boolean(globalOpts.json));
27855
+ outputRecord(list, Boolean(globalOpts.json), "Task list updated:");
27638
27856
  return;
27639
27857
  }
27640
27858
  if (opts.delete) {
@@ -32185,9 +32403,11 @@ async function updateTask2(id, input, store) {
32185
32403
  throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
32186
32404
  }
32187
32405
  const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
32406
+ const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
32188
32407
  const task = {
32189
32408
  ...existing,
32190
32409
  ...definedPatch(input),
32410
+ ...terminalNow ? { locked_by: null, locked_at: null } : {},
32191
32411
  version: existing.version + 1,
32192
32412
  updated_at: new Date().toISOString(),
32193
32413
  tags: input.tags ?? existing.tags,
@@ -34528,8 +34748,19 @@ function handleStats(_ctx, json2) {
34528
34748
  recurring_tasks: countRecurringTasks()
34529
34749
  });
34530
34750
  }
34751
+ function taskStatusQueryParam(url) {
34752
+ const raw = url.searchParams.get("status");
34753
+ if (!raw)
34754
+ return { ok: true, value: undefined };
34755
+ const result = resolveEnumVocabulary(raw, { name: "status", vocabulary: TASK_STATUSES });
34756
+ if (!result.ok)
34757
+ return { ok: false, message: result.message };
34758
+ return { ok: true, value: collapseEnumValues(result.values) };
34759
+ }
34531
34760
  async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
34532
- const status = url.searchParams.get("status") || undefined;
34761
+ const statusParam = taskStatusQueryParam(url);
34762
+ if (!statusParam.ok)
34763
+ return json2({ error: statusParam.message }, 400);
34533
34764
  const projectId = url.searchParams.get("project_id") || undefined;
34534
34765
  const sessionId = url.searchParams.get("session_id") || undefined;
34535
34766
  const agentId = url.searchParams.get("agent_id") || undefined;
@@ -34537,7 +34768,7 @@ async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
34537
34768
  const offsetParam = url.searchParams.get("offset");
34538
34769
  const fields = parseFieldsParam(url);
34539
34770
  const tasks = listTasks({
34540
- status,
34771
+ status: statusParam.value,
34541
34772
  project_id: projectId,
34542
34773
  session_id: sessionId,
34543
34774
  agent_id: agentId,
@@ -34602,9 +34833,19 @@ async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
34602
34833
  }
34603
34834
  function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
34604
34835
  const format = url.searchParams.get("format") || "json";
34605
- const status = url.searchParams.get("status") || undefined;
34836
+ const statusParam = taskStatusQueryParam(url);
34837
+ if (!statusParam.ok) {
34838
+ return new Response(JSON.stringify({ error: statusParam.message }), {
34839
+ status: 400,
34840
+ headers: { "Content-Type": "application/json" }
34841
+ });
34842
+ }
34606
34843
  const projectId = url.searchParams.get("project_id") || undefined;
34607
- const tasks = listTasks({ status, project_id: projectId, limit: 1e4 });
34844
+ const tasks = listTasks({
34845
+ status: statusParam.value,
34846
+ project_id: projectId,
34847
+ limit: 1e4
34848
+ });
34608
34849
  const summaries = tasks.map((t) => taskToSummary2(t));
34609
34850
  if (format === "csv") {
34610
34851
  const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
@@ -35186,6 +35427,7 @@ var init_routes = __esm(() => {
35186
35427
  init_tasks();
35187
35428
  init_database();
35188
35429
  init_types();
35430
+ init_types();
35189
35431
  init_projects();
35190
35432
  init_agents();
35191
35433
  init_plans();
@@ -37070,8 +37312,38 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
37070
37312
  operationId: "listTasks",
37071
37313
  summary: "List tasks",
37072
37314
  parameters: [
37073
- { name: "status", in: "query", schema: { type: "string" } },
37074
- { name: "priority", in: "query", schema: { type: "string" } },
37315
+ {
37316
+ name: "status",
37317
+ in: "query",
37318
+ description: `Task status, or a comma-separated list of statuses. Allowed values: ${TASK_STATUSES.join(", ")}.`,
37319
+ style: "form",
37320
+ explode: false,
37321
+ schema: {
37322
+ oneOf: [
37323
+ { type: "string", enum: [...TASK_STATUSES] },
37324
+ {
37325
+ type: "array",
37326
+ items: { type: "string", enum: [...TASK_STATUSES] }
37327
+ }
37328
+ ]
37329
+ }
37330
+ },
37331
+ {
37332
+ name: "priority",
37333
+ in: "query",
37334
+ description: `Task priority, or a comma-separated list of priorities. Allowed values: ${TASK_PRIORITIES.join(", ")}.`,
37335
+ style: "form",
37336
+ explode: false,
37337
+ schema: {
37338
+ oneOf: [
37339
+ { type: "string", enum: [...TASK_PRIORITIES] },
37340
+ {
37341
+ type: "array",
37342
+ items: { type: "string", enum: [...TASK_PRIORITIES] }
37343
+ }
37344
+ ]
37345
+ }
37346
+ },
37075
37347
  { name: "project_id", in: "query", schema: { type: "string" } },
37076
37348
  { name: "parent_id", in: "query", schema: { type: "string", nullable: true } },
37077
37349
  { name: "include_subtasks", in: "query", schema: { type: "boolean" } },
@@ -37583,6 +37855,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
37583
37855
  var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
37584
37856
  var init_openapi = __esm(() => {
37585
37857
  init_package_version();
37858
+ init_types();
37586
37859
  taskSchema = {
37587
37860
  type: "object",
37588
37861
  properties: {
@@ -37850,6 +38123,15 @@ function json3(body, status = 200) {
37850
38123
  function error(status, message, extra) {
37851
38124
  return json3({ error: message, ...extra ?? {} }, status);
37852
38125
  }
38126
+ function enumQueryParam(url, name, vocabulary) {
38127
+ const raw = url.searchParams.get(name);
38128
+ if (raw === null || raw === "")
38129
+ return { ok: true, value: undefined };
38130
+ const result = resolveEnumVocabulary(raw, { name, vocabulary });
38131
+ if (!result.ok)
38132
+ return { ok: false, response: error(400, result.message) };
38133
+ return { ok: true, value: collapseEnumValues(result.values) };
38134
+ }
37853
38135
  function validateTaskCompletion(value) {
37854
38136
  if (!value || typeof value !== "object" || Array.isArray(value))
37855
38137
  return { ok: false, message: "completion body must be an object" };
@@ -38270,14 +38552,16 @@ async function handleV1Request(req, url, dependencies = {}) {
38270
38552
  return error(400, "include_subtasks must be true or false");
38271
38553
  }
38272
38554
  const hasParentFilter = url.searchParams.has("parent_id");
38555
+ const statusParam = enumQueryParam(url, "status", TASK_STATUSES);
38556
+ if (!statusParam.ok)
38557
+ return statusParam.response;
38558
+ const priorityParam = enumQueryParam(url, "priority", TASK_PRIORITIES);
38559
+ if (!priorityParam.ok)
38560
+ return priorityParam.response;
38273
38561
  const filter = {
38274
38562
  ...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
38275
- ...url.searchParams.get("status") ? {
38276
- status: url.searchParams.get("status").includes(",") ? url.searchParams.get("status").split(",") : url.searchParams.get("status")
38277
- } : {},
38278
- ...url.searchParams.get("priority") ? {
38279
- priority: url.searchParams.get("priority").includes(",") ? url.searchParams.get("priority").split(",") : url.searchParams.get("priority")
38280
- } : {},
38563
+ ...statusParam.value !== undefined ? { status: statusParam.value } : {},
38564
+ ...priorityParam.value !== undefined ? { priority: priorityParam.value } : {},
38281
38565
  ...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
38282
38566
  ...hasParentFilter ? { parent_id: url.searchParams.get("parent_id") || null, include_subtasks: true } : includeSubtasks !== null ? { include_subtasks: includeSubtasks === "true" } : {},
38283
38567
  ...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {},
@@ -50421,10 +50705,23 @@ function registerTaskAutoTools(server, ctx) {
50421
50705
  return { content: [{ type: "text", text: "No active agents available for rebalancing." }] };
50422
50706
  const activeTasks = listTasks4({ project_id: resolvedProjectId, status: ["pending", "in_progress"], limit: 1000 }, undefined);
50423
50707
  const agentKeyByAlias = new Map;
50708
+ const ambiguousAgentAliases = new Set;
50709
+ const indexAgentAlias = (alias, agentId) => {
50710
+ const normalizedAlias2 = alias.toLowerCase();
50711
+ if (ambiguousAgentAliases.has(normalizedAlias2))
50712
+ return;
50713
+ const existingAgentId = agentKeyByAlias.get(normalizedAlias2);
50714
+ if (existingAgentId !== undefined && existingAgentId !== agentId) {
50715
+ agentKeyByAlias.delete(normalizedAlias2);
50716
+ ambiguousAgentAliases.add(normalizedAlias2);
50717
+ return;
50718
+ }
50719
+ agentKeyByAlias.set(normalizedAlias2, agentId);
50720
+ };
50424
50721
  for (const agent of agents) {
50425
- agentKeyByAlias.set(String(agent.id).toLowerCase(), agent.id);
50722
+ indexAgentAlias(String(agent.id), agent.id);
50426
50723
  if (agent.name)
50427
- agentKeyByAlias.set(String(agent.name).toLowerCase(), agent.id);
50724
+ indexAgentAlias(String(agent.name), agent.id);
50428
50725
  }
50429
50726
  const canonicalAgentKey = (assignedTo) => assignedTo ? agentKeyByAlias.get(assignedTo.toLowerCase()) : undefined;
50430
50727
  const load = new Map(agents.map((agent) => [agent.id, activeTasks.filter((t) => canonicalAgentKey(t.assigned_to) === agent.id).length]));
@@ -61810,9 +62107,16 @@ ${text2}` }] };
61810
62107
  return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
61811
62108
  }
61812
62109
  const oldName = agent.name;
61813
- const updated = updateAgent(agent.id, { name: new_name });
61814
62110
  const db = getDatabase();
61815
- const tasksResult = db.run("UPDATE tasks SET assigned_to = ? WHERE LOWER(assigned_to) = LOWER(?)", [new_name, oldName]);
62111
+ let oldNameUniquelyIdentifiesAgent = false;
62112
+ try {
62113
+ oldNameUniquelyIdentifiesAgent = resolvePartialId(db, "agents", oldName) === agent.id;
62114
+ } catch (error2) {
62115
+ if (!(error2 instanceof IdentityAliasAmbiguousError))
62116
+ throw error2;
62117
+ }
62118
+ const updated = updateAgent(agent.id, { name: new_name });
62119
+ 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]);
61816
62120
  const taskNote = tasksResult.changes > 0 ? `
61817
62121
  Updated assigned_to on ${tasksResult.changes} task(s).` : "";
61818
62122
  return {
@@ -61996,6 +62300,7 @@ var init_agents2 = __esm(() => {
61996
62300
  init_agents();
61997
62301
  init_config();
61998
62302
  init_database();
62303
+ init_types();
61999
62304
  init_cloud_router();
62000
62305
  });
62001
62306
 
@@ -72161,7 +72466,11 @@ function registerDispatchCommands(program2) {
72161
72466
  process.exit(1);
72162
72467
  }
72163
72468
  }
72164
- const statuses = opts.status ? opts.status.split(",").map((s) => s.trim()) : undefined;
72469
+ const statuses = parseEnumFlagList(opts.status, {
72470
+ name: "--status",
72471
+ vocabulary: DISPATCH_STATUSES,
72472
+ normalize: (value) => value.toLowerCase().trim()
72473
+ });
72165
72474
  const dispatches = listDispatches({ status: statuses, limit: opts.limit ?? 20 });
72166
72475
  if (useJson) {
72167
72476
  console.log(JSON.stringify(dispatches, null, 2));
@@ -72194,6 +72503,8 @@ var init_dispatch3 = __esm(() => {
72194
72503
  init_dispatch();
72195
72504
  init_dispatch_formatter();
72196
72505
  init_tmux();
72506
+ init_types();
72507
+ init_helpers();
72197
72508
  });
72198
72509
 
72199
72510
  // src/cli/commands/machines.tsx