@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.
- package/README.md +76 -0
- package/package.json +106 -0
- package/src/api-key-hash.ts +28 -0
- package/src/calendar/events.ts +34 -0
- package/src/calendar/route.ts +114 -0
- package/src/chat/credit-source.ts +1 -0
- package/src/chat/google/chat-request-schema.ts +150 -0
- package/src/chat/google/default-system-instruction.ts +198 -0
- package/src/chat/google/message-file-processing.ts +212 -0
- package/src/chat/google/mira-step-preparation.ts +221 -0
- package/src/chat/google/new/route.ts +368 -0
- package/src/chat/google/route-auth.ts +81 -0
- package/src/chat/google/route-chat-resolution.ts +98 -0
- package/src/chat/google/route-credits.ts +61 -0
- package/src/chat/google/route-message-preparation.ts +331 -0
- package/src/chat/google/route-mira-runtime.ts +206 -0
- package/src/chat/google/route.ts +632 -0
- package/src/chat/google/stream-finish-persistence.ts +722 -0
- package/src/chat/google/summary/route.ts +153 -0
- package/src/chat/mira-render-ui-policy.ts +540 -0
- package/src/chat/mira-system-instruction.ts +484 -0
- package/src/chat-sdk/adapters.ts +389 -0
- package/src/chat-sdk/registry.ts +197 -0
- package/src/chat-sdk.ts +33 -0
- package/src/core.ts +3 -0
- package/src/credits/cap-output-tokens.ts +90 -0
- package/src/credits/check-credits.ts +232 -0
- package/src/credits/constants.ts +30 -0
- package/src/credits/index.ts +46 -0
- package/src/credits/model-mapping.ts +92 -0
- package/src/credits/reservations.ts +514 -0
- package/src/credits/resolve-plan-model.ts +219 -0
- package/src/credits/sync-gateway-models.ts +351 -0
- package/src/credits/types.ts +109 -0
- package/src/credits/use-ai-credits.ts +3 -0
- package/src/embeddings/metered.ts +283 -0
- package/src/executions/route.ts +137 -0
- package/src/generate/route.ts +411 -0
- package/src/hooks.ts +7 -0
- package/src/meetings/summary/route.ts +7 -0
- package/src/meetings/transcription/route.ts +134 -0
- package/src/memory/client.ts +158 -0
- package/src/memory/config.ts +38 -0
- package/src/memory/index.ts +32 -0
- package/src/memory/ingest.ts +51 -0
- package/src/memory/middleware.ts +35 -0
- package/src/memory/operations.ts +480 -0
- package/src/memory/scope.ts +102 -0
- package/src/memory/settings.ts +121 -0
- package/src/memory/types.ts +101 -0
- package/src/memory/workspace.ts +36 -0
- package/src/memory.ts +1 -0
- package/src/mind/patch.ts +146 -0
- package/src/mind/route.ts +687 -0
- package/src/mind/tools.ts +1500 -0
- package/src/mind/types.ts +20 -0
- package/src/object/core.ts +3 -0
- package/src/object/flashcards/route.ts +140 -0
- package/src/object/quizzes/explanation/route.ts +145 -0
- package/src/object/quizzes/route.ts +142 -0
- package/src/object/types.ts +187 -0
- package/src/object/year-plan/route.ts +196 -0
- package/src/react.ts +1 -0
- package/src/scheduling/algorithm.ts +791 -0
- package/src/scheduling/default.ts +36 -0
- package/src/scheduling/duration-optimizer.ts +689 -0
- package/src/scheduling/index.ts +79 -0
- package/src/scheduling/priority-calculator.ts +187 -0
- package/src/scheduling/recurrence-calculator.ts +621 -0
- package/src/scheduling/templates.ts +892 -0
- package/src/scheduling/types.ts +136 -0
- package/src/scheduling/web-adapter.ts +308 -0
- package/src/scheduling.ts +6 -0
- package/src/supported-actions.ts +1 -0
- package/src/supported-providers.ts +6 -0
- package/src/tools/context-builder.ts +372 -0
- package/src/tools/core.ts +1 -0
- package/src/tools/definitions/calendar.ts +106 -0
- package/src/tools/definitions/finance.ts +197 -0
- package/src/tools/definitions/image.ts +74 -0
- package/src/tools/definitions/memory.ts +83 -0
- package/src/tools/definitions/meta.ts +154 -0
- package/src/tools/definitions/render-ui.ts +81 -0
- package/src/tools/definitions/tasks.ts +343 -0
- package/src/tools/definitions/time-tracking.ts +381 -0
- package/src/tools/definitions/workspace-context.ts +45 -0
- package/src/tools/definitions/workspace-user-chat.ts +111 -0
- package/src/tools/executors/calendar.ts +371 -0
- package/src/tools/executors/chat.ts +15 -0
- package/src/tools/executors/finance.ts +638 -0
- package/src/tools/executors/helpers/encryption.ts +107 -0
- package/src/tools/executors/image.ts +247 -0
- package/src/tools/executors/markitdown.ts +684 -0
- package/src/tools/executors/memory.ts +277 -0
- package/src/tools/executors/parallel-checks.ts +176 -0
- package/src/tools/executors/qr.ts +170 -0
- package/src/tools/executors/scope-helpers.ts +192 -0
- package/src/tools/executors/search.ts +149 -0
- package/src/tools/executors/settings.ts +40 -0
- package/src/tools/executors/tasks.ts +1087 -0
- package/src/tools/executors/theme.ts +23 -0
- package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
- package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
- package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
- package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
- package/src/tools/executors/timer/timer-helpers.ts +372 -0
- package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
- package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
- package/src/tools/executors/timer/timer-mutations.ts +19 -0
- package/src/tools/executors/timer/timer-queries.ts +18 -0
- package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
- package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
- package/src/tools/executors/timer/timer-session-queries.ts +153 -0
- package/src/tools/executors/timer/timer-session-updates.ts +200 -0
- package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
- package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
- package/src/tools/executors/timer.ts +22 -0
- package/src/tools/executors/user.ts +60 -0
- package/src/tools/executors/workspace.ts +135 -0
- package/src/tools/json-render-catalog.ts +875 -0
- package/src/tools/mira-tool-definitions.ts +55 -0
- package/src/tools/mira-tool-dispatcher.ts +265 -0
- package/src/tools/mira-tool-metadata.ts +164 -0
- package/src/tools/mira-tool-names.ts +95 -0
- package/src/tools/mira-tool-render-ui.ts +54 -0
- package/src/tools/mira-tool-types.ts +17 -0
- package/src/tools/mira-tools.ts +167 -0
- package/src/tools/normalize-render-ui-input.ts +321 -0
- package/src/tools/workspace-context.ts +233 -0
- 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
|
+
}
|