donebear 0.4.0 → 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;
@@ -7534,37 +7988,35 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7534
7988
  const taskId = idempotencyId("model") ?? randomUUID();
7535
7989
  const now = Date.now();
7536
7990
  const view = parseTaskView(options.when);
7537
- const startFields = buildTaskStartFields(view);
7538
7991
  const deadlineEpoch = parseDeadline(options.deadline);
7992
+ const clientId = options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId;
7539
7993
  await mutateModel(context, {
7540
7994
  baseUrl: commandContext.apiBaseUrl,
7541
- request: {
7995
+ request: buildMutateEnvelope({
7542
7996
  batchId: idempotencyId("batch") ?? randomUUID(),
7997
+ clientId,
7998
+ clientTxIds: [idempotencyId("tx") ?? randomUUID()],
7543
7999
  transactions: [{
7544
8000
  action: "INSERT",
7545
- clientId: options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId,
7546
- clientTxId: idempotencyId("tx") ?? randomUUID(),
7547
8001
  modelId: taskId,
7548
8002
  modelName: "Task",
7549
- payload: {
7550
- archivedAt: null,
7551
- completedAt: null,
7552
- createdAt: now,
7553
- creatorId: userId,
8003
+ payload: buildTaskInsertFields({
7554
8004
  deadlineAt: deadlineEpoch,
7555
8005
  description,
7556
- key: taskId.replaceAll("-", "").slice(0, 8),
8006
+ ids: {
8007
+ creatorId: userId,
8008
+ taskId,
8009
+ workspaceId: workspace.id
8010
+ },
8011
+ now,
7557
8012
  projectId,
7558
- sortOrder: 0,
7559
8013
  teamId,
7560
8014
  title: normalizedTitle,
7561
- todaySortOrder: 0,
7562
- updatedAt: now,
7563
- workspaceId: workspace.id,
7564
- ...startFields
7565
- }
8015
+ todayEpoch: getTodayDateOnlyEpoch(new Date(now)),
8016
+ view
8017
+ })
7566
8018
  }]
7567
- },
8019
+ }),
7568
8020
  token: commandContext.token
7569
8021
  });
7570
8022
  if (context.json) {
@@ -7998,6 +8450,13 @@ View (--when): inbox | anytime | today | upcoming | someday
7998
8450
  });
7999
8451
  const itemId = randomUUID();
8000
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;
8001
8460
  await mutateModel(context, {
8002
8461
  baseUrl: commandContext.apiBaseUrl,
8003
8462
  request: {
@@ -8011,7 +8470,7 @@ View (--when): inbox | anytime | today | upcoming | someday
8011
8470
  payload: {
8012
8471
  completedAt: null,
8013
8472
  createdAt: now,
8014
- sortOrder: now,
8473
+ sortOrder,
8015
8474
  taskId: resolvedTask.id,
8016
8475
  title: title.trim(),
8017
8476
  updatedAt: now,
@@ -8235,7 +8694,7 @@ const runSearch = async (context, query, options, workspaceOverride) => {
8235
8694
  writeInfo(context, `No tasks found matching "${trimmedQuery}".`);
8236
8695
  return;
8237
8696
  }
8238
- writeInfo(context, `${scopeLabel(scope)} — "${trimmedQuery}" (${limited.length}/${filtered.length})`);
8697
+ writeInfo(context, `${scopeLabel(scope)}: "${trimmedQuery}" (${limited.length}/${filtered.length})`);
8239
8698
  for (const line of taskListLines(limited)) writeInfo(context, line);
8240
8699
  if (context.copy) copyToClipboard(taskListLines(limited).join("\n"));
8241
8700
  };
@@ -9177,7 +9636,7 @@ const TOP_LEVEL_SPECS = {
9177
9636
  list: "List all API keys with scopes and expiration",
9178
9637
  revoke: "Revoke an API key by id"
9179
9638
  },
9180
- 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"
9181
9640
  },
9182
9641
  auth: {
9183
9642
  command: "auth",
@@ -9188,13 +9647,13 @@ const TOP_LEVEL_SPECS = {
9188
9647
  logout: "Sign out and remove stored session",
9189
9648
  status: "Show auth status"
9190
9649
  },
9191
- tip: "donebear spec auth login — get full spec for auth login"
9650
+ tip: "donebear spec auth login: get full spec for auth login"
9192
9651
  },
9193
9652
  import: {
9194
9653
  command: "import",
9195
9654
  description: "Import external data into Done Bear",
9196
9655
  subcommands: { things: "Import a Things 3 SQLite export into the selected workspace" },
9197
- 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"
9198
9657
  },
9199
9658
  label: {
9200
9659
  command: "label",
@@ -9219,7 +9678,7 @@ const TOP_LEVEL_SPECS = {
9219
9678
  show: "Get project details by id, key, or name",
9220
9679
  unarchive: "Restore an archived project"
9221
9680
  },
9222
- tip: "donebear spec project add — get full spec for project add"
9681
+ tip: "donebear spec project add: get full spec for project add"
9223
9682
  },
9224
9683
  task: {
9225
9684
  command: "task",
@@ -9240,7 +9699,7 @@ const TOP_LEVEL_SPECS = {
9240
9699
  show: "Get task details by id",
9241
9700
  unarchive: "Restore an archived task"
9242
9701
  },
9243
- tip: "donebear spec task add — get full spec for task add"
9702
+ tip: "donebear spec task add: get full spec for task add"
9244
9703
  },
9245
9704
  team: {
9246
9705
  command: "team",
@@ -9264,7 +9723,7 @@ const TOP_LEVEL_SPECS = {
9264
9723
  members: "List workspace members",
9265
9724
  use: "Set the default workspace"
9266
9725
  },
9267
- tip: "donebear spec workspace use — get full spec for workspace use"
9726
+ tip: "donebear spec workspace use: get full spec for workspace use"
9268
9727
  }
9269
9728
  };
9270
9729
  const runSpec = (_context, args) => {
@@ -9298,7 +9757,7 @@ const runSpec = (_context, args) => {
9298
9757
  });
9299
9758
  };
9300
9759
  const registerSpecCommand = (program) => {
9301
- 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", `
9302
9761
  Examples:
9303
9762
  donebear spec Full CLI overview (~500 tokens)
9304
9763
  donebear spec task All task subcommands
@@ -9455,7 +9914,7 @@ const runToday = async (context, options, workspaceOverride) => {
9455
9914
  writeInfo(context, "No tasks for today.");
9456
9915
  return;
9457
9916
  }
9458
- writeInfo(context, `${scopeLabel(scope)} — Today (${limited.length}/${todayTasks.length})`);
9917
+ writeInfo(context, `${scopeLabel(scope)}: Today (${limited.length}/${todayTasks.length})`);
9459
9918
  for (const line of taskListLines(limited)) writeInfo(context, line);
9460
9919
  if (context.copy) copyToClipboard(taskListLines(limited).join("\n"));
9461
9920
  };
@@ -9880,7 +10339,7 @@ With auth:
9880
10339
 
9881
10340
  Auth:
9882
10341
  donebear auth login OAuth browser login
9883
- DONEBEAR_TOKEN=<token> Non-interactive / CI — set this env var
10342
+ DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
9884
10343
 
9885
10344
  Workspace targeting:
9886
10345
  donebear workspace=myorg task list