@sunsteel/contracts 0.85.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,7 +10,9 @@ var REP_TYPES = ["FIXED", "RANGE"];
10
10
  var PROGRESSION_SCHEMES = [
11
11
  "NONE",
12
12
  "DOUBLE_PROGRESSION",
13
- "DYNAMIC_DOUBLE_PROGRESSION"
13
+ "DYNAMIC_DOUBLE_PROGRESSION",
14
+ // ROUT-17: an 8-step block by percentage of a reference max and target RIR.
15
+ "LINEAR_PERIODIZATION"
14
16
  ];
15
17
  var MUSCLE_GROUPS = [
16
18
  "PECTORAL",
@@ -593,6 +595,224 @@ function requiredToFinish(kind) {
593
595
  return k === "WORKING" || k === "DROP";
594
596
  }
595
597
 
598
+ // src/linear-periodization.ts
599
+ var LP_BLOCK_STEPS = 8;
600
+ var LP_WORKING_SETS = 3;
601
+ var LP_REST_SECONDS = 180;
602
+ var LP_REFERENCE_MAX_KG = 1e3;
603
+ var EASY = { rirMin: 2, rirMax: 3, amrap: false };
604
+ var AMRAP = { rirMin: 0, rirMax: 0, amrap: true };
605
+ var HARD = { rirMin: 1, rirMax: 2, amrap: false };
606
+ var LP_STEPS = [
607
+ { step: 1, percentage: 0.63, sets: [EASY, EASY, AMRAP] },
608
+ { step: 2, percentage: 0.66, sets: [EASY, EASY, AMRAP] },
609
+ { step: 3, percentage: 0.69, sets: [EASY, EASY, AMRAP] },
610
+ { step: 4, percentage: 0.72, sets: [EASY, EASY, AMRAP] },
611
+ { step: 5, percentage: 0.75, sets: [HARD, HARD, HARD] },
612
+ { step: 6, percentage: 0.78, sets: [HARD, HARD, HARD] },
613
+ { step: 7, percentage: 0.81, sets: [HARD, HARD, HARD] },
614
+ { step: 8, percentage: 0.85, sets: [HARD, HARD, HARD] }
615
+ ];
616
+ function lpStep(step) {
617
+ const index = Math.min(Math.max(Math.trunc(step) || 1, 1), LP_BLOCK_STEPS);
618
+ return LP_STEPS[index - 1];
619
+ }
620
+ var LP_RECOVERY = { percentage: 0.7, sets: 2, reps: 5 };
621
+ var tidy = (value) => Math.round(value * 1e6) / 1e6;
622
+ function roundToIncrement(value, increment) {
623
+ if (!(increment > 0)) return tidy(value);
624
+ const steps = value / increment;
625
+ const floor = Math.floor(steps + 1e-9);
626
+ const fraction = steps - floor;
627
+ return tidy((fraction > 0.5 + 1e-9 ? floor + 1 : floor) * increment);
628
+ }
629
+ function roundDownToIncrement(value, increment) {
630
+ if (!(increment > 0)) return tidy(value);
631
+ return tidy(Math.floor(value / increment + 1e-9) * increment);
632
+ }
633
+ function lpLoadKg(referenceMaxKg, percentage, incrementKg) {
634
+ return roundToIncrement(referenceMaxKg * percentage, incrementKg);
635
+ }
636
+ var LP_PHASES = ["BLOCK", "RECOVERY", "FINISHED"];
637
+ function startLinearPeriodization(referenceMaxKg, cycle = 1) {
638
+ return {
639
+ referenceMaxKg,
640
+ phase: "BLOCK",
641
+ step: 1,
642
+ cycle,
643
+ samples: [],
644
+ finishedAt: null,
645
+ estimatedMaxKg: null,
646
+ nextReferenceMaxKg: null
647
+ };
648
+ }
649
+ function linearPeriodizationProblem(state) {
650
+ if (!Number.isFinite(state.referenceMaxKg) || state.referenceMaxKg <= 0 || state.referenceMaxKg > LP_REFERENCE_MAX_KG) {
651
+ return "The reference max must be above 0";
652
+ }
653
+ if (!LP_PHASES.includes(state.phase)) {
654
+ return "Unknown block phase";
655
+ }
656
+ if (!Number.isInteger(state.step) || state.step < 1 || state.step > LP_BLOCK_STEPS) {
657
+ return "The block step must be between 1 and 8";
658
+ }
659
+ if (!Number.isInteger(state.cycle) || state.cycle < 1) {
660
+ return "The block number must be 1 or more";
661
+ }
662
+ if (state.phase === "RECOVERY" && !((state.nextReferenceMaxKg ?? 0) > 0)) {
663
+ return "A recovery step needs the next block's reference max";
664
+ }
665
+ return null;
666
+ }
667
+ function lpPrescription(state, incrementKg) {
668
+ if (state.phase === "FINISHED") return [];
669
+ if (state.phase === "RECOVERY") {
670
+ const loadKg2 = lpLoadKg(
671
+ state.referenceMaxKg,
672
+ LP_RECOVERY.percentage,
673
+ incrementKg
674
+ );
675
+ return Array.from({ length: LP_RECOVERY.sets }, () => ({
676
+ loadKg: loadKg2,
677
+ target: null,
678
+ reps: LP_RECOVERY.reps
679
+ }));
680
+ }
681
+ const step = lpStep(state.step);
682
+ const loadKg = lpLoadKg(state.referenceMaxKg, step.percentage, incrementKg);
683
+ return step.sets.map((target) => ({ loadKg, target, reps: null }));
684
+ }
685
+ function lpWorkingSets(sets) {
686
+ return sets.filter((set) => setKindOf(set) !== "WARMUP");
687
+ }
688
+ var LP_ESTIMATE_STEPS = [7, 8];
689
+ var LP_ESTIMATE_MAX_REPS_TO_FAILURE = 10;
690
+ var LP_ESTIMATE_MIN_SETS = 3;
691
+ function lpRirMidpoint(target) {
692
+ return (target.rirMin + target.rirMax) / 2;
693
+ }
694
+ function lpSampleRir(sample) {
695
+ const target = lpStep(sample.step).sets[sample.set - 1] ?? HARD;
696
+ return lpRirMidpoint(target);
697
+ }
698
+ function lpSetEstimateKg(loadKg, reps, rir) {
699
+ return loadKg * (1 + (reps + rir) / 30);
700
+ }
701
+ function lpEstimate(samples, incrementKg) {
702
+ const sets = samples.filter((sample) => LP_ESTIMATE_STEPS.includes(sample.step)).map((sample) => ({ ...sample, rir: lpSampleRir(sample) })).filter(
703
+ (sample) => sample.reps >= 1 && sample.loadKg > 0 && sample.reps + sample.rir <= LP_ESTIMATE_MAX_REPS_TO_FAILURE
704
+ ).map((sample) => ({
705
+ ...sample,
706
+ estimateKg: tidy(lpSetEstimateKg(sample.loadKg, sample.reps, sample.rir))
707
+ })).sort((a, b) => a.step - b.step || a.set - b.set);
708
+ if (sets.length < LP_ESTIMATE_MIN_SETS) {
709
+ return { estimatedMaxKg: null, sets };
710
+ }
711
+ const values = sets.map((set) => set.estimateKg).sort((a, b) => a - b);
712
+ const middle = Math.floor(values.length / 2);
713
+ const median = values.length % 2 ? values[middle] : (values[middle - 1] + values[middle]) / 2;
714
+ return { estimatedMaxKg: roundDownToIncrement(median, incrementKg), sets };
715
+ }
716
+ var LP_PROGRESS_THRESHOLD = 0.025;
717
+ var LP_PROGRESS_CAP = 0.05;
718
+ var LP_NEXT_BLOCK_KINDS = ["PROGRESS", "REPEAT", "RESET"];
719
+ function lpRecommendation(referenceMaxKg, estimatedMaxKg, incrementKg) {
720
+ if (!(estimatedMaxKg && estimatedMaxKg > 0) || !(referenceMaxKg > 0)) {
721
+ return { kind: "REPEAT", nextReferenceMaxKg: referenceMaxKg, change: null };
722
+ }
723
+ const change = estimatedMaxKg / referenceMaxKg - 1;
724
+ if (change >= LP_PROGRESS_THRESHOLD - 1e-9) {
725
+ const capped = Math.min(
726
+ estimatedMaxKg,
727
+ referenceMaxKg * (1 + LP_PROGRESS_CAP)
728
+ );
729
+ const next = roundDownToIncrement(capped, incrementKg);
730
+ return {
731
+ kind: "PROGRESS",
732
+ nextReferenceMaxKg: next > referenceMaxKg ? next : referenceMaxKg,
733
+ change
734
+ };
735
+ }
736
+ if (change < -LP_PROGRESS_THRESHOLD - 1e-9) {
737
+ return {
738
+ kind: "RESET",
739
+ nextReferenceMaxKg: roundDownToIncrement(estimatedMaxKg, incrementKg),
740
+ change
741
+ };
742
+ }
743
+ return { kind: "REPEAT", nextReferenceMaxKg: referenceMaxKg, change };
744
+ }
745
+ function sameLpPosition(a, b) {
746
+ if (!a || !b) return !a && !b;
747
+ return a.phase === b.phase && a.step === b.step && a.cycle === b.cycle && Math.abs(a.referenceMaxKg - b.referenceMaxKg) < 1e-6;
748
+ }
749
+ function advanceLinearPeriodization(state, logged, context) {
750
+ if (state.phase === "FINISHED") return null;
751
+ const prescribed = lpPrescription(state, context.incrementKg).length;
752
+ for (let set = 1; set <= prescribed; set += 1) {
753
+ if (!logged.some((entry) => entry.set === set && entry.completed)) {
754
+ return null;
755
+ }
756
+ }
757
+ if (state.phase === "RECOVERY") {
758
+ return startLinearPeriodization(
759
+ state.nextReferenceMaxKg ?? state.referenceMaxKg,
760
+ state.cycle + 1
761
+ );
762
+ }
763
+ const samples = LP_ESTIMATE_STEPS.includes(state.step) ? [
764
+ ...state.samples.filter((sample) => sample.step !== state.step),
765
+ ...logged.filter((entry) => entry.completed && entry.set <= prescribed).map((entry) => ({
766
+ sessionId: context.sessionId,
767
+ step: state.step,
768
+ set: entry.set,
769
+ loadKg: entry.loadKg,
770
+ reps: entry.reps
771
+ }))
772
+ ] : state.samples;
773
+ if (state.step < LP_BLOCK_STEPS) {
774
+ return { ...state, step: state.step + 1, samples };
775
+ }
776
+ return {
777
+ ...state,
778
+ phase: "FINISHED",
779
+ samples,
780
+ finishedAt: context.finishedAt,
781
+ estimatedMaxKg: lpEstimate(samples, context.incrementKg).estimatedMaxKg,
782
+ nextReferenceMaxKg: null
783
+ };
784
+ }
785
+ function continueLinearPeriodization(state, choice) {
786
+ if (state.phase !== "FINISHED") return null;
787
+ if (choice.recovery) {
788
+ return {
789
+ ...state,
790
+ phase: "RECOVERY",
791
+ nextReferenceMaxKg: choice.referenceMaxKg
792
+ };
793
+ }
794
+ return startLinearPeriodization(choice.referenceMaxKg, state.cycle + 1);
795
+ }
796
+ function lpStoredSets(state, incrementKg) {
797
+ if (!state) {
798
+ return Array.from({ length: LP_WORKING_SETS }, () => ({
799
+ repType: "FIXED",
800
+ reps: null,
801
+ weight: null,
802
+ rir: null,
803
+ kind: "WORKING"
804
+ }));
805
+ }
806
+ const prescription = state.phase === "FINISHED" ? lpPrescription({ ...state, phase: "BLOCK", step: LP_BLOCK_STEPS }, incrementKg) : lpPrescription(state, incrementKg);
807
+ return prescription.map((set) => ({
808
+ repType: "FIXED",
809
+ reps: set.reps,
810
+ weight: set.loadKg,
811
+ rir: set.target ? set.target.rirMin : null,
812
+ kind: "WORKING"
813
+ }));
814
+ }
815
+
596
816
  // src/plate-loading.ts
597
817
  var CANONICAL_SCALE = 1e4;
598
818
  var DENSE_CAPACITY_LIMIT = 2e5;
@@ -1225,7 +1445,9 @@ var NOTIFICATION_KINDS = [
1225
1445
  "TRAINING_PARTNER_ENCOURAGEMENT",
1226
1446
  // NOTIF-07: selected activity from a partner who currently shares it.
1227
1447
  "TRAINING_PARTNER_SESSION",
1228
- "TRAINING_PARTNER_ACHIEVEMENT"
1448
+ "TRAINING_PARTNER_ACHIEVEMENT",
1449
+ // ROUT-18: an LP exercise finished its 8-step block.
1450
+ "LINEAR_BLOCK_FINISHED"
1229
1451
  ];
1230
1452
  var NOTIFICATIONS_LOOKBACK_DAYS = 30;
1231
1453
  var NOTIFICATIONS_RETENTION_DAYS = 90;
@@ -1330,6 +1552,8 @@ var API_ERROR_MESSAGES = {
1330
1552
  REPORT_NOT_FOUND: "Report not found",
1331
1553
  REPORT_SUBJECT_NOT_FOUND: "Subject not found",
1332
1554
  REPORTED_CONTENT_GONE: "The reported content no longer exists",
1555
+ CONVERSATION_NOT_FOUND: "Conversation not found",
1556
+ MESSAGE_NOT_FOUND: "Message not found",
1333
1557
  SHARE_LINK_NOT_FOUND: "Share link not found",
1334
1558
  LINK_NOT_FOUND: "Link not found",
1335
1559
  SHARED_SESSION_NOT_FOUND: "Shared session not found",
@@ -1410,6 +1634,14 @@ var API_ERROR_MESSAGES = {
1410
1634
  SWAP_SESSION_NOT_ACTIVE: "Exercises can only be swapped in an active session",
1411
1635
  SWAP_SETS_COMPLETED: "This exercise already has completed sets. Swap it before completing any.",
1412
1636
  NOTES_DISCARDED_SESSION: "A discarded workout cannot take notes",
1637
+ // ── Linear periodization (ROUT-17 to ROUT-19) ──────────────────────────
1638
+ LINEAR_BLOCK_INVALID: "This exercise's block is not valid: {reason}",
1639
+ LINEAR_BLOCK_EXTRA_SET: "An exercise on an 8-week block takes no extra sets",
1640
+ LINEAR_BLOCK_KIND_FIXED: "The working sets of an 8-week block cannot change kind",
1641
+ LINEAR_BLOCK_LOAD_FIXED: "The load of an 8-week block is prescribed and cannot be changed",
1642
+ LINEAR_BLOCK_SWAP_ROUTINE: "An exercise on an 8-week block can be swapped for this workout only",
1643
+ LINEAR_BLOCK_NOT_LINEAR: "This exercise is not on an 8-week block",
1644
+ LINEAR_BLOCK_NOT_FINISHED: "This exercise's block has not finished, so there is nothing to choose yet",
1413
1645
  // ── Corrections (LIVE-17) ──────────────────────────────────────────────
1414
1646
  CORRECTION_NOT_COMPLETED: "Only a completed workout can be corrected",
1415
1647
  CORRECTION_NOT_LATEST: "Only your most recent workout can be corrected",
@@ -1455,6 +1687,14 @@ var API_ERROR_MESSAGES = {
1455
1687
  PARTNERS_MAX: "An account can have at most {max} training partners.",
1456
1688
  ENCOURAGEMENTS_MAX: "You can send this partner at most {max} encouragements in 24 hours.",
1457
1689
  ENCOURAGEMENT_RETRY: "Please retry the encouragement.",
1690
+ // ── Direct messages (MSG-01) ───────────────────────────────────────────
1691
+ MESSAGE_SELF: "You cannot message yourself",
1692
+ MESSAGE_LENGTH: "A message must not be empty and may be at most {max} characters.",
1693
+ MESSAGES_PER_MINUTE: "You can send at most {max} messages a minute.",
1694
+ CONVERSATIONS_PER_DAY: "You can start at most {max} new conversations a day.",
1695
+ MESSAGE_NOT_ADMITTED: "This member is not taking messages from you.",
1696
+ CONVERSATION_CLOSED: "This conversation can no longer receive messages.",
1697
+ MESSAGING_UNAVAILABLE: "Messaging is not available for this account.",
1458
1698
  // ── Account and settings ───────────────────────────────────────────────
1459
1699
  ACCOUNT_CONFLICT: "Account conflict for this email",
1460
1700
  ACCOUNT_DELETE_CONFIRMATION: "Type your username exactly as it appears to confirm.",
@@ -1507,7 +1747,7 @@ var RECENT_SEARCH_KINDS = ["MEMBER", "EXERCISE", "ROUTINE", "WORKOUT"];
1507
1747
 
1508
1748
  // src/realtime.ts
1509
1749
  var REALTIME_STREAM_PATH = "/realtime/stream";
1510
- var REALTIME_TOPICS = ["notifications"];
1750
+ var REALTIME_TOPICS = ["notifications", "conversations"];
1511
1751
  var REALTIME_HEARTBEAT_MS = 25e3;
1512
1752
  var REALTIME_MAX_STREAM_MS = 14 * 6e4;
1513
1753
  var REALTIME_MAX_STREAMS_PER_MEMBER = 5;
@@ -1532,6 +1772,28 @@ function parseRealtimeEvent(data) {
1532
1772
  }
1533
1773
  return null;
1534
1774
  }
1775
+
1776
+ // src/messages.ts
1777
+ var MESSAGE_PERMISSIONS = ["FOLLOWED", "NOBODY"];
1778
+ var DEFAULT_MESSAGE_PERMISSION = "FOLLOWED";
1779
+ var MESSAGE_BODY_MAX = 2e3;
1780
+ var MESSAGES_PER_MINUTE_MAX = 30;
1781
+ var NEW_CONVERSATIONS_PER_DAY_MAX = 20;
1782
+ var CONVERSATIONS_PAGE_SIZE = 20;
1783
+ var MESSAGES_PAGE_SIZE = 30;
1784
+ function messageBodyLength(body) {
1785
+ return Array.from(body).length;
1786
+ }
1787
+ function normalizeMessageBody(body) {
1788
+ const text = body.replace(/\r\n?/g, "\n").trim();
1789
+ if (text.length === 0 || messageBodyLength(text) > MESSAGE_BODY_MAX) {
1790
+ return null;
1791
+ }
1792
+ return text;
1793
+ }
1794
+ function isMessagePermission(value) {
1795
+ return MESSAGE_PERMISSIONS.includes(value);
1796
+ }
1535
1797
  export {
1536
1798
  ACCOUNT_EXPORT_FORMAT,
1537
1799
  ACCOUNT_EXPORT_VERSION,
@@ -1565,6 +1827,7 @@ export {
1565
1827
  COMEBACK_REQUIRED_ACTIVE_DAYS,
1566
1828
  COMEBACK_SESSION_LOOKBACK,
1567
1829
  COMEBACK_WINDOW_DAYS,
1830
+ CONVERSATIONS_PAGE_SIZE,
1568
1831
  CUSTOM_EXERCISES_MAX,
1569
1832
  CUSTOM_EXERCISE_NAME_MAX,
1570
1833
  CUSTOM_EXERCISE_NOTE_MAX,
@@ -1573,6 +1836,7 @@ export {
1573
1836
  DASHBOARD_SECTION_IDS,
1574
1837
  DEFAULT_DASHBOARD_LAYOUT,
1575
1838
  DEFAULT_LOCALE,
1839
+ DEFAULT_MESSAGE_PERMISSION,
1576
1840
  DEFAULT_WEEK_STARTS_ON,
1577
1841
  DELOAD_DEFAULT_DAYS,
1578
1842
  DELOAD_DEFAULT_LOAD_REDUCTION,
@@ -1607,10 +1871,27 @@ export {
1607
1871
  INCHES_PER_FOOT,
1608
1872
  LENGTH_UNITS,
1609
1873
  LIMITED_SHORTFALL_KG,
1874
+ LP_BLOCK_STEPS,
1875
+ LP_ESTIMATE_MAX_REPS_TO_FAILURE,
1876
+ LP_ESTIMATE_MIN_SETS,
1877
+ LP_ESTIMATE_STEPS,
1878
+ LP_NEXT_BLOCK_KINDS,
1879
+ LP_PHASES,
1880
+ LP_PROGRESS_CAP,
1881
+ LP_PROGRESS_THRESHOLD,
1882
+ LP_RECOVERY,
1883
+ LP_REFERENCE_MAX_KG,
1884
+ LP_REST_SECONDS,
1885
+ LP_STEPS,
1886
+ LP_WORKING_SETS,
1610
1887
  MAX_SETS_PER_EXERCISE,
1611
1888
  MEASURABLE_GOALS_MAX,
1612
1889
  MEASURABLE_GOAL_DIRECTIONS,
1613
1890
  MEASURABLE_GOAL_TYPES,
1891
+ MESSAGES_PAGE_SIZE,
1892
+ MESSAGES_PER_MINUTE_MAX,
1893
+ MESSAGE_BODY_MAX,
1894
+ MESSAGE_PERMISSIONS,
1614
1895
  MINUTES_IN_DAY,
1615
1896
  MODERATION_ACTION_KINDS,
1616
1897
  MODERATION_HISTORY_PAGE_SIZE,
@@ -1618,6 +1899,7 @@ export {
1618
1899
  MODERATION_QUEUE_PAGE_SIZE,
1619
1900
  MOVEMENT_PATTERNS,
1620
1901
  MUSCLE_GROUPS,
1902
+ NEW_CONVERSATIONS_PER_DAY_MAX,
1621
1903
  NOTIFICATIONS_LIST_LIMIT,
1622
1904
  NOTIFICATIONS_LOOKBACK_DAYS,
1623
1905
  NOTIFICATIONS_PAGE_SIZE,
@@ -1743,6 +2025,7 @@ export {
1743
2025
  WEIGHT_UNITS,
1744
2026
  WORKOUT_SESSION_STATUSES,
1745
2027
  accountExportFileName,
2028
+ advanceLinearPeriodization,
1746
2029
  apiError,
1747
2030
  apiErrorMessage,
1748
2031
  applyDeload,
@@ -1753,6 +2036,7 @@ export {
1753
2036
  canLinkToNext,
1754
2037
  cmToFeetInches,
1755
2038
  cmToInches,
2039
+ continueLinearPeriodization,
1756
2040
  countsAsWork,
1757
2041
  countsForProgression,
1758
2042
  customExerciseProblem,
@@ -1774,6 +2058,7 @@ export {
1774
2058
  isDashboardSectionId,
1775
2059
  isDefaultDashboardLayout,
1776
2060
  isGoalSuggestionKey,
2061
+ isMessagePermission,
1777
2062
  isOnboardingStepId,
1778
2063
  isRealtimeTopic,
1779
2064
  isSetKind,
@@ -1781,20 +2066,37 @@ export {
1781
2066
  isWithinQuietHours,
1782
2067
  latestBodyWeight,
1783
2068
  leadWorkingWeight,
2069
+ linearPeriodizationProblem,
2070
+ lpEstimate,
2071
+ lpLoadKg,
2072
+ lpPrescription,
2073
+ lpRecommendation,
2074
+ lpRirMidpoint,
2075
+ lpSampleRir,
2076
+ lpSetEstimateKg,
2077
+ lpStep,
2078
+ lpStoredSets,
2079
+ lpWorkingSets,
1784
2080
  matchesDeletionConfirmation,
2081
+ messageBodyLength,
1785
2082
  normalizeDashboardLayout,
1786
2083
  normalizeExerciseLinks,
1787
2084
  normalizeExerciseName,
2085
+ normalizeMessageBody,
1788
2086
  normalizeSearchQuery,
1789
2087
  parseRealtimeEvent,
1790
2088
  primaryEquipment,
1791
2089
  requiredToFinish,
1792
2090
  resolveActiveTrainingBlock,
1793
2091
  resolveRoutinePlan,
2092
+ roundDownToIncrement,
2093
+ roundToIncrement,
1794
2094
  routineDayLabel,
2095
+ sameLpPosition,
1795
2096
  sessionCorrectionWindow,
1796
2097
  setKindOf,
1797
2098
  setLogValuesDiffer,
2099
+ startLinearPeriodization,
1798
2100
  summarizeBodyProgress,
1799
2101
  trainingBlockCovers,
1800
2102
  warmUpLoad,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunsteel/contracts",
3
- "version": "0.85.0",
3
+ "version": "0.87.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ezee969/sunsteel-contracts.git"