donebear 0.3.9 → 0.4.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/dist/cli.mjs CHANGED
@@ -14,6 +14,7 @@ import { accessSync, constants, existsSync, readdirSync, statSync } from "node:f
14
14
  import { config } from "dotenv";
15
15
  import { Command, InvalidArgumentError } from "commander";
16
16
  import { createHash, randomBytes, randomUUID } from "node:crypto";
17
+ import { DEFAULT_RANK_GAP } from "@donebear/rank-logic";
17
18
  import { createServer } from "node:http";
18
19
  import { fileURLToPath } from "node:url";
19
20
  import Database from "better-sqlite3";
@@ -308,17 +309,17 @@ const refreshAccessToken = (config, refreshToken) => {
308
309
  };
309
310
  //#endregion
310
311
  //#region src/storage.ts
311
- const isRecord = (value) => typeof value === "object" && value !== null;
312
+ const isRecord$1 = (value) => typeof value === "object" && value !== null;
312
313
  const isOAuthProvider = (value) => value === "google" || value === "github" || value === "oauth2";
313
314
  const parseStoredAuthSession = (value) => {
314
- if (!isRecord(value)) return null;
315
+ if (!isRecord$1(value)) return null;
315
316
  if (typeof value.accessToken !== "string") return null;
316
317
  if (value.refreshToken !== null && typeof value.refreshToken !== "string") return null;
317
318
  if (value.tokenType !== null && typeof value.tokenType !== "string") return null;
318
319
  if (value.expiresAt !== null && typeof value.expiresAt !== "string") return null;
319
320
  if (!isOAuthProvider(value.provider)) return null;
320
321
  const { user } = value;
321
- if (user !== null && (!isRecord(user) || typeof user.id !== "string" || user.email !== null && typeof user.email !== "string")) return null;
322
+ if (user !== null && (!isRecord$1(user) || typeof user.id !== "string" || user.email !== null && typeof user.email !== "string")) return null;
322
323
  if (typeof value.createdAt !== "string") return null;
323
324
  const parsedUser = user === null ? null : {
324
325
  email: user.email ?? null,
@@ -336,7 +337,7 @@ const parseStoredAuthSession = (value) => {
336
337
  };
337
338
  const parseAuthFile = (payload) => {
338
339
  const parsed = JSON.parse(payload);
339
- if (!isRecord(parsed) || parsed.version !== 1) throw new CliError(`Invalid auth cache format in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
340
+ if (!isRecord$1(parsed) || parsed.version !== 1) throw new CliError(`Invalid auth cache format in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
340
341
  const session = parseStoredAuthSession(parsed.session);
341
342
  if (!session) throw new CliError(`Invalid auth session in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
342
343
  return {
@@ -348,7 +349,7 @@ const readStoredAuthSession = async () => {
348
349
  try {
349
350
  return parseAuthFile(await readFile(AUTH_FILE_PATH, "utf-8")).session;
350
351
  } catch (error) {
351
- if (isRecord(error) && error.code === "ENOENT") return null;
352
+ if (isRecord$1(error) && error.code === "ENOENT") return null;
352
353
  if (error instanceof CliError) throw error;
353
354
  throw new CliError(`Failed to read credentials from ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
354
355
  }
@@ -486,6 +487,568 @@ const resolveTokenStatus = (token) => {
486
487
  expiresInSeconds
487
488
  };
488
489
  };
490
+ /**
491
+ * Curated GraphQL selection for agent-facing task reads.
492
+ *
493
+ * This intentionally remains smaller than the Task sync bootstrap surface.
494
+ */
495
+ const TASK_FIELDS = {
496
+ bootstrapFields: [
497
+ "id",
498
+ "key",
499
+ "title",
500
+ "description",
501
+ "sortOrder",
502
+ "todaySortOrder",
503
+ "todayIndexReferenceDate",
504
+ "createdAt",
505
+ "updatedAt",
506
+ "completedAt",
507
+ "archivedAt",
508
+ "start",
509
+ "startDate",
510
+ "startBucket",
511
+ "deadlineAt",
512
+ "deadlineSuppressionAt",
513
+ "reminderAt",
514
+ "lastReminderInteractionAt",
515
+ "creatorId",
516
+ "workspaceId",
517
+ "assigneeId",
518
+ "projectId",
519
+ "headingId",
520
+ "teamId",
521
+ "projectSortOrder",
522
+ "repeatRule",
523
+ "repeatType",
524
+ "repeatStatus",
525
+ "repeatTemplateId",
526
+ "repeatInstanceDate",
527
+ "repeatNextAt"
528
+ ],
529
+ insertFields: [
530
+ "archivedAt",
531
+ "assigneeId",
532
+ "completedAt",
533
+ "createdAt",
534
+ "creatorId",
535
+ "deadlineAt",
536
+ "deadlineSuppressionAt",
537
+ "description",
538
+ "headingId",
539
+ "key",
540
+ "lastReminderInteractionAt",
541
+ "projectId",
542
+ "projectSortOrder",
543
+ "reminderAt",
544
+ "repeatInstanceDate",
545
+ "repeatNextAt",
546
+ "repeatRule",
547
+ "repeatStatus",
548
+ "repeatTemplateId",
549
+ "repeatType",
550
+ "sortOrder",
551
+ "start",
552
+ "startBucket",
553
+ "startDate",
554
+ "teamId",
555
+ "title",
556
+ "todayIndexReferenceDate",
557
+ "todaySortOrder",
558
+ "updatedAt",
559
+ "workspaceId"
560
+ ],
561
+ iosKeys: [
562
+ "id",
563
+ "key",
564
+ "title",
565
+ "description",
566
+ "sortOrder",
567
+ "todaySortOrder",
568
+ "todayIndexReferenceDate",
569
+ "createdAt",
570
+ "updatedAt",
571
+ "completedAt",
572
+ "archivedAt",
573
+ "start",
574
+ "startDate",
575
+ "startBucket",
576
+ "deadlineAt",
577
+ "deadlineSuppressionAt",
578
+ "reminderAt",
579
+ "lastReminderInteractionAt",
580
+ "creatorId",
581
+ "workspaceId",
582
+ "assigneeId",
583
+ "projectId",
584
+ "headingId",
585
+ "teamId",
586
+ "projectSortOrder",
587
+ "repeatRule",
588
+ "repeatType",
589
+ "repeatStatus",
590
+ "repeatTemplateId",
591
+ "repeatInstanceDate",
592
+ "repeatNextAt"
593
+ ],
594
+ model: "Task",
595
+ readProjection: [
596
+ "id",
597
+ "title",
598
+ "description",
599
+ "createdAt",
600
+ "updatedAt",
601
+ "completedAt",
602
+ "archivedAt",
603
+ "start",
604
+ "startDate",
605
+ "startBucket",
606
+ "todayIndexReferenceDate",
607
+ "deadlineAt",
608
+ "creatorId",
609
+ "workspaceId",
610
+ "projectId",
611
+ "teamId",
612
+ "assigneeId",
613
+ "headingId",
614
+ "repeatRule",
615
+ "repeatTemplateId"
616
+ ],
617
+ updateFields: [
618
+ "title",
619
+ "description",
620
+ "sortOrder",
621
+ "todaySortOrder",
622
+ "todayIndexReferenceDate",
623
+ "createdAt",
624
+ "updatedAt",
625
+ "completedAt",
626
+ "archivedAt",
627
+ "start",
628
+ "startDate",
629
+ "startBucket",
630
+ "deadlineAt",
631
+ "deadlineSuppressionAt",
632
+ "reminderAt",
633
+ "lastReminderInteractionAt",
634
+ "creatorId",
635
+ "assigneeId",
636
+ "projectId",
637
+ "headingId",
638
+ "teamId",
639
+ "projectSortOrder",
640
+ "repeatRule",
641
+ "repeatType",
642
+ "repeatStatus",
643
+ "repeatTemplateId",
644
+ "repeatInstanceDate",
645
+ "repeatNextAt"
646
+ ]
647
+ }.readProjection.join("\n");
648
+ //#endregion
649
+ //#region ../../packages/task-logic/src/date-only.ts
650
+ const DATE_ONLY_DAY_MS = 1440 * 60 * 1e3;
651
+ const DATE_ONLY_INPUT_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/u;
652
+ const isFiniteEpoch$1 = (value) => typeof value === "number" && Number.isFinite(value);
653
+ const isCanonicalDateOnlyEpoch = (value) => value % DATE_ONLY_DAY_MS === 0;
654
+ const getUtcDateOnlyParts = (date) => ({
655
+ day: date.getUTCDate(),
656
+ month: date.getUTCMonth() + 1,
657
+ year: date.getUTCFullYear()
658
+ });
659
+ const isValidDateOnlyParts = (parts) => {
660
+ if (!(Number.isInteger(parts.year) && Number.isInteger(parts.month) && Number.isInteger(parts.day))) return false;
661
+ if (parts.year < 1 || parts.month < 1 || parts.month > 12 || parts.day < 1 || parts.day > 31) return false;
662
+ const candidate = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
663
+ return candidate.getUTCFullYear() === parts.year && candidate.getUTCMonth() + 1 === parts.month && candidate.getUTCDate() === parts.day;
664
+ };
665
+ const parseDateOnlyInput = (value) => {
666
+ if (typeof value !== "string") return null;
667
+ const match = DATE_ONLY_INPUT_PATTERN.exec(value.trim());
668
+ if (!match) return null;
669
+ const parts = {
670
+ day: Number(match[3]),
671
+ month: Number(match[2]),
672
+ year: Number(match[1])
673
+ };
674
+ return isValidDateOnlyParts(parts) ? parts : null;
675
+ };
676
+ const dateOnlyPartsToEpoch = (parts) => isValidDateOnlyParts(parts) ? Date.UTC(parts.year, parts.month - 1, parts.day) : null;
677
+ const epochToDateOnlyParts = (epoch) => {
678
+ if (!isFiniteEpoch$1(epoch)) return null;
679
+ const date = new Date(epoch);
680
+ return Number.isNaN(date.getTime()) ? null : getUtcDateOnlyParts(date);
681
+ };
682
+ const formatDateOnlyParts = (parts) => isValidDateOnlyParts(parts) ? `${String(parts.year).padStart(4, "0")}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}` : null;
683
+ const formatDateOnlyEpoch = (epoch) => {
684
+ const parts = epochToDateOnlyParts(epoch);
685
+ return parts ? formatDateOnlyParts(parts) : null;
686
+ };
687
+ const normalizeDateOnlyEpoch = (value) => {
688
+ if (!(isFiniteEpoch$1(value) && isCanonicalDateOnlyEpoch(value))) return null;
689
+ return value;
690
+ };
691
+ const parseDateOnlyInputToEpoch = (value) => {
692
+ const parts = parseDateOnlyInput(value);
693
+ return parts ? dateOnlyPartsToEpoch(parts) : null;
694
+ };
695
+ const localDateToDateOnlyEpoch = (date) => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());
696
+ const getTodayDateOnlyEpoch = (now = /* @__PURE__ */ new Date()) => localDateToDateOnlyEpoch(now);
697
+ //#endregion
698
+ //#region ../../packages/task-logic/src/task-status.ts
699
+ const normalizeStart = (value) => {
700
+ if (typeof value !== "string") return null;
701
+ const trimmed = value.trim().toLowerCase();
702
+ if (trimmed === "not_started") return "not_started";
703
+ if (trimmed === "started") return "started";
704
+ if (trimmed === "someday") return "someday";
705
+ return null;
706
+ };
707
+ const isNullish = (value) => value === null || value === void 0;
708
+ const isRepeatTemplate = (task) => !isNullish(task.repeatRule) && isNullish(task.repeatTemplateId);
709
+ const getTaskState$1 = (task) => {
710
+ if (!isNullish(task.archivedAt)) return "archived";
711
+ if (!isNullish(task.completedAt)) return "done";
712
+ return "open";
713
+ };
714
+ /**
715
+ * Derive the GTD bucket a task belongs to. `todayEpoch` is a canonical
716
+ * UTC-midnight date-only epoch (compute it via getTodayDateOnlyEpoch).
717
+ *
718
+ * Note: this deliberately does NOT hide repeat templates. Template hiding is a
719
+ * list/retention concern that callers apply via isRepeatTemplate.
720
+ */
721
+ const getTaskView = (task, todayEpoch) => {
722
+ if (!isNullish(task.archivedAt)) return "trash";
723
+ if (!isNullish(task.completedAt)) return "logbook";
724
+ const start = normalizeStart(task.start);
725
+ const startDate = normalizeDateOnlyEpoch(task.startDate);
726
+ const deadline = normalizeDateOnlyEpoch(task.deadlineAt);
727
+ const isDeadlineDue = deadline !== null && deadline <= todayEpoch;
728
+ const tomorrow = todayEpoch + DATE_ONLY_DAY_MS;
729
+ if (start === null) return null;
730
+ if (start === "not_started") return isDeadlineDue ? "today" : "inbox";
731
+ if (start === "someday") return isDeadlineDue ? "today" : "someday";
732
+ if (startDate === null) return isDeadlineDue ? "today" : "anytime";
733
+ if (startDate < tomorrow || isDeadlineDue) return "today";
734
+ return "upcoming";
735
+ };
736
+ /**
737
+ * Canonical 3-field start shape for a target view. (cli/raycast additionally
738
+ * emit a 4th `todayIndexReferenceDate`; that is a client concern and is
739
+ * intentionally NOT produced here.)
740
+ */
741
+ const buildTaskStartFields$1 = (view, options = {}) => {
742
+ const today = options.todayEpoch ?? getTodayDateOnlyEpoch(options.now);
743
+ const tomorrow = today + DATE_ONLY_DAY_MS;
744
+ const startDate = normalizeDateOnlyEpoch(options.startDate);
745
+ switch (view) {
746
+ case "inbox": return {
747
+ start: "not_started",
748
+ startBucket: "today",
749
+ startDate: null
750
+ };
751
+ case "anytime": return {
752
+ start: "started",
753
+ startBucket: "today",
754
+ startDate: null
755
+ };
756
+ case "someday": return {
757
+ start: "someday",
758
+ startBucket: "today",
759
+ startDate: null
760
+ };
761
+ case "today": return {
762
+ start: "started",
763
+ startBucket: "today",
764
+ startDate: startDate ?? today
765
+ };
766
+ case "upcoming": return {
767
+ start: "started",
768
+ startBucket: "upcoming",
769
+ startDate: startDate ?? tomorrow
770
+ };
771
+ default: return {
772
+ start: "not_started",
773
+ startBucket: "today",
774
+ startDate: null
775
+ };
776
+ }
777
+ };
778
+ //#endregion
779
+ //#region ../../packages/manage-client/src/mutations.ts
780
+ const startFieldsForView = (view, todayEpoch, startDate) => ({
781
+ ...buildTaskStartFields$1(view, {
782
+ startDate,
783
+ todayEpoch
784
+ }),
785
+ todayIndexReferenceDate: view === "today" ? todayEpoch : null
786
+ });
787
+ const buildTaskInsertFields = (input) => ({
788
+ archivedAt: null,
789
+ completedAt: null,
790
+ createdAt: input.now,
791
+ creatorId: input.ids.creatorId,
792
+ deadlineAt: input.deadlineAt ?? null,
793
+ description: input.description ?? null,
794
+ key: input.ids.taskId.replaceAll("-", "").slice(0, 8),
795
+ projectId: input.projectId ?? null,
796
+ sortOrder: input.sortOrder ?? DEFAULT_RANK_GAP,
797
+ teamId: input.teamId ?? null,
798
+ title: input.title,
799
+ todaySortOrder: input.todaySortOrder ?? DEFAULT_RANK_GAP,
800
+ updatedAt: input.now,
801
+ workspaceId: input.ids.workspaceId,
802
+ ...startFieldsForView(input.view, input.todayEpoch, input.startDate)
803
+ });
804
+ const buildMutateEnvelope = ({ batchId, clientId, clientTxIds, transactions }) => {
805
+ if (transactions.length !== clientTxIds.length) throw new Error("A client transaction ID is required for every transaction");
806
+ return {
807
+ batchId,
808
+ transactions: transactions.map((transaction, index) => {
809
+ const clientTxId = clientTxIds[index];
810
+ if (!clientTxId) throw new Error("Client transaction IDs cannot be empty");
811
+ return {
812
+ ...transaction,
813
+ clientId,
814
+ clientTxId
815
+ };
816
+ })
817
+ };
818
+ };
819
+ //#endregion
820
+ //#region ../../packages/manage-client/src/sync-mutate.ts
821
+ const normalizeBaseUrl$1 = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
822
+ const isRecord = (value) => typeof value === "object" && value !== null;
823
+ const parseResult = (value) => {
824
+ if (!isRecord(value)) return null;
825
+ if (typeof value.clientTxId !== "string" || typeof value.success !== "boolean") return null;
826
+ if (value.error !== void 0 && typeof value.error !== "string") return null;
827
+ if (value.syncId !== void 0 && typeof value.syncId !== "string") return null;
828
+ return {
829
+ clientTxId: value.clientTxId,
830
+ success: value.success,
831
+ ...value.error === void 0 ? {} : { error: value.error },
832
+ ...value.syncId === void 0 ? {} : { syncId: value.syncId }
833
+ };
834
+ };
835
+ const parseResponse = (value) => {
836
+ if (!isRecord(value) || typeof value.success !== "boolean" || typeof value.lastSyncId !== "string" || !Array.isArray(value.results)) return null;
837
+ const results = value.results.map(parseResult);
838
+ if (results.some((result) => result === null)) return null;
839
+ return {
840
+ lastSyncId: value.lastSyncId,
841
+ results,
842
+ success: value.success
843
+ };
844
+ };
845
+ const defaultErrorMapper$1 = (failure) => {
846
+ if (failure.type === "timeout") return new Error(`Sync mutation timed out after ${failure.timeoutMs}ms`, { cause: failure.cause });
847
+ if (failure.type === "http") return /* @__PURE__ */ new Error(`Sync mutation failed with HTTP ${failure.status}`);
848
+ if (failure.type === "mutation") {
849
+ const result = failure.response.results.find((item) => !item.success);
850
+ return new Error(result?.error ?? "Sync mutation failed");
851
+ }
852
+ if (failure.type === "invalid-response") return new Error("Sync mutation returned an invalid response", { cause: failure.cause });
853
+ return failure.cause instanceof Error ? failure.cause : new Error("Sync mutation request failed", { cause: failure.cause });
854
+ };
855
+ const isTimeoutError$1 = (error) => error instanceof Error && error.name === "TimeoutError";
856
+ const createSyncMutateClient = ({ baseUrl, errorMapper = defaultErrorMapper$1, fetch: fetchImplementation = globalThis.fetch, getToken, timeoutMs }) => {
857
+ if (!fetchImplementation) throw new Error("A fetch implementation is required");
858
+ const mutate = async (request, options = {}) => {
859
+ const timeoutSignal = timeoutMs === void 0 ? void 0 : AbortSignal.timeout(timeoutMs);
860
+ const signal = options.signal && timeoutSignal ? AbortSignal.any([options.signal, timeoutSignal]) : options.signal ?? timeoutSignal;
861
+ let response;
862
+ try {
863
+ response = await fetchImplementation(`${normalizeBaseUrl$1(baseUrl)}/sync/mutate`, {
864
+ body: JSON.stringify(request),
865
+ headers: {
866
+ Accept: "application/json",
867
+ Authorization: `Bearer ${await getToken()}`,
868
+ "Content-Type": "application/json"
869
+ },
870
+ method: "POST",
871
+ signal
872
+ });
873
+ } catch (error) {
874
+ if (timeoutMs !== void 0 && (timeoutSignal?.aborted || isTimeoutError$1(error))) throw errorMapper({
875
+ cause: error,
876
+ timeoutMs,
877
+ type: "timeout"
878
+ });
879
+ throw errorMapper({
880
+ cause: error,
881
+ type: "network"
882
+ });
883
+ }
884
+ if (!response.ok) throw errorMapper({
885
+ body: await response.text(),
886
+ status: response.status,
887
+ type: "http"
888
+ });
889
+ let raw;
890
+ try {
891
+ raw = await response.json();
892
+ } catch (error) {
893
+ throw errorMapper({
894
+ cause: error,
895
+ type: "invalid-response"
896
+ });
897
+ }
898
+ const payload = parseResponse(raw);
899
+ if (!payload) throw errorMapper({
900
+ cause: /* @__PURE__ */ new TypeError("Malformed sync mutation response"),
901
+ type: "invalid-response"
902
+ });
903
+ if (!payload.success) throw errorMapper({
904
+ response: payload,
905
+ type: "mutation"
906
+ });
907
+ return payload;
908
+ };
909
+ return { mutate };
910
+ };
911
+ //#endregion
912
+ //#region ../../packages/manage-client/src/index.ts
913
+ const TASKS_QUERY = `
914
+ query DonebearTasks($first: Int!, $after: String, $workspaceId: ID!) {
915
+ tasks(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
916
+ nodes { ${TASK_FIELDS} }
917
+ pageInfo { hasNextPage endCursor }
918
+ }
919
+ }
920
+ `;
921
+ const MY_WORKSPACES_QUERY = `
922
+ query MyWorkspaces {
923
+ myWorkspaces {
924
+ id name urlKey logoUrl role createdAt updatedAt
925
+ }
926
+ }
927
+ `;
928
+ const normalizeBaseUrl = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
929
+ const toInstantEpoch = (value) => {
930
+ if (typeof value !== "string") return null;
931
+ const epoch = Date.parse(value);
932
+ return Number.isFinite(epoch) ? epoch : null;
933
+ };
934
+ const toTaskRecord$1 = (node) => ({
935
+ archivedAt: toInstantEpoch(node.archivedAt),
936
+ assigneeId: node.assigneeId,
937
+ completedAt: toInstantEpoch(node.completedAt),
938
+ createdAt: toInstantEpoch(node.createdAt),
939
+ creatorId: node.creatorId,
940
+ deadlineAt: parseDateOnlyInputToEpoch(node.deadlineAt),
941
+ description: node.description,
942
+ headingId: node.headingId,
943
+ id: node.id,
944
+ projectId: node.projectId,
945
+ repeatRule: node.repeatRule,
946
+ repeatTemplateId: node.repeatTemplateId,
947
+ start: node.start,
948
+ startBucket: node.startBucket,
949
+ startDate: parseDateOnlyInputToEpoch(node.startDate),
950
+ teamId: node.teamId,
951
+ title: node.title,
952
+ todayIndexReferenceDate: parseDateOnlyInputToEpoch(node.todayIndexReferenceDate),
953
+ updatedAt: toInstantEpoch(node.updatedAt),
954
+ workspaceId: node.workspaceId
955
+ });
956
+ const defaultErrorMapper = (failure) => {
957
+ if (failure.type === "timeout") return new Error(`GraphQL request timed out after ${failure.timeoutMs}ms`, { cause: failure.cause });
958
+ if (failure.type === "http") return /* @__PURE__ */ new Error(`GraphQL request failed with HTTP ${failure.status}`);
959
+ if (failure.type === "graphql") return new Error(failure.errors[0]?.message ?? "GraphQL request failed");
960
+ if (failure.type === "missing-data") return /* @__PURE__ */ new Error("GraphQL request returned no data");
961
+ return failure.cause instanceof Error ? failure.cause : new Error("GraphQL request failed", { cause: failure.cause });
962
+ };
963
+ const isTimeoutError = (error) => error instanceof Error && error.name === "TimeoutError";
964
+ const createGraphqlClient = ({ baseUrl, errorMapper = defaultErrorMapper, fetch: fetchImplementation = globalThis.fetch, getToken, timeoutMs }) => {
965
+ if (!fetchImplementation) throw new Error("A fetch implementation is required");
966
+ const graphqlRequest = async ({ query, signal: requestSignal, variables = {} }) => {
967
+ const timeoutSignal = timeoutMs === void 0 ? void 0 : AbortSignal.timeout(timeoutMs);
968
+ const signal = requestSignal && timeoutSignal ? AbortSignal.any([requestSignal, timeoutSignal]) : requestSignal ?? timeoutSignal;
969
+ let response;
970
+ try {
971
+ response = await fetchImplementation(`${normalizeBaseUrl(baseUrl)}/graphql`, {
972
+ body: JSON.stringify({
973
+ query,
974
+ variables
975
+ }),
976
+ headers: {
977
+ Authorization: `Bearer ${await getToken()}`,
978
+ "Content-Type": "application/json"
979
+ },
980
+ method: "POST",
981
+ signal
982
+ });
983
+ } catch (error) {
984
+ if (timeoutMs !== void 0 && (timeoutSignal?.aborted || isTimeoutError(error))) throw errorMapper({
985
+ cause: error,
986
+ timeoutMs,
987
+ type: "timeout"
988
+ });
989
+ throw errorMapper({
990
+ cause: error,
991
+ type: "network"
992
+ });
993
+ }
994
+ if (!response.ok) throw errorMapper({
995
+ body: await response.text(),
996
+ status: response.status,
997
+ type: "http"
998
+ });
999
+ let payload;
1000
+ try {
1001
+ payload = await response.json();
1002
+ } catch (error) {
1003
+ throw errorMapper({
1004
+ cause: error,
1005
+ type: "network"
1006
+ });
1007
+ }
1008
+ if (payload.errors?.length) throw errorMapper({
1009
+ errors: payload.errors,
1010
+ type: "graphql"
1011
+ });
1012
+ if (payload.data === void 0) throw errorMapper({ type: "missing-data" });
1013
+ return payload.data;
1014
+ };
1015
+ const paginate = async ({ nodeKey, pageSize = 100, query, signal, transform, variables = {} }) => {
1016
+ const records = [];
1017
+ let after = null;
1018
+ for (let pageCount = 0; pageCount < 1e3; pageCount += 1) {
1019
+ const connection = (await graphqlRequest({
1020
+ query,
1021
+ signal,
1022
+ variables: {
1023
+ ...variables,
1024
+ after,
1025
+ first: pageSize
1026
+ }
1027
+ }))[nodeKey];
1028
+ if (!connection) throw new Error(`GraphQL response is missing connection: ${nodeKey}`);
1029
+ for (const node of connection.nodes) records.push(transform(node));
1030
+ if (!connection.pageInfo.hasNextPage) return records;
1031
+ if (!connection.pageInfo.endCursor) throw new Error("GraphQL page has no end cursor");
1032
+ after = connection.pageInfo.endCursor;
1033
+ }
1034
+ throw new Error("GraphQL pagination exceeded safe page limit");
1035
+ };
1036
+ const listWorkspaces = async () => {
1037
+ return (await graphqlRequest({ query: MY_WORKSPACES_QUERY })).myWorkspaces.map((workspace) => ({ ...workspace }));
1038
+ };
1039
+ const listTasks = (workspaceId) => paginate({
1040
+ nodeKey: "tasks",
1041
+ query: TASKS_QUERY,
1042
+ transform: toTaskRecord$1,
1043
+ variables: { workspaceId }
1044
+ });
1045
+ return {
1046
+ graphqlRequest,
1047
+ listTasks,
1048
+ listWorkspaces,
1049
+ paginate
1050
+ };
1051
+ };
489
1052
  //#endregion
490
1053
  //#region src/api/http.ts
491
1054
  const extractErrorMessage = (value) => {
@@ -536,62 +1099,15 @@ const requestNdjson = async (context, endpoint, options) => {
536
1099
  };
537
1100
  //#endregion
538
1101
  //#region src/date-codecs.ts
539
- const ISO_DATE_ONLY_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/u;
540
1102
  const ISO_INSTANT_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/u;
541
- const pad = (value, length = 2) => String(value).padStart(length, "0");
542
1103
  const isFiniteEpoch = (value) => typeof value === "number" && Number.isFinite(value);
543
- const isValidDateOnlyParts = (parts) => {
544
- if (!(Number.isInteger(parts.year) && Number.isInteger(parts.month) && Number.isInteger(parts.day))) return false;
545
- if (parts.year < 1 || parts.month < 1 || parts.month > 12 || parts.day < 1 || parts.day > 31) return false;
546
- const candidate = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
547
- return candidate.getUTCFullYear() === parts.year && candidate.getUTCMonth() === parts.month - 1 && candidate.getUTCDate() === parts.day;
548
- };
549
- const dateOnlyPartsToEpoch = (parts) => {
550
- if (!isValidDateOnlyParts(parts)) return null;
551
- return Date.UTC(parts.year, parts.month - 1, parts.day);
552
- };
553
- const epochToDateOnlyParts = (epochMs) => {
554
- if (!isFiniteEpoch(epochMs)) return null;
555
- const date = new Date(epochMs);
556
- if (Number.isNaN(date.getTime())) return null;
557
- return {
558
- day: date.getUTCDate(),
559
- month: date.getUTCMonth() + 1,
560
- year: date.getUTCFullYear()
561
- };
562
- };
563
- const formatDateOnlyParts = (parts) => {
564
- if (!isValidDateOnlyParts(parts)) return null;
565
- return `${pad(parts.year, 4)}-${pad(parts.month)}-${pad(parts.day)}`;
566
- };
567
- const formatDateOnlyEpoch = (epochMs) => {
568
- const parts = epochToDateOnlyParts(epochMs);
569
- return parts ? formatDateOnlyParts(parts) : null;
570
- };
571
1104
  const formatInstantEpoch = (epochMs) => {
572
1105
  if (!isFiniteEpoch(epochMs)) return null;
573
1106
  const date = new Date(epochMs);
574
1107
  if (Number.isNaN(date.getTime())) return null;
575
1108
  return date.toISOString();
576
1109
  };
577
- const parseDateOnlyString = (value) => {
578
- if (typeof value !== "string") return null;
579
- const trimmed = value.trim();
580
- const match = ISO_DATE_ONLY_REGEX.exec(trimmed);
581
- if (!match) return null;
582
- const year = Number.parseInt(match[1] ?? "", 10);
583
- const month = Number.parseInt(match[2] ?? "", 10);
584
- const parts = {
585
- day: Number.parseInt(match[3] ?? "", 10),
586
- month,
587
- year
588
- };
589
- return isValidDateOnlyParts(parts) ? parts : null;
590
- };
591
- const parseApiDateOnlyToEpoch = (value) => {
592
- const parts = parseDateOnlyString(value);
593
- return parts ? dateOnlyPartsToEpoch(parts) : null;
594
- };
1110
+ const parseApiDateOnlyToEpoch = (value) => parseDateOnlyInputToEpoch(value);
595
1111
  const parseApiInstantToEpoch = (value) => {
596
1112
  if (typeof value !== "string") return null;
597
1113
  const trimmed = value.trim();
@@ -601,47 +1117,19 @@ const parseApiInstantToEpoch = (value) => {
601
1117
  };
602
1118
  const parseCliDateOnlyToEpoch = (value, fieldLabel) => {
603
1119
  if (!value) return null;
604
- const parts = parseDateOnlyString(value);
605
- if (!parts) throw new Error(`Invalid ${fieldLabel}: ${value}. Expected YYYY-MM-DD.`);
606
- return dateOnlyPartsToEpoch(parts);
1120
+ const epoch = parseDateOnlyInputToEpoch(value);
1121
+ if (epoch === null) throw new Error(`Invalid ${fieldLabel}: ${value}. Expected YYYY-MM-DD.`);
1122
+ return epoch;
607
1123
  };
608
- const dateOnlyEpochFromLocalDate = (now = /* @__PURE__ */ new Date()) => Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
1124
+ const dateOnlyEpochFromLocalDate = (now = /* @__PURE__ */ new Date()) => localDateToDateOnlyEpoch(now);
609
1125
  //#endregion
610
1126
  //#region src/manage-api.ts
611
- const TASK_FIELDS = `
612
- id
613
- title
614
- description
615
- createdAt
616
- updatedAt
617
- completedAt
618
- archivedAt
619
- start
620
- startDate
621
- startBucket
622
- todayIndexReferenceDate
623
- deadlineAt
624
- creatorId
625
- workspaceId
626
- projectId
627
- teamId
628
- assigneeId
629
- headingId
630
- repeatRule
631
- repeatTemplateId
632
- `;
633
- const TASKS_QUERY = `
634
- query DonebearTasks($first: Int!, $after: String, $workspaceId: ID!) {
635
- tasks(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
636
- nodes { ${TASK_FIELDS} }
637
- pageInfo { hasNextPage endCursor }
638
- }
639
- }
640
- `;
641
1127
  const TASKS_ALL_QUERY = `
642
1128
  query DonebearTasksAll($first: Int!, $after: String, $workspaceIds: [ID!]!) {
643
1129
  tasks(first: $first, after: $after, filter: { workspaceId: { in: $workspaceIds } }) {
644
- nodes { ${TASK_FIELDS} }
1130
+ nodes {
1131
+ ${TASK_FIELDS}
1132
+ }
645
1133
  pageInfo { hasNextPage endCursor }
646
1134
  }
647
1135
  }
@@ -700,13 +1188,6 @@ const TEAMS_ALL_QUERY = `
700
1188
  }
701
1189
  }
702
1190
  `;
703
- const MY_WORKSPACES_QUERY = `
704
- query MyWorkspaces {
705
- myWorkspaces {
706
- id name urlKey logoUrl role createdAt updatedAt
707
- }
708
- }
709
- `;
710
1191
  const WORKSPACE_CREATE_MUTATION = `
711
1192
  mutation WorkspaceCreate($input: WorkspaceCreateInput!) {
712
1193
  workspaceCreate(input: $input) {
@@ -778,8 +1259,9 @@ const API_KEY_REVOKE_MUTATION = `
778
1259
  }
779
1260
  `;
780
1261
  const normalizeApiUrl = (url) => url.endsWith("/") ? url.slice(0, -1) : url;
781
- const requestGraphql = async (context, endpoint, options) => {
782
- const response = await requestJson(context, endpoint, {
1262
+ const omitUndefined = (entries) => Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== void 0));
1263
+ const requestGraphql = async (context, baseUrl, options) => {
1264
+ const response = await requestJson(context, `${baseUrl}/graphql`, {
783
1265
  body: {
784
1266
  query: options.query,
785
1267
  variables: options.variables
@@ -795,14 +1277,13 @@ const requestGraphql = async (context, endpoint, options) => {
795
1277
  return response.data;
796
1278
  };
797
1279
  const paginateGraphql = async (context, options) => {
798
- const endpoint = `${options.baseUrl}/graphql`;
799
1280
  const results = [];
800
1281
  let after = null;
801
1282
  let pageCount = 0;
802
1283
  while (true) {
803
1284
  pageCount += 1;
804
1285
  if (pageCount > 1e3) throw new CliError("GraphQL pagination exceeded safe page limit", EXIT_CODES.ERROR);
805
- const connection = (await requestGraphql(context, endpoint, {
1286
+ const connection = (await requestGraphql(context, options.baseUrl, {
806
1287
  query: options.query,
807
1288
  token: options.token,
808
1289
  variables: {
@@ -851,28 +1332,6 @@ const toNullableNumber = (input) => {
851
1332
  if (input === null || input === void 0) return null;
852
1333
  return typeof input === "number" ? input : null;
853
1334
  };
854
- const toTaskRecordFromGraphqlNode = (node) => ({
855
- archivedAt: parseApiInstantToEpoch(node.archivedAt),
856
- assigneeId: node.assigneeId,
857
- completedAt: parseApiInstantToEpoch(node.completedAt),
858
- createdAt: parseApiInstantToEpoch(node.createdAt),
859
- creatorId: node.creatorId,
860
- deadlineAt: parseApiDateOnlyToEpoch(node.deadlineAt),
861
- description: node.description,
862
- headingId: node.headingId,
863
- id: node.id,
864
- projectId: node.projectId,
865
- repeatRule: node.repeatRule,
866
- repeatTemplateId: node.repeatTemplateId,
867
- start: node.start,
868
- startBucket: node.startBucket,
869
- startDate: parseApiDateOnlyToEpoch(node.startDate),
870
- teamId: node.teamId,
871
- title: node.title,
872
- todayIndexReferenceDate: parseApiDateOnlyToEpoch(node.todayIndexReferenceDate),
873
- updatedAt: parseApiInstantToEpoch(node.updatedAt),
874
- workspaceId: node.workspaceId
875
- });
876
1335
  const toProjectRecordFromGraphqlNode = (node) => ({
877
1336
  archivedAt: parseApiInstantToEpoch(node.archivedAt),
878
1337
  completedAt: parseApiInstantToEpoch(node.completedAt),
@@ -905,15 +1364,6 @@ const toTeamRecordFromGraphqlNode = (node) => ({
905
1364
  updatedAt: parseApiInstantToEpoch(node.updatedAt),
906
1365
  workspaceId: node.workspaceId
907
1366
  });
908
- const toWorkspaceSummaryFromGraphqlNode = (node) => ({
909
- createdAt: node.createdAt,
910
- id: node.id,
911
- logoUrl: node.logoUrl,
912
- name: node.name,
913
- role: node.role,
914
- updatedAt: node.updatedAt,
915
- urlKey: node.urlKey
916
- });
917
1367
  const toWorkspaceRecordFromGraphqlNode = (node) => ({
918
1368
  archivedAt: node.archivedAt,
919
1369
  createdAt: node.createdAt,
@@ -993,7 +1443,7 @@ const VIEWER_QUERY = `
993
1443
  }
994
1444
  `;
995
1445
  const loadViewer = async (context, options) => {
996
- const data = await requestGraphql(context, `${options.baseUrl}/graphql`, {
1446
+ const data = await requestGraphql(context, options.baseUrl, {
997
1447
  query: VIEWER_QUERY,
998
1448
  token: options.token,
999
1449
  variables: {}
@@ -1004,22 +1454,19 @@ const loadViewer = async (context, options) => {
1004
1454
  id: data.viewer.id
1005
1455
  };
1006
1456
  };
1007
- const listWorkspaces = async (context, options) => {
1008
- return (await requestGraphql(context, `${options.baseUrl}/graphql`, {
1009
- query: MY_WORKSPACES_QUERY,
1010
- token: options.token,
1011
- variables: {}
1012
- })).myWorkspaces.map(toWorkspaceSummaryFromGraphqlNode);
1013
- };
1457
+ const listWorkspaces = async (_context, options) => await createGraphqlClient({
1458
+ baseUrl: options.baseUrl,
1459
+ getToken: () => options.token
1460
+ }).listWorkspaces();
1014
1461
  const createWorkspace = async (context, options) => {
1015
- return toWorkspaceRecordFromGraphqlNode((await requestGraphql(context, `${options.baseUrl}/graphql`, {
1462
+ return toWorkspaceRecordFromGraphqlNode((await requestGraphql(context, options.baseUrl, {
1016
1463
  query: WORKSPACE_CREATE_MUTATION,
1017
1464
  token: options.token,
1018
1465
  variables: { input: options.body }
1019
1466
  })).workspaceCreate.workspace);
1020
1467
  };
1021
1468
  const joinWorkspace = async (context, options) => {
1022
- const data = await requestGraphql(context, `${options.baseUrl}/graphql`, {
1469
+ const data = await requestGraphql(context, options.baseUrl, {
1023
1470
  query: WORKSPACE_JOIN_MUTATION,
1024
1471
  token: options.token,
1025
1472
  variables: { code: options.code }
@@ -1029,20 +1476,16 @@ const joinWorkspace = async (context, options) => {
1029
1476
  workspace: toWorkspaceRecordFromGraphqlNode(data.workspaceJoin.workspace)
1030
1477
  };
1031
1478
  };
1032
- const loadTasksByWorkspace = (context, options) => paginateGraphql(context, {
1479
+ const loadTasksByWorkspace = (_context, options) => createGraphqlClient({
1033
1480
  baseUrl: options.baseUrl,
1034
- nodeKey: "tasks",
1035
- query: TASKS_QUERY,
1036
- token: options.token,
1037
- transform: toTaskRecordFromGraphqlNode,
1038
- variables: { workspaceId: options.workspaceId }
1039
- });
1481
+ getToken: () => options.token
1482
+ }).listTasks(options.workspaceId);
1040
1483
  const loadTasksByWorkspaceIds = (context, options) => paginateGraphql(context, {
1041
1484
  baseUrl: options.baseUrl,
1042
1485
  nodeKey: "tasks",
1043
1486
  query: TASKS_ALL_QUERY,
1044
1487
  token: options.token,
1045
- transform: toTaskRecordFromGraphqlNode,
1488
+ transform: toTaskRecord$1,
1046
1489
  variables: { workspaceIds: options.workspaceIds }
1047
1490
  });
1048
1491
  const loadTaskById = async (context, options) => {
@@ -1124,6 +1567,21 @@ const loadChecklistItemsByTask = async (context, options) => {
1124
1567
  }
1125
1568
  return results;
1126
1569
  };
1570
+ const mapSyncMutateFailure = (failure) => {
1571
+ if (failure.type === "http") {
1572
+ const { body: responseBody, status } = failure;
1573
+ let body = responseBody;
1574
+ try {
1575
+ body = responseBody.length > 0 ? JSON.parse(responseBody) : null;
1576
+ } catch {}
1577
+ const exitCode = status === 401 || status === 403 ? EXIT_CODES.AUTH_REQUIRED : EXIT_CODES.ERROR;
1578
+ return new CliError(extractErrorMessage(body), exitCode);
1579
+ }
1580
+ if (failure.type === "mutation") return new CliError(failure.response.results.find((item) => !item.success)?.error ?? "Sync mutation failed", EXIT_CODES.ERROR);
1581
+ if (failure.type === "timeout") return new CliError(`Sync mutation timed out after ${failure.timeoutMs}ms`, EXIT_CODES.ERROR);
1582
+ if (failure.cause instanceof Error) return failure.cause;
1583
+ return new CliError("Sync mutation request failed", EXIT_CODES.ERROR);
1584
+ };
1127
1585
  const mutateModel = async (context, options) => {
1128
1586
  if (context.dryRun) {
1129
1587
  writeWarning(context, "dry run: mutation skipped");
@@ -1138,16 +1596,11 @@ const mutateModel = async (context, options) => {
1138
1596
  });
1139
1597
  return;
1140
1598
  }
1141
- const response = await requestJson(context, `${options.baseUrl}/sync/mutate`, {
1142
- body: options.request,
1143
- method: "POST",
1144
- token: options.token
1145
- });
1146
- if (!response.success) {
1147
- const error = response.results.find((result) => !result.success);
1148
- if (error?.error) throw new CliError(error.error, EXIT_CODES.ERROR);
1149
- throw new CliError("Sync mutation failed", EXIT_CODES.ERROR);
1150
- }
1599
+ await createSyncMutateClient({
1600
+ baseUrl: options.baseUrl,
1601
+ errorMapper: mapSyncMutateFailure,
1602
+ getToken: () => options.token
1603
+ }).mutate(options.request);
1151
1604
  };
1152
1605
  const listWorkspaceMembers = async (context, options) => await paginateGraphql(context, {
1153
1606
  baseUrl: options.baseUrl,
@@ -1166,36 +1619,37 @@ const listWorkspaceInvitations = async (context, options) => await paginateGraph
1166
1619
  variables: { workspaceId: options.workspaceId }
1167
1620
  });
1168
1621
  const createWorkspaceInvitation = async (context, options) => {
1169
- const endpoint = `${options.baseUrl}/graphql`;
1170
- const input = { workspaceId: options.workspaceId };
1171
- if (options.email !== void 0) input.email = options.email;
1172
- if (options.role !== void 0) input.role = options.role;
1173
- return toWorkspaceInvitationRecordFromGraphqlNode((await requestGraphql(context, endpoint, {
1622
+ const input = omitUndefined({
1623
+ email: options.email,
1624
+ role: options.role,
1625
+ workspaceId: options.workspaceId
1626
+ });
1627
+ return toWorkspaceInvitationRecordFromGraphqlNode((await requestGraphql(context, options.baseUrl, {
1174
1628
  query: WORKSPACE_INVITATION_CREATE_MUTATION,
1175
1629
  token: options.token,
1176
1630
  variables: { input }
1177
1631
  })).workspaceInvitationCreate.invitation);
1178
1632
  };
1179
1633
  const listWorkspaceHistory = async (context, options) => {
1180
- const endpoint = `${options.baseUrl}/graphql`;
1181
- const variables = {
1634
+ const variables = omitUndefined({
1182
1635
  limit: options.limit,
1636
+ model: options.model || void 0,
1183
1637
  workspaceId: options.workspaceId
1184
- };
1185
- if (options.model) variables.model = options.model;
1186
- return (await requestGraphql(context, endpoint, {
1638
+ });
1639
+ return (await requestGraphql(context, options.baseUrl, {
1187
1640
  query: WORKSPACE_HISTORY_QUERY,
1188
1641
  token: options.token,
1189
1642
  variables
1190
1643
  })).workspaceHistory.map(toWorkspaceHistoryEntryFromGraphqlNode);
1191
1644
  };
1192
1645
  const createApiKey = async (context, options) => {
1193
- const endpoint = `${options.baseUrl}/graphql`;
1194
- const variables = { label: options.label };
1195
- if (options.scopes !== void 0) variables.scopes = options.scopes;
1196
- if (options.workspaceIds !== void 0) variables.workspaceIds = options.workspaceIds;
1197
- if (options.expiresAt !== void 0) variables.expiresAt = options.expiresAt;
1198
- const data = await requestGraphql(context, endpoint, {
1646
+ const variables = omitUndefined({
1647
+ expiresAt: options.expiresAt,
1648
+ label: options.label,
1649
+ scopes: options.scopes,
1650
+ workspaceIds: options.workspaceIds
1651
+ });
1652
+ const data = await requestGraphql(context, options.baseUrl, {
1199
1653
  query: API_KEY_CREATE_MUTATION,
1200
1654
  token: options.token,
1201
1655
  variables
@@ -1206,14 +1660,14 @@ const createApiKey = async (context, options) => {
1206
1660
  };
1207
1661
  };
1208
1662
  const listApiKeys = async (context, options) => {
1209
- return (await requestGraphql(context, `${options.baseUrl}/graphql`, {
1663
+ return (await requestGraphql(context, options.baseUrl, {
1210
1664
  query: API_KEYS_QUERY,
1211
1665
  token: options.token,
1212
1666
  variables: {}
1213
1667
  })).apiKeys.map(toApiKeyRecordFromGraphqlNode);
1214
1668
  };
1215
1669
  const revokeApiKey = async (context, options) => {
1216
- await requestGraphql(context, `${options.baseUrl}/graphql`, {
1670
+ await requestGraphql(context, options.baseUrl, {
1217
1671
  query: API_KEY_REVOKE_MUTATION,
1218
1672
  token: options.token,
1219
1673
  variables: { id: options.keyId }
@@ -1420,7 +1874,7 @@ const runCreate = async (context, label, options) => {
1420
1874
  if (key.workspaceIds && key.workspaceIds.length > 0) writeInfo(context, ` Restricted to workspaces: ${key.workspaceIds.join(", ")}`);
1421
1875
  if (key.expiresAt) writeInfo(context, ` Expires: ${new Date(key.expiresAt).toLocaleDateString()}`);
1422
1876
  writeInfo(context, "");
1423
- writeWarning(context, "Save this key now — it will not be shown again.");
1877
+ writeWarning(context, "Save this key now; it will not be shown again.");
1424
1878
  };
1425
1879
  const runList = async (context) => {
1426
1880
  const { token } = await requireAuthToken(context);
@@ -1765,72 +2219,24 @@ Non-interactive / CI use:
1765
2219
  };
1766
2220
  //#endregion
1767
2221
  //#region src/task-defaults.ts
1768
- const DAY_MS = 1440 * 60 * 1e3;
1769
2222
  const buildTaskStartFields = (view, now = /* @__PURE__ */ new Date()) => {
1770
- const today = dateOnlyEpochFromLocalDate(now);
1771
- const tomorrow = today + DAY_MS;
1772
- switch (view) {
1773
- case "inbox": return {
1774
- start: "not_started",
1775
- startBucket: "today",
1776
- startDate: null,
1777
- todayIndexReferenceDate: null
1778
- };
1779
- case "anytime": return {
1780
- start: "started",
1781
- startBucket: "today",
1782
- startDate: null,
1783
- todayIndexReferenceDate: null
1784
- };
1785
- case "today": return {
1786
- start: "started",
1787
- startBucket: "today",
1788
- startDate: today,
1789
- todayIndexReferenceDate: today
1790
- };
1791
- case "upcoming": return {
1792
- start: "started",
1793
- startBucket: "upcoming",
1794
- startDate: tomorrow,
1795
- todayIndexReferenceDate: null
1796
- };
1797
- case "someday": return {
1798
- start: "someday",
1799
- startBucket: "today",
1800
- startDate: null,
1801
- todayIndexReferenceDate: null
1802
- };
1803
- default: return {
1804
- start: "not_started",
1805
- startBucket: "today",
1806
- startDate: null,
1807
- todayIndexReferenceDate: null
1808
- };
1809
- }
1810
- };
1811
- const getTaskState = (task) => {
1812
- if (task.archivedAt !== null) return "archived";
1813
- if (task.completedAt !== null) return "done";
1814
- return "open";
2223
+ const today = getTodayDateOnlyEpoch(now);
2224
+ return {
2225
+ ...buildTaskStartFields$1(view, { now }),
2226
+ todayIndexReferenceDate: view === "today" ? today : null
2227
+ };
1815
2228
  };
2229
+ const getTaskState = (task) => getTaskState$1(task);
1816
2230
  /**
1817
2231
  * Determine which view a task belongs to.
1818
- * Matches the frontend logic in manage-frontend/lib/tasks/task-status.ts
2232
+ *
2233
+ * Repeat templates are hidden from the CLI's list-level views (they only exist
2234
+ * to generate instances); everything else delegates to the canonical
2235
+ * task-logic derivation shared across every client.
1819
2236
  */
1820
2237
  const resolveTaskView = (task) => {
1821
- if (task.repeatRule !== null && task.repeatTemplateId === null) return null;
1822
- if (task.archivedAt !== null) return "trash";
1823
- if (task.completedAt !== null) return "logbook";
1824
- const { start, startDate, deadlineAt } = task;
1825
- const today = dateOnlyEpochFromLocalDate();
1826
- const tomorrow = today + DAY_MS;
1827
- const hasDeadlineToday = deadlineAt !== null && deadlineAt >= today && deadlineAt < tomorrow;
1828
- if (start === "not_started") return hasDeadlineToday ? "today" : "inbox";
1829
- if (start === "someday") return hasDeadlineToday ? "today" : "someday";
1830
- if (start !== "started") return null;
1831
- if (startDate === null) return hasDeadlineToday ? "today" : "anytime";
1832
- if (startDate < tomorrow || hasDeadlineToday) return "today";
1833
- return "upcoming";
2238
+ if (isRepeatTemplate(task)) return null;
2239
+ return getTaskView(task, getTodayDateOnlyEpoch());
1834
2240
  };
1835
2241
  const formatEpochDate = (epochMs) => formatInstantEpoch(epochMs);
1836
2242
  //#endregion
@@ -2002,7 +2408,7 @@ const formatMarkdownOutput = (context, user, workspace, taskCounts, recentTasks,
2002
2408
  lines.push("", "## Recent Open Tasks");
2003
2409
  for (const t of recentTasks) {
2004
2410
  const deadline = t.deadline ? `, due ${t.deadline}` : "";
2005
- lines.push(`- [${t.id.slice(0, 8)}] ${t.title} — ${t.view}${deadline}`);
2411
+ lines.push(`- [${t.id.slice(0, 8)}] ${t.title} (${t.view})${deadline}`);
2006
2412
  }
2007
2413
  }
2008
2414
  if (activeProjects.length > 0) {
@@ -2371,39 +2777,75 @@ const summarizeThingsImportResult = (result) => {
2371
2777
  //#region src/import/things-labels.ts
2372
2778
  const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase();
2373
2779
  //#endregion
2374
- //#region src/import/model-id.ts
2375
- const IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
2780
+ //#region ../../packages/sync-ids/node_modules/uuid/dist-node/regex.js
2781
+ var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
2782
+ //#endregion
2783
+ //#region ../../packages/sync-ids/node_modules/uuid/dist-node/validate.js
2784
+ function validate(uuid) {
2785
+ return typeof uuid === "string" && regex_default.test(uuid);
2786
+ }
2787
+ //#endregion
2788
+ //#region ../../packages/sync-ids/node_modules/uuid/dist-node/parse.js
2789
+ function parse(uuid) {
2790
+ if (!validate(uuid)) throw TypeError("Invalid UUID");
2791
+ let v;
2792
+ return Uint8Array.of((v = parseInt(uuid.slice(0, 8), 16)) >>> 24, v >>> 16 & 255, v >>> 8 & 255, v & 255, (v = parseInt(uuid.slice(9, 13), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(14, 18), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(19, 23), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(24, 36), 16)) / 1099511627776 & 255, v / 4294967296 & 255, v >>> 24 & 255, v >>> 16 & 255, v >>> 8 & 255, v & 255);
2793
+ }
2794
+ //#endregion
2795
+ //#region ../../packages/sync-ids/node_modules/uuid/dist-node/stringify.js
2796
+ const byteToHex = [];
2797
+ for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
2798
+ function unsafeStringify(arr, offset = 0) {
2799
+ return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
2800
+ }
2801
+ //#endregion
2802
+ //#region ../../packages/sync-ids/node_modules/uuid/dist-node/v35.js
2803
+ function stringToBytes(str) {
2804
+ str = unescape(encodeURIComponent(str));
2805
+ const bytes = new Uint8Array(str.length);
2806
+ for (let i = 0; i < str.length; ++i) bytes[i] = str.charCodeAt(i);
2807
+ return bytes;
2808
+ }
2809
+ const DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
2810
+ const URL$1 = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
2811
+ function v35(version, hash, value, namespace, buf, offset) {
2812
+ const valueBytes = typeof value === "string" ? stringToBytes(value) : value;
2813
+ const namespaceBytes = typeof namespace === "string" ? parse(namespace) : namespace;
2814
+ if (typeof namespace === "string") namespace = parse(namespace);
2815
+ if (namespace?.length !== 16) throw TypeError("Namespace must be array-like (16 iterable integer values, 0-255)");
2816
+ let bytes = new Uint8Array(16 + valueBytes.length);
2817
+ bytes.set(namespaceBytes);
2818
+ bytes.set(valueBytes, namespaceBytes.length);
2819
+ bytes = hash(bytes);
2820
+ bytes[6] = bytes[6] & 15 | version;
2821
+ bytes[8] = bytes[8] & 63 | 128;
2822
+ if (buf) {
2823
+ offset ??= 0;
2824
+ if (offset < 0 || offset + 16 > buf.length) throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
2825
+ for (let i = 0; i < 16; ++i) buf[offset + i] = bytes[i];
2826
+ return buf;
2827
+ }
2828
+ return unsafeStringify(bytes);
2829
+ }
2830
+ //#endregion
2831
+ //#region ../../packages/sync-ids/node_modules/uuid/dist-node/sha1.js
2832
+ function sha1(bytes) {
2833
+ if (Array.isArray(bytes)) bytes = Buffer.from(bytes);
2834
+ else if (typeof bytes === "string") bytes = Buffer.from(bytes, "utf8");
2835
+ return createHash("sha1").update(bytes).digest();
2836
+ }
2837
+ //#endregion
2838
+ //#region ../../packages/sync-ids/node_modules/uuid/dist-node/v5.js
2839
+ function v5(value, namespace, buf, offset) {
2840
+ return v35(80, sha1, value, namespace, buf, offset);
2841
+ }
2842
+ v5.DNS = DNS;
2843
+ v5.URL = URL$1;
2844
+ //#endregion
2845
+ //#region ../../packages/sync-ids/src/index.ts
2376
2846
  const SYNC_COMPOSITE_ID_NAMESPACE = "92a73695-d772-4b43-9fb4-d79f5fbef300";
2377
- const IDEMPOTENCY_ID_NAMESPACE = "4c8af5e2-1d36-5a0b-9c47-2f81b3d6e955";
2378
- const uuidToBytes = (uuid) => {
2379
- const normalized = uuid.replaceAll("-", "");
2380
- if (normalized.length !== 32) throw new Error(`Invalid UUID namespace: ${uuid}`);
2381
- return Uint8Array.from(Buffer.from(normalized, "hex"));
2382
- };
2383
- const bytesToUuid = (bytes) => {
2384
- const hex = Buffer.from(bytes).toString("hex");
2385
- return [
2386
- hex.slice(0, 8),
2387
- hex.slice(8, 12),
2388
- hex.slice(12, 16),
2389
- hex.slice(16, 20),
2390
- hex.slice(20, 32)
2391
- ].join("-");
2392
- };
2393
- const createUuidV5 = (name, namespace) => {
2394
- const namespaceBytes = uuidToBytes(namespace);
2395
- const hash = createHash("sha1").update(namespaceBytes).update(Buffer.from(name, "utf-8")).digest();
2396
- const bytes = Uint8Array.from(hash.subarray(0, 16));
2397
- const versionByte = bytes.at(6);
2398
- const variantByte = bytes.at(8);
2399
- if (typeof versionByte !== "number" || typeof variantByte !== "number") throw new TypeError("Failed to generate a deterministic UUID.");
2400
- bytes[6] = versionByte & 15 | 80;
2401
- bytes[8] = variantByte & 63 | 128;
2402
- return bytesToUuid(bytes);
2403
- };
2404
- const createCompositeSyncId = (model, parts) => createUuidV5(`${model}:${JSON.stringify(parts)}`, SYNC_COMPOSITE_ID_NAMESPACE);
2405
- const createDeterministicImportId = (params) => createUuidV5(`things:${params.workspaceId}:${params.modelName}:${params.sourceId}`, IMPORT_ID_NAMESPACE);
2406
- const createIdempotentMutationId = (params) => createUuidV5(`idem:${params.workspaceId}:${params.modelName}:${params.part}:${params.key}`, IDEMPOTENCY_ID_NAMESPACE);
2847
+ const createDeterministicId = (namespace, name) => v5(name, namespace);
2848
+ const createCompositeSyncId = (model, parts) => createDeterministicId(SYNC_COMPOSITE_ID_NAMESPACE, `${model}:${JSON.stringify(parts)}`);
2407
2849
  const createTaskLabelId = (params) => createCompositeSyncId("TaskLabel", [
2408
2850
  params.workspaceId,
2409
2851
  params.taskId,
@@ -2415,6 +2857,12 @@ const createTeamProjectId = (params) => createCompositeSyncId("TeamProject", [
2415
2857
  params.projectId
2416
2858
  ]);
2417
2859
  //#endregion
2860
+ //#region src/import/model-id.ts
2861
+ const IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
2862
+ const IDEMPOTENCY_ID_NAMESPACE = "4c8af5e2-1d36-5a0b-9c47-2f81b3d6e955";
2863
+ const createDeterministicImportId = (params) => createDeterministicId(IMPORT_ID_NAMESPACE, `things:${params.workspaceId}:${params.modelName}:${params.sourceId}`);
2864
+ const createIdempotentMutationId = (params) => createDeterministicId(IDEMPOTENCY_ID_NAMESPACE, `idem:${params.workspaceId}:${params.modelName}:${params.part}:${params.key}`);
2865
+ //#endregion
2418
2866
  //#region src/import/things-values.ts
2419
2867
  var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
2420
2868
  (function webpackUniversalModuleDefinition(root, factory) {
@@ -5550,21 +5998,27 @@ const pickCompletedAt = (task) => {
5550
5998
  return decodeThingsUnixSeconds(task.stopDate) ?? decodeThingsUnixSeconds(task.userModificationDate) ?? decodeThingsUnixSeconds(task.creationDate);
5551
5999
  };
5552
6000
  const sortValue = (primary, fallback) => typeof primary === "number" ? primary : fallback;
5553
- const sanitizeBuffer = (buffer) => {
5554
- const asText = buffer.toString("utf-8");
6001
+ const textDecoder = new TextDecoder();
6002
+ const encodeBase64 = (value) => {
6003
+ let binary = "";
6004
+ for (const byte of value) binary += String.fromCodePoint(byte);
6005
+ return btoa(binary);
6006
+ };
6007
+ const sanitizeBinary = (value) => {
6008
+ const asText = textDecoder.decode(value);
5555
6009
  if (!asText.includes("�") && READABLE_TEXT_PATTERN.test(asText)) {
5556
6010
  const trimmed = asText.trim();
5557
6011
  if (trimmed.length > 0) return trimmed;
5558
6012
  }
5559
6013
  return {
5560
- data: buffer.toString("base64"),
6014
+ data: encodeBase64(value),
5561
6015
  encoding: "base64"
5562
6016
  };
5563
6017
  };
5564
6018
  const sanitizeValue = (value) => {
5565
6019
  if (value === void 0 || value === null) return;
5566
6020
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
5567
- if (Buffer.isBuffer(value)) return sanitizeBuffer(value);
6021
+ if (value instanceof Uint8Array) return sanitizeBinary(value);
5568
6022
  if (Array.isArray(value)) {
5569
6023
  const items = value.map((entry) => sanitizeValue(entry)).filter((entry) => entry !== void 0);
5570
6024
  return items.length > 0 ? items : void 0;
@@ -7395,8 +7849,8 @@ const filterTasks = (tasks, options) => {
7395
7849
  if (query.length === 0) return viewFiltered;
7396
7850
  return viewFiltered.filter((task) => {
7397
7851
  const title = task.title.toLowerCase();
7398
- const notes = task.description?.toLowerCase() ?? "";
7399
- return title.includes(query) || notes.includes(query);
7852
+ const description = task.description?.toLowerCase() ?? "";
7853
+ return title.includes(query) || description.includes(query);
7400
7854
  });
7401
7855
  };
7402
7856
  const loadTasksForScope = (context, scope, apiOptions) => {
@@ -7477,7 +7931,7 @@ const runTaskShow = async (context, taskRef, options, workspaceOverride) => {
7477
7931
  if (task.projectId) writeInfo(context, `projectId: ${task.projectId}`);
7478
7932
  if (task.teamId) writeInfo(context, `teamId: ${task.teamId}`);
7479
7933
  if (task.assigneeId) writeInfo(context, `assigneeId: ${task.assigneeId}`);
7480
- if (task.description) writeInfo(context, `notes: ${task.description}`);
7934
+ if (task.description) writeInfo(context, `description: ${task.description}`);
7481
7935
  };
7482
7936
  const resolveTitle = async (context, rawTitle) => {
7483
7937
  if (rawTitle) {
@@ -7488,20 +7942,28 @@ const resolveTitle = async (context, rawTitle) => {
7488
7942
  if (prompted) return prompted;
7489
7943
  throw new CliError("Task title is required.", EXIT_CODES.VALIDATION, "Provide a title as an argument: donebear task add \"My task\"");
7490
7944
  };
7491
- const resolveNotes = async (_context, flagNotes) => {
7492
- if (typeof flagNotes === "string") {
7493
- const trimmed = flagNotes.trim();
7494
- return trimmed.length > 0 ? trimmed : null;
7945
+ const resolveDescription = async (context, flagDescription) => {
7946
+ if (typeof flagDescription !== "string") return null;
7947
+ if (flagDescription === "-") {
7948
+ const piped = await readStdin();
7949
+ if (!piped) {
7950
+ writeWarning(context, "No description received on stdin (--description -); task created without a description.");
7951
+ return null;
7952
+ }
7953
+ return piped;
7495
7954
  }
7496
- const stdinContent = await readStdin();
7497
- if (stdinContent) return stdinContent;
7498
- return null;
7955
+ const trimmed = flagDescription.trim();
7956
+ if (trimmed.length === 0) {
7957
+ writeWarning(context, "Empty --description value; task created without a description.");
7958
+ return null;
7959
+ }
7960
+ return trimmed;
7499
7961
  };
7500
7962
  const runTaskAdd = async (context, title, options, workspaceOverride) => {
7501
7963
  const commandContext = await resolveCommandContext(context);
7502
7964
  const userId = await requireUserId(context, commandContext);
7503
7965
  const normalizedTitle = await resolveTitle(context, title);
7504
- const notes = await resolveNotes(context, options.notes);
7966
+ const description = await resolveDescription(context, options.description);
7505
7967
  const { workspace, autoSelected } = await resolveWorkspace(context, {
7506
7968
  apiBaseUrl: commandContext.apiBaseUrl,
7507
7969
  storedWorkspaceId: commandContext.localContext.workspaceId,
@@ -7526,37 +7988,35 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7526
7988
  const taskId = idempotencyId("model") ?? randomUUID();
7527
7989
  const now = Date.now();
7528
7990
  const view = parseTaskView(options.when);
7529
- const startFields = buildTaskStartFields(view);
7530
7991
  const deadlineEpoch = parseDeadline(options.deadline);
7992
+ const clientId = options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId;
7531
7993
  await mutateModel(context, {
7532
7994
  baseUrl: commandContext.apiBaseUrl,
7533
- request: {
7995
+ request: buildMutateEnvelope({
7534
7996
  batchId: idempotencyId("batch") ?? randomUUID(),
7997
+ clientId,
7998
+ clientTxIds: [idempotencyId("tx") ?? randomUUID()],
7535
7999
  transactions: [{
7536
8000
  action: "INSERT",
7537
- clientId: options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId,
7538
- clientTxId: idempotencyId("tx") ?? randomUUID(),
7539
8001
  modelId: taskId,
7540
8002
  modelName: "Task",
7541
- payload: {
7542
- archivedAt: null,
7543
- completedAt: null,
7544
- createdAt: now,
7545
- creatorId: userId,
8003
+ payload: buildTaskInsertFields({
7546
8004
  deadlineAt: deadlineEpoch,
7547
- description: notes,
7548
- key: taskId.replaceAll("-", "").slice(0, 8),
8005
+ description,
8006
+ ids: {
8007
+ creatorId: userId,
8008
+ taskId,
8009
+ workspaceId: workspace.id
8010
+ },
8011
+ now,
7549
8012
  projectId,
7550
- sortOrder: 0,
7551
8013
  teamId,
7552
8014
  title: normalizedTitle,
7553
- todaySortOrder: 0,
7554
- updatedAt: now,
7555
- workspaceId: workspace.id,
7556
- ...startFields
7557
- }
8015
+ todayEpoch: getTodayDateOnlyEpoch(new Date(now)),
8016
+ view
8017
+ })
7558
8018
  }]
7559
- },
8019
+ }),
7560
8020
  token: commandContext.token
7561
8021
  });
7562
8022
  if (context.json) {
@@ -7564,6 +8024,7 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7564
8024
  autoSelected,
7565
8025
  created: true,
7566
8026
  deadlineAt: deadlineEpoch,
8027
+ description,
7567
8028
  id: taskId,
7568
8029
  projectId,
7569
8030
  teamId,
@@ -7577,6 +8038,7 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7577
8038
  if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
7578
8039
  writeSuccess(context, `Added task: ${normalizedTitle}`);
7579
8040
  writeInfo(context, `id: ${taskId}`);
8041
+ if (description) writeInfo(context, `description: ${description}`);
7580
8042
  };
7581
8043
  const runTaskMutation = async (context, taskRef, payload) => {
7582
8044
  const commandContext = await resolveCommandContext(context);
@@ -7644,7 +8106,7 @@ const resolveTeamId = async (context, commandContext, teamRef, workspaceRef) =>
7644
8106
  }), teamRef, "Team");
7645
8107
  };
7646
8108
  const validateTaskEditOptions = (options) => {
7647
- if (options.clearNotes && typeof options.notes === "string") throw new Error("Use either --notes or --clear-notes, not both.");
8109
+ if (options.clearDescription && typeof options.description === "string") throw new Error("Use either --description or --clear-description, not both.");
7648
8110
  if (options.clearDeadline && typeof options.deadline === "string") throw new Error("Use either --deadline or --clear-deadline, not both.");
7649
8111
  if (options.clearProject && typeof options.project === "string") throw new Error("Use either --project or --clear-project, not both.");
7650
8112
  if (options.clearTeam && typeof options.team === "string") throw new Error("Use either --team or --clear-team, not both.");
@@ -7655,10 +8117,10 @@ const applyScalarEdits = (updates, options) => {
7655
8117
  if (title.length === 0) throw new Error("Task title cannot be empty.");
7656
8118
  updates.title = title;
7657
8119
  }
7658
- if (options.clearNotes) updates.description = null;
7659
- else if (typeof options.notes === "string") {
7660
- const notes = options.notes.trim();
7661
- updates.description = notes.length > 0 ? notes : null;
8120
+ if (options.clearDescription) updates.description = null;
8121
+ else if (typeof options.description === "string") {
8122
+ const description = options.description.trim();
8123
+ updates.description = description.length > 0 ? description : null;
7662
8124
  }
7663
8125
  if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(parseTaskView(options.when)));
7664
8126
  if (options.clearDeadline) updates.deadlineAt = null;
@@ -7674,7 +8136,7 @@ const runTaskEdit = async (context, taskRef, options, workspaceOverride) => {
7674
8136
  else if (typeof options.project === "string") updates.projectId = await resolveProjectId(context, commandContext, options.project, wsRef);
7675
8137
  if (options.clearTeam) updates.teamId = null;
7676
8138
  else if (typeof options.team === "string") updates.teamId = await resolveTeamId(context, commandContext, options.team, wsRef);
7677
- if (Object.keys(updates).length === 0) throw new Error("No updates provided. Use --title, --notes, --clear-notes, --when, --deadline, --clear-deadline, --project, --clear-project, --team, or --clear-team.");
8139
+ if (Object.keys(updates).length === 0) throw new Error("No updates provided. Use --title, --description, --clear-description, --when, --deadline, --clear-deadline, --project, --clear-project, --team, or --clear-team.");
7678
8140
  updates.updatedAt = Date.now();
7679
8141
  await runTaskMutation(context, taskRef, {
7680
8142
  action: "UPDATE",
@@ -7781,7 +8243,7 @@ const runTaskRandom = async (context, options, workspaceOverride) => {
7781
8243
  writeInfo(context, `start: ${task.start}`);
7782
8244
  writeInfo(context, `startBucket: ${task.startBucket}`);
7783
8245
  writeInfo(context, `deadline: ${formatDateOnlyEpoch(task.deadlineAt) ?? "(none)"}`);
7784
- if (task.description) writeInfo(context, `notes: ${task.description}`);
8246
+ if (task.description) writeInfo(context, `description: ${task.description}`);
7785
8247
  };
7786
8248
  const resolveChecklistItemReference = async (context, commandContext, taskId, itemRef) => {
7787
8249
  const trimmed = itemRef.trim();
@@ -7852,7 +8314,7 @@ Agent tip: run "donebear spec task add" for full option reference
7852
8314
  }, workspaceOverride);
7853
8315
  });
7854
8316
  });
7855
- task.command("list").alias("ls").description("List tasks").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "open").option("-n, --limit <count>", "Maximum number of tasks", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_TASK_LIST_LIMIT).option("-q, --search <query>", "Filter by title or notes").option("-v, --view <view>", "Filter by view (inbox|anytime|today|upcoming|someday|logbook|trash)").addHelpText("after", `
8317
+ task.command("list").alias("ls").description("List tasks").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "open").option("-n, --limit <count>", "Maximum number of tasks", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_TASK_LIST_LIMIT).option("-q, --search <query>", "Filter by title or description").option("-v, --view <view>", "Filter by view (inbox|anytime|today|upcoming|someday|logbook|trash)").addHelpText("after", `
7856
8318
  Examples:
7857
8319
  donebear task list --json
7858
8320
  donebear task list --state done --json
@@ -7873,7 +8335,7 @@ Views: inbox | anytime | today | upcoming | someday | logbook | trash
7873
8335
  await runTaskShow(context, taskId, options, workspaceOverride);
7874
8336
  });
7875
8337
  });
7876
- task.command("read").description("Print the description (notes) of a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
8338
+ task.command("read").description("Print the description of a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
7877
8339
  const context = contextFromCommand(command);
7878
8340
  await runWithErrorHandling(context, async () => {
7879
8341
  await runTaskRead(context, taskId, options, workspaceOverride);
@@ -7897,12 +8359,12 @@ Views: inbox | anytime | today | upcoming | someday | logbook | trash
7897
8359
  await runTaskRandom(context, options, workspaceOverride);
7898
8360
  });
7899
8361
  });
7900
- task.command("add").alias("create").description("Create a task").argument("[title]", "Task title (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-n, --notes <text>", "Task notes (or pipe via stdin)").option("--when <view>", "inbox|anytime|today|upcoming|someday", "inbox").option("--deadline <date>", "Deadline (YYYY-MM-DD or ISO datetime)").option("--project <key-or-id>", "Project key or id").option("--team <key-or-id>", "Team key or id").option("--idempotency-key <key>", "Stable key making retries safe (repeated calls create only one task)").addHelpText("after", `
8362
+ task.command("add").alias("create").description("Create a task").argument("[title]", "Task title (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-d, --description <text>", "Task description (use --description - to read from piped stdin)").option("--when <view>", "inbox|anytime|today|upcoming|someday", "inbox").option("--deadline <date>", "Deadline (YYYY-MM-DD or ISO datetime)").option("--project <key-or-id>", "Project key or id").option("--team <key-or-id>", "Team key or id").option("--idempotency-key <key>", "Stable key making retries safe (repeated calls create only one task)").addHelpText("after", `
7901
8363
  Examples:
7902
8364
  donebear task add "Fix login bug" --when today --json
7903
8365
  donebear task add "Write tests" --deadline 2030-03-15 --project PROJ --json
7904
- donebear task add "Review PR" --when anytime --notes "Check for XSS" --json
7905
- echo "Multi-line notes" | donebear task add "My task"
8366
+ donebear task add "Review PR" --when anytime --description "Check for XSS" --json
8367
+ echo "Multi-line description" | donebear task add "My task" --description -
7906
8368
  donebear task add # prompts for title interactively
7907
8369
 
7908
8370
  Date format: YYYY-MM-DD (e.g. 2030-03-15)
@@ -7913,7 +8375,7 @@ View (--when): inbox | anytime | today | upcoming | someday
7913
8375
  await runTaskAdd(context, title, options, workspaceOverride);
7914
8376
  });
7915
8377
  });
7916
- task.command("edit").description("Update task fields").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--title <title>", "Replace task title").option("--notes <text>", "Replace task notes (empty string clears)").option("--clear-notes", "Clear task notes").option("--when <view>", "Set view bucket (inbox|anytime|today|upcoming|someday)").option("--deadline <date>", "Set deadline (YYYY-MM-DD or ISO datetime)").option("--clear-deadline", "Clear deadline").option("--project <key-or-id>", "Set project (key or id)").option("--clear-project", "Remove project assignment").option("--team <key-or-id>", "Set team (key or id)").option("--clear-team", "Remove team assignment").action(async (taskId, options, command) => {
8378
+ task.command("edit").description("Update task fields").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--title <title>", "Replace task title").option("--description <text>", "Replace task description (empty string clears)").option("--clear-description", "Clear task description").option("--when <view>", "Set view bucket (inbox|anytime|today|upcoming|someday)").option("--deadline <date>", "Set deadline (YYYY-MM-DD or ISO datetime)").option("--clear-deadline", "Clear deadline").option("--project <key-or-id>", "Set project (key or id)").option("--clear-project", "Remove project assignment").option("--team <key-or-id>", "Set team (key or id)").option("--clear-team", "Remove team assignment").action(async (taskId, options, command) => {
7917
8379
  const context = contextFromCommand(command);
7918
8380
  await runWithErrorHandling(context, async () => {
7919
8381
  await runTaskEdit(context, taskId, options, workspaceOverride);
@@ -7988,6 +8450,13 @@ View (--when): inbox | anytime | today | upcoming | someday
7988
8450
  });
7989
8451
  const itemId = randomUUID();
7990
8452
  const now = Date.now();
8453
+ const checklistItems = await loadChecklistItemsByTask(context, {
8454
+ baseUrl: commandContext.apiBaseUrl,
8455
+ taskId: resolvedTask.id,
8456
+ token: commandContext.token
8457
+ });
8458
+ const maxSortOrder = Math.max(...checklistItems.map((item) => item.sortOrder));
8459
+ const sortOrder = checklistItems.length === 0 ? DEFAULT_RANK_GAP : maxSortOrder + DEFAULT_RANK_GAP;
7991
8460
  await mutateModel(context, {
7992
8461
  baseUrl: commandContext.apiBaseUrl,
7993
8462
  request: {
@@ -8001,7 +8470,7 @@ View (--when): inbox | anytime | today | upcoming | someday
8001
8470
  payload: {
8002
8471
  completedAt: null,
8003
8472
  createdAt: now,
8004
- sortOrder: now,
8473
+ sortOrder,
8005
8474
  taskId: resolvedTask.id,
8006
8475
  title: title.trim(),
8007
8476
  updatedAt: now,
@@ -8225,12 +8694,12 @@ const runSearch = async (context, query, options, workspaceOverride) => {
8225
8694
  writeInfo(context, `No tasks found matching "${trimmedQuery}".`);
8226
8695
  return;
8227
8696
  }
8228
- writeInfo(context, `${scopeLabel(scope)} — "${trimmedQuery}" (${limited.length}/${filtered.length})`);
8697
+ writeInfo(context, `${scopeLabel(scope)}: "${trimmedQuery}" (${limited.length}/${filtered.length})`);
8229
8698
  for (const line of taskListLines(limited)) writeInfo(context, line);
8230
8699
  if (context.copy) copyToClipboard(taskListLines(limited).join("\n"));
8231
8700
  };
8232
8701
  const registerSearchCommand = (program, workspaceOverride) => {
8233
- program.command("search").description("Search tasks by title or notes").argument("[query]", "Search query (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "all").option("-n, --limit <count>", "Maximum number of results", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_SEARCH_LIMIT).addHelpText("after", `
8702
+ program.command("search").description("Search tasks by title or description").argument("[query]", "Search query (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "all").option("-n, --limit <count>", "Maximum number of results", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_SEARCH_LIMIT).addHelpText("after", `
8234
8703
  Examples:
8235
8704
  donebear search "meeting"
8236
8705
  donebear search "launch" --state open
@@ -8777,13 +9246,14 @@ const COMMAND_SPECS = {
8777
9246
  examples: [
8778
9247
  "donebear task add \"Fix login bug\" --when today --json",
8779
9248
  "donebear task add \"Write tests\" --deadline 2026-03-15 --project PROJ --json",
8780
- "donebear task add \"Review PR\" --when anytime --notes \"Triage for bugs\" --json",
8781
- "echo \"Multi-line notes\" | donebear task add \"My task\"",
9249
+ "donebear task add \"Review PR\" --when anytime --description \"Triage for bugs\" --json",
9250
+ "echo \"Multi-line description\" | donebear task add \"My task\" --description -",
8782
9251
  "donebear task add # prompts for title interactively in TTY"
8783
9252
  ],
8784
9253
  json_output: {
8785
9254
  created: "boolean",
8786
9255
  deadlineAt: "number | null",
9256
+ description: "string | null",
8787
9257
  id: "string (UUID)",
8788
9258
  projectId: "string | null",
8789
9259
  teamId: "string | null",
@@ -8792,19 +9262,19 @@ const COMMAND_SPECS = {
8792
9262
  workspaceId: "string",
8793
9263
  workspaceName: "string"
8794
9264
  },
8795
- notes: "Title is prompted interactively when omitted in TTY. Notes can be piped via stdin. Task id accepts full UUID or prefix of >= 4 chars in all other task subcommands.",
9265
+ notes: "Title is prompted interactively when omitted in TTY. The description is NOT read from stdin unless you pass --description - explicitly; without --description the task is created with no description (and the saved description is echoed back in the output). Task id accepts full UUID or prefix of >= 4 chars in all other task subcommands.",
8796
9266
  options: {
8797
9267
  "--deadline": {
8798
9268
  description: "Due date",
8799
9269
  format: "YYYY-MM-DD",
8800
9270
  type: "string"
8801
9271
  },
8802
- "--idempotency-key": {
8803
- description: "Stable key making retries safe (repeated calls create only one task)",
9272
+ "--description": {
9273
+ description: "Body / description text. Pass --description - to read from piped stdin.",
8804
9274
  type: "string"
8805
9275
  },
8806
- "--notes": {
8807
- description: "Body / description text (or pipe via stdin)",
9276
+ "--idempotency-key": {
9277
+ description: "Stable key making retries safe (repeated calls create only one task)",
8808
9278
  type: "string"
8809
9279
  },
8810
9280
  "--project": {
@@ -8847,7 +9317,7 @@ const COMMAND_SPECS = {
8847
9317
  }
8848
9318
  },
8849
9319
  command: "task append",
8850
- description: "Append text to task notes",
9320
+ description: "Append text to a task's description",
8851
9321
  examples: ["donebear task append abc1 \"Additional context\" --json"],
8852
9322
  json_output: {
8853
9323
  id: "string",
@@ -8908,8 +9378,8 @@ const COMMAND_SPECS = {
8908
9378
  description: "Clear the due date",
8909
9379
  type: "boolean"
8910
9380
  },
8911
- "--clear-notes": {
8912
- description: "Clear notes/description",
9381
+ "--clear-description": {
9382
+ description: "Clear the description",
8913
9383
  type: "boolean"
8914
9384
  },
8915
9385
  "--clear-project": {
@@ -8925,8 +9395,8 @@ const COMMAND_SPECS = {
8925
9395
  format: "YYYY-MM-DD",
8926
9396
  type: "string"
8927
9397
  },
8928
- "--notes": {
8929
- description: "Replace notes/description",
9398
+ "--description": {
9399
+ description: "Replace the task description",
8930
9400
  type: "string"
8931
9401
  },
8932
9402
  "--project": {
@@ -9024,7 +9494,7 @@ const COMMAND_SPECS = {
9024
9494
  }
9025
9495
  },
9026
9496
  command: "task prepend",
9027
- description: "Prepend text to task notes",
9497
+ description: "Prepend text to a task's description",
9028
9498
  examples: ["donebear task prepend abc1 \"URGENT: \" --json"],
9029
9499
  json_output: {
9030
9500
  id: "string",
@@ -9040,7 +9510,7 @@ const COMMAND_SPECS = {
9040
9510
  type: "string"
9041
9511
  } },
9042
9512
  command: "task read",
9043
- description: "Print task description/notes as plain text",
9513
+ description: "Print task description as plain text",
9044
9514
  examples: ["donebear task read abc1"],
9045
9515
  options: { "--workspace": { type: "string" } },
9046
9516
  usage: "donebear task read <id> [options]"
@@ -9166,7 +9636,7 @@ const TOP_LEVEL_SPECS = {
9166
9636
  list: "List all API keys with scopes and expiration",
9167
9637
  revoke: "Revoke an API key by id"
9168
9638
  },
9169
- tip: "donebear spec api-key create — get full spec for api-key create"
9639
+ tip: "donebear spec api-key create: get full spec for api-key create"
9170
9640
  },
9171
9641
  auth: {
9172
9642
  command: "auth",
@@ -9177,13 +9647,13 @@ const TOP_LEVEL_SPECS = {
9177
9647
  logout: "Sign out and remove stored session",
9178
9648
  status: "Show auth status"
9179
9649
  },
9180
- tip: "donebear spec auth login — get full spec for auth login"
9650
+ tip: "donebear spec auth login: get full spec for auth login"
9181
9651
  },
9182
9652
  import: {
9183
9653
  command: "import",
9184
9654
  description: "Import external data into Done Bear",
9185
9655
  subcommands: { things: "Import a Things 3 SQLite export into the selected workspace" },
9186
- tip: "donebear spec import things — get full spec for the Things importer"
9656
+ tip: "donebear spec import things: get full spec for the Things importer"
9187
9657
  },
9188
9658
  label: {
9189
9659
  command: "label",
@@ -9208,7 +9678,7 @@ const TOP_LEVEL_SPECS = {
9208
9678
  show: "Get project details by id, key, or name",
9209
9679
  unarchive: "Restore an archived project"
9210
9680
  },
9211
- tip: "donebear spec project add — get full spec for project add"
9681
+ tip: "donebear spec project add: get full spec for project add"
9212
9682
  },
9213
9683
  task: {
9214
9684
  command: "task",
@@ -9216,20 +9686,20 @@ const TOP_LEVEL_SPECS = {
9216
9686
  notes: "Task id accepts full UUID or a prefix of >= 4 chars.",
9217
9687
  subcommands: {
9218
9688
  add: "Create a new task",
9219
- append: "Append text to task notes",
9689
+ append: "Append text to a task's description",
9220
9690
  archive: "Archive a task",
9221
9691
  checklist: "Manage task checklist items",
9222
9692
  done: "Mark a task as complete",
9223
9693
  edit: "Update task fields",
9224
9694
  list: "List tasks (--state, --view, --search, --limit)",
9225
- prepend: "Prepend text to task notes",
9695
+ prepend: "Prepend text to a task's description",
9226
9696
  random: "Pick a random task (--state to filter)",
9227
- read: "Print task notes as plain text",
9697
+ read: "Print task description as plain text",
9228
9698
  reopen: "Reopen a completed task",
9229
9699
  show: "Get task details by id",
9230
9700
  unarchive: "Restore an archived task"
9231
9701
  },
9232
- tip: "donebear spec task add — get full spec for task add"
9702
+ tip: "donebear spec task add: get full spec for task add"
9233
9703
  },
9234
9704
  team: {
9235
9705
  command: "team",
@@ -9253,7 +9723,7 @@ const TOP_LEVEL_SPECS = {
9253
9723
  members: "List workspace members",
9254
9724
  use: "Set the default workspace"
9255
9725
  },
9256
- tip: "donebear spec workspace use — get full spec for workspace use"
9726
+ tip: "donebear spec workspace use: get full spec for workspace use"
9257
9727
  }
9258
9728
  };
9259
9729
  const runSpec = (_context, args) => {
@@ -9287,7 +9757,7 @@ const runSpec = (_context, args) => {
9287
9757
  });
9288
9758
  };
9289
9759
  const registerSpecCommand = (program) => {
9290
- program.command("spec").description("Progressive CLI spec for agent discovery — no auth required").argument("[command]", "Command name (e.g. task, project)").argument("[subcommand]", "Subcommand name (e.g. add, list)").addHelpText("after", `
9760
+ program.command("spec").description("Progressive CLI spec for agent discovery (no auth required)").argument("[command]", "Command name (e.g. task, project)").argument("[subcommand]", "Subcommand name (e.g. add, list)").addHelpText("after", `
9291
9761
  Examples:
9292
9762
  donebear spec Full CLI overview (~500 tokens)
9293
9763
  donebear spec task All task subcommands
@@ -9444,7 +9914,7 @@ const runToday = async (context, options, workspaceOverride) => {
9444
9914
  writeInfo(context, "No tasks for today.");
9445
9915
  return;
9446
9916
  }
9447
- writeInfo(context, `${scopeLabel(scope)} — Today (${limited.length}/${todayTasks.length})`);
9917
+ writeInfo(context, `${scopeLabel(scope)}: Today (${limited.length}/${todayTasks.length})`);
9448
9918
  for (const line of taskListLines(limited)) writeInfo(context, line);
9449
9919
  if (context.copy) copyToClipboard(taskListLines(limited).join("\n"));
9450
9920
  };
@@ -9869,7 +10339,7 @@ With auth:
9869
10339
 
9870
10340
  Auth:
9871
10341
  donebear auth login OAuth browser login
9872
- DONEBEAR_TOKEN=<token> Non-interactive / CI — set this env var
10342
+ DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
9873
10343
 
9874
10344
  Workspace targeting:
9875
10345
  donebear workspace=myorg task list