@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,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
|
+
}
|