@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,638 @@
1
+ import type { TablesInsert, TablesUpdate } from '@tuturuuu/types';
2
+ import { z } from 'zod';
3
+ import type { MiraToolContext } from '../mira-tools';
4
+ import { getWorkspaceContextWorkspaceId } from '../workspace-context';
5
+ import { hasTransactionCategoryAccess, hasWalletAccess } from './scope-helpers';
6
+
7
+ const createWalletArgsSchema = z.object({
8
+ name: z.string().trim().min(1),
9
+ currency: z.string().trim().min(1).optional(),
10
+ balance: z.coerce.number().optional(),
11
+ type: z.string().trim().min(1).optional(),
12
+ });
13
+
14
+ const updateWalletArgsSchema = z
15
+ .object({
16
+ walletId: z.guid(),
17
+ name: z.string().trim().min(1).optional(),
18
+ currency: z.string().trim().min(1).optional(),
19
+ balance: z.coerce.number().optional(),
20
+ type: z.string().trim().min(1).optional(),
21
+ })
22
+ .refine(
23
+ (value) =>
24
+ value.name !== undefined ||
25
+ value.currency !== undefined ||
26
+ value.balance !== undefined ||
27
+ value.type !== undefined,
28
+ { message: 'At least one field to update is required' }
29
+ );
30
+
31
+ const createTransactionTagArgsSchema = z.object({
32
+ name: z.string().trim().min(1),
33
+ color: z
34
+ .string()
35
+ .regex(/^[#][0-9a-fA-F]{3,8}$/)
36
+ .optional(),
37
+ description: z.string().trim().optional(),
38
+ });
39
+
40
+ const updateTransactionTagArgsSchema = z
41
+ .object({
42
+ tagId: z.guid(),
43
+ name: z.string().trim().min(1).optional(),
44
+ color: z
45
+ .string()
46
+ .regex(/^[#][0-9a-fA-F]{3,8}$/)
47
+ .optional(),
48
+ description: z.string().trim().optional(),
49
+ })
50
+ .refine(
51
+ (value) =>
52
+ value.name !== undefined ||
53
+ value.color !== undefined ||
54
+ value.description !== undefined,
55
+ { message: 'At least one field to update is required' }
56
+ );
57
+
58
+ // ── Workspace default currency ──
59
+
60
+ export async function executeSetDefaultCurrency(
61
+ args: Record<string, unknown>,
62
+ ctx: MiraToolContext
63
+ ) {
64
+ const currency = (args.currency as string).toUpperCase();
65
+ const workspaceId = getWorkspaceContextWorkspaceId(ctx);
66
+
67
+ const { error } = await ctx.supabase.from('workspace_configs').upsert(
68
+ {
69
+ id: 'DEFAULT_CURRENCY',
70
+ ws_id: workspaceId,
71
+ value: currency,
72
+ updated_at: new Date().toISOString(),
73
+ },
74
+ { onConflict: 'ws_id,id' }
75
+ );
76
+
77
+ if (error) return { error: error.message };
78
+ return {
79
+ success: true,
80
+ message: `Default workspace currency set to ${currency}`,
81
+ currency,
82
+ };
83
+ }
84
+
85
+ export async function executeLogTransaction(
86
+ args: Record<string, unknown>,
87
+ ctx: MiraToolContext
88
+ ) {
89
+ const amount = args.amount as number;
90
+ let walletId = args.walletId as string | null;
91
+
92
+ if (!walletId) {
93
+ const { data: wallet } = await ctx.supabase
94
+ .schema('private')
95
+ .from('workspace_wallets')
96
+ .select('id')
97
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
98
+ .limit(1)
99
+ .single();
100
+
101
+ if (!wallet) return { error: 'No wallet found in workspace' };
102
+ walletId = wallet.id;
103
+ } else {
104
+ try {
105
+ if (!(await hasWalletAccess(ctx, walletId))) {
106
+ return { error: 'Wallet not found in current workspace' };
107
+ }
108
+ } catch (error) {
109
+ return {
110
+ error: error instanceof Error ? error.message : 'Wallet lookup failed',
111
+ };
112
+ }
113
+ }
114
+
115
+ if (args.categoryId) {
116
+ try {
117
+ if (
118
+ !(await hasTransactionCategoryAccess(ctx, args.categoryId as string))
119
+ ) {
120
+ return { error: 'Category not found in current workspace' };
121
+ }
122
+ } catch (error) {
123
+ return {
124
+ error:
125
+ error instanceof Error ? error.message : 'Category lookup failed',
126
+ };
127
+ }
128
+ }
129
+
130
+ const { data: tx, error } = await ctx.supabase
131
+ .from('wallet_transactions')
132
+ .insert({
133
+ amount,
134
+ description: (args.description as string) ?? null,
135
+ wallet_id: walletId,
136
+ category_id: (args.categoryId as string | undefined) ?? null,
137
+ taken_at: new Date().toISOString(),
138
+ })
139
+ .select('id, amount, description, taken_at')
140
+ .single();
141
+
142
+ if (error) return { error: error.message };
143
+ return {
144
+ success: true,
145
+ message: `Transaction of ${amount} logged`,
146
+ transaction: tx,
147
+ };
148
+ }
149
+
150
+ export async function executeGetSpendingSummary(
151
+ args: Record<string, unknown>,
152
+ ctx: MiraToolContext
153
+ ) {
154
+ const days = (args.days as number) || 30;
155
+ const since = new Date();
156
+ since.setDate(since.getDate() - days);
157
+
158
+ const { data: wallets } = await ctx.supabase
159
+ .schema('private')
160
+ .from('workspace_wallets')
161
+ .select('id, name, currency, balance')
162
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
163
+
164
+ if (!wallets?.length)
165
+ return { wallets: [], totalIncome: 0, totalExpenses: 0, net: 0 };
166
+
167
+ type Wallet = { id: string; name: string; currency: string; balance: number };
168
+ const walletIds = (wallets as Wallet[]).map((w) => w.id);
169
+
170
+ const { data: transactions } = await ctx.supabase
171
+ .from('wallet_transactions')
172
+ .select('amount, wallet_id')
173
+ .in('wallet_id', walletIds)
174
+ .gte('taken_at', since.toISOString());
175
+
176
+ let totalIncome = 0;
177
+ let totalExpenses = 0;
178
+
179
+ for (const tx of transactions || []) {
180
+ if (tx.amount && tx.amount > 0) totalIncome += tx.amount;
181
+ else if (tx.amount && tx.amount < 0) totalExpenses += Math.abs(tx.amount);
182
+ }
183
+
184
+ return {
185
+ period: `Last ${days} days`,
186
+ totalIncome,
187
+ totalExpenses,
188
+ net: totalIncome - totalExpenses,
189
+ wallets: (wallets as Wallet[]).map((w) => ({
190
+ id: w.id,
191
+ name: w.name,
192
+ currency: w.currency,
193
+ balance: w.balance,
194
+ })),
195
+ };
196
+ }
197
+
198
+ // ── New CRUD tools ──
199
+
200
+ export async function executeListWallets(
201
+ _args: Record<string, unknown>,
202
+ ctx: MiraToolContext
203
+ ) {
204
+ const { data, error } = await ctx.supabase
205
+ .schema('private')
206
+ .from('workspace_wallets')
207
+ .select('id, name, currency, balance, type, created_at')
208
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
209
+ .order('created_at', { ascending: true });
210
+
211
+ if (error) return { error: error.message };
212
+ return { count: data?.length ?? 0, wallets: data ?? [] };
213
+ }
214
+
215
+ export async function executeCreateWallet(
216
+ args: Record<string, unknown>,
217
+ ctx: MiraToolContext
218
+ ) {
219
+ const parsed = createWalletArgsSchema.safeParse(args);
220
+ if (!parsed.success) {
221
+ return { error: parsed.error.issues[0]?.message ?? 'Invalid wallet data' };
222
+ }
223
+
224
+ const validated = parsed.data;
225
+ const insertData: TablesInsert<{ schema: 'private' }, 'workspace_wallets'> = {
226
+ name: validated.name,
227
+ ws_id: getWorkspaceContextWorkspaceId(ctx),
228
+ };
229
+ if (validated.currency) insertData.currency = validated.currency;
230
+ if (validated.balance !== undefined) insertData.balance = validated.balance;
231
+ if (validated.type) insertData.type = validated.type;
232
+
233
+ const { data, error } = await ctx.supabase
234
+ .schema('private')
235
+ .from('workspace_wallets')
236
+ .insert(insertData)
237
+ .select('id, name, currency, balance')
238
+ .single();
239
+
240
+ if (error) return { error: error.message };
241
+ return {
242
+ success: true,
243
+ message: `Wallet "${validated.name}" created`,
244
+ wallet: data,
245
+ };
246
+ }
247
+
248
+ export async function executeUpdateWallet(
249
+ args: Record<string, unknown>,
250
+ ctx: MiraToolContext
251
+ ) {
252
+ const parsed = updateWalletArgsSchema.safeParse(args);
253
+ if (!parsed.success) {
254
+ return { error: parsed.error.issues[0]?.message ?? 'Invalid wallet data' };
255
+ }
256
+
257
+ const validated = parsed.data;
258
+ const walletId = validated.walletId;
259
+ const updates: TablesUpdate<{ schema: 'private' }, 'workspace_wallets'> = {};
260
+
261
+ if (validated.name !== undefined) updates.name = validated.name;
262
+ if (validated.currency !== undefined) updates.currency = validated.currency;
263
+ if (validated.balance !== undefined) updates.balance = validated.balance;
264
+ if (validated.type !== undefined) updates.type = validated.type;
265
+
266
+ const { error } = await ctx.supabase
267
+ .schema('private')
268
+ .from('workspace_wallets')
269
+ .update(updates)
270
+ .eq('id', walletId)
271
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
272
+
273
+ if (error) return { error: error.message };
274
+ return { success: true, message: `Wallet ${walletId} updated` };
275
+ }
276
+
277
+ export async function executeDeleteWallet(
278
+ args: Record<string, unknown>,
279
+ ctx: MiraToolContext
280
+ ) {
281
+ const walletId = args.walletId as string;
282
+
283
+ const { error } = await ctx.supabase
284
+ .schema('private')
285
+ .from('workspace_wallets')
286
+ .delete()
287
+ .eq('id', walletId)
288
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
289
+
290
+ if (error) return { error: error.message };
291
+ return { success: true, message: `Wallet ${walletId} deleted` };
292
+ }
293
+
294
+ export async function executeListTransactions(
295
+ args: Record<string, unknown>,
296
+ ctx: MiraToolContext
297
+ ) {
298
+ const limit = (args.limit as number) || 50;
299
+
300
+ // Get all wallet IDs in workspace to scope query
301
+ const { data: wallets } = await ctx.supabase
302
+ .schema('private')
303
+ .from('workspace_wallets')
304
+ .select('id')
305
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
306
+
307
+ if (!wallets?.length) return { count: 0, transactions: [] };
308
+
309
+ const walletIds = wallets.map((w: { id: string }) => w.id);
310
+
311
+ let query = ctx.supabase
312
+ .from('wallet_transactions')
313
+ .select('id, amount, description, taken_at, wallet_id, category_id')
314
+ .in('wallet_id', walletIds)
315
+ .order('taken_at', { ascending: false })
316
+ .limit(limit);
317
+
318
+ if (args.walletId) query = query.eq('wallet_id', args.walletId as string);
319
+ if (args.categoryId)
320
+ query = query.eq('category_id', args.categoryId as string);
321
+ if (args.days) {
322
+ const since = new Date();
323
+ since.setDate(since.getDate() - (args.days as number));
324
+ query = query.gte('taken_at', since.toISOString());
325
+ }
326
+
327
+ const { data, error } = await query;
328
+
329
+ if (error) return { error: error.message };
330
+ return { count: data?.length ?? 0, transactions: data ?? [] };
331
+ }
332
+
333
+ export async function executeGetTransaction(
334
+ args: Record<string, unknown>,
335
+ ctx: MiraToolContext
336
+ ) {
337
+ const transactionId = args.transactionId as string;
338
+
339
+ const { data: wallets } = await ctx.supabase
340
+ .schema('private')
341
+ .from('workspace_wallets')
342
+ .select('id')
343
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
344
+ const walletIds = wallets?.map((w) => w.id) ?? [];
345
+
346
+ if (!walletIds.length) return { error: 'No wallets in workspace' };
347
+
348
+ const { data, error } = await ctx.supabase
349
+ .from('wallet_transactions')
350
+ .select('id, amount, description, taken_at, wallet_id, category_id')
351
+ .eq('id', transactionId)
352
+ .in('wallet_id', walletIds)
353
+ .single();
354
+
355
+ if (error) return { error: error.message };
356
+ return { transaction: data };
357
+ }
358
+
359
+ export async function executeUpdateTransaction(
360
+ args: Record<string, unknown>,
361
+ ctx: MiraToolContext
362
+ ) {
363
+ const transactionId = args.transactionId as string;
364
+ const updates: TablesUpdate<'wallet_transactions'> = {};
365
+
366
+ if (args.amount !== undefined) {
367
+ if (args.amount !== null && typeof args.amount !== 'number') {
368
+ return { error: 'amount must be a number or null' };
369
+ }
370
+ updates.amount = args.amount;
371
+ }
372
+ if (args.description !== undefined) {
373
+ if (args.description !== null && typeof args.description !== 'string') {
374
+ return { error: 'description must be a string or null' };
375
+ }
376
+ updates.description = args.description;
377
+ }
378
+ if (args.categoryId !== undefined) {
379
+ const categoryId = args.categoryId as string | null;
380
+ if (categoryId) {
381
+ try {
382
+ if (!(await hasTransactionCategoryAccess(ctx, categoryId))) {
383
+ return { error: 'Category not found in current workspace' };
384
+ }
385
+ } catch (error) {
386
+ return {
387
+ error:
388
+ error instanceof Error ? error.message : 'Category lookup failed',
389
+ };
390
+ }
391
+ }
392
+ updates.category_id = categoryId;
393
+ }
394
+ if (args.walletId !== undefined) {
395
+ if (args.walletId === null) {
396
+ return { error: 'walletId cannot be null' };
397
+ }
398
+
399
+ const walletId = args.walletId as string;
400
+ if (walletId) {
401
+ try {
402
+ if (!(await hasWalletAccess(ctx, walletId))) {
403
+ return { error: 'Wallet not found in current workspace' };
404
+ }
405
+ } catch (error) {
406
+ return {
407
+ error:
408
+ error instanceof Error ? error.message : 'Wallet lookup failed',
409
+ };
410
+ }
411
+ }
412
+ updates.wallet_id = walletId;
413
+ }
414
+
415
+ if (Object.keys(updates).length === 0) {
416
+ return { success: true, message: 'No fields to update' };
417
+ }
418
+
419
+ const { data: wallets } = await ctx.supabase
420
+ .schema('private')
421
+ .from('workspace_wallets')
422
+ .select('id')
423
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
424
+ const walletIds = wallets?.map((w) => w.id) ?? [];
425
+
426
+ if (!walletIds.length) return { error: 'No wallets in workspace' };
427
+
428
+ const { error } = await ctx.supabase
429
+ .from('wallet_transactions')
430
+ .update(updates)
431
+ .eq('id', transactionId)
432
+ .in('wallet_id', walletIds);
433
+
434
+ if (error) return { error: error.message };
435
+ return { success: true, message: `Transaction ${transactionId} updated` };
436
+ }
437
+
438
+ export async function executeDeleteTransaction(
439
+ args: Record<string, unknown>,
440
+ ctx: MiraToolContext
441
+ ) {
442
+ const transactionId = args.transactionId as string;
443
+
444
+ const { data: wallets } = await ctx.supabase
445
+ .schema('private')
446
+ .from('workspace_wallets')
447
+ .select('id')
448
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
449
+ const walletIds = wallets?.map((w) => w.id) ?? [];
450
+
451
+ if (!walletIds.length) return { error: 'No wallets in workspace' };
452
+
453
+ const { error } = await ctx.supabase
454
+ .from('wallet_transactions')
455
+ .delete()
456
+ .eq('id', transactionId)
457
+ .in('wallet_id', walletIds);
458
+
459
+ if (error) return { error: error.message };
460
+ return { success: true, message: `Transaction ${transactionId} deleted` };
461
+ }
462
+
463
+ export async function executeListTransactionCategories(
464
+ _args: Record<string, unknown>,
465
+ ctx: MiraToolContext
466
+ ) {
467
+ const { data, error } = await ctx.supabase
468
+ .from('transaction_categories')
469
+ .select('id, name, is_expense, ws_id')
470
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
471
+
472
+ if (error) return { error: error.message };
473
+ return { count: data?.length ?? 0, categories: data ?? [] };
474
+ }
475
+
476
+ export async function executeCreateTransactionCategory(
477
+ args: Record<string, unknown>,
478
+ ctx: MiraToolContext
479
+ ) {
480
+ const { data, error } = await ctx.supabase
481
+ .from('transaction_categories')
482
+ .insert({
483
+ name: args.name as string,
484
+ is_expense: (args.isExpense as boolean) ?? true,
485
+ ws_id: getWorkspaceContextWorkspaceId(ctx),
486
+ })
487
+ .select('id, name, is_expense')
488
+ .single();
489
+
490
+ if (error) return { error: error.message };
491
+ return {
492
+ success: true,
493
+ message: `Category "${args.name}" created`,
494
+ category: data,
495
+ };
496
+ }
497
+
498
+ export async function executeUpdateTransactionCategory(
499
+ args: Record<string, unknown>,
500
+ ctx: MiraToolContext
501
+ ) {
502
+ const categoryId = args.categoryId as string;
503
+ const updates: TablesUpdate<'transaction_categories'> = {};
504
+
505
+ if (args.name !== undefined) {
506
+ if (typeof args.name !== 'string') {
507
+ return { error: 'name must be a string' };
508
+ }
509
+ updates.name = args.name;
510
+ }
511
+ if (args.isExpense !== undefined) {
512
+ if (args.isExpense !== null && typeof args.isExpense !== 'boolean') {
513
+ return { error: 'isExpense must be a boolean or null' };
514
+ }
515
+ updates.is_expense = args.isExpense;
516
+ }
517
+
518
+ if (Object.keys(updates).length === 0) {
519
+ return { success: true, message: 'No fields to update' };
520
+ }
521
+
522
+ const { error } = await ctx.supabase
523
+ .from('transaction_categories')
524
+ .update(updates)
525
+ .eq('id', categoryId)
526
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
527
+
528
+ if (error) return { error: error.message };
529
+ return { success: true, message: `Category ${categoryId} updated` };
530
+ }
531
+
532
+ export async function executeDeleteTransactionCategory(
533
+ args: Record<string, unknown>,
534
+ ctx: MiraToolContext
535
+ ) {
536
+ const categoryId = args.categoryId as string;
537
+
538
+ const { error } = await ctx.supabase
539
+ .from('transaction_categories')
540
+ .delete()
541
+ .eq('id', categoryId)
542
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
543
+
544
+ if (error) return { error: error.message };
545
+ return { success: true, message: `Category ${categoryId} deleted` };
546
+ }
547
+
548
+ export async function executeListTransactionTags(
549
+ _args: Record<string, unknown>,
550
+ ctx: MiraToolContext
551
+ ) {
552
+ const { data, error } = await ctx.supabase
553
+ .from('transaction_tags')
554
+ .select('id, name, color, description, ws_id')
555
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
556
+
557
+ if (error) return { error: error.message };
558
+ return { count: data?.length ?? 0, tags: data ?? [] };
559
+ }
560
+
561
+ export async function executeCreateTransactionTag(
562
+ args: Record<string, unknown>,
563
+ ctx: MiraToolContext
564
+ ) {
565
+ const parsed = createTransactionTagArgsSchema.safeParse(args);
566
+ if (!parsed.success) {
567
+ return {
568
+ error: parsed.error.issues[0]?.message ?? 'Invalid transaction tag data',
569
+ };
570
+ }
571
+
572
+ const validated = parsed.data;
573
+ const insertData: TablesInsert<'transaction_tags'> = {
574
+ name: validated.name,
575
+ ws_id: getWorkspaceContextWorkspaceId(ctx),
576
+ };
577
+ if (validated.color) insertData.color = validated.color;
578
+ if (validated.description) insertData.description = validated.description;
579
+
580
+ const { data, error } = await ctx.supabase
581
+ .from('transaction_tags')
582
+ .insert(insertData)
583
+ .select('id, name, color')
584
+ .single();
585
+
586
+ if (error) return { error: error.message };
587
+ return {
588
+ success: true,
589
+ message: `Tag "${validated.name}" created`,
590
+ tag: data,
591
+ };
592
+ }
593
+
594
+ export async function executeUpdateTransactionTag(
595
+ args: Record<string, unknown>,
596
+ ctx: MiraToolContext
597
+ ) {
598
+ const parsed = updateTransactionTagArgsSchema.safeParse(args);
599
+ if (!parsed.success) {
600
+ return {
601
+ error: parsed.error.issues[0]?.message ?? 'Invalid transaction tag data',
602
+ };
603
+ }
604
+
605
+ const validated = parsed.data;
606
+ const tagId = validated.tagId;
607
+ const updates: TablesUpdate<'transaction_tags'> = {};
608
+
609
+ if (validated.name !== undefined) updates.name = validated.name;
610
+ if (validated.color !== undefined) updates.color = validated.color;
611
+ if (validated.description !== undefined)
612
+ updates.description = validated.description;
613
+
614
+ const { error } = await ctx.supabase
615
+ .from('transaction_tags')
616
+ .update(updates)
617
+ .eq('id', tagId)
618
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
619
+
620
+ if (error) return { error: error.message };
621
+ return { success: true, message: `Tag ${tagId} updated` };
622
+ }
623
+
624
+ export async function executeDeleteTransactionTag(
625
+ args: Record<string, unknown>,
626
+ ctx: MiraToolContext
627
+ ) {
628
+ const tagId = args.tagId as string;
629
+
630
+ const { error } = await ctx.supabase
631
+ .from('transaction_tags')
632
+ .delete()
633
+ .eq('id', tagId)
634
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
635
+
636
+ if (error) return { error: error.message };
637
+ return { success: true, message: `Tag ${tagId} deleted` };
638
+ }