@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,277 @@
1
+ import {
2
+ forgetAiMemory,
3
+ listAiMemories,
4
+ rememberAiMemory,
5
+ resolveAiMemoryScope,
6
+ searchAiMemories,
7
+ } from '../../memory';
8
+ import {
9
+ MIRA_MEMORY_CATEGORIES,
10
+ type MiraMemoryCategory,
11
+ } from '../definitions/memory';
12
+ import type { MiraToolContext } from '../mira-tools';
13
+
14
+ function toMemoryCategory(value: unknown): MiraMemoryCategory | undefined {
15
+ if (typeof value !== 'string') return undefined;
16
+ return MIRA_MEMORY_CATEGORIES.includes(value as MiraMemoryCategory)
17
+ ? (value as MiraMemoryCategory)
18
+ : undefined;
19
+ }
20
+
21
+ function parseMemoryCategory(
22
+ categoryInput: unknown,
23
+ options: { fieldName?: string; required?: boolean } = {}
24
+ ): { category: MiraMemoryCategory | null; error: string | null } {
25
+ const { fieldName = 'category', required = false } = options;
26
+ const allowed = MIRA_MEMORY_CATEGORIES.join(', ');
27
+
28
+ if (categoryInput === undefined || categoryInput === null) {
29
+ if (!required) return { category: null, error: null };
30
+ return {
31
+ category: null,
32
+ error: `${fieldName} is required. Allowed: ${allowed}`,
33
+ };
34
+ }
35
+
36
+ const parsedCategory = toMemoryCategory(categoryInput);
37
+ if (!parsedCategory) {
38
+ return {
39
+ category: null,
40
+ error: `Invalid ${fieldName}. Allowed: ${allowed}`,
41
+ };
42
+ }
43
+
44
+ return { category: parsedCategory, error: null };
45
+ }
46
+
47
+ function getMemoryWorkspaceId(ctx: MiraToolContext) {
48
+ return ctx.workspaceContext?.wsId ?? ctx.wsId;
49
+ }
50
+
51
+ function resolveMiraMemoryScope(ctx: MiraToolContext) {
52
+ return resolveAiMemoryScope({
53
+ customId: ctx.chatId ?? 'mira-chat',
54
+ product: 'mira',
55
+ source: 'mira_chat',
56
+ surface: 'mira_chat',
57
+ userId: ctx.userId,
58
+ wsId: getMemoryWorkspaceId(ctx),
59
+ });
60
+ }
61
+
62
+ function keyValueMemory(memory: {
63
+ category?: string | null;
64
+ key?: string | null;
65
+ updatedAt?: string | null;
66
+ value: string;
67
+ }) {
68
+ return {
69
+ category: memory.category ?? 'fact',
70
+ key: memory.key ?? memory.value.slice(0, 80),
71
+ updatedAt: memory.updatedAt ?? new Date().toISOString(),
72
+ value: memory.value,
73
+ };
74
+ }
75
+
76
+ export async function executeRemember(
77
+ args: Record<string, unknown>,
78
+ ctx: MiraToolContext
79
+ ) {
80
+ const key = args.key as string;
81
+ const value = args.value as string;
82
+ const { category, error: categoryError } = parseMemoryCategory(
83
+ args.category,
84
+ {
85
+ required: true,
86
+ }
87
+ );
88
+
89
+ if (categoryError || !category) {
90
+ return {
91
+ error:
92
+ categoryError ??
93
+ 'Invalid category. Allowed: preference, fact, conversation_topic, event, person',
94
+ };
95
+ }
96
+
97
+ const result = await rememberAiMemory({
98
+ category,
99
+ key,
100
+ scope: resolveMiraMemoryScope(ctx),
101
+ value,
102
+ });
103
+
104
+ if (!result.ok) return { error: result.error };
105
+ if (result.skipped) {
106
+ return {
107
+ action: 'skipped',
108
+ message: `Memory "${key}" was not saved: ${result.reason}`,
109
+ success: false,
110
+ };
111
+ }
112
+
113
+ return {
114
+ action: 'created',
115
+ message: `Remembered: "${key}"`,
116
+ success: true,
117
+ };
118
+ }
119
+
120
+ export async function executeRecall(
121
+ args: Record<string, unknown>,
122
+ ctx: MiraToolContext
123
+ ) {
124
+ const query = (args.query as string | null | undefined) ?? null;
125
+ const { category, error: categoryError } = parseMemoryCategory(args.category);
126
+ if (categoryError) return { error: categoryError };
127
+
128
+ const maxResults = (args.maxResults as number) || 10;
129
+ const scope = resolveMiraMemoryScope(ctx);
130
+
131
+ const result = query?.trim()
132
+ ? await searchAiMemories({
133
+ category,
134
+ limit: maxResults,
135
+ query,
136
+ scope,
137
+ })
138
+ : await listAiMemories({
139
+ category,
140
+ limit: maxResults,
141
+ scope,
142
+ });
143
+
144
+ if (!result.ok) return { error: result.error };
145
+
146
+ const memories = (result.value ?? []).map((memory) =>
147
+ keyValueMemory({
148
+ category:
149
+ 'category' in memory
150
+ ? memory.category
151
+ : typeof memory.metadata?.memoryCategory === 'string'
152
+ ? memory.metadata.memoryCategory
153
+ : category,
154
+ key:
155
+ 'key' in memory
156
+ ? memory.key
157
+ : typeof memory.metadata?.memoryKey === 'string'
158
+ ? memory.metadata.memoryKey
159
+ : null,
160
+ updatedAt: memory.updatedAt,
161
+ value:
162
+ 'value' in memory
163
+ ? memory.value
164
+ : memory.summary || memory.content || memory.title || '',
165
+ })
166
+ );
167
+
168
+ return {
169
+ count: memories.length,
170
+ memories,
171
+ };
172
+ }
173
+
174
+ export async function executeDeleteMemory(
175
+ args: Record<string, unknown>,
176
+ ctx: MiraToolContext
177
+ ) {
178
+ const key = args.key as string;
179
+ const result = await forgetAiMemory({
180
+ key,
181
+ scope: resolveMiraMemoryScope(ctx),
182
+ });
183
+
184
+ if (!result.ok) return { error: result.error };
185
+ if (result.skipped) {
186
+ return {
187
+ message: `Memory "${key}" was not deleted: ${result.reason}`,
188
+ success: false,
189
+ };
190
+ }
191
+
192
+ return { message: `Memory "${key}" deleted`, success: true };
193
+ }
194
+
195
+ export async function executeListMemories(
196
+ args: Record<string, unknown>,
197
+ ctx: MiraToolContext
198
+ ) {
199
+ const { category, error: categoryError } = parseMemoryCategory(args.category);
200
+ if (categoryError) return { error: categoryError };
201
+
202
+ const result = await listAiMemories({
203
+ category,
204
+ scope: resolveMiraMemoryScope(ctx),
205
+ });
206
+
207
+ if (!result.ok) return { error: result.error };
208
+
209
+ const memories = (result.value ?? []).map((memory) =>
210
+ keyValueMemory({
211
+ category: memory.category,
212
+ key: memory.key,
213
+ updatedAt: memory.updatedAt,
214
+ value: memory.summary || memory.content || memory.title || '',
215
+ })
216
+ );
217
+
218
+ return {
219
+ count: memories.length,
220
+ memories,
221
+ };
222
+ }
223
+
224
+ export async function executeMergeMemories(
225
+ args: Record<string, unknown>,
226
+ ctx: MiraToolContext
227
+ ) {
228
+ const keysToDelete = args.keysToDelete as string[];
229
+ const newKey = args.newKey as string;
230
+ const newValue = args.newValue as string;
231
+ const { category: newCategory, error: categoryError } = parseMemoryCategory(
232
+ args.newCategory,
233
+ {
234
+ fieldName: 'newCategory',
235
+ required: true,
236
+ }
237
+ );
238
+
239
+ if (categoryError || !newCategory) {
240
+ return {
241
+ error:
242
+ categoryError ??
243
+ 'Invalid newCategory. Allowed: preference, fact, conversation_topic, event, person',
244
+ };
245
+ }
246
+
247
+ if (!keysToDelete || keysToDelete.length === 0) {
248
+ return { error: 'No keys provided to delete' };
249
+ }
250
+
251
+ const scope = resolveMiraMemoryScope(ctx);
252
+ const rememberResult = await rememberAiMemory({
253
+ category: newCategory,
254
+ key: newKey,
255
+ scope,
256
+ value: newValue,
257
+ });
258
+
259
+ if (!rememberResult.ok) return { error: rememberResult.error };
260
+
261
+ await Promise.all(
262
+ keysToDelete
263
+ .filter((key) => key !== newKey)
264
+ .map((key) =>
265
+ forgetAiMemory({
266
+ key,
267
+ reason: `Merged into ${newKey}`,
268
+ scope,
269
+ })
270
+ )
271
+ );
272
+
273
+ return {
274
+ message: `Merged ${keysToDelete.length} memories into "${newKey}"`,
275
+ success: !rememberResult.skipped,
276
+ };
277
+ }
@@ -0,0 +1,176 @@
1
+ import { google } from '@ai-sdk/google';
2
+ import { stepCountIs, ToolLoopAgent } from 'ai';
3
+ import { z } from 'zod';
4
+ import { withAiMemory } from '../../memory';
5
+ import type { MiraToolContext } from '../mira-tools';
6
+
7
+ const PARALLEL_CHECKS_MODEL = 'gemini-3.1-flash-lite';
8
+
9
+ const ParallelChecksArgsSchema = z.object({
10
+ question: z
11
+ .string()
12
+ .transform((value) => value.trim())
13
+ .refine((value) => value.length > 0, {
14
+ message: 'Missing required `question`.',
15
+ })
16
+ .transform((value) => value.slice(0, 2000)),
17
+ context: z
18
+ .string()
19
+ .transform((value) => value.trim())
20
+ .optional()
21
+ .transform((value) => (value ? value.slice(0, 8000) : undefined)),
22
+ checks: z
23
+ .array(z.enum(['assumptions', 'factuality', 'risk', 'implementation']))
24
+ .min(1)
25
+ .max(4)
26
+ .optional(),
27
+ });
28
+
29
+ type CheckKind = z.infer<typeof ParallelChecksArgsSchema>['checks'] extends
30
+ | Array<infer T>
31
+ | undefined
32
+ ? T
33
+ : never;
34
+
35
+ type ParallelCheckResult = {
36
+ label: string;
37
+ finding: string;
38
+ };
39
+
40
+ const CHECK_INSTRUCTIONS: Record<CheckKind, string> = {
41
+ assumptions:
42
+ 'You identify hidden assumptions, missing premises, and unclear decision points. Return concise findings only.',
43
+ factuality:
44
+ 'You check whether claims need verification, external evidence, or clearer source attribution. Return concise findings only.',
45
+ implementation:
46
+ 'You review implementation feasibility, sequencing, interfaces, and likely integration risks. Return concise findings only.',
47
+ risk: 'You review failure modes, user-impact risks, regressions, and test gaps. Return concise findings only.',
48
+ };
49
+
50
+ const DEFAULT_CHECKS: CheckKind[] = ['assumptions', 'factuality', 'risk'];
51
+
52
+ function buildPrompt({
53
+ check,
54
+ context,
55
+ question,
56
+ }: {
57
+ check: CheckKind;
58
+ context?: string;
59
+ question: string;
60
+ }) {
61
+ return `Review this request from the "${check}" perspective.
62
+
63
+ Question or scenario:
64
+ ${question}
65
+
66
+ ${context ? `Relevant context:\n${context}\n\n` : ''}Return:
67
+ - 1 to 3 concise findings
68
+ - Any blocker, if present
69
+ - "No material issues" if this perspective has nothing important`;
70
+ }
71
+
72
+ async function runCheck({
73
+ abortSignal,
74
+ check,
75
+ context,
76
+ question,
77
+ toolContext,
78
+ }: {
79
+ abortSignal?: AbortSignal;
80
+ check: CheckKind;
81
+ context?: string;
82
+ question: string;
83
+ toolContext: MiraToolContext;
84
+ }): Promise<ParallelCheckResult> {
85
+ const agent = new ToolLoopAgent({
86
+ model: await withAiMemory({
87
+ addMemory: 'never',
88
+ customId: toolContext.chatId
89
+ ? `${toolContext.chatId}-parallel-checks-${check}`
90
+ : `parallel-checks-${check}`,
91
+ model: google(PARALLEL_CHECKS_MODEL),
92
+ product: 'mira',
93
+ source: 'mira_parallel_checks_tool',
94
+ surface: 'mira_parallel_checks_tool',
95
+ userId: toolContext.userId,
96
+ wsId: toolContext.workspaceContext?.wsId ?? toolContext.wsId,
97
+ }),
98
+ instructions: CHECK_INSTRUCTIONS[check],
99
+ stopWhen: stepCountIs(2),
100
+ providerOptions: {
101
+ google: {
102
+ thinkingConfig: {
103
+ thinkingBudget: 0,
104
+ includeThoughts: false,
105
+ },
106
+ },
107
+ },
108
+ });
109
+
110
+ const result = await agent.generate({
111
+ prompt: buildPrompt({ check, context, question }),
112
+ abortSignal,
113
+ });
114
+
115
+ return {
116
+ label: check,
117
+ finding: result.text.trim() || 'No material issues.',
118
+ };
119
+ }
120
+
121
+ export async function executeParallelChecks(
122
+ args: Record<string, unknown>,
123
+ ctx: MiraToolContext,
124
+ options?: { abortSignal?: AbortSignal }
125
+ ) {
126
+ const parsed = ParallelChecksArgsSchema.safeParse(args);
127
+ if (!parsed.success) {
128
+ return {
129
+ ok: false,
130
+ error: parsed.error.issues[0]?.message ?? 'Invalid parallel check input.',
131
+ };
132
+ }
133
+
134
+ const { context, question } = parsed.data;
135
+ const checks = parsed.data.checks ?? DEFAULT_CHECKS;
136
+
137
+ try {
138
+ const results = await Promise.all(
139
+ checks.map((check) =>
140
+ runCheck({
141
+ abortSignal: options?.abortSignal,
142
+ check,
143
+ context,
144
+ question,
145
+ toolContext: ctx,
146
+ })
147
+ )
148
+ );
149
+
150
+ const issueCount = results.filter(
151
+ (result) => !/no material issues/i.test(result.finding)
152
+ ).length;
153
+
154
+ return {
155
+ ok: true,
156
+ summary:
157
+ issueCount > 0
158
+ ? `Parallel checks found ${issueCount} perspective(s) with material notes.`
159
+ : 'Parallel checks found no material issues.',
160
+ checks: results,
161
+ };
162
+ } catch (error) {
163
+ if (error instanceof DOMException && error.name === 'AbortError') {
164
+ return {
165
+ ok: false,
166
+ error: 'Parallel checks were cancelled.',
167
+ };
168
+ }
169
+
170
+ console.error('executeParallelChecks provider error:', error);
171
+ return {
172
+ ok: false,
173
+ error: 'Parallel checks failed. Please try again.',
174
+ };
175
+ }
176
+ }
@@ -0,0 +1,170 @@
1
+ import { createAdminClient } from '@tuturuuu/supabase/next/server';
2
+ import QRCode from 'qrcode';
3
+ import type { MiraToolContext } from '../mira-tools';
4
+ import { getWorkspaceContextWorkspaceId } from '../workspace-context';
5
+
6
+ const DEFAULT_SIZE = 512;
7
+ const DEFAULT_FOREGROUND = '#000000';
8
+ const DEFAULT_BACKGROUND = '#FFFFFF';
9
+ const DRIVE_QR_PREFIX = 'drive/mira/qr';
10
+
11
+ function normalizeHexColor(value: unknown, fallback: string): string {
12
+ if (typeof value !== 'string') return fallback;
13
+ const normalized = value.trim();
14
+ if (!/^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(normalized)) {
15
+ return fallback;
16
+ }
17
+ return normalized.toUpperCase();
18
+ }
19
+
20
+ function normalizeSize(value: unknown): number {
21
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
22
+ return DEFAULT_SIZE;
23
+ }
24
+ const rounded = Math.round(value);
25
+ return Math.min(2048, Math.max(128, rounded));
26
+ }
27
+
28
+ function sanitizeFileName(value: unknown): string {
29
+ if (typeof value !== 'string') {
30
+ return `qr-${Date.now()}.png`;
31
+ }
32
+
33
+ const trimmed = value.trim();
34
+ if (!trimmed) {
35
+ return `qr-${Date.now()}.png`;
36
+ }
37
+
38
+ const withoutExt = trimmed.replace(/\.[A-Za-z0-9]+$/, '');
39
+ const safeBase = withoutExt
40
+ .replace(/[^A-Za-z0-9-_ ]/g, '')
41
+ .replace(/\s+/g, '-')
42
+ .replace(/-+/g, '-')
43
+ .replace(/^-|-$/g, '')
44
+ .slice(0, 64);
45
+
46
+ const finalBase = safeBase || `qr-${Date.now()}`;
47
+ return `${finalBase}.png`;
48
+ }
49
+
50
+ function toPngBytes(dataUrl: string): Uint8Array {
51
+ if (!dataUrl.startsWith('data:')) {
52
+ throw new Error("Malformed data URL: expected 'data:[mime];base64,<data>'");
53
+ }
54
+
55
+ const commaIndex = dataUrl.indexOf(',');
56
+ if (commaIndex < 0) {
57
+ throw new Error("Malformed data URL: expected 'data:[mime];base64,<data>'");
58
+ }
59
+
60
+ const metadata = dataUrl.slice(5, commaIndex);
61
+ if (!/^[^,]*;base64$/i.test(metadata)) {
62
+ throw new Error("Malformed data URL: expected 'data:[mime];base64,<data>'");
63
+ }
64
+
65
+ const mime = metadata.slice(0, -';base64'.length).toLowerCase();
66
+ if (mime && mime !== 'image/png') {
67
+ throw new Error("Malformed data URL: expected 'data:[mime];base64,<data>'");
68
+ }
69
+
70
+ const base64 = dataUrl.slice(commaIndex + 1).trim();
71
+ if (!base64) {
72
+ throw new Error("Malformed data URL: expected 'data:[mime];base64,<data>'");
73
+ }
74
+
75
+ return Uint8Array.from(Buffer.from(base64, 'base64'));
76
+ }
77
+
78
+ function readQrValue(args: Record<string, unknown>): string {
79
+ for (const key of ['value', 'text', 'url', 'content', 'payload']) {
80
+ const value = args[key];
81
+ if (typeof value === 'string' && value.trim()) {
82
+ return value.trim();
83
+ }
84
+ }
85
+
86
+ return '';
87
+ }
88
+
89
+ export async function executeCreateQrCode(
90
+ args: Record<string, unknown>,
91
+ ctx: MiraToolContext
92
+ ): Promise<unknown> {
93
+ const rawValue = readQrValue(args);
94
+ if (!rawValue) {
95
+ return {
96
+ success: false,
97
+ error: 'QR value is required.',
98
+ };
99
+ }
100
+
101
+ const size = normalizeSize(args.size);
102
+ const foregroundColor = normalizeHexColor(
103
+ args.foregroundColor,
104
+ DEFAULT_FOREGROUND
105
+ );
106
+ const backgroundColor = normalizeHexColor(
107
+ args.backgroundColor,
108
+ DEFAULT_BACKGROUND
109
+ );
110
+ const fileName = sanitizeFileName(args.fileName);
111
+
112
+ const fileId = crypto.randomUUID();
113
+ const wsId = getWorkspaceContextWorkspaceId(ctx);
114
+ const storagePath = `${wsId}/${DRIVE_QR_PREFIX}/${fileId}-${fileName}`;
115
+
116
+ try {
117
+ const qrDataUrl = await QRCode.toDataURL(rawValue, {
118
+ type: 'image/png',
119
+ width: size,
120
+ margin: 2,
121
+ color: {
122
+ dark: foregroundColor,
123
+ light: backgroundColor,
124
+ },
125
+ errorCorrectionLevel: 'M',
126
+ });
127
+
128
+ const sbAdmin = await createAdminClient();
129
+ const pngBytes = toPngBytes(qrDataUrl);
130
+
131
+ const { error: uploadError } = await sbAdmin.storage
132
+ .from('workspaces')
133
+ .upload(storagePath, pngBytes, {
134
+ contentType: 'image/png',
135
+ upsert: false,
136
+ });
137
+
138
+ if (uploadError) {
139
+ throw new Error(`Upload failed: ${uploadError.message}`);
140
+ }
141
+
142
+ const { data: urlData, error: signedUrlError } = await sbAdmin.storage
143
+ .from('workspaces')
144
+ .createSignedUrl(storagePath, 60 * 60 * 24 * 30);
145
+
146
+ if (signedUrlError || !urlData?.signedUrl) {
147
+ throw new Error(
148
+ `Signed URL failed: ${signedUrlError?.message ?? 'No signed URL returned'}`
149
+ );
150
+ }
151
+
152
+ return {
153
+ success: true,
154
+ qrCodeUrl: urlData.signedUrl,
155
+ downloadUrl: urlData.signedUrl,
156
+ storagePath,
157
+ drivePath: storagePath.replace(`${wsId}/`, ''),
158
+ fileName,
159
+ size,
160
+ foregroundColor,
161
+ backgroundColor,
162
+ };
163
+ } catch (error) {
164
+ return {
165
+ success: false,
166
+ error:
167
+ error instanceof Error ? error.message : 'Failed to generate QR code.',
168
+ };
169
+ }
170
+ }