@tuturuuu/ai 0.0.10

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 (130) hide show
  1. package/README.md +76 -0
  2. package/package.json +106 -0
  3. package/src/api-key-hash.ts +28 -0
  4. package/src/calendar/events.ts +34 -0
  5. package/src/calendar/route.ts +114 -0
  6. package/src/chat/credit-source.ts +1 -0
  7. package/src/chat/google/chat-request-schema.ts +150 -0
  8. package/src/chat/google/default-system-instruction.ts +198 -0
  9. package/src/chat/google/message-file-processing.ts +212 -0
  10. package/src/chat/google/mira-step-preparation.ts +221 -0
  11. package/src/chat/google/new/route.ts +368 -0
  12. package/src/chat/google/route-auth.ts +81 -0
  13. package/src/chat/google/route-chat-resolution.ts +98 -0
  14. package/src/chat/google/route-credits.ts +61 -0
  15. package/src/chat/google/route-message-preparation.ts +331 -0
  16. package/src/chat/google/route-mira-runtime.ts +206 -0
  17. package/src/chat/google/route.ts +632 -0
  18. package/src/chat/google/stream-finish-persistence.ts +722 -0
  19. package/src/chat/google/summary/route.ts +153 -0
  20. package/src/chat/mira-render-ui-policy.ts +540 -0
  21. package/src/chat/mira-system-instruction.ts +484 -0
  22. package/src/chat-sdk/adapters.ts +389 -0
  23. package/src/chat-sdk/registry.ts +197 -0
  24. package/src/chat-sdk.ts +33 -0
  25. package/src/core.ts +3 -0
  26. package/src/credits/cap-output-tokens.ts +90 -0
  27. package/src/credits/check-credits.ts +232 -0
  28. package/src/credits/constants.ts +30 -0
  29. package/src/credits/index.ts +46 -0
  30. package/src/credits/model-mapping.ts +92 -0
  31. package/src/credits/reservations.ts +514 -0
  32. package/src/credits/resolve-plan-model.ts +219 -0
  33. package/src/credits/sync-gateway-models.ts +351 -0
  34. package/src/credits/types.ts +109 -0
  35. package/src/credits/use-ai-credits.ts +3 -0
  36. package/src/embeddings/metered.ts +283 -0
  37. package/src/executions/route.ts +137 -0
  38. package/src/generate/route.ts +411 -0
  39. package/src/hooks.ts +7 -0
  40. package/src/meetings/summary/route.ts +7 -0
  41. package/src/meetings/transcription/route.ts +134 -0
  42. package/src/memory/client.ts +158 -0
  43. package/src/memory/config.ts +38 -0
  44. package/src/memory/index.ts +32 -0
  45. package/src/memory/ingest.ts +51 -0
  46. package/src/memory/middleware.ts +35 -0
  47. package/src/memory/operations.ts +480 -0
  48. package/src/memory/scope.ts +102 -0
  49. package/src/memory/settings.ts +121 -0
  50. package/src/memory/types.ts +101 -0
  51. package/src/memory/workspace.ts +36 -0
  52. package/src/memory.ts +1 -0
  53. package/src/mind/patch.ts +146 -0
  54. package/src/mind/route.ts +687 -0
  55. package/src/mind/tools.ts +1500 -0
  56. package/src/mind/types.ts +20 -0
  57. package/src/object/core.ts +3 -0
  58. package/src/object/flashcards/route.ts +140 -0
  59. package/src/object/quizzes/explanation/route.ts +145 -0
  60. package/src/object/quizzes/route.ts +142 -0
  61. package/src/object/types.ts +187 -0
  62. package/src/object/year-plan/route.ts +196 -0
  63. package/src/react.ts +1 -0
  64. package/src/scheduling/algorithm.ts +791 -0
  65. package/src/scheduling/default.ts +36 -0
  66. package/src/scheduling/duration-optimizer.ts +689 -0
  67. package/src/scheduling/index.ts +79 -0
  68. package/src/scheduling/priority-calculator.ts +187 -0
  69. package/src/scheduling/recurrence-calculator.ts +621 -0
  70. package/src/scheduling/templates.ts +892 -0
  71. package/src/scheduling/types.ts +136 -0
  72. package/src/scheduling/web-adapter.ts +308 -0
  73. package/src/scheduling.ts +6 -0
  74. package/src/supported-actions.ts +1 -0
  75. package/src/supported-providers.ts +6 -0
  76. package/src/tools/context-builder.ts +372 -0
  77. package/src/tools/core.ts +1 -0
  78. package/src/tools/definitions/calendar.ts +106 -0
  79. package/src/tools/definitions/finance.ts +197 -0
  80. package/src/tools/definitions/image.ts +74 -0
  81. package/src/tools/definitions/memory.ts +83 -0
  82. package/src/tools/definitions/meta.ts +154 -0
  83. package/src/tools/definitions/render-ui.ts +81 -0
  84. package/src/tools/definitions/tasks.ts +343 -0
  85. package/src/tools/definitions/time-tracking.ts +381 -0
  86. package/src/tools/definitions/workspace-context.ts +45 -0
  87. package/src/tools/definitions/workspace-user-chat.ts +111 -0
  88. package/src/tools/executors/calendar.ts +371 -0
  89. package/src/tools/executors/chat.ts +15 -0
  90. package/src/tools/executors/finance.ts +638 -0
  91. package/src/tools/executors/helpers/encryption.ts +107 -0
  92. package/src/tools/executors/image.ts +247 -0
  93. package/src/tools/executors/markitdown.ts +684 -0
  94. package/src/tools/executors/memory.ts +277 -0
  95. package/src/tools/executors/parallel-checks.ts +176 -0
  96. package/src/tools/executors/qr.ts +170 -0
  97. package/src/tools/executors/scope-helpers.ts +192 -0
  98. package/src/tools/executors/search.ts +149 -0
  99. package/src/tools/executors/settings.ts +40 -0
  100. package/src/tools/executors/tasks.ts +1087 -0
  101. package/src/tools/executors/theme.ts +23 -0
  102. package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
  103. package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
  104. package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
  105. package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
  106. package/src/tools/executors/timer/timer-helpers.ts +372 -0
  107. package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
  108. package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
  109. package/src/tools/executors/timer/timer-mutations.ts +19 -0
  110. package/src/tools/executors/timer/timer-queries.ts +18 -0
  111. package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
  112. package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
  113. package/src/tools/executors/timer/timer-session-queries.ts +153 -0
  114. package/src/tools/executors/timer/timer-session-updates.ts +200 -0
  115. package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
  116. package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
  117. package/src/tools/executors/timer.ts +22 -0
  118. package/src/tools/executors/user.ts +60 -0
  119. package/src/tools/executors/workspace.ts +135 -0
  120. package/src/tools/json-render-catalog.ts +875 -0
  121. package/src/tools/mira-tool-definitions.ts +55 -0
  122. package/src/tools/mira-tool-dispatcher.ts +265 -0
  123. package/src/tools/mira-tool-metadata.ts +164 -0
  124. package/src/tools/mira-tool-names.ts +95 -0
  125. package/src/tools/mira-tool-render-ui.ts +54 -0
  126. package/src/tools/mira-tool-types.ts +17 -0
  127. package/src/tools/mira-tools.ts +167 -0
  128. package/src/tools/normalize-render-ui-input.ts +321 -0
  129. package/src/tools/workspace-context.ts +233 -0
  130. package/src/types.ts +38 -0
@@ -0,0 +1,272 @@
1
+ import dayjs from 'dayjs';
2
+ import timezone from 'dayjs/plugin/timezone';
3
+ import utc from 'dayjs/plugin/utc';
4
+ import type { MiraToolContext } from '../../mira-tools';
5
+ import { getWorkspaceContextWorkspaceId } from '../../workspace-context';
6
+ import { buildToolFailure } from './timer-helpers';
7
+ import { fetchTimeTrackerStats } from './timer-stats-executor';
8
+
9
+ dayjs.extend(utc);
10
+ dayjs.extend(timezone);
11
+
12
+ export type TimeTrackerGoalRow = {
13
+ id: string;
14
+ category_id: string | null;
15
+ daily_goal_minutes: number;
16
+ weekly_goal_minutes: number | null;
17
+ is_active: boolean;
18
+ created_at: string;
19
+ updated_at: string;
20
+ category:
21
+ | {
22
+ id: string;
23
+ name: string | null;
24
+ color: string | null;
25
+ }
26
+ | Array<{
27
+ id: string;
28
+ name: string | null;
29
+ color: string | null;
30
+ }>
31
+ | null;
32
+ };
33
+
34
+ export function normalizeCategory(
35
+ category: TimeTrackerGoalRow['category']
36
+ ): { id: string; name: string | null; color: string | null } | null {
37
+ if (!category) return null;
38
+ if (Array.isArray(category)) return category[0] ?? null;
39
+ return category;
40
+ }
41
+
42
+ export async function executeGetTimeTrackerGoals(
43
+ args: Record<string, unknown>,
44
+ ctx: MiraToolContext
45
+ ) {
46
+ const includeInactive = Boolean(args.includeInactive);
47
+ const includeProgress =
48
+ typeof args.includeProgress === 'boolean' ? args.includeProgress : true;
49
+ const timezoneArg =
50
+ typeof args.timezone === 'string' ? args.timezone : undefined;
51
+ const workspaceId = getWorkspaceContextWorkspaceId(ctx);
52
+
53
+ const { count: totalGoalCount, error: totalCountError } = await ctx.supabase
54
+ .from('time_tracking_goals')
55
+ .select('id', { count: 'exact', head: true })
56
+ .eq('ws_id', workspaceId)
57
+ .eq('user_id', ctx.userId);
58
+
59
+ if (totalCountError) {
60
+ return buildToolFailure(
61
+ 'TT_GOALS_COUNT_FAILED',
62
+ totalCountError.message,
63
+ true
64
+ );
65
+ }
66
+
67
+ let query = ctx.supabase
68
+ .from('time_tracking_goals')
69
+ .select(
70
+ `
71
+ id,
72
+ category_id,
73
+ daily_goal_minutes,
74
+ weekly_goal_minutes,
75
+ is_active,
76
+ created_at,
77
+ updated_at,
78
+ category:time_tracking_categories(id, name, color)
79
+ `
80
+ )
81
+ .eq('ws_id', workspaceId)
82
+ .eq('user_id', ctx.userId)
83
+ .order('created_at', { ascending: false });
84
+
85
+ if (!includeInactive) {
86
+ query = query.eq('is_active', true);
87
+ }
88
+
89
+ const { data, error } = await query;
90
+ if (error)
91
+ return buildToolFailure('TT_GOALS_FETCH_FAILED', error.message, true);
92
+
93
+ const goals = ((data ?? []) as TimeTrackerGoalRow[]).map((goal) => ({
94
+ ...goal,
95
+ category: normalizeCategory(goal.category),
96
+ }));
97
+
98
+ const activeGoalCount = goals.filter((goal) => goal.is_active).length;
99
+
100
+ if (!includeProgress) {
101
+ return {
102
+ success: true,
103
+ workspaceId,
104
+ count: goals.length,
105
+ goals,
106
+ meta: {
107
+ workspaceId,
108
+ workspaceContextId:
109
+ ctx.workspaceContext?.workspaceContextId ?? ctx.wsId,
110
+ isPersonalContext: ctx.workspaceContext?.personal ?? false,
111
+ filtersApplied: {
112
+ includeInactive,
113
+ includeProgress,
114
+ },
115
+ counts: {
116
+ totalGoalCount: totalGoalCount ?? 0,
117
+ activeGoalCount,
118
+ returnedGoalCount: goals.length,
119
+ },
120
+ units: {
121
+ durations: 'seconds',
122
+ goalTargets: 'minutes',
123
+ progress: 'percent',
124
+ },
125
+ },
126
+ };
127
+ }
128
+
129
+ const statsResult = await fetchTimeTrackerStats(ctx, {
130
+ timezone: timezoneArg,
131
+ summaryOnly: true,
132
+ });
133
+ if (!statsResult.success) return statsResult;
134
+
135
+ const categoryIds = Array.from(
136
+ new Set(
137
+ goals
138
+ .map((goal) => goal.category_id)
139
+ .filter(
140
+ (categoryId): categoryId is string => typeof categoryId === 'string'
141
+ )
142
+ )
143
+ );
144
+
145
+ const categoryTodayTime = new Map<string, number>();
146
+ const categoryWeekTime = new Map<string, number>();
147
+
148
+ if (categoryIds.length > 0) {
149
+ const nowInTimezone = dayjs().tz(statsResult.timezone);
150
+ const startOfDay = nowInTimezone.startOf('day');
151
+ const daysFromMonday = (startOfDay.day() + 6) % 7;
152
+ const startOfWeek = startOfDay.subtract(daysFromMonday, 'day');
153
+
154
+ const { data: categorySessions, error: categorySessionsError } =
155
+ await ctx.supabase
156
+ .from('time_tracking_sessions')
157
+ .select('category_id, start_time, duration_seconds')
158
+ .eq('ws_id', workspaceId)
159
+ .eq('user_id', ctx.userId)
160
+ .in('category_id', categoryIds)
161
+ .not('duration_seconds', 'is', null)
162
+ .gte('start_time', startOfWeek.toISOString());
163
+
164
+ if (categorySessionsError) {
165
+ return buildToolFailure(
166
+ 'TT_GOALS_PROGRESS_STATS_FAILED',
167
+ categorySessionsError.message,
168
+ true
169
+ );
170
+ }
171
+
172
+ for (const session of categorySessions ?? []) {
173
+ if (
174
+ typeof session.category_id !== 'string' ||
175
+ typeof session.start_time !== 'string' ||
176
+ typeof session.duration_seconds !== 'number'
177
+ ) {
178
+ continue;
179
+ }
180
+
181
+ const sessionStart = dayjs(session.start_time);
182
+ if (!sessionStart.isValid()) continue;
183
+
184
+ const durationSeconds = session.duration_seconds;
185
+ const categoryId = session.category_id;
186
+
187
+ categoryWeekTime.set(
188
+ categoryId,
189
+ (categoryWeekTime.get(categoryId) ?? 0) + durationSeconds
190
+ );
191
+
192
+ if (sessionStart.isSame(startOfDay) || sessionStart.isAfter(startOfDay)) {
193
+ categoryTodayTime.set(
194
+ categoryId,
195
+ (categoryTodayTime.get(categoryId) ?? 0) + durationSeconds
196
+ );
197
+ }
198
+ }
199
+ }
200
+
201
+ const progressGoals = goals.map((goal) => {
202
+ const goalTodayTime =
203
+ goal.category_id === null
204
+ ? statsResult.todayTime
205
+ : (categoryTodayTime.get(goal.category_id) ?? 0);
206
+ const goalWeekTime =
207
+ goal.category_id === null
208
+ ? statsResult.weekTime
209
+ : (categoryWeekTime.get(goal.category_id) ?? 0);
210
+
211
+ const dailyProgress =
212
+ goal.daily_goal_minutes > 0
213
+ ? Math.min((goalTodayTime / 60 / goal.daily_goal_minutes) * 100, 100)
214
+ : 0;
215
+
216
+ const weeklyProgress =
217
+ goal.weekly_goal_minutes && goal.weekly_goal_minutes > 0
218
+ ? Math.min((goalWeekTime / 60 / goal.weekly_goal_minutes) * 100, 100)
219
+ : null;
220
+
221
+ return {
222
+ ...goal,
223
+ progress: {
224
+ dailyPercent: Number(dailyProgress.toFixed(2)),
225
+ dailyCompleted: dailyProgress >= 100,
226
+ weeklyPercent:
227
+ weeklyProgress === null ? null : Number(weeklyProgress.toFixed(2)),
228
+ weeklyCompleted: weeklyProgress === null ? null : weeklyProgress >= 100,
229
+ },
230
+ };
231
+ });
232
+
233
+ return {
234
+ success: true,
235
+ workspaceId,
236
+ timezone: statsResult.timezone,
237
+ count: progressGoals.length,
238
+ current: {
239
+ todayTime: statsResult.todayTime,
240
+ weekTime: statsResult.weekTime,
241
+ monthTime: statsResult.monthTime,
242
+ streak: statsResult.streak,
243
+ },
244
+ goals: progressGoals,
245
+ meta: {
246
+ workspaceId,
247
+ workspaceContextId: ctx.workspaceContext?.workspaceContextId ?? ctx.wsId,
248
+ isPersonalContext: ctx.workspaceContext?.personal ?? false,
249
+ filtersApplied: {
250
+ includeInactive,
251
+ includeProgress,
252
+ },
253
+ counts: {
254
+ totalGoalCount: totalGoalCount ?? 0,
255
+ activeGoalCount,
256
+ returnedGoalCount: progressGoals.length,
257
+ },
258
+ timezone: {
259
+ requested: statsResult.timezoneResolution.requested,
260
+ resolved: statsResult.timezoneResolution.resolved,
261
+ validRequested: statsResult.timezoneResolution.validRequested,
262
+ usedFallback: statsResult.timezoneResolution.usedFallback,
263
+ },
264
+ units: {
265
+ durations: 'seconds',
266
+ streak: 'days',
267
+ goalTargets: 'minutes',
268
+ progress: 'percent',
269
+ },
270
+ },
271
+ };
272
+ }
@@ -0,0 +1,372 @@
1
+ import dayjs from 'dayjs';
2
+ import timezone from 'dayjs/plugin/timezone';
3
+ import utc from 'dayjs/plugin/utc';
4
+ import type { MiraToolContext } from '../../mira-tools';
5
+ import { getWorkspaceContextWorkspaceId } from '../../workspace-context';
6
+
7
+ dayjs.extend(utc);
8
+ dayjs.extend(timezone);
9
+
10
+ export const MIN_DURATION_SECONDS = 60;
11
+ const ENABLE_APPROVAL_BYPASS_CHECK = false;
12
+
13
+ export type ToolFailure = {
14
+ success: false;
15
+ error: string;
16
+ errorCode: string;
17
+ retryable: boolean;
18
+ };
19
+
20
+ function parseDateOnly(
21
+ value: unknown,
22
+ fieldName: string
23
+ ): { ok: true; value: string } | { ok: false; error: string } {
24
+ if (typeof value !== 'string' || !value.trim()) {
25
+ return { ok: false, error: `${fieldName} is required` };
26
+ }
27
+
28
+ const trimmed = value.trim();
29
+ const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
30
+ if (!match) {
31
+ return { ok: false, error: `${fieldName} must use YYYY-MM-DD format` };
32
+ }
33
+
34
+ const [, yearString, monthString, dayString] = match;
35
+ const year = Number(yearString);
36
+ const month = Number(monthString);
37
+ const day = Number(dayString);
38
+
39
+ if (month < 1 || month > 12 || day < 1 || day > 31) {
40
+ return { ok: false, error: `${fieldName} must be a valid date` };
41
+ }
42
+
43
+ const parsed = new Date(Date.UTC(year, month - 1, day));
44
+ if (
45
+ Number.isNaN(parsed.getTime()) ||
46
+ parsed.getUTCFullYear() !== year ||
47
+ parsed.getUTCMonth() + 1 !== month ||
48
+ parsed.getUTCDate() !== day
49
+ ) {
50
+ return { ok: false, error: `${fieldName} must be a valid date` };
51
+ }
52
+
53
+ return { ok: true, value: trimmed };
54
+ }
55
+
56
+ export function parseFlexibleDateTime(
57
+ value: unknown,
58
+ fieldName: string,
59
+ options?: { date?: unknown; timezone?: string }
60
+ ): { ok: true; value: Date } | { ok: false; error: string } {
61
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
62
+ return { ok: true, value };
63
+ }
64
+
65
+ if (typeof value !== 'string' || !value.trim()) {
66
+ return { ok: false, error: `${fieldName} is required` };
67
+ }
68
+
69
+ const resolvedTimezone =
70
+ typeof options?.timezone === 'string' &&
71
+ options.timezone.trim().length > 0 &&
72
+ isValidIanaTimezone(options.timezone.trim())
73
+ ? options.timezone.trim()
74
+ : null;
75
+
76
+ const parseNaiveDateTime = (input: string): Date | null => {
77
+ if (resolvedTimezone) {
78
+ const tzParsed = dayjs.tz(input, resolvedTimezone);
79
+ if (tzParsed.isValid()) {
80
+ return tzParsed.toDate();
81
+ }
82
+ } else {
83
+ const utcParsed = dayjs.utc(input);
84
+ if (utcParsed.isValid()) {
85
+ return utcParsed.toDate();
86
+ }
87
+ }
88
+
89
+ const parsed = new Date(input);
90
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
91
+ };
92
+
93
+ const trimmed = value.trim();
94
+ const isoDateTimeMatch = trimmed.match(
95
+ /^(\d{4}-\d{2}-\d{2})T([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d)(?:\.\d{1,3})?)?(?:Z|[+-][01]\d:[0-5]\d)?$/
96
+ );
97
+ if (isoDateTimeMatch) {
98
+ const [, datePart] = isoDateTimeMatch;
99
+ const dateParsed = parseDateOnly(datePart, fieldName);
100
+ if (!dateParsed.ok) {
101
+ return { ok: false, error: dateParsed.error };
102
+ }
103
+
104
+ const hasExplicitOffset = /(?:Z|[+-][01]\d:[0-5]\d)$/i.test(trimmed);
105
+ const parsed = hasExplicitOffset
106
+ ? new Date(trimmed)
107
+ : parseNaiveDateTime(trimmed);
108
+
109
+ if (parsed && !Number.isNaN(parsed.getTime())) {
110
+ return { ok: true, value: parsed };
111
+ }
112
+ }
113
+
114
+ const dateTimeWithSpaceMatch = trimmed.match(
115
+ /^(\d{4}-\d{2}-\d{2})\s+([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/
116
+ );
117
+ if (dateTimeWithSpaceMatch) {
118
+ const [, datePart, hours, minutes, seconds = '00'] = dateTimeWithSpaceMatch;
119
+ const dateParsed = parseDateOnly(datePart, fieldName);
120
+ if (!dateParsed.ok) {
121
+ return { ok: false, error: dateParsed.error };
122
+ }
123
+
124
+ const combined = parseNaiveDateTime(
125
+ `${datePart}T${hours}:${minutes}:${seconds}`
126
+ );
127
+ if (combined) {
128
+ return { ok: true, value: combined };
129
+ }
130
+ }
131
+
132
+ const timeOnlyMatch = trimmed.match(
133
+ /^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/
134
+ );
135
+ if (timeOnlyMatch) {
136
+ const dateParsed = parseDateOnly(options?.date, 'date');
137
+ if (!dateParsed.ok) {
138
+ return {
139
+ ok: false,
140
+ error: `${fieldName} must be a valid ISO datetime, or HH:mm/HH:mm:ss when date is provided in YYYY-MM-DD format`,
141
+ };
142
+ }
143
+
144
+ const [, hours, minutes, seconds = '00'] = timeOnlyMatch;
145
+ const combined = parseNaiveDateTime(
146
+ `${dateParsed.value}T${hours}:${minutes}:${seconds}`
147
+ );
148
+ if (combined) {
149
+ return { ok: true, value: combined };
150
+ }
151
+ }
152
+
153
+ return {
154
+ ok: false,
155
+ error:
156
+ `${fieldName} must be a valid ISO datetime, YYYY-MM-DD HH:mm, ` +
157
+ `or HH:mm/HH:mm:ss when date is provided in YYYY-MM-DD format`,
158
+ };
159
+ }
160
+
161
+ export function normalizeCursor(cursor: unknown):
162
+ | {
163
+ ok: true;
164
+ lastStartTime: string;
165
+ lastId: string;
166
+ }
167
+ | {
168
+ ok: false;
169
+ error: string;
170
+ } {
171
+ if (typeof cursor !== 'string' || !cursor.includes('|')) {
172
+ return { ok: false, error: 'Invalid cursor format' };
173
+ }
174
+
175
+ const [lastStartTime, lastId] = cursor.split('|');
176
+ if (!lastStartTime || !lastId) {
177
+ return { ok: false, error: 'Invalid cursor format' };
178
+ }
179
+
180
+ return { ok: true, lastStartTime, lastId };
181
+ }
182
+
183
+ export function coerceOptionalString(value: unknown): string | null {
184
+ if (typeof value !== 'string') return null;
185
+ const trimmed = value.trim();
186
+ return trimmed.length > 0 ? trimmed : null;
187
+ }
188
+
189
+ export function toFiniteNumber(value: unknown, fallback = 0): number {
190
+ const coerced = Number(value);
191
+ return Number.isFinite(coerced) ? coerced : fallback;
192
+ }
193
+
194
+ export function buildToolFailure(
195
+ errorCode: string,
196
+ message: string,
197
+ retryable: boolean
198
+ ): ToolFailure {
199
+ return {
200
+ success: false,
201
+ error: message,
202
+ errorCode,
203
+ retryable,
204
+ };
205
+ }
206
+
207
+ export function isValidIanaTimezone(value: string): boolean {
208
+ try {
209
+ // RangeError for invalid IANA timezone names.
210
+ new Intl.DateTimeFormat('en-US', { timeZone: value }).format();
211
+ return true;
212
+ } catch {
213
+ return false;
214
+ }
215
+ }
216
+
217
+ export function resolveTimezone(
218
+ argsTimezone: unknown,
219
+ contextTimezone: string | undefined
220
+ ): {
221
+ requested: string | null;
222
+ resolved: string;
223
+ usedFallback: boolean;
224
+ validRequested: boolean;
225
+ } {
226
+ const requestedRaw =
227
+ typeof argsTimezone === 'string' && argsTimezone.trim().length > 0
228
+ ? argsTimezone.trim()
229
+ : null;
230
+
231
+ if (requestedRaw && isValidIanaTimezone(requestedRaw)) {
232
+ return {
233
+ requested: requestedRaw,
234
+ resolved: requestedRaw,
235
+ usedFallback: false,
236
+ validRequested: true,
237
+ };
238
+ }
239
+
240
+ const contextTz =
241
+ typeof contextTimezone === 'string' && contextTimezone.trim().length > 0
242
+ ? contextTimezone.trim()
243
+ : null;
244
+
245
+ if (contextTz && isValidIanaTimezone(contextTz)) {
246
+ return {
247
+ requested: requestedRaw,
248
+ resolved: contextTz,
249
+ usedFallback: true,
250
+ validRequested: requestedRaw === null,
251
+ };
252
+ }
253
+
254
+ return {
255
+ requested: requestedRaw,
256
+ resolved: 'UTC',
257
+ usedFallback: true,
258
+ validRequested: requestedRaw === null,
259
+ };
260
+ }
261
+
262
+ async function hasBypassApprovalPermission(
263
+ ctx: MiraToolContext
264
+ ): Promise<boolean> {
265
+ const workspaceId = getWorkspaceContextWorkspaceId(ctx);
266
+
267
+ const { data: workspace, error: workspaceError } = await ctx.supabase
268
+ .from('workspaces')
269
+ .select('creator_id')
270
+ .eq('id', workspaceId)
271
+ .maybeSingle();
272
+
273
+ if (workspaceError) {
274
+ throw new Error(workspaceError.message);
275
+ }
276
+
277
+ if (workspace?.creator_id === ctx.userId) return true;
278
+
279
+ const { data: defaults, error: defaultsError } = await ctx.supabase
280
+ .from('workspace_default_permissions')
281
+ .select('permission')
282
+ .eq('ws_id', workspaceId)
283
+ .eq('member_type', 'MEMBER')
284
+ .eq('enabled', true)
285
+ .eq('permission', 'bypass_time_tracking_request_approval')
286
+ .limit(1);
287
+
288
+ if (defaultsError) {
289
+ throw new Error(defaultsError.message);
290
+ }
291
+
292
+ if (defaults?.length) return true;
293
+
294
+ const { data: rolePermissions, error: rolePermissionsError } =
295
+ await ctx.supabase
296
+ .from('workspace_role_members')
297
+ .select(
298
+ 'workspace_roles!inner(ws_id, workspace_role_permissions(permission, enabled))'
299
+ )
300
+ .eq('user_id', ctx.userId)
301
+ .eq('workspace_roles.ws_id', workspaceId);
302
+
303
+ if (rolePermissionsError) {
304
+ throw new Error(rolePermissionsError.message);
305
+ }
306
+
307
+ if (!rolePermissions?.length) return false;
308
+
309
+ return rolePermissions.some((membership) =>
310
+ (membership.workspace_roles?.workspace_role_permissions || []).some(
311
+ (permission) =>
312
+ permission.enabled &&
313
+ permission.permission === 'bypass_time_tracking_request_approval'
314
+ )
315
+ );
316
+ }
317
+
318
+ export async function shouldRequireApproval(
319
+ startTime: Date,
320
+ ctx: MiraToolContext
321
+ ): Promise<{ requiresApproval: boolean; reason?: string }> {
322
+ const workspaceId = getWorkspaceContextWorkspaceId(ctx);
323
+
324
+ const { data: settings, error } = await ctx.supabase
325
+ .from('workspace_settings')
326
+ .select('missed_entry_date_threshold')
327
+ .eq('ws_id', workspaceId)
328
+ .maybeSingle();
329
+
330
+ if (error) {
331
+ return { requiresApproval: true };
332
+ }
333
+
334
+ const rawThresholdDays = settings?.missed_entry_date_threshold;
335
+ if (rawThresholdDays === null || rawThresholdDays === undefined) {
336
+ return { requiresApproval: false };
337
+ }
338
+
339
+ const thresholdDays = Number(rawThresholdDays);
340
+ if (!Number.isFinite(thresholdDays) || thresholdDays < 0) {
341
+ return { requiresApproval: false };
342
+ }
343
+
344
+ if (ENABLE_APPROVAL_BYPASS_CHECK) {
345
+ let bypassAllowed = false;
346
+ try {
347
+ bypassAllowed = await hasBypassApprovalPermission(ctx);
348
+ } catch {
349
+ return { requiresApproval: true };
350
+ }
351
+ if (bypassAllowed) return { requiresApproval: false };
352
+ }
353
+
354
+ if (thresholdDays === 0) {
355
+ return {
356
+ requiresApproval: true,
357
+ reason: 'Workspace requires approval for all missed entries.',
358
+ };
359
+ }
360
+
361
+ const thresholdAgo = new Date();
362
+ thresholdAgo.setDate(thresholdAgo.getDate() - thresholdDays);
363
+
364
+ if (startTime < thresholdAgo) {
365
+ return {
366
+ requiresApproval: true,
367
+ reason: `Entry is older than ${thresholdDays} day${thresholdDays === 1 ? '' : 's'} threshold.`,
368
+ };
369
+ }
370
+
371
+ return { requiresApproval: false };
372
+ }