@hasna/todos 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -741,6 +741,8 @@ todos update <task-id> --plan <plan-id> # move one task to another plan
741
741
  todos update <task-id> --clear-plan # detach from its plan
742
742
  todos bulk plan --plan <plan-id> <id> <id> # move many tasks at once
743
743
  todos bulk move-plan --clear-plan <id> <id> # detach many tasks at once
744
+ todos bulk tag <id> <id> --tag a,b # add tags to many tasks (merges; never replaces)
745
+ todos bulk untag <id> <id> --tag a # remove tags from many tasks
744
746
  todos plans --show <plan-id> # verify the plan's task set
745
747
  ```
746
748
 
@@ -1 +1 @@
1
- {"version":3,"file":"config-serve-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/config-serve-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWzC,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,QA44C3D"}
1
+ {"version":3,"file":"config-serve-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/config-serve-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWzC,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,QAo5C3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-hooks-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/mcp-hooks-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgPzC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,QAitCxD"}
1
+ {"version":3,"file":"mcp-hooks-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/mcp-hooks-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiPzC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,QAitCxD"}
@@ -1 +1 @@
1
- {"version":3,"file":"task-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/task-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4DzC,oFAAoF;AACpF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAqPD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA2iDpD"}
1
+ {"version":3,"file":"task-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/task-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgEzC,oFAAoF;AACpF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAmUD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAmxDpD"}
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.14.0",
2126
+ version: "0.15.1",
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",
@@ -2444,11 +2444,30 @@ var init_types = __esm(() => {
2444
2444
  });
2445
2445
 
2446
2446
  // src/lib/sync-utils.ts
2447
+ var exports_sync_utils = {};
2448
+ __export(exports_sync_utils, {
2449
+ writeJsonFile: () => writeJsonFile,
2450
+ writeHighWaterMark: () => writeHighWaterMark,
2451
+ withSyncFingerprint: () => withSyncFingerprint,
2452
+ readJsonFile: () => readJsonFile,
2453
+ readHighWaterMark: () => readHighWaterMark,
2454
+ parseTimestamp: () => parseTimestamp,
2455
+ listJsonFiles: () => listJsonFiles,
2456
+ hasSyncFingerprintChanged: () => hasSyncFingerprintChanged,
2457
+ getTodosGlobalDir: () => getTodosGlobalDir,
2458
+ getHomeDir: () => getHomeDir,
2459
+ getFileMtimeMs: () => getFileMtimeMs,
2460
+ ensureDir: () => ensureDir,
2461
+ appendSyncConflict: () => appendSyncConflict,
2462
+ TODO_SYNC_FINGERPRINT_KEY: () => TODO_SYNC_FINGERPRINT_KEY,
2463
+ HOME: () => HOME
2464
+ });
2447
2465
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
2448
2466
  import { createHash } from "crypto";
2467
+ import { homedir } from "os";
2449
2468
  import { join } from "path";
2450
2469
  function getHomeDir() {
2451
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
2470
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
2452
2471
  }
2453
2472
  function getTodosGlobalDir() {
2454
2473
  return join(getHomeDir(), ".hasna", "todos");
@@ -2536,7 +2555,7 @@ function hasSyncFingerprintChanged(record) {
2536
2555
  }
2537
2556
  var TODO_SYNC_FINGERPRINT_KEY = "todos_sync_fingerprint", HOME;
2538
2557
  var init_sync_utils = __esm(() => {
2539
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2558
+ HOME = getHomeDir();
2540
2559
  });
2541
2560
 
2542
2561
  // src/lib/config.ts
@@ -6414,6 +6433,15 @@ function hasOption(args, option) {
6414
6433
  function positionalArgs(args) {
6415
6434
  return args.filter((arg) => !arg.startsWith("-"));
6416
6435
  }
6436
+ function isBundledStaticInvocation(invocation) {
6437
+ const command = invocation.command;
6438
+ if (!command)
6439
+ return false;
6440
+ const storeBackedOptions = BUNDLED_STATIC_STORE_BACKED_OPTIONS.get(command);
6441
+ if (!storeBackedOptions)
6442
+ return false;
6443
+ return !storeBackedOptions.some((option) => hasOption(invocation.commandArgs, option));
6444
+ }
6417
6445
  function isReadOnlyConfigInvocation(invocation) {
6418
6446
  if (invocation.command !== "config")
6419
6447
  return false;
@@ -6434,6 +6462,8 @@ function isMetadataInvocation(args, invocation) {
6434
6462
  return true;
6435
6463
  if (invocation.command === "manual" && invocation.commandArgs.length === 0)
6436
6464
  return true;
6465
+ if (isBundledStaticInvocation(invocation))
6466
+ return true;
6437
6467
  if (invocation.command === "help" && invocation.commandArgs.every((arg) => !arg.startsWith("-")))
6438
6468
  return true;
6439
6469
  if (invocation.command === "config") {
@@ -6451,6 +6481,14 @@ function firstPresentOption(args, candidates) {
6451
6481
  function disqualifyingArgument(invocation) {
6452
6482
  const command = invocation.command;
6453
6483
  const args = invocation.commandArgs;
6484
+ const storeBackedOptions = BUNDLED_STATIC_STORE_BACKED_OPTIONS.get(command);
6485
+ if (storeBackedOptions) {
6486
+ const option = storeBackedOptions.find((candidate) => hasOption(args, candidate));
6487
+ return option ? {
6488
+ blame: option,
6489
+ remedy: `it imports bundled fixtures into the local store, which this route disables; re-run \`${command}\` without it to read the bundled content`
6490
+ } : null;
6491
+ }
6454
6492
  switch (command) {
6455
6493
  case "task":
6456
6494
  return positionalArgs(args)[0] === "upsert" ? null : { blame: "any subcommand other than `upsert`", remedy: "use `todos task upsert`" };
@@ -6478,13 +6516,22 @@ function disqualifyingArgument(invocation) {
6478
6516
  const action = positionalArgs(args)[0];
6479
6517
  if (action === "plan" || action === "move-plan")
6480
6518
  return null;
6519
+ if (action === "tag" || action === "untag") {
6520
+ if (!hasOption(args, "--tag") && !hasOption(args, "--tags")) {
6521
+ return {
6522
+ blame: `\`bulk ${action}\` without --tag`,
6523
+ remedy: `pass --tag <comma-separated>, e.g. bulk ${action} <ids...> --tag directive:k_msd4cz8t_ste6f4`
6524
+ };
6525
+ }
6526
+ return firstPresentOption(args, ["--plan", "--clear-plan"]);
6527
+ }
6481
6528
  if (!action) {
6482
- return { blame: "a missing action", remedy: "pass one of done, complete, start, delete, plan, move-plan" };
6529
+ return { blame: "a missing action", remedy: "pass one of done, complete, start, delete, plan, move-plan, tag, untag" };
6483
6530
  }
6484
6531
  if (!["done", "complete", "start", "delete"].includes(action)) {
6485
6532
  return {
6486
6533
  blame: `the \`${action}\` action`,
6487
- remedy: "use one of done, complete, start, delete, plan, move-plan"
6534
+ remedy: "use one of done, complete, start, delete, plan, move-plan, tag, untag"
6488
6535
  };
6489
6536
  }
6490
6537
  return firstPresentOption(args, ["--plan", "--clear-plan"]);
@@ -6541,7 +6588,8 @@ function assertRemoteCommandSupported(invocation) {
6541
6588
  }
6542
6589
  if (!command || !commandSupportsRemote(invocation)) {
6543
6590
  const blame = command ? disqualifyingArgument(invocation) : null;
6544
- const detail = blame ? `\`${command}\` is served by the Todos /v1 authority but ${blame.blame} is not; ${blame.remedy}` : `${invocationLabel(invocation)} is not supported by the Todos /v1 CLI; local SQLite fallback is disabled`;
6591
+ const servedBy = command && BUNDLED_STATIC_STORE_BACKED_OPTIONS.has(command) ? `\`${command}\` renders bundled content on this route` : `\`${command}\` is served by the Todos /v1 authority`;
6592
+ const detail = blame ? `${servedBy} but ${blame.blame} is not; ${blame.remedy}` : `${invocationLabel(invocation)} is not supported by the Todos /v1 CLI; local SQLite fallback is disabled`;
6545
6593
  throw new Error(`REMOTE_COMMAND_UNSUPPORTED: ${detail}`);
6546
6594
  }
6547
6595
  }
@@ -6561,7 +6609,7 @@ function initializeTodosCliAuthority(args = process.argv.slice(2), env = process
6561
6609
  }
6562
6610
  return { route: "remote-http", v1_base_url: client.baseUrl };
6563
6611
  }
6564
- var REGISTERED_CANONICAL_COMMANDS, TODOS_CLI_COMMAND_ALIASES, DIAGNOSTIC_COMMANDS, REMOTE_COMMANDS, COMMAND_CAPABILITY_MATRIX, GLOBAL_OPTIONS_WITH_VALUES, GLOBAL_FLAGS, HELP_FLAGS, VERSION_FLAGS, dropIt = (blame) => ({ blame, remedy: "re-run without it" });
6612
+ var REGISTERED_CANONICAL_COMMANDS, TODOS_CLI_COMMAND_ALIASES, BUNDLED_STATIC_COMMANDS, BUNDLED_STATIC_STORE_BACKED_OPTIONS, DIAGNOSTIC_COMMANDS, REMOTE_COMMANDS, COMMAND_CAPABILITY_MATRIX, GLOBAL_OPTIONS_WITH_VALUES, GLOBAL_FLAGS, HELP_FLAGS, VERSION_FLAGS, dropIt = (blame) => ({ blame, remedy: "re-run without it" });
6565
6613
  var init_stage_a = __esm(() => {
6566
6614
  init_esm();
6567
6615
  init_cloud_router();
@@ -6751,7 +6799,28 @@ var init_stage_a = __esm(() => {
6751
6799
  reliability: ["scorecards"],
6752
6800
  lists: ["task-lists", "tl"]
6753
6801
  };
6754
- DIAGNOSTIC_COMMANDS = new Set(["help", "manual", "completions", "completion", "config", "storage"]);
6802
+ BUNDLED_STATIC_COMMANDS = {
6803
+ workflows: [],
6804
+ "template-library": [],
6805
+ onboarding: ["--import"],
6806
+ "sdk-fixtures": ["--show", "--write"]
6807
+ };
6808
+ BUNDLED_STATIC_STORE_BACKED_OPTIONS = new Map;
6809
+ for (const [canonical, storeBackedOptions] of Object.entries(BUNDLED_STATIC_COMMANDS)) {
6810
+ BUNDLED_STATIC_STORE_BACKED_OPTIONS.set(canonical, storeBackedOptions);
6811
+ const aliases = TODOS_CLI_COMMAND_ALIASES[canonical] ?? [];
6812
+ for (const alias of aliases)
6813
+ BUNDLED_STATIC_STORE_BACKED_OPTIONS.set(alias, storeBackedOptions);
6814
+ }
6815
+ DIAGNOSTIC_COMMANDS = new Set([
6816
+ "help",
6817
+ "manual",
6818
+ "completions",
6819
+ "completion",
6820
+ "config",
6821
+ "storage",
6822
+ ...Object.keys(BUNDLED_STATIC_COMMANDS)
6823
+ ]);
6755
6824
  REMOTE_COMMANDS = new Set([
6756
6825
  "active",
6757
6826
  "add",
@@ -10773,8 +10842,7 @@ function findGitRoot(startDir) {
10773
10842
  return null;
10774
10843
  }
10775
10844
  function getGlobalDbPath() {
10776
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
10777
- return join3(home, ".hasna", "todos", "todos.db");
10845
+ return join3(getHomeDir(), ".hasna", "todos", "todos.db");
10778
10846
  }
10779
10847
  function hasExplicitProjectArg(args = process.argv.slice(2)) {
10780
10848
  return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
@@ -10996,6 +11064,7 @@ var init_database = __esm(() => {
10996
11064
  init_machines();
10997
11065
  init_identity_mapping();
10998
11066
  init_types();
11067
+ init_sync_utils();
10999
11068
  ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
11000
11069
  });
11001
11070
 
@@ -11502,14 +11571,21 @@ function suggestVocabularyMatches(value, vocabulary, limit = 3) {
11502
11571
  }
11503
11572
  function resolveEnumVocabulary(raw, spec) {
11504
11573
  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) {
11574
+ const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim());
11575
+ if (rawElements.every((element) => element.length === 0)) {
11507
11576
  return {
11508
11577
  ok: false,
11509
11578
  message: `${spec.name} requires a value. Allowed values: ${spec.vocabulary.join(", ")}.`,
11510
11579
  invalid: []
11511
11580
  };
11512
11581
  }
11582
+ if (rawElements.some((element) => element.length === 0)) {
11583
+ return {
11584
+ ok: false,
11585
+ message: `${spec.name} has an empty value in "${raw}" \u2014 remove the stray comma. ` + `Allowed values: ${spec.vocabulary.join(", ")}.`,
11586
+ invalid: []
11587
+ };
11588
+ }
11513
11589
  const normalized = rawElements.map((element) => spec.normalize ? spec.normalize(element) : element);
11514
11590
  const allowed = new Set(spec.vocabulary);
11515
11591
  const invalid2 = [];
@@ -12707,7 +12783,7 @@ var init_event_hooks = __esm(() => {
12707
12783
  // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
12708
12784
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
12709
12785
  import { existsSync as existsSync6 } from "fs";
12710
- import { homedir } from "os";
12786
+ import { homedir as homedir2 } from "os";
12711
12787
  import { join as join5 } from "path";
12712
12788
  import { createHmac, timingSafeEqual } from "crypto";
12713
12789
  import { randomUUID as randomUUID2 } from "crypto";
@@ -12809,7 +12885,7 @@ function channelMatchesEvent(channel, event) {
12809
12885
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
12810
12886
  }
12811
12887
  function getEventsDataDir(override) {
12812
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir(), ".hasna", "events");
12888
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir2(), ".hasna", "events");
12813
12889
  }
12814
12890
 
12815
12891
  class JsonEventsStore {
@@ -19241,6 +19317,49 @@ var init_tasks = __esm(() => {
19241
19317
  init_calendar();
19242
19318
  });
19243
19319
 
19320
+ // src/lib/bulk-tags.ts
19321
+ function parseTagList(raw) {
19322
+ if (!raw)
19323
+ return [];
19324
+ const seen = new Set;
19325
+ const tags = [];
19326
+ for (const segment of raw.split(",")) {
19327
+ const tag = segment.trim();
19328
+ if (!tag || seen.has(tag))
19329
+ continue;
19330
+ seen.add(tag);
19331
+ tags.push(tag);
19332
+ }
19333
+ return tags;
19334
+ }
19335
+ function resolveTagArgument(tag, tags) {
19336
+ if (tag === undefined)
19337
+ return { ok: true, raw: tags };
19338
+ if (tags === undefined)
19339
+ return { ok: true, raw: tag };
19340
+ const left = parseTagList(tag);
19341
+ const right = parseTagList(tags);
19342
+ const same = left.length === right.length && left.every((entry) => right.includes(entry));
19343
+ if (!same)
19344
+ return { ok: false, conflict: { tag, tags } };
19345
+ return { ok: true, raw: tag };
19346
+ }
19347
+ function resolveBulkTags(current, action, tags) {
19348
+ const existing = Array.isArray(current) ? current.filter((tag) => typeof tag === "string") : [];
19349
+ if (tags.length === 0)
19350
+ return { tags: [...existing], changed: false };
19351
+ if (action === "tag") {
19352
+ const present = new Set(existing);
19353
+ const additions = tags.filter((tag) => !present.has(tag));
19354
+ if (additions.length === 0)
19355
+ return { tags: [...existing], changed: false };
19356
+ return { tags: [...existing, ...additions], changed: true };
19357
+ }
19358
+ const removing = new Set(tags);
19359
+ const remaining = existing.filter((tag) => !removing.has(tag));
19360
+ return { tags: remaining, changed: remaining.length !== existing.length };
19361
+ }
19362
+
19244
19363
  // src/cli/claim-guard.ts
19245
19364
  function resolveClaimIdentity(verb, explicit) {
19246
19365
  const resolved = resolveWritableIdentity(explicit);
@@ -19261,10 +19380,10 @@ function normalizeAgentNameInput(name) {
19261
19380
 
19262
19381
  // src/lib/assignee-validation.ts
19263
19382
  import { readFileSync as readFileSync4 } from "fs";
19264
- import { homedir as homedir2 } from "os";
19383
+ import { homedir as homedir3 } from "os";
19265
19384
  import { join as join8 } from "path";
19266
19385
  function defaultSeatRosterPath() {
19267
- return process.env["TODOS_SEAT_ROSTER_PATH"] || join8(homedir2(), ".hasna", "identities", "hasna-seats.roster.json");
19386
+ return process.env["TODOS_SEAT_ROSTER_PATH"] || join8(homedir3(), ".hasna", "identities", "hasna-seats.roster.json");
19268
19387
  }
19269
19388
  function loadSeatSlugs(path = defaultSeatRosterPath()) {
19270
19389
  try {
@@ -20398,11 +20517,31 @@ function resolveProjectIdOrSlug(input) {
20398
20517
  return row.id;
20399
20518
  handleError(new Error(`Project not found: ${input}`));
20400
20519
  }
20520
+ function projectRefFromOpts(opts, globalOpts) {
20521
+ const own = typeof opts.project === "string" ? opts.project : undefined;
20522
+ const global = typeof globalOpts.project === "string" ? globalOpts.project : undefined;
20523
+ return own || global;
20524
+ }
20525
+ function projectOptOut(opts) {
20526
+ return opts.project === false;
20527
+ }
20528
+ function warnMissingProject(resolvedProjectId, optedOut) {
20529
+ if (resolvedProjectId || optedOut)
20530
+ return;
20531
+ console.error(chalk3.yellow("Warning: task filed with no project \u2014 it will not appear in any project list, per-seat report, or drain. " + "Pass --project <id-or-slug>, or --no-project if filing it globally is deliberate."));
20532
+ }
20401
20533
  function parseStatus(value) {
20402
20534
  if (!value)
20403
20535
  return;
20404
20536
  return parseEnumFlagList(value, { ...TASK_STATUS_FLAG, allowList: false })?.[0];
20405
20537
  }
20538
+ function listScanLimit() {
20539
+ const raw = process.env["TODOS_LIST_SCAN_LIMIT"];
20540
+ if (raw === undefined || raw.trim() === "")
20541
+ return DEFAULT_LIST_SCAN_LIMIT;
20542
+ const parsed = Number.parseInt(raw, 10);
20543
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_LIST_SCAN_LIMIT;
20544
+ }
20406
20545
  function parseIntOption(value, flag) {
20407
20546
  if (value === undefined)
20408
20547
  return;
@@ -20540,7 +20679,7 @@ function computeLocalReparent(current, opts) {
20540
20679
  return patch;
20541
20680
  }
20542
20681
  function registerTaskCommands(program2) {
20543
- program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--approval", "Require approval before completion").option("--recurrence <rule>", "Recurrence rule, e.g. 'every day', 'every weekday', 'every 2 weeks'").option("--due <date>", "Due date (ISO string or YYYY-MM-DD)").option("--reason <text>", "Why this task exists").option("--project <id>", "Assign to project by ID or slug (overrides auto-detect)").option("--unassigned", "Deliberately file this task with no assignee").option("--assign-seat", "Allow --assign to name a durable seat (a seat queue has no session watching it)").option("--created-by <agent>", "Record a different filer than the resolved agent identity").action(async (title, opts) => {
20682
+ program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--approval", "Require approval before completion").option("--recurrence <rule>", "Recurrence rule, e.g. 'every day', 'every weekday', 'every 2 weeks'").option("--due <date>", "Due date (ISO string or YYYY-MM-DD)").option("--reason <text>", "Why this task exists").option("--project <id>", "Assign to project by ID or slug (overrides auto-detect)").option("--no-project", "Deliberately file this task with no project (silences the orphan warning)").option("--unassigned", "Deliberately file this task with no assignee").option("--assign-seat", "Allow --assign to name a durable seat (a seat queue has no session watching it)").option("--created-by <agent>", "Record a different filer than the resolved agent identity").action(async (title, opts) => {
20544
20683
  const globalOpts = program2.opts();
20545
20684
  opts.tags = opts.tags || opts.tag;
20546
20685
  opts.list = opts.list || opts.taskList;
@@ -20555,8 +20694,9 @@ function registerTaskCommands(program2) {
20555
20694
  if (cloud) {
20556
20695
  let task3;
20557
20696
  try {
20558
- const cloudProjectRef = opts.project || globalOpts.project;
20697
+ const cloudProjectRef = projectRefFromOpts(opts, globalOpts);
20559
20698
  const cloudProjectId = cloudProjectRef ? await cloudResolveProjectRef(cloud, cloudProjectRef) : undefined;
20699
+ warnMissingProject(cloudProjectId, projectOptOut(opts));
20560
20700
  const cloudTaskListId = opts.list ? await cloudResolveTaskListRef(cloud, opts.list, cloudProjectId) : undefined;
20561
20701
  if (opts.list && !cloudTaskListId) {
20562
20702
  throw new Error(`Could not resolve task list ID or slug: ${opts.list}`);
@@ -20579,6 +20719,7 @@ function registerTaskCommands(program2) {
20579
20719
  created_by: creator.agent_id || undefined,
20580
20720
  session_id: globalOpts.session,
20581
20721
  project_id: cloudProjectId,
20722
+ working_dir: process.cwd(),
20582
20723
  estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
20583
20724
  sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
20584
20725
  requires_approval: opts.approval || undefined,
@@ -20597,8 +20738,9 @@ function registerTaskCommands(program2) {
20597
20738
  }
20598
20739
  return;
20599
20740
  }
20600
- const explicitProject = opts.project || globalOpts.project;
20601
- const projectId = explicitProject ? resolveProjectIdOrSlug(explicitProject) : autoProject(globalOpts);
20741
+ const explicitProject = projectRefFromOpts(opts, globalOpts);
20742
+ const projectId = explicitProject ? resolveProjectIdOrSlug(explicitProject) : projectOptOut(opts) ? undefined : autoProject(globalOpts);
20743
+ warnMissingProject(projectId, projectOptOut(opts));
20602
20744
  opts.tags = opts.tags || opts.tag;
20603
20745
  opts.list = opts.list || opts.taskList;
20604
20746
  const taskListId = opts.list ? (() => {
@@ -20810,15 +20952,19 @@ function registerTaskCommands(program2) {
20810
20952
  }
20811
20953
  filter["task_list_id"] = listId;
20812
20954
  }
20813
- if (opts.status) {
20955
+ if (opts.status !== undefined) {
20814
20956
  filter["status"] = parseEnumFlag(opts.status, TASK_STATUS_FLAG);
20815
20957
  } else if (!opts.all) {
20816
20958
  filter["status"] = ["pending", "in_progress"];
20817
20959
  }
20818
- if (opts.priority)
20960
+ if (opts.priority !== undefined)
20819
20961
  filter["priority"] = parseEnumFlag(opts.priority, TASK_PRIORITY_FLAG);
20820
- if (opts.assigned)
20821
- filter["assigned_to"] = opts.assigned;
20962
+ let assignedFilter;
20963
+ let assignedWasTyped = false;
20964
+ if (opts.assigned) {
20965
+ assignedFilter = opts.assigned;
20966
+ assignedWasTyped = true;
20967
+ }
20822
20968
  if (opts.createdBy)
20823
20969
  filter["created_by"] = canonicalAgentRef(opts.createdBy);
20824
20970
  if (opts.notCreatedBy)
@@ -20829,7 +20975,8 @@ function registerTaskCommands(program2) {
20829
20975
  console.error(chalk3.red("--inbox needs an agent identity. Run `todos init <name>` or pass --agent <id>."));
20830
20976
  process.exit(1);
20831
20977
  }
20832
- filter["assigned_to"] = me.agent_id;
20978
+ assignedFilter = me.agent_id;
20979
+ assignedWasTyped = false;
20833
20980
  filter["not_created_by"] = me.agent_id;
20834
20981
  }
20835
20982
  if (opts.tags)
@@ -20845,8 +20992,11 @@ function registerTaskCommands(program2) {
20845
20992
  }
20846
20993
  }
20847
20994
  if (opts.agentName) {
20848
- filter["assigned_to"] = opts.agentName;
20995
+ assignedFilter = opts.agentName;
20996
+ assignedWasTyped = true;
20849
20997
  }
20998
+ if (assignedFilter !== undefined)
20999
+ filter["assigned_to"] = assignedFilter;
20850
21000
  if (opts.recurring)
20851
21001
  filter["has_recurrence"] = true;
20852
21002
  if (opts.limit !== undefined) {
@@ -20858,11 +21008,23 @@ function registerTaskCommands(program2) {
20858
21008
  }
20859
21009
  const creatorFilterActive = Boolean(filter["created_by"] || filter["not_created_by"]);
20860
21010
  const requestedLimit = filter["limit"];
20861
- const serverFilter = creatorFilterActive && cloud && requestedLimit !== undefined ? (() => {
20862
- const { limit: _dropped, ...rest } = filter;
20863
- return rest;
20864
- })() : filter;
21011
+ const reordersAfterQuery = Boolean(opts.sort);
21012
+ const narrowsAfterQuery = Boolean(opts.dueToday) || Boolean(opts.overdue) || creatorFilterActive && cloud;
21013
+ const withholdLimit = requestedLimit !== undefined && (reordersAfterQuery || narrowsAfterQuery);
21014
+ const scanCeiling = cloud && (withholdLimit || requestedLimit === undefined) ? Math.max(requestedLimit ?? 0, listScanLimit()) : undefined;
21015
+ const serverFilter = (() => {
21016
+ const base = withholdLimit ? (() => {
21017
+ const { limit: _dropped, ...rest } = filter;
21018
+ return rest;
21019
+ })() : filter;
21020
+ return scanCeiling === undefined ? base : { ...base, limit: scanCeiling };
21021
+ })();
20865
21022
  let tasks = cloud ? await cloudListTasks(cloud, serverFilter) : listTasks(serverFilter);
21023
+ if (scanCeiling !== undefined && tasks.length >= scanCeiling) {
21024
+ console.error(chalk3.yellow(`Warning: this query reached its ${scanCeiling}-row scan limit, so the rows below were
21025
+ ` + ` selected from a truncated set and may not be the true result.
21026
+ ` + ` Narrow it (--project, --status, --assigned) or raise TODOS_LIST_SCAN_LIMIT.`));
21027
+ }
20866
21028
  if (cloud && creatorFilterActive) {
20867
21029
  if (tasks.length > 0 && tasks.every((t) => !("created_by" in t))) {
20868
21030
  console.error(chalk3.yellow(`Warning: this server does not record task authorship, so the creator filter matched nothing to exclude.
@@ -20879,8 +21041,6 @@ function registerTaskCommands(program2) {
20879
21041
  return false;
20880
21042
  return true;
20881
21043
  });
20882
- if (requestedLimit !== undefined)
20883
- tasks = tasks.slice(0, requestedLimit);
20884
21044
  }
20885
21045
  if (opts.dueToday) {
20886
21046
  const todayEnd = new Date;
@@ -20905,6 +21065,21 @@ function registerTaskCommands(program2) {
20905
21065
  return 0;
20906
21066
  });
20907
21067
  }
21068
+ if (withholdLimit && requestedLimit !== undefined)
21069
+ tasks = tasks.slice(0, requestedLimit);
21070
+ if (assignedWasTyped && assignedFilter && tasks.length === 0) {
21071
+ try {
21072
+ const roster = await loadAssigneeContext(() => cloud ? cloudListAgents(cloud) : listAgents(), true);
21073
+ if (!roster.degraded) {
21074
+ const target = canonicalAgentRef(assignedFilter);
21075
+ const known = roster.agents.some((a) => canonicalAgentRef(a.name) === target || canonicalAgentRef(a.id) === target);
21076
+ if (!known) {
21077
+ console.error(chalk3.yellow(`Warning: no agent named '${assignedFilter}' is registered, so this empty result may be a
21078
+ ` + ` mistyped name rather than an empty queue. Check with 'todos agents'.`));
21079
+ }
21080
+ }
21081
+ } catch {}
21082
+ }
20908
21083
  const fmt = opts.format || (globalOpts.json ? "json" : "table");
20909
21084
  const outputTasks = redactBroadTasks(tasks);
20910
21085
  if (fmt === "json") {
@@ -21676,17 +21851,26 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21676
21851
  handleError(new Error("Task not found."));
21677
21852
  }
21678
21853
  });
21679
- program2.command("bulk <action> <ids...>").description("Bulk operation on multiple tasks (done, start, delete, plan/move-plan)").option("--plan <id>", "Plan ID for the plan/move-plan action").option("--clear-plan", "Remove plan assignment for the plan/move-plan action").action(async (action, ids, opts) => {
21854
+ program2.command("bulk <action> <ids...>").description("Bulk operation on multiple tasks (done, start, delete, plan/move-plan, tag, untag)").option("--plan <id>", "Plan ID for the plan/move-plan action").option("--clear-plan", "Remove plan assignment for the plan/move-plan action").option("--tag <tags>", "Comma-separated tags for the tag/untag action").option("--tags <tags>", "Comma-separated tags for the tag/untag action (alias for --tag)").action(async (action, ids, opts) => {
21680
21855
  const globalOpts = program2.opts();
21681
21856
  const results = [];
21682
21857
  const cloud = getTodosCloudClient();
21683
21858
  const isPlanAction = action === "plan" || action === "move-plan";
21859
+ const isTagAction = action === "tag" || action === "untag";
21684
21860
  if (isPlanAction && Boolean(opts.plan) === Boolean(opts.clearPlan)) {
21685
21861
  handleError(new Error("Use exactly one of --plan or --clear-plan with bulk plan."));
21686
21862
  }
21687
- const knownActions = new Set(["done", "complete", "start", "delete", "plan", "move-plan"]);
21863
+ const knownActions = new Set(["done", "complete", "start", "delete", "plan", "move-plan", "tag", "untag"]);
21688
21864
  if (!knownActions.has(action)) {
21689
- handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan)`));
21865
+ handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan), tag, untag`));
21866
+ }
21867
+ const tagArgument = resolveTagArgument(opts.tag, opts.tags);
21868
+ if (isTagAction && !tagArgument.ok) {
21869
+ handleError(new Error(`--tag and --tags name different tag sets (--tag "${tagArgument.conflict.tag}" vs --tags "${tagArgument.conflict.tags}"). ` + "They are aliases; pass one, or pass the same set to both."));
21870
+ }
21871
+ const bulkTags = isTagAction && tagArgument.ok ? parseTagList(tagArgument.raw) : [];
21872
+ if (isTagAction && bulkTags.length === 0) {
21873
+ handleError(new Error(`bulk ${action} needs --tag. Example: todos bulk ${action} <ids...> --tag directive:k_msd4cz8t_ste6f4`));
21690
21874
  }
21691
21875
  const bulkClaimAgentId = action === "start" ? resolveClaimIdentity("start", globalOpts.agent) : undefined;
21692
21876
  if (cloud) {
@@ -21715,6 +21899,14 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21715
21899
  await cloudTaskAction(cloud, resolvedId, "start", { agent_id: bulkClaimAgentId });
21716
21900
  } else if (action === "delete") {
21717
21901
  await cloudDeleteTask(cloud, resolvedId);
21902
+ } else if (isTagAction) {
21903
+ const current = await cloudGetTask(cloud, resolvedId);
21904
+ if (!current)
21905
+ throw new Error(`Task not found: ${rawId}`);
21906
+ const resolution = resolveBulkTags(current.tags, action, bulkTags);
21907
+ if (resolution.changed) {
21908
+ await cloudUpdateTask(cloud, resolvedId, { version: current.version, tags: resolution.tags });
21909
+ }
21718
21910
  } else {
21719
21911
  const current = await cloudGetTask(cloud, resolvedId);
21720
21912
  if (!current)
@@ -21751,6 +21943,16 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21751
21943
  } else if (action === "delete") {
21752
21944
  deleteTask(resolvedId);
21753
21945
  results.push({ id: resolvedId, success: true });
21946
+ } else if (isTagAction) {
21947
+ const current = getTask(resolvedId);
21948
+ if (!current) {
21949
+ throw new Error(`Task not found: ${rawId}`);
21950
+ }
21951
+ const resolution = resolveBulkTags(current.tags, action, bulkTags);
21952
+ if (resolution.changed) {
21953
+ updateTask(resolvedId, { version: current.version, tags: resolution.tags });
21954
+ }
21955
+ results.push({ id: resolvedId, success: true });
21754
21956
  } else if (isPlanAction) {
21755
21957
  const current = getTask(resolvedId);
21756
21958
  if (!current) {
@@ -21759,7 +21961,7 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21759
21961
  updateTask(resolvedId, { version: current.version, plan_id: planId });
21760
21962
  results.push({ id: resolvedId, success: true });
21761
21963
  } else {
21762
- handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan)`));
21964
+ handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan), tag, untag`));
21763
21965
  }
21764
21966
  } catch (e) {
21765
21967
  results.push({ id: rawId, success: false, error: e instanceof Error ? e.message : String(e) });
@@ -21777,6 +21979,7 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21777
21979
  }
21778
21980
  });
21779
21981
  }
21982
+ var DEFAULT_LIST_SCAN_LIMIT = 1e4;
21780
21983
  var init_task_commands = __esm(() => {
21781
21984
  init_database();
21782
21985
  init_projects();
@@ -21787,6 +21990,8 @@ var init_task_commands = __esm(() => {
21787
21990
  init_lock_display();
21788
21991
  init_claim_guard();
21789
21992
  init_assignee_guard();
21993
+ init_assignee_context();
21994
+ init_agents();
21790
21995
  init_helpers();
21791
21996
  init_output_redaction();
21792
21997
  });
@@ -39195,7 +39400,13 @@ async function handleV1Request(req, url, dependencies = {}) {
39195
39400
  return error(405, `method ${method} not allowed on /v1/refs/:ref`);
39196
39401
  if (!store.gitRefs)
39197
39402
  return error(501, "git ref links are not supported by this storage backend");
39198
- const refs = await store.gitRefs.find(id);
39403
+ let decodedRef;
39404
+ try {
39405
+ decodedRef = decodeURIComponent(id);
39406
+ } catch {
39407
+ return error(400, "ref path segment has invalid percent encoding");
39408
+ }
39409
+ const refs = await store.gitRefs.find(decodedRef);
39199
39410
  return json3({ refs, count: refs.length });
39200
39411
  }
39201
39412
  if (resource === "next" && !id) {
@@ -43793,7 +44004,7 @@ async function validateMcpAssignee(value, allowSeat) {
43793
44004
  return verdict.assignee;
43794
44005
  }
43795
44006
  function registerTaskCrudTools(server, ctx) {
43796
- const { shouldRegisterTool, resolveId, formatError, formatTask } = ctx;
44007
+ const { shouldRegisterTool, resolveId, formatError, formatTask, applyFocus } = ctx;
43797
44008
  function mutationTaskResponse(task) {
43798
44009
  const compact = compactTask(task, 240);
43799
44010
  compact["version"] = task.version;
@@ -43869,6 +44080,7 @@ function registerTaskCrudTools(server, ctx) {
43869
44080
  payload.max_retries = retry_count;
43870
44081
  if (deadline)
43871
44082
  payload.due_at = deadline;
44083
+ applyFocus(payload, router.agent_id || undefined);
43872
44084
  const created = await cloudCreateTask(cloud, payload);
43873
44085
  return { content: [{ type: "text", text: mutationTaskResponse(created) }] };
43874
44086
  }
@@ -43895,6 +44107,7 @@ function registerTaskCrudTools(server, ctx) {
43895
44107
  resolved.max_retries = retry_count;
43896
44108
  if (deadline)
43897
44109
  resolved.due_at = deadline;
44110
+ applyFocus(resolved, router.agent_id || undefined);
43898
44111
  const task = createTask(resolved);
43899
44112
  return { content: [{ type: "text", text: mutationTaskResponse(task) }] };
43900
44113
  } catch (e) {
@@ -63133,7 +63346,8 @@ function buildServer() {
63133
63346
  formatError,
63134
63347
  formatTask,
63135
63348
  formatTaskDetail,
63136
- getAgentFocus
63349
+ getAgentFocus,
63350
+ applyFocus
63137
63351
  };
63138
63352
  registerTaskCrudTools(server, toolContext);
63139
63353
  registerTaskProjectTools(server, toolContext);
@@ -66580,7 +66794,7 @@ ${error2.message}
66580
66794
  const globalOpts = program2.opts();
66581
66795
  const projectId = autoProject(globalOpts);
66582
66796
  const interval = parseInt(opts.interval, 10) * 1000;
66583
- const statusFilter = opts.status ? opts.status.split(",").map((s) => normalizeStatus(s.trim())) : ["pending", "in_progress"];
66797
+ const statusFilter = parseEnumFlagList(opts.status, TASK_STATUS_FLAG) ?? ["pending", "in_progress"];
66584
66798
  function render2() {
66585
66799
  const tasks = listTasks({ project_id: projectId, status: statusFilter });
66586
66800
  const all = listTasks({ project_id: projectId });
@@ -69187,8 +69401,8 @@ Findings`));
69187
69401
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
69188
69402
  const { statSync: statSync12 } = await import("fs");
69189
69403
  const { join: join27 } = await import("path");
69190
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
69191
- const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join27(home, ".hasna", "todos", "todos.db");
69404
+ const { getHomeDir: getHomeDir2 } = await Promise.resolve().then(() => (init_sync_utils(), exports_sync_utils));
69405
+ const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join27(getHomeDir2(), ".hasna", "todos", "todos.db");
69192
69406
  let size = "unknown";
69193
69407
  try {
69194
69408
  size = `${(statSync12(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
@@ -72353,7 +72567,8 @@ var init_mcp_hooks_commands = __esm(() => {
72353
72567
  init_tasks();
72354
72568
  init_helpers();
72355
72569
  init_cloud_router();
72356
- HOME2 = process.env["HOME"] || process.env["USERPROFILE"] || "~";
72570
+ init_sync_utils();
72571
+ HOME2 = getHomeDir();
72357
72572
  });
72358
72573
 
72359
72574
  // src/cli/commands/dispatch.tsx
@@ -1 +1 @@
1
- {"version":3,"file":"stage-a.d.ts","sourceRoot":"","sources":["../../src/cli/stage-a.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAQ,MAAM,WAAW,CAAC;AAO1C,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE9C,MAAM,MAAM,+BAA+B,GACvC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,IAAI,CAAA;CAAE,GACrC;IAAE,KAAK,EAAE,mBAAmB,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC1D;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,aAAa,GAAG,YAAY,CAAC;AAyB/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;CA8BgB,CAAC;AAsBvD,wBAAgB,kCAAkC,IAAI,WAAW,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAE9F;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,+BAA+B,CAAC,OAAO,CAAC,GAC9C,OAAO,CAKT;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,+BAA+B,CAAC,OAAO,CAAC,GAAG,IAAI,CASnH;AAkTD;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,GAAE,MAAM,EAA0B,EACtC,GAAG,GAAE,GAAwB,GAC5B,+BAA+B,CAgBjC"}
1
+ {"version":3,"file":"stage-a.d.ts","sourceRoot":"","sources":["../../src/cli/stage-a.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAQ,MAAM,WAAW,CAAC;AAO1C,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE9C,MAAM,MAAM,+BAA+B,GACvC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,IAAI,CAAA;CAAE,GACrC;IAAE,KAAK,EAAE,mBAAmB,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC1D;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,aAAa,GAAG,YAAY,CAAC;AAyB/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;CA8BgB,CAAC;AAoEvD,wBAAgB,kCAAkC,IAAI,WAAW,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAE9F;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,+BAA+B,CAAC,OAAO,CAAC,GAC9C,OAAO,CAKT;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,+BAA+B,CAAC,OAAO,CAAC,GAAG,IAAI,CASnH;AAuWD;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,GAAE,MAAM,EAA0B,EACtC,GAAG,GAAE,GAAwB,GAC5B,+BAA+B,CAgBjC"}