@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,160 @@
1
+ import { z } from 'zod';
2
+
3
+ const dateOnlyStringSchema = z
4
+ .string()
5
+ .trim()
6
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'date must use YYYY-MM-DD format');
7
+
8
+ const flexibleDateTimeStringSchema = z
9
+ .string()
10
+ .trim()
11
+ .regex(
12
+ /^(?:\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)?|\d{4}-\d{2}-\d{2}\s+(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?|(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?)$/,
13
+ 'must be a valid ISO datetime, YYYY-MM-DD HH:mm, or HH:mm/HH:mm:ss'
14
+ );
15
+
16
+ const temporalDateTimeInputSchema = z.union([
17
+ z.date(),
18
+ flexibleDateTimeStringSchema,
19
+ ]);
20
+ const temporalDateInputSchema = z.union([z.date(), dateOnlyStringSchema]);
21
+
22
+ export const startTimerArgsSchema = z.object({
23
+ title: z.string().trim().min(1, 'title is required'),
24
+ description: z.union([z.string(), z.null()]).optional(),
25
+ });
26
+
27
+ export type StartTimerArgs = z.infer<typeof startTimerArgsSchema>;
28
+
29
+ export const stopTimerArgsSchema = z.object({
30
+ sessionId: z.union([z.string(), z.null()]).optional(),
31
+ });
32
+
33
+ export type StopTimerArgs = z.infer<typeof stopTimerArgsSchema>;
34
+
35
+ export const createTimeTrackingEntryArgsSchema = z.object({
36
+ title: z.string().trim().min(1, 'title is required'),
37
+ description: z.union([z.string(), z.null()]).optional(),
38
+ categoryId: z.union([z.string(), z.null()]).optional(),
39
+ taskId: z.union([z.string(), z.null()]).optional(),
40
+ startTime: temporalDateTimeInputSchema,
41
+ endTime: temporalDateTimeInputSchema,
42
+ date: temporalDateInputSchema.optional(),
43
+ });
44
+
45
+ export type CreateTimeTrackingEntryArgs = z.infer<
46
+ typeof createTimeTrackingEntryArgsSchema
47
+ >;
48
+
49
+ export const updateTimeTrackingSessionArgsSchema = z.object({
50
+ sessionId: z.union([z.string(), z.null()]).optional(),
51
+ id: z.union([z.string(), z.null()]).optional(),
52
+ title: z.union([z.string(), z.null()]).optional(),
53
+ description: z.union([z.string(), z.null()]).optional(),
54
+ categoryId: z.union([z.string(), z.null()]).optional(),
55
+ taskId: z.union([z.string(), z.null()]).optional(),
56
+ startTime: temporalDateTimeInputSchema.optional(),
57
+ endTime: temporalDateTimeInputSchema.optional(),
58
+ date: temporalDateInputSchema.optional(),
59
+ });
60
+
61
+ export type UpdateTimeTrackingSessionArgs = z.infer<
62
+ typeof updateTimeTrackingSessionArgsSchema
63
+ >;
64
+
65
+ export const deleteTimeTrackingSessionArgsSchema = z.object({
66
+ sessionId: z.union([z.string(), z.null()]).optional(),
67
+ id: z.union([z.string(), z.null()]).optional(),
68
+ });
69
+
70
+ export type DeleteTimeTrackingSessionArgs = z.infer<
71
+ typeof deleteTimeTrackingSessionArgsSchema
72
+ >;
73
+
74
+ export const moveTimeTrackingSessionArgsSchema = z.object({
75
+ sessionId: z.union([z.string(), z.null()]).optional(),
76
+ id: z.union([z.string(), z.null()]).optional(),
77
+ targetWorkspaceId: z.string().trim().min(1, 'targetWorkspaceId is required'),
78
+ });
79
+
80
+ export type MoveTimeTrackingSessionArgs = z.infer<
81
+ typeof moveTimeTrackingSessionArgsSchema
82
+ >;
83
+
84
+ export const createTimeTrackerGoalArgsSchema = z.object({
85
+ categoryId: z.union([z.string(), z.null()]).optional(),
86
+ dailyGoalMinutes: z
87
+ .number()
88
+ .int()
89
+ .min(1, 'dailyGoalMinutes must be greater than 0'),
90
+ weeklyGoalMinutes: z.union([z.number().int().min(1), z.null()]).optional(),
91
+ isActive: z.boolean().optional(),
92
+ });
93
+
94
+ export type CreateTimeTrackerGoalArgs = z.infer<
95
+ typeof createTimeTrackerGoalArgsSchema
96
+ >;
97
+
98
+ export const updateTimeTrackerGoalArgsSchema = z.object({
99
+ goalId: z.union([z.string(), z.null()]).optional(),
100
+ id: z.union([z.string(), z.null()]).optional(),
101
+ categoryId: z.union([z.string(), z.null()]).optional(),
102
+ dailyGoalMinutes: z
103
+ .number()
104
+ .int()
105
+ .min(1, 'dailyGoalMinutes must be greater than 0')
106
+ .optional(),
107
+ weeklyGoalMinutes: z.union([z.number().int().min(1), z.null()]).optional(),
108
+ isActive: z.boolean().optional(),
109
+ });
110
+
111
+ export type UpdateTimeTrackerGoalArgs = z.infer<
112
+ typeof updateTimeTrackerGoalArgsSchema
113
+ >;
114
+
115
+ export const deleteTimeTrackerGoalArgsSchema = z.object({
116
+ goalId: z.union([z.string(), z.null()]).optional(),
117
+ id: z.union([z.string(), z.null()]).optional(),
118
+ });
119
+
120
+ export type DeleteTimeTrackerGoalArgs = z.infer<
121
+ typeof deleteTimeTrackerGoalArgsSchema
122
+ >;
123
+
124
+ export const createTimeTrackingCategoryArgsSchema = z.object({
125
+ name: z.string().trim().min(1, 'name is required'),
126
+ description: z.union([z.string(), z.null()]).optional(),
127
+ color: z.union([z.string(), z.null()]).optional(),
128
+ });
129
+
130
+ export type CreateTimeTrackingCategoryArgs = z.infer<
131
+ typeof createTimeTrackingCategoryArgsSchema
132
+ >;
133
+
134
+ export const updateTimeTrackingCategoryArgsSchema = z.object({
135
+ categoryId: z.union([z.string(), z.null()]).optional(),
136
+ id: z.union([z.string(), z.null()]).optional(),
137
+ name: z.union([z.string(), z.null()]).optional(),
138
+ description: z.union([z.string(), z.null()]).optional(),
139
+ color: z.union([z.string(), z.null()]).optional(),
140
+ });
141
+
142
+ export type UpdateTimeTrackingCategoryArgs = z.infer<
143
+ typeof updateTimeTrackingCategoryArgsSchema
144
+ >;
145
+
146
+ export const deleteTimeTrackingCategoryArgsSchema = z.object({
147
+ categoryId: z.union([z.string(), z.null()]).optional(),
148
+ id: z.union([z.string(), z.null()]).optional(),
149
+ });
150
+
151
+ export type DeleteTimeTrackingCategoryArgs = z.infer<
152
+ typeof deleteTimeTrackingCategoryArgsSchema
153
+ >;
154
+
155
+ export function getZodErrorMessage(error: unknown): string {
156
+ if (error instanceof z.ZodError) {
157
+ return error.issues[0]?.message ?? 'Invalid arguments';
158
+ }
159
+ return 'Invalid arguments';
160
+ }
@@ -0,0 +1,212 @@
1
+ import type { Tables } from '@tuturuuu/types';
2
+
3
+ export type MutationError = { error: string };
4
+
5
+ export type TimerRelatedEntity = {
6
+ id: string;
7
+ name: string | null;
8
+ color: string | null;
9
+ } | null;
10
+
11
+ export type TimeTrackingCategory = Tables<'time_tracking_categories'>;
12
+
13
+ export interface TimerSession {
14
+ id: string;
15
+ title: string | null;
16
+ startedAt: string | number;
17
+ endedAt?: string | number | null;
18
+ pausedAt?: string | number | null;
19
+ elapsedMs?: number;
20
+ durationSeconds?: number | null;
21
+ durationFormatted?: string;
22
+ description?: string | null;
23
+ categoryId?: string | null;
24
+ taskId?: string | null;
25
+ isRunning?: boolean;
26
+ pendingApproval?: boolean;
27
+ wsId?: string;
28
+ category?: TimerRelatedEntity;
29
+ task?: TimerRelatedEntity;
30
+ }
31
+
32
+ function asTimerRelatedEntity(value: unknown): TimerRelatedEntity {
33
+ if (!value || typeof value !== 'object') return null;
34
+
35
+ const candidate = value as Record<string, unknown>;
36
+ const id = typeof candidate.id === 'string' ? candidate.id : null;
37
+ if (!id) return null;
38
+
39
+ const name =
40
+ typeof candidate.name === 'string' || candidate.name === null
41
+ ? candidate.name
42
+ : null;
43
+ const color =
44
+ typeof candidate.color === 'string' || candidate.color === null
45
+ ? candidate.color
46
+ : null;
47
+
48
+ return {
49
+ id,
50
+ name,
51
+ color,
52
+ };
53
+ }
54
+
55
+ export function toTimerSession(
56
+ row: Record<string, unknown>,
57
+ overrides: Partial<TimerSession> = {}
58
+ ): TimerSession {
59
+ const sessionId =
60
+ typeof row.id === 'string' && row.id.trim().length > 0 ? row.id : null;
61
+ if (!sessionId) {
62
+ throw new Error('Invalid or missing session id');
63
+ }
64
+
65
+ const startedAt =
66
+ typeof row.start_time === 'string' || typeof row.start_time === 'number'
67
+ ? row.start_time
68
+ : typeof row.created_at === 'string' || typeof row.created_at === 'number'
69
+ ? row.created_at
70
+ : typeof row.updated_at === 'string' ||
71
+ typeof row.updated_at === 'number'
72
+ ? row.updated_at
73
+ : new Date(0).toISOString();
74
+
75
+ const baseSession: TimerSession = {
76
+ id: sessionId,
77
+ title: typeof row.title === 'string' ? row.title : null,
78
+ startedAt,
79
+ endedAt:
80
+ typeof row.end_time === 'string' || typeof row.end_time === 'number'
81
+ ? row.end_time
82
+ : null,
83
+ durationSeconds:
84
+ typeof row.duration_seconds === 'number' ? row.duration_seconds : null,
85
+ description: typeof row.description === 'string' ? row.description : null,
86
+ categoryId: typeof row.category_id === 'string' ? row.category_id : null,
87
+ taskId: typeof row.task_id === 'string' ? row.task_id : null,
88
+ isRunning: typeof row.is_running === 'boolean' ? row.is_running : undefined,
89
+ pendingApproval:
90
+ typeof row.pending_approval === 'boolean'
91
+ ? row.pending_approval
92
+ : undefined,
93
+ wsId: typeof row.ws_id === 'string' ? row.ws_id : undefined,
94
+ category: asTimerRelatedEntity(row.category),
95
+ task: asTimerRelatedEntity(row.task),
96
+ };
97
+
98
+ return { ...baseSession, ...overrides };
99
+ }
100
+
101
+ export type StartTimerResult =
102
+ | MutationError
103
+ | {
104
+ success: true;
105
+ message: string;
106
+ session: TimerSession;
107
+ };
108
+
109
+ export type StopTimerResult =
110
+ | MutationError
111
+ | {
112
+ success: true;
113
+ message: string;
114
+ session: TimerSession;
115
+ };
116
+
117
+ export type CreateTimeTrackingEntryResult =
118
+ | MutationError
119
+ | {
120
+ success: true;
121
+ requiresApproval: false;
122
+ message: string;
123
+ session: TimerSession;
124
+ }
125
+ | {
126
+ success: true;
127
+ requiresApproval: true;
128
+ requestCreated: boolean;
129
+ message?: string;
130
+ nextStep?: string;
131
+ approvalRequest?: {
132
+ startTime: string;
133
+ endTime: string;
134
+ titleHint: string;
135
+ descriptionHint: string | null;
136
+ };
137
+ };
138
+
139
+ export type UpdateTimeTrackingSessionResult =
140
+ | MutationError
141
+ | {
142
+ success: true;
143
+ message: string;
144
+ session?: TimerSession;
145
+ };
146
+
147
+ export type DeleteTimeTrackingSessionResult =
148
+ | MutationError
149
+ | {
150
+ success: true;
151
+ message: string;
152
+ };
153
+
154
+ export type MoveTimeTrackingSessionResult =
155
+ | MutationError
156
+ | {
157
+ success: true;
158
+ message: string;
159
+ session: TimerSession;
160
+ };
161
+
162
+ export type TimerGoal = {
163
+ id: string;
164
+ ws_id: string;
165
+ user_id: string;
166
+ category_id: string | null;
167
+ daily_goal_minutes: number;
168
+ weekly_goal_minutes: number | null;
169
+ is_active: boolean | null;
170
+ created_at: string;
171
+ updated_at: string;
172
+ category:
173
+ | {
174
+ id: string;
175
+ name: string | null;
176
+ color: string | null;
177
+ }
178
+ | Array<{
179
+ id: string;
180
+ name: string | null;
181
+ color: string | null;
182
+ }>
183
+ | null;
184
+ };
185
+
186
+ export function normalizeGoalCategory(category: TimerGoal['category']) {
187
+ if (!category) return null;
188
+ if (Array.isArray(category)) return category[0] ?? null;
189
+ return category;
190
+ }
191
+
192
+ export function normalizeGoalCategoryIdInput(
193
+ value: unknown
194
+ ): { ok: true; categoryId: string | null } | { ok: false; error: string } {
195
+ if (value === undefined || value === null) {
196
+ return { ok: true, categoryId: null };
197
+ }
198
+
199
+ if (typeof value !== 'string') {
200
+ return { ok: false, error: 'categoryId must be a string or null' };
201
+ }
202
+
203
+ const trimmed = value.trim();
204
+ // User-facing normalization: treat blank and "general" category inputs as
205
+ // the same "no category" intent, so the trimmed value reaches the branch
206
+ // below that returns { ok: true, categoryId: null }.
207
+ if (!trimmed || trimmed.toLowerCase() === 'general') {
208
+ return { ok: true, categoryId: null };
209
+ }
210
+
211
+ return { ok: true, categoryId: trimmed };
212
+ }
@@ -0,0 +1,19 @@
1
+ export {
2
+ executeCreateTimeTrackingCategory,
3
+ executeDeleteTimeTrackingCategory,
4
+ executeUpdateTimeTrackingCategory,
5
+ } from './timer-category-mutations';
6
+ export {
7
+ executeCreateTimeTrackerGoal,
8
+ executeDeleteTimeTrackerGoal,
9
+ executeUpdateTimeTrackerGoal,
10
+ } from './timer-goal-mutations';
11
+ export type { TimerSession } from './timer-mutation-types';
12
+ export {
13
+ executeCreateTimeTrackingEntry,
14
+ executeDeleteTimeTrackingSession,
15
+ executeMoveTimeTrackingSession,
16
+ executeStartTimer,
17
+ executeStopTimer,
18
+ executeUpdateTimeTrackingSession,
19
+ } from './timer-session-mutations';
@@ -0,0 +1,18 @@
1
+ export {
2
+ executeListTimeTrackingCategories,
3
+ type TimeTrackingCategoryRow,
4
+ } from './timer-categories-executor';
5
+ export {
6
+ executeGetTimeTrackerGoals,
7
+ normalizeCategory,
8
+ type TimeTrackerGoalRow,
9
+ } from './timer-goals-executor';
10
+ export {
11
+ executeGetTimeTrackingSession,
12
+ executeListTimeTrackingSessions,
13
+ } from './timer-sessions-executor';
14
+ export {
15
+ executeGetTimeTrackerStats,
16
+ fetchTimeTrackerStats,
17
+ type TimeTrackerStatsRow,
18
+ } from './timer-stats-executor';
@@ -0,0 +1,299 @@
1
+ import type { MiraToolContext } from '../../mira-tools';
2
+ import { getWorkspaceContextWorkspaceId } from '../../workspace-context';
3
+ import { hasTaskAccess, hasTimeTrackingCategoryAccess } from '../scope-helpers';
4
+ import {
5
+ coerceOptionalString,
6
+ MIN_DURATION_SECONDS,
7
+ parseFlexibleDateTime,
8
+ shouldRequireApproval,
9
+ } from './timer-helpers';
10
+ import {
11
+ type CreateTimeTrackingEntryArgs,
12
+ createTimeTrackingEntryArgsSchema,
13
+ getZodErrorMessage,
14
+ type StartTimerArgs,
15
+ type StopTimerArgs,
16
+ startTimerArgsSchema,
17
+ stopTimerArgsSchema,
18
+ } from './timer-mutation-schemas';
19
+ import {
20
+ type CreateTimeTrackingEntryResult,
21
+ type StartTimerResult,
22
+ type StopTimerResult,
23
+ toTimerSession,
24
+ } from './timer-mutation-types';
25
+
26
+ export async function executeStartTimer(
27
+ args: Record<string, unknown>,
28
+ ctx: MiraToolContext
29
+ ): Promise<StartTimerResult> {
30
+ let parsedArgs: StartTimerArgs;
31
+ try {
32
+ parsedArgs = startTimerArgsSchema.parse(args);
33
+ } catch (error) {
34
+ return { error: getZodErrorMessage(error) };
35
+ }
36
+
37
+ const workspaceId = getWorkspaceContextWorkspaceId(ctx);
38
+ const title = parsedArgs.title;
39
+ const now = new Date();
40
+
41
+ const { data: runningSessions, error: runningSelectError } =
42
+ await ctx.supabase
43
+ .from('time_tracking_sessions')
44
+ .select('id, start_time')
45
+ .eq('user_id', ctx.userId)
46
+ .eq('ws_id', workspaceId)
47
+ .eq('is_running', true);
48
+
49
+ if (runningSelectError) return { error: runningSelectError.message };
50
+
51
+ for (const runningSession of runningSessions ?? []) {
52
+ const runningStartTime = new Date(runningSession.start_time);
53
+ const durationSeconds = Number.isNaN(runningStartTime.getTime())
54
+ ? 0
55
+ : Math.max(
56
+ 0,
57
+ Math.floor((now.getTime() - runningStartTime.getTime()) / 1000)
58
+ );
59
+
60
+ const { data: stoppedRows, error: stopError } = await ctx.supabase
61
+ .from('time_tracking_sessions')
62
+ .update({
63
+ is_running: false,
64
+ end_time: now.toISOString(),
65
+ duration_seconds: durationSeconds,
66
+ })
67
+ .eq('id', runningSession.id)
68
+ .eq('user_id', ctx.userId)
69
+ .eq('ws_id', workspaceId)
70
+ .eq('is_running', true)
71
+ .select('id');
72
+
73
+ if (stopError) {
74
+ return {
75
+ error: `Failed to stop running timer before starting a new one: ${stopError.message}`,
76
+ };
77
+ }
78
+
79
+ if (!stoppedRows?.length) {
80
+ return {
81
+ error:
82
+ 'Failed to stop running timer before starting a new one: no session was updated',
83
+ };
84
+ }
85
+ }
86
+
87
+ const { data: session, error } = await ctx.supabase
88
+ .from('time_tracking_sessions')
89
+ .insert({
90
+ title,
91
+ description: coerceOptionalString(parsedArgs.description),
92
+ start_time: now.toISOString(),
93
+ is_running: true,
94
+ user_id: ctx.userId,
95
+ ws_id: workspaceId,
96
+ })
97
+ .select('id, title, start_time')
98
+ .single();
99
+
100
+ if (error) return { error: error.message };
101
+ return {
102
+ success: true,
103
+ message: `Timer started: "${title}"`,
104
+ session: toTimerSession(session as Record<string, unknown>),
105
+ };
106
+ }
107
+
108
+ export async function executeStopTimer(
109
+ args: Record<string, unknown>,
110
+ ctx: MiraToolContext
111
+ ): Promise<StopTimerResult> {
112
+ let parsedArgs: StopTimerArgs;
113
+ try {
114
+ parsedArgs = stopTimerArgsSchema.parse(args);
115
+ } catch (error) {
116
+ return { error: getZodErrorMessage(error) };
117
+ }
118
+
119
+ const workspaceId = getWorkspaceContextWorkspaceId(ctx);
120
+ const sessionId = coerceOptionalString(parsedArgs.sessionId);
121
+
122
+ let query = ctx.supabase
123
+ .from('time_tracking_sessions')
124
+ .select('id, title, start_time')
125
+ .eq('user_id', ctx.userId)
126
+ .eq('ws_id', workspaceId)
127
+ .eq('is_running', true);
128
+
129
+ if (sessionId) query = query.eq('id', sessionId);
130
+
131
+ const { data: session, error: sessionError } = await query
132
+ .limit(1)
133
+ .maybeSingle();
134
+
135
+ if (sessionError) return { error: sessionError.message };
136
+ if (!session) return { error: 'No running timer found' };
137
+
138
+ const endTime = new Date();
139
+ const startTime = new Date(session.start_time);
140
+ const durationSeconds = Math.round(
141
+ (endTime.getTime() - startTime.getTime()) / 1000
142
+ );
143
+
144
+ const { data: stoppedRows, error } = await ctx.supabase
145
+ .from('time_tracking_sessions')
146
+ .update({
147
+ is_running: false,
148
+ end_time: endTime.toISOString(),
149
+ duration_seconds: durationSeconds,
150
+ })
151
+ .eq('id', session.id)
152
+ .eq('user_id', ctx.userId)
153
+ .eq('ws_id', workspaceId)
154
+ .eq('is_running', true)
155
+ .select(
156
+ `
157
+ *,
158
+ category:time_tracking_categories(*),
159
+ task:tasks(*)
160
+ `
161
+ )
162
+ .single();
163
+
164
+ if (error) return { error: error.message };
165
+ if (!stoppedRows) {
166
+ return { error: 'Failed to stop timer: no running session was updated' };
167
+ }
168
+
169
+ const hours = Math.floor(durationSeconds / 3600);
170
+ const minutes = Math.floor((durationSeconds % 3600) / 60);
171
+
172
+ return {
173
+ success: true,
174
+ message: `Timer stopped: "${session.title}" — ${hours}h ${minutes}m`,
175
+ session: toTimerSession(stoppedRows as Record<string, unknown>, {
176
+ durationSeconds,
177
+ durationFormatted: `${hours}h ${minutes}m`,
178
+ }),
179
+ };
180
+ }
181
+
182
+ export async function executeCreateTimeTrackingEntry(
183
+ args: Record<string, unknown>,
184
+ ctx: MiraToolContext
185
+ ): Promise<CreateTimeTrackingEntryResult> {
186
+ let parsedArgs: CreateTimeTrackingEntryArgs;
187
+ try {
188
+ parsedArgs = createTimeTrackingEntryArgsSchema.parse(args);
189
+ } catch (error) {
190
+ return { error: getZodErrorMessage(error) };
191
+ }
192
+
193
+ const workspaceId = getWorkspaceContextWorkspaceId(ctx);
194
+ const title = parsedArgs.title;
195
+
196
+ const startParsed = parseFlexibleDateTime(parsedArgs.startTime, 'startTime', {
197
+ date: parsedArgs.date,
198
+ timezone: ctx.timezone,
199
+ });
200
+ if (!startParsed.ok) return { error: startParsed.error };
201
+ const endParsed = parseFlexibleDateTime(parsedArgs.endTime, 'endTime', {
202
+ date: parsedArgs.date,
203
+ timezone: ctx.timezone,
204
+ });
205
+ if (!endParsed.ok) return { error: endParsed.error };
206
+
207
+ const startTime = startParsed.value;
208
+ const endTime = endParsed.value;
209
+ if (endTime <= startTime) {
210
+ return { error: 'endTime must be after startTime' };
211
+ }
212
+
213
+ const durationSeconds = Math.floor(
214
+ (endTime.getTime() - startTime.getTime()) / 1000
215
+ );
216
+ if (durationSeconds < MIN_DURATION_SECONDS) {
217
+ return { error: 'Session must be at least 1 minute long' };
218
+ }
219
+
220
+ const approvalCheck = await shouldRequireApproval(startTime, ctx);
221
+ if (approvalCheck.requiresApproval) {
222
+ return {
223
+ success: true,
224
+ requiresApproval: true,
225
+ requestCreated: false,
226
+ message:
227
+ `${approvalCheck.reason ?? 'This missed entry requires approval.'} ` +
228
+ 'No request has been created yet.',
229
+ nextStep:
230
+ 'Inform the user to upload proof images and submit a time tracking request to complete this entry.',
231
+ approvalRequest: {
232
+ startTime: startTime.toISOString(),
233
+ endTime: endTime.toISOString(),
234
+ titleHint: title,
235
+ descriptionHint: coerceOptionalString(parsedArgs.description),
236
+ },
237
+ };
238
+ }
239
+
240
+ const categoryId = coerceOptionalString(parsedArgs.categoryId);
241
+ if (categoryId) {
242
+ try {
243
+ if (!(await hasTimeTrackingCategoryAccess(ctx, categoryId))) {
244
+ return { error: 'Category not found in current workspace' };
245
+ }
246
+ } catch (error) {
247
+ return {
248
+ error:
249
+ error instanceof Error ? error.message : 'Category lookup failed',
250
+ };
251
+ }
252
+ }
253
+
254
+ const taskId = coerceOptionalString(parsedArgs.taskId);
255
+ if (taskId) {
256
+ try {
257
+ if (!(await hasTaskAccess(ctx, taskId))) {
258
+ return { error: 'Task not found in current workspace' };
259
+ }
260
+ } catch (error) {
261
+ return {
262
+ error: error instanceof Error ? error.message : 'Task lookup failed',
263
+ };
264
+ }
265
+ }
266
+
267
+ const { data, error } = await ctx.supabase
268
+ .from('time_tracking_sessions')
269
+ .insert({
270
+ ws_id: workspaceId,
271
+ user_id: ctx.userId,
272
+ title,
273
+ description: coerceOptionalString(parsedArgs.description),
274
+ category_id: categoryId,
275
+ task_id: taskId,
276
+ start_time: startTime.toISOString(),
277
+ end_time: endTime.toISOString(),
278
+ duration_seconds: durationSeconds,
279
+ is_running: false,
280
+ pending_approval: false,
281
+ })
282
+ .select(
283
+ `
284
+ *,
285
+ category:time_tracking_categories(*),
286
+ task:tasks(*)
287
+ `
288
+ )
289
+ .single();
290
+
291
+ if (error) return { error: error.message };
292
+
293
+ return {
294
+ success: true,
295
+ requiresApproval: false,
296
+ message: 'Time tracking entry created.',
297
+ session: toTimerSession(data as Record<string, unknown>),
298
+ };
299
+ }