@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,1087 @@
1
+ import type { SupabaseClient } from '@tuturuuu/supabase';
2
+ import type { Enums, TablesInsert, TablesUpdate } from '@tuturuuu/types';
3
+ import { parseTaskDateToUTCISO } from '@tuturuuu/utils/task-date-timezone';
4
+ import type { MiraToolContext } from '../mira-tools';
5
+ import { getWorkspaceContextWorkspaceId } from '../workspace-context';
6
+ import {
7
+ getTaskScope,
8
+ hasProjectAccess,
9
+ hasTaskAccess,
10
+ hasTaskListAccess,
11
+ hasWorkspaceBoardAccess,
12
+ isWorkspaceMember,
13
+ type TaskScope,
14
+ } from './scope-helpers';
15
+
16
+ type RpcTask = {
17
+ task_id: string;
18
+ task_name: string;
19
+ task_priority: string | null;
20
+ task_end_date: string | null;
21
+ task_completed_at: string | null;
22
+ task_closed_at: string | null;
23
+ };
24
+
25
+ const mapTask = (t: RpcTask) => ({
26
+ id: t.task_id,
27
+ name: t.task_name,
28
+ priority: t.task_priority,
29
+ dueDate: t.task_end_date,
30
+ });
31
+
32
+ function getTaskScopePredicate(taskScope: TaskScope) {
33
+ const value = taskScope.boardId ?? taskScope.listId;
34
+ if (!value) {
35
+ return null;
36
+ }
37
+
38
+ return {
39
+ column: taskScope.boardId ? 'board_id' : 'list_id',
40
+ value,
41
+ } as const;
42
+ }
43
+
44
+ export async function executeGetMyTasks(
45
+ args: Record<string, unknown>,
46
+ ctx: MiraToolContext
47
+ ) {
48
+ const { userId, supabase } = ctx;
49
+ const wsId = getWorkspaceContextWorkspaceId(ctx);
50
+ const category = ((args.category ?? args.status) as string) || 'all';
51
+
52
+ const { data: tasks, error } = await supabase.rpc(
53
+ 'get_user_accessible_tasks',
54
+ {
55
+ p_user_id: userId,
56
+ p_ws_id: wsId,
57
+ p_include_deleted: false,
58
+ p_list_statuses: ['not_started', 'active', 'done'],
59
+ }
60
+ );
61
+
62
+ if (error) return { error: error.message };
63
+
64
+ const now = new Date();
65
+ const todayStart = new Date(now);
66
+ todayStart.setHours(0, 0, 0, 0);
67
+ const todayEnd = new Date(now);
68
+ todayEnd.setHours(23, 59, 59, 999);
69
+
70
+ const active = ((tasks as RpcTask[]) || []).filter(
71
+ (t) => !t.task_completed_at && !t.task_closed_at
72
+ );
73
+
74
+ const overdue = active
75
+ .filter((t) => t.task_end_date && t.task_end_date < now.toISOString())
76
+ .map(mapTask)
77
+ .slice(0, 30);
78
+
79
+ const today = active
80
+ .filter(
81
+ (t) =>
82
+ t.task_end_date &&
83
+ t.task_end_date >= todayStart.toISOString() &&
84
+ t.task_end_date <= todayEnd.toISOString()
85
+ )
86
+ .map(mapTask)
87
+ .slice(0, 30);
88
+
89
+ const upcoming = active
90
+ .filter((t) => !t.task_end_date || t.task_end_date > todayEnd.toISOString())
91
+ .map(mapTask)
92
+ .slice(0, 30);
93
+
94
+ const result: Record<string, unknown> = { totalActive: active.length };
95
+ if (category === 'all' || category === 'overdue')
96
+ result.overdue = { count: overdue.length, tasks: overdue };
97
+ if (category === 'all' || category === 'today')
98
+ result.today = { count: today.length, tasks: today };
99
+ if (category === 'all' || category === 'upcoming')
100
+ result.upcoming = { count: upcoming.length, tasks: upcoming };
101
+ return result;
102
+ }
103
+
104
+ export async function executeCreateTask(
105
+ args: Record<string, unknown>,
106
+ ctx: MiraToolContext
107
+ ) {
108
+ const { userId, supabase } = ctx;
109
+ const wsId = getWorkspaceContextWorkspaceId(ctx);
110
+ const name = args.name as string;
111
+ const description = args.description as string | null;
112
+ const priority = (args.priority as Enums<'task_priority'> | null) ?? null;
113
+ const boardIdArg = args.boardId as string | undefined;
114
+ const listIdArg = args.listId as string | undefined;
115
+ const assignToSelf = (args.assignToSelf as boolean | undefined) !== false;
116
+
117
+ // If listId is provided directly, validate it exists and use it
118
+ if (listIdArg) {
119
+ const { data: targetList } = await supabase
120
+ .from('task_lists')
121
+ .select('id, board_id, workspace_boards!inner(ws_id)')
122
+ .eq('id', listIdArg)
123
+ .eq('archived', false)
124
+ .single();
125
+
126
+ if (!targetList) {
127
+ return {
128
+ error: `Task list "${listIdArg}" not found or archived. Use list_task_lists to discover valid lists.`,
129
+ };
130
+ }
131
+
132
+ const listWsId = (
133
+ targetList.workspace_boards as unknown as { ws_id: string }
134
+ )?.ws_id;
135
+ if (listWsId && listWsId !== wsId) {
136
+ return {
137
+ error: `Task list does not belong to the current workspace context. Switch workspace first with set_workspace_context.`,
138
+ };
139
+ }
140
+
141
+ // Use the validated list directly — skip board/list auto-resolution
142
+ const { data: task, error } = await supabase
143
+ .from('tasks')
144
+ .insert({
145
+ name,
146
+ description: description
147
+ ? JSON.stringify({
148
+ type: 'doc',
149
+ content: [
150
+ {
151
+ type: 'paragraph',
152
+ content: [{ type: 'text', text: description }],
153
+ },
154
+ ],
155
+ })
156
+ : null,
157
+ list_id: listIdArg,
158
+ priority,
159
+ completed: false,
160
+ })
161
+ .select('id, name, priority, created_at')
162
+ .single();
163
+
164
+ if (error) return { error: error.message };
165
+
166
+ if (assignToSelf && task) {
167
+ const { error: assignErr } = await supabase
168
+ .from('task_assignees')
169
+ .insert({ task_id: task.id, user_id: userId });
170
+ if (assignErr) {
171
+ return {
172
+ success: true,
173
+ message: `Task "${name}" created in specified list, but auto-assignment failed: ${assignErr.message}`,
174
+ task,
175
+ };
176
+ }
177
+ return {
178
+ success: true,
179
+ message: `Task "${name}" created in specified list and assigned to you`,
180
+ task,
181
+ };
182
+ }
183
+
184
+ return {
185
+ success: true,
186
+ message: `Task "${name}" created in specified list (unassigned)`,
187
+ task,
188
+ };
189
+ }
190
+
191
+ // Resolve board: use provided boardId or fall back to first workspace board
192
+ let board: { id: string } | null = null;
193
+
194
+ if (boardIdArg) {
195
+ const { data: targetBoard } = await supabase
196
+ .from('workspace_boards')
197
+ .select('id')
198
+ .eq('id', boardIdArg)
199
+ .eq('ws_id', wsId)
200
+ .single();
201
+
202
+ if (!targetBoard) {
203
+ return {
204
+ error: `Board "${boardIdArg}" not found in this workspace. Use list_boards to discover valid boards.`,
205
+ };
206
+ }
207
+ board = targetBoard;
208
+ } else {
209
+ const { data: firstBoard } = await supabase
210
+ .from('workspace_boards')
211
+ .select('id')
212
+ .eq('ws_id', wsId)
213
+ .limit(1)
214
+ .single();
215
+ board = firstBoard;
216
+ }
217
+
218
+ if (!board) {
219
+ const { data: newBoard, error: boardErr } = await supabase
220
+ .from('workspace_boards')
221
+ .insert({ name: 'Tasks', ws_id: wsId })
222
+ .select('id')
223
+ .single();
224
+ if (boardErr || !newBoard)
225
+ return {
226
+ error: `Failed to create board: ${boardErr?.message ?? 'Unknown error'}`,
227
+ };
228
+ board = newBoard;
229
+ }
230
+
231
+ let { data: list } = await supabase
232
+ .from('task_lists')
233
+ .select('id')
234
+ .eq('board_id', board.id)
235
+ .eq('archived', false)
236
+ .order('created_at', { ascending: true })
237
+ .limit(1)
238
+ .single();
239
+
240
+ if (!list) {
241
+ const { data: newList, error: listErr } = await supabase
242
+ .from('task_lists')
243
+ .insert({ name: 'To Do', board_id: board.id })
244
+ .select('id')
245
+ .single();
246
+ if (listErr || !newList)
247
+ return {
248
+ error: `Failed to create list: ${listErr?.message ?? 'Unknown error'}`,
249
+ };
250
+ list = newList;
251
+ }
252
+
253
+ const { data: task, error } = await supabase
254
+ .from('tasks')
255
+ .insert({
256
+ name,
257
+ description: description
258
+ ? JSON.stringify({
259
+ type: 'doc',
260
+ content: [
261
+ {
262
+ type: 'paragraph',
263
+ content: [{ type: 'text', text: description }],
264
+ },
265
+ ],
266
+ })
267
+ : null,
268
+ list_id: list.id,
269
+ priority,
270
+ completed: false,
271
+ })
272
+ .select('id, name, priority, created_at')
273
+ .single();
274
+
275
+ if (error) return { error: error.message };
276
+
277
+ if (assignToSelf && task) {
278
+ const { error: assignErr } = await supabase
279
+ .from('task_assignees')
280
+ .insert({ task_id: task.id, user_id: userId });
281
+ if (assignErr) {
282
+ return {
283
+ success: true,
284
+ message: `Task "${name}" created, but auto-assignment failed: ${assignErr.message}`,
285
+ task,
286
+ };
287
+ }
288
+ return {
289
+ success: true,
290
+ message: `Task "${name}" created and assigned to you`,
291
+ task,
292
+ };
293
+ }
294
+
295
+ return {
296
+ success: true,
297
+ message: `Task "${name}" created (unassigned)`,
298
+ task,
299
+ };
300
+ }
301
+
302
+ export async function executeCompleteTask(
303
+ args: Record<string, unknown>,
304
+ ctx: MiraToolContext
305
+ ) {
306
+ const taskId = args.taskId as string;
307
+ let taskScope: TaskScope | null = null;
308
+
309
+ try {
310
+ taskScope = await getTaskScope(ctx, taskId);
311
+ if (!taskScope) {
312
+ return { error: 'Task not found in current workspace' };
313
+ }
314
+ } catch (error) {
315
+ return {
316
+ error: error instanceof Error ? error.message : 'Task lookup failed',
317
+ };
318
+ }
319
+ const taskScopePredicate = getTaskScopePredicate(taskScope);
320
+ if (!taskScopePredicate) {
321
+ return { error: 'Task not found in current workspace' };
322
+ }
323
+
324
+ const { error } = await ctx.supabase
325
+ .from('tasks')
326
+ .update({ completed: true, completed_at: new Date().toISOString() })
327
+ .eq('id', taskId)
328
+ .eq(taskScopePredicate.column, taskScopePredicate.value);
329
+
330
+ if (error) return { error: error.message };
331
+ return { success: true, message: 'Task marked as completed' };
332
+ }
333
+
334
+ // ── New CRUD tools ──
335
+
336
+ const UPDATE_TASK_FIELDS_HINT =
337
+ 'Accepted fields: taskId (or id), endDate (or dueDate, ISO), name, description, priority, startDate, listId, estimationPoints.';
338
+
339
+ export async function executeUpdateTask(
340
+ args: Record<string, unknown>,
341
+ ctx: MiraToolContext
342
+ ) {
343
+ const taskId = (args.taskId ?? args.id) as string | undefined;
344
+ if (!taskId) {
345
+ return {
346
+ error: `Task ID required. Use taskId or id. ${UPDATE_TASK_FIELDS_HINT}`,
347
+ };
348
+ }
349
+ const updates: TablesUpdate<'tasks'> = {};
350
+
351
+ if (args.name !== undefined) updates.name = args.name as string;
352
+ if (args.description !== undefined) {
353
+ const desc = args.description as string | null;
354
+ updates.description = desc
355
+ ? JSON.stringify({
356
+ type: 'doc',
357
+ content: [
358
+ { type: 'paragraph', content: [{ type: 'text', text: desc }] },
359
+ ],
360
+ })
361
+ : null;
362
+ }
363
+ if (args.priority !== undefined)
364
+ updates.priority = args.priority as Enums<'task_priority'>;
365
+ if (args.startDate !== undefined) {
366
+ const v = args.startDate as string | null;
367
+ updates.start_date =
368
+ v == null
369
+ ? null
370
+ : ctx.timezone
371
+ ? parseTaskDateToUTCISO(v, ctx.timezone, false)
372
+ : v;
373
+ }
374
+ const endDateValue = args.endDate ?? args.dueDate;
375
+ if (endDateValue !== undefined) {
376
+ const v = endDateValue as string | null;
377
+ updates.end_date =
378
+ v == null
379
+ ? null
380
+ : ctx.timezone
381
+ ? parseTaskDateToUTCISO(v, ctx.timezone, true)
382
+ : v;
383
+ }
384
+ if (args.estimationPoints !== undefined)
385
+ updates.estimation_points = args.estimationPoints as number;
386
+ if (args.listId !== undefined) {
387
+ const nextListId = args.listId as string;
388
+ try {
389
+ if (!(await hasTaskListAccess(ctx, nextListId))) {
390
+ return { error: 'Target list not found in current workspace' };
391
+ }
392
+ } catch (error) {
393
+ return {
394
+ error: error instanceof Error ? error.message : 'List lookup failed',
395
+ };
396
+ }
397
+ updates.list_id = nextListId;
398
+ }
399
+
400
+ if (Object.keys(updates).length === 0) {
401
+ return {
402
+ success: true,
403
+ message: `No fields to update. ${UPDATE_TASK_FIELDS_HINT}`,
404
+ };
405
+ }
406
+
407
+ let taskScope: TaskScope | null = null;
408
+ try {
409
+ taskScope = await getTaskScope(ctx, taskId);
410
+ if (!taskScope) {
411
+ return { error: 'Task not found in current workspace' };
412
+ }
413
+ } catch (error) {
414
+ return {
415
+ error: error instanceof Error ? error.message : 'Task lookup failed',
416
+ };
417
+ }
418
+ const taskScopePredicate = getTaskScopePredicate(taskScope);
419
+ if (!taskScopePredicate) {
420
+ return { error: 'Task not found in current workspace' };
421
+ }
422
+
423
+ const { error } = await ctx.supabase
424
+ .from('tasks')
425
+ .update(updates)
426
+ .eq('id', taskId)
427
+ .eq(taskScopePredicate.column, taskScopePredicate.value);
428
+
429
+ if (error) return { error: error.message };
430
+ return { success: true, message: `Task ${taskId} updated` };
431
+ }
432
+
433
+ export async function executeDeleteTask(
434
+ args: Record<string, unknown>,
435
+ ctx: MiraToolContext
436
+ ) {
437
+ const taskId = args.taskId as string;
438
+ let taskScope: TaskScope | null = null;
439
+
440
+ try {
441
+ taskScope = await getTaskScope(ctx, taskId);
442
+ if (!taskScope) {
443
+ return { error: 'Task not found in current workspace' };
444
+ }
445
+ } catch (error) {
446
+ return {
447
+ error: error instanceof Error ? error.message : 'Task lookup failed',
448
+ };
449
+ }
450
+ const taskScopePredicate = getTaskScopePredicate(taskScope);
451
+ if (!taskScopePredicate) {
452
+ return { error: 'Task not found in current workspace' };
453
+ }
454
+
455
+ const { error } = await ctx.supabase
456
+ .from('tasks')
457
+ .update({ deleted_at: new Date().toISOString() })
458
+ .eq('id', taskId)
459
+ .eq(taskScopePredicate.column, taskScopePredicate.value);
460
+
461
+ if (error) return { error: error.message };
462
+ return { success: true, message: `Task ${taskId} deleted` };
463
+ }
464
+
465
+ export async function executeListBoards(
466
+ _args: Record<string, unknown>,
467
+ ctx: MiraToolContext
468
+ ) {
469
+ const { data, error } = await ctx.supabase
470
+ .from('workspace_boards')
471
+ .select('id, name, created_at')
472
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
473
+ .order('created_at', { ascending: true });
474
+
475
+ if (error) return { error: error.message };
476
+ return { count: data?.length ?? 0, boards: data ?? [] };
477
+ }
478
+
479
+ export async function executeCreateBoard(
480
+ args: Record<string, unknown>,
481
+ ctx: MiraToolContext
482
+ ) {
483
+ const { data, error } = await ctx.supabase
484
+ .from('workspace_boards')
485
+ .insert({
486
+ name: args.name as string,
487
+ ws_id: getWorkspaceContextWorkspaceId(ctx),
488
+ })
489
+ .select('id, name')
490
+ .single();
491
+
492
+ if (error) return { error: error.message };
493
+ return {
494
+ success: true,
495
+ message: `Board "${args.name}" created`,
496
+ board: data,
497
+ };
498
+ }
499
+
500
+ export async function executeUpdateBoard(
501
+ args: Record<string, unknown>,
502
+ ctx: MiraToolContext
503
+ ) {
504
+ const boardId = args.boardId as string;
505
+ const updates: TablesUpdate<'workspace_boards'> = {};
506
+
507
+ if (args.name !== undefined) {
508
+ if (args.name !== null && typeof args.name !== 'string') {
509
+ return { error: 'name must be a string or null' };
510
+ }
511
+ updates.name = args.name;
512
+ }
513
+
514
+ if (Object.keys(updates).length === 0) {
515
+ return { success: true, message: 'No fields to update' };
516
+ }
517
+
518
+ const { error } = await ctx.supabase
519
+ .from('workspace_boards')
520
+ .update(updates)
521
+ .eq('id', boardId)
522
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
523
+
524
+ if (error) return { error: error.message };
525
+ return { success: true, message: `Board ${boardId} updated` };
526
+ }
527
+
528
+ export async function executeDeleteBoard(
529
+ args: Record<string, unknown>,
530
+ ctx: MiraToolContext
531
+ ) {
532
+ const boardId = args.boardId as string;
533
+
534
+ const { error } = await ctx.supabase
535
+ .from('workspace_boards')
536
+ .delete()
537
+ .eq('id', boardId)
538
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
539
+
540
+ if (error) return { error: error.message };
541
+ return { success: true, message: `Board ${boardId} deleted` };
542
+ }
543
+
544
+ export async function executeListTaskLists(
545
+ args: Record<string, unknown>,
546
+ ctx: MiraToolContext
547
+ ) {
548
+ const boardId = args.boardId as string;
549
+
550
+ try {
551
+ if (!(await hasWorkspaceBoardAccess(ctx, boardId))) {
552
+ return { error: 'Board not found in current workspace' };
553
+ }
554
+ } catch (error) {
555
+ return {
556
+ error: error instanceof Error ? error.message : 'Board lookup failed',
557
+ };
558
+ }
559
+
560
+ const { data, error } = await ctx.supabase
561
+ .from('task_lists')
562
+ .select('id, name, board_id, color, position, archived')
563
+ .eq('board_id', boardId)
564
+ .eq('archived', false)
565
+ .order('position', { ascending: true });
566
+
567
+ if (error) return { error: error.message };
568
+ return { count: data?.length ?? 0, lists: data ?? [] };
569
+ }
570
+
571
+ export async function executeCreateTaskList(
572
+ args: Record<string, unknown>,
573
+ ctx: MiraToolContext
574
+ ) {
575
+ const boardId = args.boardId as string;
576
+
577
+ // Verify board belongs to workspace
578
+ const { data: board } = await ctx.supabase
579
+ .from('workspace_boards')
580
+ .select('id')
581
+ .eq('id', boardId)
582
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
583
+ .single();
584
+
585
+ if (!board) return { error: 'Board not found in this workspace' };
586
+
587
+ const insertData: TablesInsert<'task_lists'> = {
588
+ name: args.name as string,
589
+ board_id: boardId,
590
+ };
591
+ if (args.color) insertData.color = args.color as string;
592
+
593
+ const { data, error } = await ctx.supabase
594
+ .from('task_lists')
595
+ .insert(insertData)
596
+ .select('id, name')
597
+ .single();
598
+
599
+ if (error) return { error: error.message };
600
+ return { success: true, message: `List "${args.name}" created`, list: data };
601
+ }
602
+
603
+ export async function executeUpdateTaskList(
604
+ args: Record<string, unknown>,
605
+ ctx: MiraToolContext
606
+ ) {
607
+ const listId = args.listId as string;
608
+ const updates: TablesUpdate<'task_lists'> = {};
609
+
610
+ if (args.name !== undefined) updates.name = args.name as string;
611
+ if (args.color !== undefined) updates.color = args.color as string;
612
+ if (args.position !== undefined) updates.position = args.position as number;
613
+
614
+ if (Object.keys(updates).length === 0) {
615
+ return { success: true, message: 'No fields to update' };
616
+ }
617
+
618
+ try {
619
+ if (!(await hasTaskListAccess(ctx, listId))) {
620
+ return { error: 'List not found in current workspace' };
621
+ }
622
+ } catch (error) {
623
+ return {
624
+ error: error instanceof Error ? error.message : 'List lookup failed',
625
+ };
626
+ }
627
+
628
+ const { error } = await ctx.supabase
629
+ .from('task_lists')
630
+ .update(updates)
631
+ .eq('id', listId);
632
+
633
+ if (error) return { error: error.message };
634
+ return { success: true, message: `List ${listId} updated` };
635
+ }
636
+
637
+ export async function executeDeleteTaskList(
638
+ args: Record<string, unknown>,
639
+ ctx: MiraToolContext
640
+ ) {
641
+ const listId = args.listId as string;
642
+
643
+ try {
644
+ if (!(await hasTaskListAccess(ctx, listId))) {
645
+ return { error: 'List not found in current workspace' };
646
+ }
647
+ } catch (error) {
648
+ return {
649
+ error: error instanceof Error ? error.message : 'List lookup failed',
650
+ };
651
+ }
652
+
653
+ const { error } = await ctx.supabase
654
+ .from('task_lists')
655
+ .delete()
656
+ .eq('id', listId);
657
+
658
+ if (error) return { error: error.message };
659
+ return { success: true, message: `List ${listId} deleted` };
660
+ }
661
+
662
+ export async function executeListTaskLabels(
663
+ _args: Record<string, unknown>,
664
+ ctx: MiraToolContext
665
+ ) {
666
+ const { data, error } = await ctx.supabase
667
+ .from('workspace_task_labels')
668
+ .select('id, name, color, ws_id')
669
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
670
+
671
+ if (error) return { error: error.message };
672
+ return { count: data?.length ?? 0, labels: data ?? [] };
673
+ }
674
+
675
+ export async function executeCreateTaskLabel(
676
+ args: Record<string, unknown>,
677
+ ctx: MiraToolContext
678
+ ) {
679
+ const insertData: TablesInsert<'workspace_task_labels'> = {
680
+ name: args.name as string,
681
+ ws_id: getWorkspaceContextWorkspaceId(ctx),
682
+ color: (args.color as string) ?? '#3B82F6',
683
+ };
684
+
685
+ const { data, error } = await ctx.supabase
686
+ .from('workspace_task_labels')
687
+ .insert(insertData)
688
+ .select('id, name, color')
689
+ .single();
690
+
691
+ if (error) return { error: error.message };
692
+ return {
693
+ success: true,
694
+ message: `Label "${args.name}" created`,
695
+ label: data,
696
+ };
697
+ }
698
+
699
+ export async function executeUpdateTaskLabel(
700
+ args: Record<string, unknown>,
701
+ ctx: MiraToolContext
702
+ ) {
703
+ const labelId = args.labelId as string;
704
+ const updates: TablesUpdate<'workspace_task_labels'> = {};
705
+
706
+ if (args.name !== undefined) updates.name = args.name as string;
707
+ if (args.color !== undefined) updates.color = args.color as string;
708
+
709
+ if (Object.keys(updates).length === 0) {
710
+ return { success: true, message: 'No fields to update' };
711
+ }
712
+
713
+ const { error } = await ctx.supabase
714
+ .from('workspace_task_labels')
715
+ .update(updates)
716
+ .eq('id', labelId)
717
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
718
+
719
+ if (error) return { error: error.message };
720
+ return { success: true, message: `Label ${labelId} updated` };
721
+ }
722
+
723
+ export async function executeDeleteTaskLabel(
724
+ args: Record<string, unknown>,
725
+ ctx: MiraToolContext
726
+ ) {
727
+ const labelId = args.labelId as string;
728
+
729
+ const { error } = await ctx.supabase
730
+ .from('workspace_task_labels')
731
+ .delete()
732
+ .eq('id', labelId)
733
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
734
+
735
+ if (error) return { error: error.message };
736
+ return { success: true, message: `Label ${labelId} deleted` };
737
+ }
738
+
739
+ export async function executeAddTaskLabels(
740
+ args: Record<string, unknown>,
741
+ ctx: MiraToolContext
742
+ ) {
743
+ const taskId = args.taskId as string;
744
+ const labelIds = args.labelIds as string[];
745
+ const uniqueLabelIds = [...new Set(labelIds)];
746
+
747
+ if (uniqueLabelIds.length === 0) {
748
+ return { success: true, message: 'No labels provided' };
749
+ }
750
+
751
+ try {
752
+ if (!(await hasTaskAccess(ctx, taskId))) {
753
+ return {
754
+ error: 'Authorization failed: task does not belong to this workspace',
755
+ };
756
+ }
757
+ } catch (error) {
758
+ return {
759
+ error: error instanceof Error ? error.message : 'Task lookup failed',
760
+ };
761
+ }
762
+
763
+ const { data: labels, error: labelsError } = await ctx.supabase
764
+ .from('workspace_task_labels')
765
+ .select('id')
766
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
767
+ .in('id', uniqueLabelIds);
768
+
769
+ if (labelsError) return { error: labelsError.message };
770
+ if ((labels?.length ?? 0) !== uniqueLabelIds.length) {
771
+ return {
772
+ error:
773
+ 'Authorization failed: one or more labels do not belong to this workspace',
774
+ };
775
+ }
776
+
777
+ const rows: TablesInsert<'task_labels'>[] = uniqueLabelIds.map((labelId) => ({
778
+ task_id: taskId,
779
+ label_id: labelId,
780
+ }));
781
+
782
+ const { error } = await ctx.supabase
783
+ .from('task_labels')
784
+ .upsert(rows, { onConflict: 'task_id,label_id' });
785
+
786
+ if (error) return { error: error.message };
787
+ return {
788
+ success: true,
789
+ message: `${uniqueLabelIds.length} label(s) added to task`,
790
+ };
791
+ }
792
+
793
+ export async function executeRemoveTaskLabels(
794
+ args: Record<string, unknown>,
795
+ ctx: MiraToolContext
796
+ ) {
797
+ const taskId = args.taskId as string;
798
+ const labelIds = args.labelIds as string[];
799
+ const uniqueLabelIds = [...new Set(labelIds)];
800
+
801
+ if (uniqueLabelIds.length === 0) {
802
+ return { success: true, message: 'No labels provided' };
803
+ }
804
+
805
+ try {
806
+ if (!(await hasTaskAccess(ctx, taskId))) {
807
+ return {
808
+ error: 'Authorization failed: task does not belong to this workspace',
809
+ };
810
+ }
811
+ } catch (error) {
812
+ return {
813
+ error: error instanceof Error ? error.message : 'Task lookup failed',
814
+ };
815
+ }
816
+
817
+ const { data: labels, error: labelsError } = await ctx.supabase
818
+ .from('workspace_task_labels')
819
+ .select('id')
820
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
821
+ .in('id', uniqueLabelIds);
822
+
823
+ if (labelsError) return { error: labelsError.message };
824
+ if ((labels?.length ?? 0) !== uniqueLabelIds.length) {
825
+ return {
826
+ error:
827
+ 'Authorization failed: one or more labels do not belong to this workspace',
828
+ };
829
+ }
830
+
831
+ const { error } = await ctx.supabase
832
+ .from('task_labels')
833
+ .delete()
834
+ .eq('task_id', taskId)
835
+ .in('label_id', uniqueLabelIds);
836
+
837
+ if (error) return { error: error.message };
838
+ return {
839
+ success: true,
840
+ message: `${uniqueLabelIds.length} label(s) removed from task`,
841
+ };
842
+ }
843
+
844
+ export async function executeListProjects(
845
+ _args: Record<string, unknown>,
846
+ ctx: MiraToolContext
847
+ ) {
848
+ const { data, error } = await ctx.supabase
849
+ .from('task_projects')
850
+ .select('id, name, description, created_at')
851
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
852
+ .order('created_at', { ascending: false });
853
+
854
+ if (error) return { error: error.message };
855
+ return { count: data?.length ?? 0, projects: data ?? [] };
856
+ }
857
+
858
+ export async function executeCreateProject(
859
+ args: Record<string, unknown>,
860
+ ctx: MiraToolContext
861
+ ) {
862
+ const insertData: TablesInsert<'task_projects'> = {
863
+ name: args.name as string,
864
+ ws_id: getWorkspaceContextWorkspaceId(ctx),
865
+ };
866
+ if (args.description) insertData.description = args.description as string;
867
+
868
+ const { data, error } = await ctx.supabase
869
+ .from('task_projects')
870
+ .insert(insertData)
871
+ .select('id, name')
872
+ .single();
873
+
874
+ if (error) return { error: error.message };
875
+ return {
876
+ success: true,
877
+ message: `Project "${args.name}" created`,
878
+ project: data,
879
+ };
880
+ }
881
+
882
+ export async function executeUpdateProject(
883
+ args: Record<string, unknown>,
884
+ ctx: MiraToolContext
885
+ ) {
886
+ const projectId = args.projectId as string;
887
+ const updates: TablesUpdate<'task_projects'> = {};
888
+
889
+ if (args.name !== undefined) updates.name = args.name as string;
890
+ if (args.description !== undefined)
891
+ updates.description = args.description as string;
892
+
893
+ if (Object.keys(updates).length === 0) {
894
+ return { success: true, message: 'No fields to update' };
895
+ }
896
+
897
+ const { error } = await ctx.supabase
898
+ .from('task_projects')
899
+ .update(updates)
900
+ .eq('id', projectId)
901
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
902
+
903
+ if (error) return { error: error.message };
904
+ return { success: true, message: `Project ${projectId} updated` };
905
+ }
906
+
907
+ export async function executeDeleteProject(
908
+ args: Record<string, unknown>,
909
+ ctx: MiraToolContext
910
+ ) {
911
+ const projectId = args.projectId as string;
912
+
913
+ const { error } = await ctx.supabase
914
+ .from('task_projects')
915
+ .delete()
916
+ .eq('id', projectId)
917
+ .eq('ws_id', getWorkspaceContextWorkspaceId(ctx));
918
+
919
+ if (error) return { error: error.message };
920
+ return { success: true, message: `Project ${projectId} deleted` };
921
+ }
922
+
923
+ export async function executeAddTaskToProject(
924
+ args: Record<string, unknown>,
925
+ ctx: MiraToolContext
926
+ ) {
927
+ try {
928
+ if (!(await hasTaskAccess(ctx, args.taskId as string))) {
929
+ return { error: 'Task not found in current workspace' };
930
+ }
931
+ if (!(await hasProjectAccess(ctx, args.projectId as string))) {
932
+ return { error: 'Project not found in current workspace' };
933
+ }
934
+ } catch (error) {
935
+ return {
936
+ error:
937
+ error instanceof Error
938
+ ? error.message
939
+ : 'Task or project lookup failed',
940
+ };
941
+ }
942
+
943
+ const { error } = await ctx.supabase.from('task_project_tasks').upsert(
944
+ {
945
+ task_id: args.taskId as string,
946
+ project_id: args.projectId as string,
947
+ },
948
+ { onConflict: 'task_id,project_id' }
949
+ );
950
+
951
+ if (error) return { error: error.message };
952
+ return { success: true, message: 'Task linked to project' };
953
+ }
954
+
955
+ export async function executeRemoveTaskFromProject(
956
+ args: Record<string, unknown>,
957
+ ctx: MiraToolContext
958
+ ) {
959
+ try {
960
+ if (!(await hasTaskAccess(ctx, args.taskId as string))) {
961
+ return { error: 'Task not found in current workspace' };
962
+ }
963
+ if (!(await hasProjectAccess(ctx, args.projectId as string))) {
964
+ return { error: 'Project not found in current workspace' };
965
+ }
966
+ } catch (error) {
967
+ return {
968
+ error:
969
+ error instanceof Error
970
+ ? error.message
971
+ : 'Task or project lookup failed',
972
+ };
973
+ }
974
+
975
+ const { error } = await ctx.supabase
976
+ .from('task_project_tasks')
977
+ .delete()
978
+ .eq('task_id', args.taskId as string)
979
+ .eq('project_id', args.projectId as string);
980
+
981
+ if (error) return { error: error.message };
982
+ return { success: true, message: 'Task unlinked from project' };
983
+ }
984
+
985
+ export async function executeAddTaskAssignee(
986
+ args: Record<string, unknown>,
987
+ ctx: MiraToolContext
988
+ ) {
989
+ try {
990
+ if (!(await hasTaskAccess(ctx, args.taskId as string))) {
991
+ return { error: 'Task not found in current workspace' };
992
+ }
993
+ if (!(await isWorkspaceMember(ctx, args.userId as string))) {
994
+ return { error: 'Assignee is not a member of the current workspace' };
995
+ }
996
+ } catch (error) {
997
+ return {
998
+ error:
999
+ error instanceof Error ? error.message : 'Task or member lookup failed',
1000
+ };
1001
+ }
1002
+
1003
+ const { error } = await ctx.supabase.from('task_assignees').upsert(
1004
+ {
1005
+ task_id: args.taskId as string,
1006
+ user_id: args.userId as string,
1007
+ },
1008
+ { onConflict: 'task_id,user_id' }
1009
+ );
1010
+
1011
+ if (error) return { error: error.message };
1012
+ return { success: true, message: 'Assignee added to task' };
1013
+ }
1014
+
1015
+ export async function executeRemoveTaskAssignee(
1016
+ args: Record<string, unknown>,
1017
+ ctx: MiraToolContext
1018
+ ) {
1019
+ try {
1020
+ if (!(await hasTaskAccess(ctx, args.taskId as string))) {
1021
+ return { error: 'Task not found in current workspace' };
1022
+ }
1023
+ } catch (error) {
1024
+ return {
1025
+ error: error instanceof Error ? error.message : 'Task lookup failed',
1026
+ };
1027
+ }
1028
+
1029
+ const { error } = await ctx.supabase
1030
+ .from('task_assignees')
1031
+ .delete()
1032
+ .eq('task_id', args.taskId as string)
1033
+ .eq('user_id', args.userId as string);
1034
+
1035
+ if (error) return { error: error.message };
1036
+ return { success: true, message: 'Assignee removed from task' };
1037
+ }
1038
+
1039
+ // Re-export helper to resolve a default board + list for task creation
1040
+ async function resolveDefaultBoardAndList(
1041
+ wsId: string,
1042
+ supabase: SupabaseClient
1043
+ ): Promise<{ boardId: string; listId: string } | { error: string }> {
1044
+ let { data: board } = await supabase
1045
+ .from('workspace_boards')
1046
+ .select('id')
1047
+ .eq('ws_id', wsId)
1048
+ .limit(1)
1049
+ .single();
1050
+
1051
+ if (!board) {
1052
+ const { data: newBoard, error } = await supabase
1053
+ .from('workspace_boards')
1054
+ .insert({ name: 'Tasks', ws_id: wsId })
1055
+ .select('id')
1056
+ .single();
1057
+ if (error || !newBoard)
1058
+ return { error: `Failed to create board: ${error?.message}` };
1059
+ board = newBoard;
1060
+ }
1061
+
1062
+ let { data: list } = await supabase
1063
+ .from('task_lists')
1064
+ .select('id')
1065
+ .eq('board_id', board.id)
1066
+ .eq('archived', false)
1067
+ .order('created_at', { ascending: true })
1068
+ .limit(1)
1069
+ .single();
1070
+
1071
+ if (!list) {
1072
+ const { data: newList, error } = await supabase
1073
+ .from('task_lists')
1074
+ .insert({ name: 'To Do', board_id: board.id })
1075
+ .select('id')
1076
+ .single();
1077
+ if (error || !newList)
1078
+ return { error: `Failed to create list: ${error?.message}` };
1079
+ list = newList;
1080
+ }
1081
+
1082
+ return { boardId: board.id, listId: list.id };
1083
+ }
1084
+
1085
+ // Exported for potential reuse
1086
+ // Exported for potential reuse
1087
+ export { resolveDefaultBoardAndList };