@tuturuuu/ai 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/package.json +106 -0
- package/src/api-key-hash.ts +28 -0
- package/src/calendar/events.ts +34 -0
- package/src/calendar/route.ts +114 -0
- package/src/chat/credit-source.ts +1 -0
- package/src/chat/google/chat-request-schema.ts +150 -0
- package/src/chat/google/default-system-instruction.ts +198 -0
- package/src/chat/google/message-file-processing.ts +212 -0
- package/src/chat/google/mira-step-preparation.ts +221 -0
- package/src/chat/google/new/route.ts +368 -0
- package/src/chat/google/route-auth.ts +81 -0
- package/src/chat/google/route-chat-resolution.ts +98 -0
- package/src/chat/google/route-credits.ts +61 -0
- package/src/chat/google/route-message-preparation.ts +331 -0
- package/src/chat/google/route-mira-runtime.ts +206 -0
- package/src/chat/google/route.ts +632 -0
- package/src/chat/google/stream-finish-persistence.ts +722 -0
- package/src/chat/google/summary/route.ts +153 -0
- package/src/chat/mira-render-ui-policy.ts +540 -0
- package/src/chat/mira-system-instruction.ts +484 -0
- package/src/chat-sdk/adapters.ts +389 -0
- package/src/chat-sdk/registry.ts +197 -0
- package/src/chat-sdk.ts +33 -0
- package/src/core.ts +3 -0
- package/src/credits/cap-output-tokens.ts +90 -0
- package/src/credits/check-credits.ts +232 -0
- package/src/credits/constants.ts +30 -0
- package/src/credits/index.ts +46 -0
- package/src/credits/model-mapping.ts +92 -0
- package/src/credits/reservations.ts +514 -0
- package/src/credits/resolve-plan-model.ts +219 -0
- package/src/credits/sync-gateway-models.ts +351 -0
- package/src/credits/types.ts +109 -0
- package/src/credits/use-ai-credits.ts +3 -0
- package/src/embeddings/metered.ts +283 -0
- package/src/executions/route.ts +137 -0
- package/src/generate/route.ts +411 -0
- package/src/hooks.ts +7 -0
- package/src/meetings/summary/route.ts +7 -0
- package/src/meetings/transcription/route.ts +134 -0
- package/src/memory/client.ts +158 -0
- package/src/memory/config.ts +38 -0
- package/src/memory/index.ts +32 -0
- package/src/memory/ingest.ts +51 -0
- package/src/memory/middleware.ts +35 -0
- package/src/memory/operations.ts +480 -0
- package/src/memory/scope.ts +102 -0
- package/src/memory/settings.ts +121 -0
- package/src/memory/types.ts +101 -0
- package/src/memory/workspace.ts +36 -0
- package/src/memory.ts +1 -0
- package/src/mind/patch.ts +146 -0
- package/src/mind/route.ts +687 -0
- package/src/mind/tools.ts +1500 -0
- package/src/mind/types.ts +20 -0
- package/src/object/core.ts +3 -0
- package/src/object/flashcards/route.ts +140 -0
- package/src/object/quizzes/explanation/route.ts +145 -0
- package/src/object/quizzes/route.ts +142 -0
- package/src/object/types.ts +187 -0
- package/src/object/year-plan/route.ts +196 -0
- package/src/react.ts +1 -0
- package/src/scheduling/algorithm.ts +791 -0
- package/src/scheduling/default.ts +36 -0
- package/src/scheduling/duration-optimizer.ts +689 -0
- package/src/scheduling/index.ts +79 -0
- package/src/scheduling/priority-calculator.ts +187 -0
- package/src/scheduling/recurrence-calculator.ts +621 -0
- package/src/scheduling/templates.ts +892 -0
- package/src/scheduling/types.ts +136 -0
- package/src/scheduling/web-adapter.ts +308 -0
- package/src/scheduling.ts +6 -0
- package/src/supported-actions.ts +1 -0
- package/src/supported-providers.ts +6 -0
- package/src/tools/context-builder.ts +372 -0
- package/src/tools/core.ts +1 -0
- package/src/tools/definitions/calendar.ts +106 -0
- package/src/tools/definitions/finance.ts +197 -0
- package/src/tools/definitions/image.ts +74 -0
- package/src/tools/definitions/memory.ts +83 -0
- package/src/tools/definitions/meta.ts +154 -0
- package/src/tools/definitions/render-ui.ts +81 -0
- package/src/tools/definitions/tasks.ts +343 -0
- package/src/tools/definitions/time-tracking.ts +381 -0
- package/src/tools/definitions/workspace-context.ts +45 -0
- package/src/tools/definitions/workspace-user-chat.ts +111 -0
- package/src/tools/executors/calendar.ts +371 -0
- package/src/tools/executors/chat.ts +15 -0
- package/src/tools/executors/finance.ts +638 -0
- package/src/tools/executors/helpers/encryption.ts +107 -0
- package/src/tools/executors/image.ts +247 -0
- package/src/tools/executors/markitdown.ts +684 -0
- package/src/tools/executors/memory.ts +277 -0
- package/src/tools/executors/parallel-checks.ts +176 -0
- package/src/tools/executors/qr.ts +170 -0
- package/src/tools/executors/scope-helpers.ts +192 -0
- package/src/tools/executors/search.ts +149 -0
- package/src/tools/executors/settings.ts +40 -0
- package/src/tools/executors/tasks.ts +1087 -0
- package/src/tools/executors/theme.ts +23 -0
- package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
- package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
- package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
- package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
- package/src/tools/executors/timer/timer-helpers.ts +372 -0
- package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
- package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
- package/src/tools/executors/timer/timer-mutations.ts +19 -0
- package/src/tools/executors/timer/timer-queries.ts +18 -0
- package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
- package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
- package/src/tools/executors/timer/timer-session-queries.ts +153 -0
- package/src/tools/executors/timer/timer-session-updates.ts +200 -0
- package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
- package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
- package/src/tools/executors/timer.ts +22 -0
- package/src/tools/executors/user.ts +60 -0
- package/src/tools/executors/workspace.ts +135 -0
- package/src/tools/json-render-catalog.ts +875 -0
- package/src/tools/mira-tool-definitions.ts +55 -0
- package/src/tools/mira-tool-dispatcher.ts +265 -0
- package/src/tools/mira-tool-metadata.ts +164 -0
- package/src/tools/mira-tool-names.ts +95 -0
- package/src/tools/mira-tool-render-ui.ts +54 -0
- package/src/tools/mira-tool-types.ts +17 -0
- package/src/tools/mira-tools.ts +167 -0
- package/src/tools/normalize-render-ui-input.ts +321 -0
- package/src/tools/workspace-context.ts +233 -0
- package/src/types.ts +38 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { verifyWorkspaceMembershipType } from '@tuturuuu/utils/workspace-helper';
|
|
2
|
+
import type { MiraToolContext } from '../mira-tools';
|
|
3
|
+
import { getWorkspaceContextWorkspaceId } from '../workspace-context';
|
|
4
|
+
|
|
5
|
+
function normalizeRelation<T>(value: T | T[] | null | undefined): T | null {
|
|
6
|
+
if (!value) return null;
|
|
7
|
+
return Array.isArray(value) ? (value[0] ?? null) : value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type TaskScope = {
|
|
11
|
+
id: string;
|
|
12
|
+
boardId: string | null;
|
|
13
|
+
listId: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function hasWorkspaceBoardAccess(
|
|
17
|
+
ctx: MiraToolContext,
|
|
18
|
+
boardId: string
|
|
19
|
+
): Promise<boolean> {
|
|
20
|
+
const { data, error } = await ctx.supabase
|
|
21
|
+
.from('workspace_boards')
|
|
22
|
+
.select('id')
|
|
23
|
+
.eq('id', boardId)
|
|
24
|
+
.eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
|
|
25
|
+
.maybeSingle();
|
|
26
|
+
|
|
27
|
+
if (error) {
|
|
28
|
+
throw new Error(error.message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Boolean(data);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function hasTaskListAccess(
|
|
35
|
+
ctx: MiraToolContext,
|
|
36
|
+
listId: string
|
|
37
|
+
): Promise<boolean> {
|
|
38
|
+
const { data, error } = await ctx.supabase
|
|
39
|
+
.from('task_lists')
|
|
40
|
+
.select('id, workspace_boards!inner(ws_id)')
|
|
41
|
+
.eq('id', listId)
|
|
42
|
+
.maybeSingle();
|
|
43
|
+
|
|
44
|
+
if (error) {
|
|
45
|
+
throw new Error(error.message);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const board = normalizeRelation(
|
|
49
|
+
data?.workspace_boards as { ws_id: string } | { ws_id: string }[] | null
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return board?.ws_id === getWorkspaceContextWorkspaceId(ctx);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function hasTaskAccess(
|
|
56
|
+
ctx: MiraToolContext,
|
|
57
|
+
taskId: string
|
|
58
|
+
): Promise<boolean> {
|
|
59
|
+
return Boolean(await getTaskScope(ctx, taskId));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function getTaskScope(
|
|
63
|
+
ctx: MiraToolContext,
|
|
64
|
+
taskId: string
|
|
65
|
+
): Promise<TaskScope | null> {
|
|
66
|
+
const { data, error } = await ctx.supabase
|
|
67
|
+
.from('tasks')
|
|
68
|
+
.select('id, board_id, list_id')
|
|
69
|
+
.eq('id', taskId)
|
|
70
|
+
.maybeSingle();
|
|
71
|
+
|
|
72
|
+
if (error) {
|
|
73
|
+
throw new Error(error.message);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!data) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const taskScope: TaskScope = {
|
|
81
|
+
id: data.id,
|
|
82
|
+
boardId: data.board_id ?? null,
|
|
83
|
+
listId: data.list_id ?? null,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (taskScope.boardId) {
|
|
87
|
+
if (await hasWorkspaceBoardAccess(ctx, taskScope.boardId)) {
|
|
88
|
+
return taskScope;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (taskScope.listId) {
|
|
94
|
+
if (await hasTaskListAccess(ctx, taskScope.listId)) {
|
|
95
|
+
return taskScope;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function hasProjectAccess(
|
|
103
|
+
ctx: MiraToolContext,
|
|
104
|
+
projectId: string
|
|
105
|
+
): Promise<boolean> {
|
|
106
|
+
const { data, error } = await ctx.supabase
|
|
107
|
+
.from('task_projects')
|
|
108
|
+
.select('id')
|
|
109
|
+
.eq('id', projectId)
|
|
110
|
+
.eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
|
|
111
|
+
.maybeSingle();
|
|
112
|
+
|
|
113
|
+
if (error) {
|
|
114
|
+
throw new Error(error.message);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return Boolean(data);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function hasWalletAccess(
|
|
121
|
+
ctx: MiraToolContext,
|
|
122
|
+
walletId: string
|
|
123
|
+
): Promise<boolean> {
|
|
124
|
+
const { data, error } = await ctx.supabase
|
|
125
|
+
.schema('private')
|
|
126
|
+
.from('workspace_wallets')
|
|
127
|
+
.select('id')
|
|
128
|
+
.eq('id', walletId)
|
|
129
|
+
.eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
|
|
130
|
+
.maybeSingle();
|
|
131
|
+
|
|
132
|
+
if (error) {
|
|
133
|
+
throw new Error(error.message);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return Boolean(data);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function hasTransactionCategoryAccess(
|
|
140
|
+
ctx: MiraToolContext,
|
|
141
|
+
categoryId: string
|
|
142
|
+
): Promise<boolean> {
|
|
143
|
+
const { data, error } = await ctx.supabase
|
|
144
|
+
.from('transaction_categories')
|
|
145
|
+
.select('id')
|
|
146
|
+
.eq('id', categoryId)
|
|
147
|
+
.eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
|
|
148
|
+
.maybeSingle();
|
|
149
|
+
|
|
150
|
+
if (error) {
|
|
151
|
+
throw new Error(error.message);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return Boolean(data);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function hasTimeTrackingCategoryAccess(
|
|
158
|
+
ctx: MiraToolContext,
|
|
159
|
+
categoryId: string
|
|
160
|
+
): Promise<boolean> {
|
|
161
|
+
const { data, error } = await ctx.supabase
|
|
162
|
+
.from('time_tracking_categories')
|
|
163
|
+
.select('id')
|
|
164
|
+
.eq('id', categoryId)
|
|
165
|
+
.eq('ws_id', getWorkspaceContextWorkspaceId(ctx))
|
|
166
|
+
.maybeSingle();
|
|
167
|
+
|
|
168
|
+
if (error) {
|
|
169
|
+
throw new Error(error.message);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return Boolean(data);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function isWorkspaceMember(
|
|
176
|
+
ctx: MiraToolContext,
|
|
177
|
+
userId: string,
|
|
178
|
+
workspaceId = getWorkspaceContextWorkspaceId(ctx)
|
|
179
|
+
): Promise<boolean> {
|
|
180
|
+
const result = await verifyWorkspaceMembershipType({
|
|
181
|
+
wsId: workspaceId,
|
|
182
|
+
userId,
|
|
183
|
+
supabase: ctx.supabase,
|
|
184
|
+
requiredType: 'MEMBER',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (result.error === 'membership_lookup_failed') {
|
|
188
|
+
throw new Error('Membership lookup failed');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result.ok;
|
|
192
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { google } from '@ai-sdk/google';
|
|
2
|
+
import { generateText, stepCountIs } from 'ai';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { withAiMemory } from '../../memory';
|
|
5
|
+
import type { MiraToolContext } from '../mira-tools';
|
|
6
|
+
|
|
7
|
+
const SEARCH_WRAPPER_MODEL = 'gemini-3.1-flash-lite';
|
|
8
|
+
|
|
9
|
+
type SearchSource = {
|
|
10
|
+
sourceId?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ToolStepLike = {
|
|
16
|
+
toolCalls?: Array<{ toolName?: string }>;
|
|
17
|
+
toolResults?: Array<{ toolName?: string }>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const SearchArgsSchema = z.object({
|
|
21
|
+
query: z
|
|
22
|
+
.string()
|
|
23
|
+
.transform((value) => value.trim())
|
|
24
|
+
.refine((value) => value.length > 0, {
|
|
25
|
+
message: 'Missing required `query`.',
|
|
26
|
+
})
|
|
27
|
+
.transform((value) => value.slice(0, 500)),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function hasGoogleSearchCallInSteps(steps: unknown): boolean {
|
|
31
|
+
if (!Array.isArray(steps)) return false;
|
|
32
|
+
|
|
33
|
+
return steps.some((step) => {
|
|
34
|
+
if (!step || typeof step !== 'object') return false;
|
|
35
|
+
const typedStep = step as ToolStepLike;
|
|
36
|
+
const called = (typedStep.toolCalls ?? []).some(
|
|
37
|
+
(toolCall) => toolCall.toolName === 'google_search'
|
|
38
|
+
);
|
|
39
|
+
const hasResult = (typedStep.toolResults ?? []).some(
|
|
40
|
+
(toolResult) => toolResult.toolName === 'google_search'
|
|
41
|
+
);
|
|
42
|
+
return called || hasResult;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeSources(value: unknown): SearchSource[] {
|
|
47
|
+
if (!Array.isArray(value)) return [];
|
|
48
|
+
|
|
49
|
+
const normalized: SearchSource[] = [];
|
|
50
|
+
|
|
51
|
+
for (const item of value) {
|
|
52
|
+
if (!item || typeof item !== 'object') continue;
|
|
53
|
+
|
|
54
|
+
const source = item as Record<string, unknown>;
|
|
55
|
+
const sourceId =
|
|
56
|
+
typeof source.sourceId === 'string' ? source.sourceId : undefined;
|
|
57
|
+
const title = typeof source.title === 'string' ? source.title : undefined;
|
|
58
|
+
const url = typeof source.url === 'string' ? source.url : undefined;
|
|
59
|
+
|
|
60
|
+
if (!sourceId && !title && !url) continue;
|
|
61
|
+
|
|
62
|
+
normalized.push({ sourceId, title, url });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function runGoogleSearchWrapper(
|
|
69
|
+
query: string,
|
|
70
|
+
forceTool: boolean,
|
|
71
|
+
ctx: MiraToolContext
|
|
72
|
+
) {
|
|
73
|
+
const prompt = forceTool
|
|
74
|
+
? `You must call the google_search tool before producing the final answer.\nSearch the web for the query below and provide an accurate, concise answer with key points and cited sources.\n\nQuery: ${query}`
|
|
75
|
+
: `Search the web for the query below and provide an accurate, concise answer with key points and cited sources.\n\nQuery: ${query}`;
|
|
76
|
+
|
|
77
|
+
return generateText({
|
|
78
|
+
model: await withAiMemory({
|
|
79
|
+
addMemory: 'never',
|
|
80
|
+
customId: ctx.chatId ? `${ctx.chatId}-google-search` : query,
|
|
81
|
+
model: google(SEARCH_WRAPPER_MODEL),
|
|
82
|
+
product: 'mira',
|
|
83
|
+
source: 'mira_google_search_tool',
|
|
84
|
+
surface: 'mira_google_search_tool',
|
|
85
|
+
userId: ctx.userId,
|
|
86
|
+
wsId: ctx.workspaceContext?.wsId ?? ctx.wsId,
|
|
87
|
+
}),
|
|
88
|
+
tools: {
|
|
89
|
+
google_search: google.tools.googleSearch({}),
|
|
90
|
+
},
|
|
91
|
+
prompt,
|
|
92
|
+
stopWhen: stepCountIs(4),
|
|
93
|
+
...(forceTool ? { toolChoice: 'required' as const } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function executeGoogleSearch(
|
|
98
|
+
args: Record<string, unknown>,
|
|
99
|
+
ctx: MiraToolContext
|
|
100
|
+
) {
|
|
101
|
+
const parsed = SearchArgsSchema.safeParse(args);
|
|
102
|
+
if (!parsed.success) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: parsed.error.issues[0]?.message ?? 'Invalid `query`.',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { query } = parsed.data;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
let result = await runGoogleSearchWrapper(query, false, ctx);
|
|
113
|
+
let sources = normalizeSources((result as { sources?: unknown }).sources);
|
|
114
|
+
let wasToolCalled = hasGoogleSearchCallInSteps(
|
|
115
|
+
(result as { steps?: unknown }).steps
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!wasToolCalled) {
|
|
119
|
+
result = await runGoogleSearchWrapper(query, true, ctx);
|
|
120
|
+
sources = normalizeSources((result as { sources?: unknown }).sources);
|
|
121
|
+
wasToolCalled = hasGoogleSearchCallInSteps(
|
|
122
|
+
(result as { steps?: unknown }).steps
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!wasToolCalled && sources.length === 0) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
query,
|
|
130
|
+
error: 'Failed to invoke google_search tool for web-grounded results.',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
query,
|
|
137
|
+
answer: result.text,
|
|
138
|
+
sources,
|
|
139
|
+
sourceCount: sources.length,
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('executeGoogleSearch provider error:', error);
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
query,
|
|
146
|
+
error: 'Search provider error. Please try again.',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MiraToolContext } from '../mira-tools';
|
|
2
|
+
|
|
3
|
+
export async function executeUpdateMySettings(
|
|
4
|
+
args: Record<string, unknown>,
|
|
5
|
+
ctx: MiraToolContext
|
|
6
|
+
) {
|
|
7
|
+
const updates: Record<string, string> = {};
|
|
8
|
+
const fields = [
|
|
9
|
+
'name',
|
|
10
|
+
'tone',
|
|
11
|
+
'personality',
|
|
12
|
+
'boundaries',
|
|
13
|
+
'vibe',
|
|
14
|
+
'chat_tone',
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
for (const field of fields) {
|
|
18
|
+
const value = args[field];
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
updates[field] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (Object.keys(updates).length === 0) {
|
|
25
|
+
return { success: true, message: 'No settings to update' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { error } = await ctx.supabase
|
|
29
|
+
.from('mira_soul')
|
|
30
|
+
.upsert({ user_id: ctx.userId, ...updates }, { onConflict: 'user_id' });
|
|
31
|
+
|
|
32
|
+
if (error) return { error: error.message };
|
|
33
|
+
|
|
34
|
+
const changedFields = Object.keys(updates).join(', ');
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
message: `Settings updated: ${changedFields}`,
|
|
38
|
+
updated: updates,
|
|
39
|
+
};
|
|
40
|
+
}
|