musora-content-services 2.174.0 → 2.176.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.
Files changed (29) hide show
  1. package/.agent/decisions/2026-08-13-behltp-306-add-plan-rank-and-tier-key.md +24 -0
  2. package/.idea/codeStyles/Project.xml +61 -0
  3. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  4. package/.idea/dataSources/6549dc28-6466-4ea3-a66b-7750cfeae4e7/storage_v2/_src_/schema/information_schema.FNRwLQ.meta +2 -0
  5. package/.idea/dataSources/6549dc28-6466-4ea3-a66b-7750cfeae4e7/storage_v2/_src_/schema/mysql.osA4Bg.meta +2 -0
  6. package/.idea/dataSources/6549dc28-6466-4ea3-a66b-7750cfeae4e7/storage_v2/_src_/schema/performance_schema.kIw0nw.meta +2 -0
  7. package/.idea/dataSources/6549dc28-6466-4ea3-a66b-7750cfeae4e7/storage_v2/_src_/schema/sys.zb4BAA.meta +2 -0
  8. package/.idea/dataSources/6549dc28-6466-4ea3-a66b-7750cfeae4e7.xml +25767 -0
  9. package/.idea/dataSources/data_sources_history.xml +23 -0
  10. package/.idea/dataSources.local.xml +19 -0
  11. package/.idea/dataSources.xml +12 -0
  12. package/.idea/modules.xml +8 -0
  13. package/.idea/musora-content-services.iml +8 -0
  14. package/.idea/php.xml +19 -0
  15. package/.idea/prettier.xml +6 -0
  16. package/.idea/vcs.xml +6 -0
  17. package/.idea/workspace.xml +343 -0
  18. package/.yarn/install-state.gz +0 -0
  19. package/CHANGELOG.md +21 -0
  20. package/package.json +1 -1
  21. package/src/index.d.ts +10 -0
  22. package/src/index.js +10 -0
  23. package/src/services/offline/practices.ts +48 -5
  24. package/src/services/user/account.ts +8 -3
  25. package/src/services/user/memberships.ts +2 -0
  26. package/src/services/user/streakCalculator.ts +23 -2
  27. package/src/services/userActivity.js +146 -40
  28. package/src/version-info.js +1 -1
  29. package/.claude/settings.local.json +0 -60
@@ -3,31 +3,74 @@ import { Q } from '@nozbe/watermelondb'
3
3
  import dayjs from 'dayjs'
4
4
  import { globalConfig } from '../config'
5
5
  import { calculateLongestStreaks } from '../userActivity.js'
6
+ import { getMonday } from '../dateUtils.js'
6
7
 
7
8
  /**
8
9
  * @param offlineTimestamp - Minimum `updated_at` epoch ms to include
9
10
  * @param day
10
11
  * @param options.day - Date in YYYY-MM-DD format, defaults to today
11
- * @returns {Promise<{data: {practices: object[], practiceDuration: number}}>}
12
+ * @param options.page - Page number, only applied when `limit` is set (default 1)
13
+ * @param options.limit - Max sessions to return
14
+ * @returns {Promise<{data: {practices: object[], practiceDuration: number, total: number, currentPage: number, totalPages: number}}>}
12
15
  */
13
16
  export async function getPracticeSessionsOffline(
14
17
  offlineTimestamp: number, {
15
- day = dayjs().format('YYYY-MM-DD') }: { day?: string } = {}
18
+ day = dayjs().format('YYYY-MM-DD'),
19
+ page = 1,
20
+ limit
21
+ }: { day?: string, page?: number, limit?: number } = {}
16
22
  ) {
17
23
 
18
24
  const query = await db.practices.queryAll(
19
25
  Q.where('date', day),
20
26
  Q.sortBy('created_at', 'asc'))
21
- const practices = query.data
22
27
 
23
- if (!practices.length) return { data: { practices: [], practiceDuration: 0 } }
28
+ return formatPracticeSessionDataOffline(query.data, page, limit)
29
+ }
30
+
31
+ /**
32
+ * @param offlineTimestamp - Minimum `updated_at` epoch ms to include
33
+ * @param options.page - Page number, only applied when `limit` is set (default 1)
34
+ * @param options.limit - Max sessions to return
35
+ * @returns {Promise<{data: {practices: object[], practiceDuration: number, total: number, currentPage: number, totalPages: number}}>}
36
+ */
37
+ export async function getWeeklyPracticeSessionsOffline(
38
+ offlineTimestamp: number, {
39
+ page = 1,
40
+ limit
41
+ }: { page?: number, limit?: number } = {}
42
+ ) {
43
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
44
+ const startOfWeek = getMonday(new Date(), timeZone)
45
+ const weekDays = Array.from({ length: 7 }, (_, i) => startOfWeek.add(i, 'day').format('YYYY-MM-DD'))
46
+
47
+ const query = await db.practices.queryAll(
48
+ Q.where('date', Q.oneOf(weekDays)),
49
+ Q.sortBy('created_at', 'asc'))
50
+
51
+ return formatPracticeSessionDataOffline(query.data, page, limit)
52
+ }
53
+
54
+ function formatPracticeSessionDataOffline(practices: any[], page: number, limit?: number) {
55
+ if (!practices.length)
56
+ return { data: { practices: [], practiceDuration: 0, total: 0, currentPage: page, totalPages: 1 } }
24
57
 
25
58
  const practiceDuration = Math.round(practices.reduce(
26
59
  (total, practice) => total + (practice.duration_seconds || 0),
27
60
  0
28
61
  ))
29
62
 
30
- return { data: { practices, practiceDuration } }
63
+ const pagedPractices = limit ? practices.slice((page - 1) * limit, page * limit) : practices
64
+
65
+ return {
66
+ data: {
67
+ practices: pagedPractices,
68
+ practiceDuration,
69
+ total: practices.length,
70
+ currentPage: page,
71
+ totalPages: limit ? Math.ceil(practices.length / limit) : 1,
72
+ },
73
+ }
31
74
  }
32
75
 
33
76
  export async function otherStatsOffline(userId = globalConfig.sessionConfig.userId) {
@@ -115,18 +115,23 @@ export interface PendingAccountProps {
115
115
  previousEmail?: string
116
116
  }
117
117
 
118
+ // Not AccountSetupResponse -- this is the permanent shape for this endpoint.
119
+ export interface PendingAccountResponse {
120
+ user_id: number
121
+ }
122
+
118
123
  /**
119
124
  * @param {Object} props - The parameters for creating the pending account.
120
125
  * @property {string} email - The email address for the account.
121
126
  * @property {string} [revenuecatAppUserId] - The RevenueCat App User ID for MA environments.
122
127
  * @property {string} [previousEmail] - The previous email address, if this account replaces an existing one.
123
128
  *
124
- * @returns {Promise<AccountSetupResponse>} - A promise that resolves when the pending account is created or an HttpError if the request fails.
129
+ * @returns {Promise<PendingAccountResponse>} - A promise that resolves when the pending account is created or an HttpError if the request fails.
125
130
  * @throws {HttpError} - Throws an HttpError if the HTTP request fails.
126
131
  */
127
- export async function createPendingAccount(props: PendingAccountProps): Promise<AccountSetupResponse> {
132
+ export async function createPendingAccount(props: PendingAccountProps): Promise<PendingAccountResponse> {
128
133
  const httpClient = new HttpClient(globalConfig.baseUrl)
129
- return await httpClient.post<AccountSetupResponse>(
134
+ return await httpClient.post<PendingAccountResponse>(
130
135
  `/api/user-management-system/v1/accounts/pending`,
131
136
  {
132
137
  email: props.email,
@@ -75,6 +75,8 @@ export interface UpgradeProduct {
75
75
  tier: 'plus' | 'basic' | '' // deprecated in favour of membership_level
76
76
  membership_level: 'plus' | 'basic' | ''
77
77
  plan_type: 'family' | 'duo' | 'solo'
78
+ tier_key: string
79
+ rank: number | null
78
80
  }
79
81
 
80
82
  export interface UpgradeOption {
@@ -1,3 +1,4 @@
1
+ import dayjs from 'dayjs'
1
2
  import { db } from '../sync'
2
3
  import { getStreaksAndMessage } from '../../services/userActivity.js'
3
4
 
@@ -5,8 +6,12 @@ export interface StreakData {
5
6
  currentDailyStreak: number
6
7
  currentWeeklyStreak: number
7
8
  streakMessage: string
9
+ streakMessagePart1: string
10
+ streakMessagePart2: string
8
11
  calculatedAt: number // timestamp
9
12
  lastPracticeDate: string | null
13
+ currentWeekPracticeDays: number
14
+ todaysPracticeSeconds: number
10
15
  }
11
16
  export interface PracticeData {
12
17
  [date: string]: Array<{
@@ -27,14 +32,25 @@ class StreakCalculator {
27
32
  async recalculate(): Promise<StreakData> {
28
33
  const allPractices = await this.fetchAllPractices()
29
34
 
30
- const { currentDailyStreak, currentWeeklyStreak, streakMessage } = getStreaksAndMessage(allPractices)
35
+ const {
36
+ currentDailyStreak,
37
+ currentWeeklyStreak,
38
+ streakMessage,
39
+ streakMessagePart1,
40
+ streakMessagePart2,
41
+ currentWeekPracticeDays,
42
+ } = getStreaksAndMessage(allPractices)
31
43
 
32
44
  this.cache = {
33
45
  currentDailyStreak: currentDailyStreak,
34
46
  currentWeeklyStreak: currentWeeklyStreak,
35
47
  streakMessage: streakMessage,
48
+ streakMessagePart1: streakMessagePart1,
49
+ streakMessagePart2: streakMessagePart2,
36
50
  calculatedAt: Date.now(),
37
- lastPracticeDate: this.getLastPracticeDate(allPractices)
51
+ lastPracticeDate: this.getLastPracticeDate(allPractices),
52
+ currentWeekPracticeDays: currentWeekPracticeDays,
53
+ todaysPracticeSeconds: this.getTodaysPracticeSeconds(allPractices),
38
54
  }
39
55
  return this.cache
40
56
  }
@@ -59,6 +75,11 @@ class StreakCalculator {
59
75
  const dates = Object.keys(practices).sort()
60
76
  return dates.length > 0 ? dates[dates.length - 1] : null
61
77
  }
78
+
79
+ private getTodaysPracticeSeconds(practices: PracticeData): number {
80
+ const today = dayjs().format('YYYY-MM-DD')
81
+ return (practices[today] || []).reduce((total, practice) => total + practice.duration_seconds, 0)
82
+ }
62
83
  }
63
84
 
64
85
 
@@ -18,26 +18,40 @@ import { mapContentsThatWereLastProgressedFromMethod } from "./content-org/learn
18
18
  const DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
19
19
 
20
20
  const streakMessages = {
21
- startStreak: 'Start your streak by taking any lesson!',
22
- restartStreak: 'Restart your streak by taking any lesson!',
21
+ startStreak: { part1: 'Start your streak by taking any lesson!', part2: '' },
22
+ restartStreak: { part1: 'Restart your streak by taking any lesson!', part2: '' },
23
23
 
24
24
  // Messages when last active day is today
25
- dailyStreak: (streak) =>
26
- `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak! Way to keep it going!`,
27
- dailyStreakShort: (streak) =>
28
- `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
29
- weeklyStreak: (streak) =>
30
- `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep up the momentum!`,
31
- greatJobWeeklyStreak: (streak) =>
32
- `Great job! You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep it going!`,
25
+ dailyStreak: (streak) => ({
26
+ part1: `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
27
+ part2: 'Way to keep it going!',
28
+ }),
29
+ dailyStreakShort: (streak) => ({
30
+ part1: `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
31
+ part2: '',
32
+ }),
33
+ weeklyStreak: (streak) => ({
34
+ part1: `You have ${getIndefiniteArticle(streak)} ${streak} week streak!`,
35
+ part2: 'Way to keep up the momentum!',
36
+ }),
37
+ greatJobWeeklyStreak: (streak) => ({
38
+ part1: `Great job! You have ${getIndefiniteArticle(streak)} ${streak} week streak!`,
39
+ part2: 'Way to keep it going!',
40
+ }),
33
41
 
34
42
  // Messages when last active day is NOT today
35
- dailyStreakReminder: (streak) =>
36
- `You have ${getIndefiniteArticle(streak)} ${streak} day streak! Keep it going with any lesson or song!`,
37
- weeklyStreakKeepUp: (streak) =>
38
- `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep up the momentum!`,
39
- weeklyStreakReminder: (streak) =>
40
- `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep it going with any lesson or song!`,
43
+ dailyStreakReminder: (streak) => ({
44
+ part1: `You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
45
+ part2: 'Keep it going with any lesson or song!',
46
+ }),
47
+ weeklyStreakKeepUp: (streak) => ({
48
+ part1: `You have ${getIndefiniteArticle(streak)} ${streak} week streak!`,
49
+ part2: 'Keep up the momentum!',
50
+ }),
51
+ weeklyStreakReminder: (streak) => ({
52
+ part1: `You have ${getIndefiniteArticle(streak)} ${streak} week streak!`,
53
+ part2: 'Keep it going with any lesson or song!',
54
+ }),
41
55
  }
42
56
 
43
57
  function getIndefiniteArticle(streak) {
@@ -46,6 +60,10 @@ function getIndefiniteArticle(streak) {
46
60
  : 'a'
47
61
  }
48
62
 
63
+ function joinStreakMessageParts(part1, part2) {
64
+ return part2 ? `${part1} ${part2}` : part1
65
+ }
66
+
49
67
  async function getUserPractices(userId) {
50
68
  if (userId === globalConfig.sessionConfig.userId) {
51
69
  return getOwnPractices()
@@ -118,7 +136,11 @@ export async function getUserWeeklyStats() {
118
136
  data: {
119
137
  dailyActiveStats: dailyStats,
120
138
  streakMessage: streakData.streakMessage,
139
+ streakMessagePart1: streakData.streakMessagePart1,
140
+ streakMessagePart2: streakData.streakMessagePart2,
121
141
  practices: weekPractices,
142
+ currentWeekPracticeDays: streakData.currentWeekPracticeDays,
143
+ todaysPracticeSeconds: streakData.todaysPracticeSeconds,
122
144
  },
123
145
  }
124
146
  }
@@ -417,9 +439,14 @@ export async function restorePracticeSession(date) {
417
439
  * @param {Object} params - Parameters for fetching practice sessions.
418
440
  * @param {string} params.day - The date for which practice sessions should be retrieved (format: YYYY-MM-DD).
419
441
  * @param {number} [params.userId=globalConfig.sessionConfig.userId] - Optional user ID to retrieve sessions for a specific user. Defaults to the logged-in user.
442
+ * @param {number} [params.page=1] - The page number, only applied when `limit` is set.
443
+ * @param {number} [params.limit] - Max sessions to return; omit to return all of the day's sessions unpaginated.
420
444
  * @returns {Promise<Object>} - A promise that resolves to an object containing:
421
- * - `practices`: An array of formatted practice session data.
422
- * - `practiceDuration`: Total practice duration (in seconds) for the given day.
445
+ * - `practices`: An array of formatted practice session data (paginated if `limit` is set).
446
+ * - `practiceDuration`: Total practice duration (in seconds) for the whole day, regardless of pagination.
447
+ * - `total`: Total number of sessions for the day, regardless of pagination.
448
+ * - `currentPage`: The page number returned
449
+ * - `totalPages`: Total number of pages for the day
423
450
  *
424
451
  * @example
425
452
  * // Get practice sessions for the current user on a specific day
@@ -434,7 +461,7 @@ export async function restorePracticeSession(date) {
434
461
  * .catch(error => console.error(error));
435
462
  */
436
463
  export async function getPracticeSessions(params = {}, options = {}) {
437
- const { day, userId = globalConfig.sessionConfig.userId } = params
464
+ const { day, userId = globalConfig.sessionConfig.userId, page = 1, limit } = params
438
465
 
439
466
  let data
440
467
 
@@ -455,15 +482,66 @@ export async function getPracticeSessions(params = {}, options = {}) {
455
482
  data = query.data
456
483
  }
457
484
 
458
- if (!data.length) return { data: { practices: [], practiceDuration: 0 } }
485
+ return formatPracticeSessionData(data, page, limit)
486
+ }
459
487
 
460
- const formattedMeta = await formatPracticeMeta(data)
461
- const practiceDuration = Math.round(formattedMeta.reduce(
462
- (total, practice) => total + (practice.duration || 0),
463
- 0
464
- ))
488
+ /**
489
+ * Retrieves and formats the current user's practice sessions for the current Monday-Sunday week
490
+ *
491
+ * @param {Object} [params={}] - Parameters for fetching the week's practice sessions.
492
+ * @param {number} [params.page=1] - The page number, only applied when `limit` is set.
493
+ * @param {number} [params.limit] - Max sessions to return; omit to return all of the week's sessions unpaginated.
494
+ * @returns {Promise<Object>} - A promise that resolves to an object containing:
495
+ * - `practices`: An array of formatted practice session data for the week (paginated if `limit` is set).
496
+ * - `practiceDuration`: Total practice duration (in seconds) for the whole week, regardless of pagination.
497
+ * - `total`: Total number of sessions for the week, regardless of pagination.
498
+ * - `currentPage`: The page number returned
499
+ * - `totalPages`: Total number of pages for the week
500
+ *
501
+ * @example
502
+ * getWeeklyPracticeSessions()
503
+ * .then(response => console.log(response))
504
+ * .catch(error => console.error(error));
505
+ */
506
+ export async function getWeeklyPracticeSessions(params = {}, options = {}) {
507
+ const { page = 1, limit } = params
508
+
509
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
510
+ const startOfWeek = getMonday(new Date(), timeZone)
511
+ const weekDays = Array.from({ length: 7 }, (_, i) => startOfWeek.add(i, 'day').format('YYYY-MM-DD'))
512
+
513
+ if (options.pull) {
514
+ await db.practices.pull()
515
+ } else {
516
+ db.practices.pull()
517
+ }
518
+
519
+ const query = await db.practices.queryAll(Q.where('date', Q.oneOf(weekDays)), Q.sortBy('created_at', 'asc'))
520
+ const data = query.data
465
521
 
466
- return { data: { practices: formattedMeta, practiceDuration } }
522
+ return formatPracticeSessionData(data, page, limit)
523
+ }
524
+
525
+ async function formatPracticeSessionData(data, page, limit) {
526
+ if (!data.length)
527
+ return { data: { practices: [], practiceDuration: 0, total: 0, currentPage: page, totalPages: 1 } }
528
+
529
+ const practiceDuration = Math.round(
530
+ data.reduce((total, practice) => total + (practice.duration_seconds || 0), 0)
531
+ )
532
+
533
+ const pagedData = limit ? data.slice((page - 1) * limit, page * limit) : data
534
+ const formattedMeta = await formatPracticeMeta(pagedData)
535
+
536
+ return {
537
+ data: {
538
+ practices: formattedMeta,
539
+ practiceDuration,
540
+ total: data.length,
541
+ currentPage: page,
542
+ totalPages: limit ? Math.ceil(data.length / limit) : 1,
543
+ },
544
+ }
467
545
  }
468
546
 
469
547
  /**
@@ -575,12 +653,22 @@ export async function updatePracticeNotes(payload) {
575
653
  }
576
654
 
577
655
  export function getStreaksAndMessage(practices) {
578
- let { currentDailyStreak, currentWeeklyStreak, streakMessage } = calculateStreaks(practices, true)
656
+ let {
657
+ currentDailyStreak,
658
+ currentWeeklyStreak,
659
+ streakMessage,
660
+ streakMessagePart1,
661
+ streakMessagePart2,
662
+ currentWeekPracticeDays,
663
+ } = calculateStreaks(practices, true)
579
664
 
580
665
  return {
581
666
  currentDailyStreak,
582
667
  currentWeeklyStreak,
583
668
  streakMessage,
669
+ streakMessagePart1,
670
+ streakMessagePart2,
671
+ currentWeekPracticeDays,
584
672
  }
585
673
  }
586
674
 
@@ -589,7 +677,7 @@ function calculateStreaks(practices, includeStreakMessage = false) {
589
677
  let currentDailyStreak = 0
590
678
  let currentWeeklyStreak = 0
591
679
  let lastActiveDay = null
592
- let streakMessage = ''
680
+ let streakMessageParts = { part1: '', part2: '' }
593
681
 
594
682
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
595
683
  let sortedPracticeDays = Object.keys(practices)
@@ -602,7 +690,13 @@ function calculateStreaks(practices, includeStreakMessage = false) {
602
690
  return {
603
691
  currentDailyStreak: 0,
604
692
  currentWeeklyStreak: 0,
605
- streakMessage: streakMessages.startStreak,
693
+ currentWeekPracticeDays: 0,
694
+ streakMessage: joinStreakMessageParts(
695
+ streakMessages.startStreak.part1,
696
+ streakMessages.startStreak.part2
697
+ ),
698
+ streakMessagePart1: streakMessages.startStreak.part1,
699
+ streakMessagePart2: streakMessages.startStreak.part2,
606
700
  }
607
701
  }
608
702
  lastActiveDay = sortedPracticeDays[sortedPracticeDays.length - 1]
@@ -647,14 +741,18 @@ function calculateStreaks(practices, includeStreakMessage = false) {
647
741
  }
648
742
  currentWeeklyStreak = weeklyStreak
649
743
 
744
+ // Qualifying days (calendar days with at least one recorded practice) within the
745
+ // current Monday-anchored week — used for the weekly practice-days target progress.
746
+ let today = new Date()
747
+ let currentWeekStart = getMonday(today, timeZone)
748
+ let currentWeekPracticeDays = sortedPracticeDays.filter((date) => date >= currentWeekStart).length
749
+
650
750
  // Calculate streak message only if includeStreakMessage is true
651
751
  if (includeStreakMessage) {
652
- let today = new Date()
653
752
  today.setHours(0, 0, 0, 0)
654
753
  let yesterday = new Date(today)
655
754
  yesterday.setDate(today.getDate() - 1)
656
755
 
657
- let currentWeekStart = getMonday(today, timeZone)
658
756
  let lastWeekStart = currentWeekStart.subtract(7, 'days')
659
757
 
660
758
  let hasYesterdayPractice = sortedPracticeDays.some((date) => isSameDate(date, yesterday))
@@ -669,13 +767,13 @@ function calculateStreaks(practices, includeStreakMessage = false) {
669
767
 
670
768
  if (isSameDate(lastActiveDay, today)) {
671
769
  if (hasYesterdayPractice) {
672
- streakMessage = streakMessages.dailyStreak(currentDailyStreak)
770
+ streakMessageParts = streakMessages.dailyStreak(currentDailyStreak)
673
771
  } else if (hasCurrentWeekPreviousPractice) {
674
- streakMessage = streakMessages.weeklyStreak(currentWeeklyStreak)
772
+ streakMessageParts = streakMessages.weeklyStreak(currentWeeklyStreak)
675
773
  } else if (hasLastWeekPractice) {
676
- streakMessage = streakMessages.greatJobWeeklyStreak(currentWeeklyStreak)
774
+ streakMessageParts = streakMessages.greatJobWeeklyStreak(currentWeeklyStreak)
677
775
  } else {
678
- streakMessage = streakMessages.dailyStreakShort(currentDailyStreak)
776
+ streakMessageParts = streakMessages.dailyStreakShort(currentDailyStreak)
679
777
  }
680
778
  } else {
681
779
  if (
@@ -683,18 +781,25 @@ function calculateStreaks(practices, includeStreakMessage = false) {
683
781
  (hasYesterdayPractice && sortedPracticeDays.length == 1) ||
684
782
  (hasYesterdayPractice && !hasLastWeekPractice && hasOlderPractice)
685
783
  ) {
686
- streakMessage = streakMessages.dailyStreakReminder(currentDailyStreak)
784
+ streakMessageParts = streakMessages.dailyStreakReminder(currentDailyStreak)
687
785
  } else if (hasCurrentWeekPractice) {
688
- streakMessage = streakMessages.weeklyStreakKeepUp(currentWeeklyStreak)
786
+ streakMessageParts = streakMessages.weeklyStreakKeepUp(currentWeeklyStreak)
689
787
  } else if (hasLastWeekPractice) {
690
- streakMessage = streakMessages.weeklyStreakReminder(currentWeeklyStreak)
788
+ streakMessageParts = streakMessages.weeklyStreakReminder(currentWeeklyStreak)
691
789
  } else {
692
- streakMessage = streakMessages.restartStreak
790
+ streakMessageParts = streakMessages.restartStreak
693
791
  }
694
792
  }
695
793
  }
696
794
 
697
- return { currentDailyStreak, currentWeeklyStreak, streakMessage }
795
+ return {
796
+ currentDailyStreak,
797
+ currentWeeklyStreak,
798
+ streakMessage: joinStreakMessageParts(streakMessageParts.part1, streakMessageParts.part2),
799
+ streakMessagePart1: streakMessageParts.part1,
800
+ streakMessagePart2: streakMessageParts.part2,
801
+ currentWeekPracticeDays,
802
+ }
698
803
  }
699
804
 
700
805
  /**
@@ -805,6 +910,7 @@ async function formatPracticeMeta(practices = []) {
805
910
  return {
806
911
  id: practice.id,
807
912
  auto: practice.auto,
913
+ date: practice.date,
808
914
  thumbnail: practice.content_id ? content?.thumbnail : practice.thumbnail_url || '',
809
915
  thumbnail_url: practice.content_id ? content?.thumbnail : practice.thumbnail_url || '',
810
916
  duration: practice.duration_seconds || 0,
@@ -1 +1 @@
1
- export const MCS_VERSION = '2.174.0'
1
+ export const MCS_VERSION = '2.176.0'
@@ -1,60 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npx jest *)",
5
- "Bash(npx tsc *)",
6
- "Skill(counselors)",
7
- "Bash(counselors ls *)",
8
- "Bash(counselors groups *)",
9
- "Bash(counselors run *)",
10
- "Bash(npm test *)",
11
- "Bash(gh pr *)",
12
- "Bash(gh api *)",
13
- "Bash(mkdir -p /tmp/pr-review-v2)",
14
- "Read(//tmp/pr-review-v2/**)",
15
- "Bash(cat /home/alesevero/railenvironment/applications/musora-content-services/AGENTS.md)",
16
- "Bash(echo \"no AGENTS.md\")",
17
- "Bash(echo \"exit=$?\")",
18
- "Bash(git checkout *)",
19
- "Skill(pr)",
20
- "Skill(create-decision)",
21
- "mcp__github__pull_request_read",
22
- "mcp__github__push_files",
23
- "Bash(git push *)",
24
- "mcp__github__create_pull_request",
25
- "mcp__github__search_pull_requests",
26
- "Bash(git commit *)",
27
- "Bash(git add *)",
28
- "mcp__github__list_pull_requests",
29
- "mcp__sanity__get_schema",
30
- "mcp__sanity__query_documents",
31
- "Bash(git rm *)",
32
- "Bash(git rebase *)"
33
- ]
34
- },
35
- "model": "sonnet",
36
- "enabledMcpjsonServers": [
37
- "atlassian",
38
- "figma",
39
- "google-workspace",
40
- "snowflake",
41
- "aws",
42
- "hex",
43
- "sanity",
44
- "mysql",
45
- "slack",
46
- "langfuse",
47
- "chrome-devtools",
48
- "railway",
49
- "github",
50
- "asana"
51
- ],
52
- "disabledMcpjsonServers": [
53
- "nightwatch",
54
- "1password",
55
- "mysql-staging",
56
- "mysql-local",
57
- "mobile"
58
- ],
59
- "effortLevel": "medium"
60
- }