@tuturuuu/ai 0.13.0 → 0.14.0
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/package.json +41 -41
- package/src/chat/google/assistant-message-parts.ts +105 -0
- package/src/chat/google/chat-tools.ts +7 -0
- package/src/chat/google/google-search-usage.ts +62 -0
- package/src/chat/google/mira-step-preparation.ts +37 -16
- package/src/chat/google/route-credits.ts +6 -7
- package/src/chat/google/route-message-preparation.ts +33 -0
- package/src/chat/google/route-mira-runtime.ts +56 -32
- package/src/chat/google/route.ts +32 -71
- package/src/chat/google/stream-finish-persistence.ts +39 -150
- package/src/chat/mira-artifact-intent.ts +38 -0
- package/src/chat/mira-prompt.ts +29 -0
- package/src/chat/mira-render-ui-policy.ts +65 -20
- package/src/chat/mira-system-instruction.ts +8 -172
- package/src/chat/mira-tool-loop-guard.ts +84 -0
- package/src/credits/cap-output-tokens.ts +48 -15
- package/src/credits/check-credits.ts +14 -3
- package/src/credits/constants.ts +3 -0
- package/src/credits/index.ts +1 -0
- package/src/generate/route.ts +6 -3
- package/src/meetings/audio-contract.ts +8 -0
- package/src/meetings/audio-transcripts.ts +25 -0
- package/src/meetings/chat-source-footer.ts +1 -0
- package/src/meetings/chat.ts +4 -1
- package/src/meetings/gemini.ts +72 -20
- package/src/meetings/public-chat-search.ts +4 -1
- package/src/meetings/transcript.ts +63 -0
- package/src/meetings/workspace-tools.ts +3 -1
- package/src/studio/metering.ts +5 -1
- package/src/tools/definitions/calendar-automation.ts +43 -0
- package/src/tools/definitions/calendar.ts +27 -1
- package/src/tools/definitions/meta.ts +17 -1
- package/src/tools/definitions/tasks.ts +82 -31
- package/src/tools/definitions/workspace-user-chat.ts +24 -0
- package/src/tools/executors/calendar-automation.ts +207 -0
- package/src/tools/executors/calendar.ts +30 -1
- package/src/tools/executors/helpers/encryption.ts +12 -9
- package/src/tools/executors/search.ts +40 -9
- package/src/tools/executors/task-create.ts +232 -0
- package/src/tools/executors/task-dashboard.ts +59 -0
- package/src/tools/executors/task-read.ts +76 -0
- package/src/tools/executors/tasks.ts +50 -205
- package/src/tools/executors/theme.ts +48 -0
- package/src/tools/google-search-events.ts +4 -0
- package/src/tools/mira-sdk-tool-search.ts +73 -0
- package/src/tools/mira-task-write-guard.ts +61 -0
- package/src/tools/mira-tool-discovery.ts +138 -0
- package/src/tools/mira-tool-dispatcher.ts +33 -1
- package/src/tools/mira-tool-metadata.ts +29 -1
- package/src/tools/mira-tool-names.ts +13 -0
- package/src/tools/mira-tool-types.ts +7 -0
- package/src/tools/mira-tools.ts +70 -2
- package/src/workspace-artifacts.ts +58 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withForwardedInternalApiAuth,
|
|
3
|
+
withTaskApiBaseUrl,
|
|
4
|
+
} from '@tuturuuu/internal-api/client';
|
|
5
|
+
import { getUserTaskDashboard } from '@tuturuuu/internal-api/tasks';
|
|
6
|
+
import { Effect } from '@tuturuuu/utils/effect';
|
|
7
|
+
import type { MiraToolContext } from '../mira-tool-types';
|
|
8
|
+
import { getWorkspaceContextWorkspaceId } from '../workspace-context';
|
|
9
|
+
|
|
10
|
+
export function executeTaskDashboard(
|
|
11
|
+
args: Record<string, unknown>,
|
|
12
|
+
ctx: MiraToolContext
|
|
13
|
+
) {
|
|
14
|
+
const category = args.category ?? args.status ?? 'all';
|
|
15
|
+
const options = withForwardedInternalApiAuth(
|
|
16
|
+
ctx.requestHeaders!,
|
|
17
|
+
withTaskApiBaseUrl()
|
|
18
|
+
);
|
|
19
|
+
return Effect.runPromise(
|
|
20
|
+
Effect.tryPromise({
|
|
21
|
+
try: async () => {
|
|
22
|
+
const result = await getUserTaskDashboard(
|
|
23
|
+
{
|
|
24
|
+
wsId: getWorkspaceContextWorkspaceId(ctx),
|
|
25
|
+
isPersonal: ctx.workspaceContext?.personal === true,
|
|
26
|
+
},
|
|
27
|
+
options
|
|
28
|
+
);
|
|
29
|
+
const output: Record<string, unknown> = {
|
|
30
|
+
totalActive: result.totalActiveTasks,
|
|
31
|
+
};
|
|
32
|
+
for (const group of ['overdue', 'today', 'upcoming'] as const) {
|
|
33
|
+
if (category !== 'all' && category !== group) continue;
|
|
34
|
+
output[group] = {
|
|
35
|
+
count: result[group].length,
|
|
36
|
+
hasMore: result[group].length > 30,
|
|
37
|
+
tasks: result[group].slice(0, 30).map((task) => ({
|
|
38
|
+
id: task.id,
|
|
39
|
+
name: task.name,
|
|
40
|
+
priority: task.priority,
|
|
41
|
+
dueDate: task.end_date,
|
|
42
|
+
workspaceId: task.list?.board?.ws_id,
|
|
43
|
+
workspaceName: task.list?.board?.workspaces?.name,
|
|
44
|
+
boardId: task.list?.board?.id,
|
|
45
|
+
boardName: task.list?.board?.name,
|
|
46
|
+
listId: task.list_id,
|
|
47
|
+
listName: task.list?.name,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return output;
|
|
52
|
+
},
|
|
53
|
+
catch: (error) =>
|
|
54
|
+
error instanceof Error ? error.message : 'Task feed unavailable',
|
|
55
|
+
}).pipe(
|
|
56
|
+
Effect.catchAll((error) => Effect.succeed({ success: false, error }))
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { MiraToolContext } from '../mira-tool-types';
|
|
2
|
+
import { getWorkspaceContextWorkspaceId } from '../workspace-context';
|
|
3
|
+
|
|
4
|
+
export async function executeGetTask(
|
|
5
|
+
args: Record<string, unknown>,
|
|
6
|
+
ctx: MiraToolContext
|
|
7
|
+
) {
|
|
8
|
+
const taskId = args.taskId as string;
|
|
9
|
+
const wsId = getWorkspaceContextWorkspaceId(ctx);
|
|
10
|
+
const { data, error } = await ctx.supabase
|
|
11
|
+
.from('tasks')
|
|
12
|
+
.select(
|
|
13
|
+
'id, name, description, priority, start_date, end_date, estimation_points, completed, completed_at, board_id, list_id, direct_board:workspace_boards!fk_tasks_board_id(id, name, ws_id), task_lists(id, name, workspace_boards!inner(id, name, ws_id)), task_assignees(user_id), task_labels(label_id)'
|
|
14
|
+
)
|
|
15
|
+
.eq('id', taskId)
|
|
16
|
+
.eq('direct_board.ws_id', wsId)
|
|
17
|
+
.eq('task_lists.workspace_boards.ws_id', wsId)
|
|
18
|
+
.or('direct_board.not.is.null,and(board_id.is.null,task_lists.not.is.null)')
|
|
19
|
+
.maybeSingle();
|
|
20
|
+
return error
|
|
21
|
+
? { error: error.message }
|
|
22
|
+
: data
|
|
23
|
+
? { task: data, workspaceId: wsId }
|
|
24
|
+
: { error: 'Task not found in the current workspace.' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function executeSearchTasks(
|
|
28
|
+
args: Record<string, unknown>,
|
|
29
|
+
ctx: MiraToolContext
|
|
30
|
+
) {
|
|
31
|
+
const wsId = getWorkspaceContextWorkspaceId(ctx);
|
|
32
|
+
const title = String(args.query ?? '').trim();
|
|
33
|
+
if (!title) return { error: 'Provide a task title to search for.' };
|
|
34
|
+
const page = Math.max(1, Number(args.page) || 1);
|
|
35
|
+
const limit = Math.min(50, Math.max(1, Number(args.limit) || 20));
|
|
36
|
+
const query = title.replace(/[\\%_]/g, '\\$&');
|
|
37
|
+
let builder = ctx.supabase
|
|
38
|
+
.from('tasks')
|
|
39
|
+
.select(
|
|
40
|
+
'id, name, priority, start_date, end_date, completed, board_id, list_id, direct_board:workspace_boards!fk_tasks_board_id(id, name, ws_id), task_lists(id, name, workspace_boards!inner(id, name, ws_id))',
|
|
41
|
+
{ count: 'exact' }
|
|
42
|
+
)
|
|
43
|
+
.eq('direct_board.ws_id', wsId)
|
|
44
|
+
.is('direct_board.archived_at', null)
|
|
45
|
+
.is('direct_board.deleted_at', null)
|
|
46
|
+
.eq('task_lists.workspace_boards.ws_id', wsId)
|
|
47
|
+
.is('task_lists.workspace_boards.archived_at', null)
|
|
48
|
+
.is('task_lists.workspace_boards.deleted_at', null)
|
|
49
|
+
.eq('task_lists.archived', false)
|
|
50
|
+
.eq('task_lists.deleted', false)
|
|
51
|
+
// Board ownership takes precedence, matching getTaskScope. Nullable embeds
|
|
52
|
+
// allow board-only tasks without admitting rows from another workspace.
|
|
53
|
+
.or(
|
|
54
|
+
'and(direct_board.not.is.null,list_id.is.null),and(direct_board.not.is.null,task_lists.not.is.null),and(board_id.is.null,task_lists.not.is.null)'
|
|
55
|
+
)
|
|
56
|
+
.is('deleted_at', null)
|
|
57
|
+
.ilike('name', `%${query}%`)
|
|
58
|
+
.order('created_at', { ascending: false })
|
|
59
|
+
.order('id', { ascending: true })
|
|
60
|
+
.range((page - 1) * limit, page * limit - 1);
|
|
61
|
+
if (args.includeCompleted !== true)
|
|
62
|
+
builder = builder
|
|
63
|
+
.eq('completed', false)
|
|
64
|
+
.is('completed_at', null)
|
|
65
|
+
.is('closed_at', null);
|
|
66
|
+
const { data, count, error } = await builder;
|
|
67
|
+
return error
|
|
68
|
+
? { error: error.message }
|
|
69
|
+
: {
|
|
70
|
+
workspaceId: wsId,
|
|
71
|
+
tasks: data,
|
|
72
|
+
total: count,
|
|
73
|
+
page,
|
|
74
|
+
hasMore: page * limit < (count ?? 0),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isWorkspaceMember,
|
|
13
13
|
type TaskScope,
|
|
14
14
|
} from './scope-helpers';
|
|
15
|
+
import { executeTaskDashboard } from './task-dashboard';
|
|
15
16
|
|
|
16
17
|
type RpcTask = {
|
|
17
18
|
task_id: string;
|
|
@@ -45,6 +46,7 @@ export async function executeGetMyTasks(
|
|
|
45
46
|
args: Record<string, unknown>,
|
|
46
47
|
ctx: MiraToolContext
|
|
47
48
|
) {
|
|
49
|
+
if (ctx.requestHeaders) return executeTaskDashboard(args, ctx);
|
|
48
50
|
const { userId, supabase } = ctx;
|
|
49
51
|
const wsId = getWorkspaceContextWorkspaceId(ctx);
|
|
50
52
|
const category = ((args.category ?? args.status) as string) || 'all';
|
|
@@ -101,203 +103,7 @@ export async function executeGetMyTasks(
|
|
|
101
103
|
return result;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
export
|
|
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
|
-
}
|
|
106
|
+
export { executeCreateTask } from './task-create';
|
|
301
107
|
|
|
302
108
|
export async function executeCompleteTask(
|
|
303
109
|
args: Record<string, unknown>,
|
|
@@ -323,7 +129,11 @@ export async function executeCompleteTask(
|
|
|
323
129
|
|
|
324
130
|
const { error } = await ctx.supabase
|
|
325
131
|
.from('tasks')
|
|
326
|
-
.update({
|
|
132
|
+
.update({
|
|
133
|
+
completed: true,
|
|
134
|
+
completed_at: new Date().toISOString(),
|
|
135
|
+
closed_at: new Date().toISOString(),
|
|
136
|
+
})
|
|
327
137
|
.eq('id', taskId)
|
|
328
138
|
.eq(taskScopePredicate.column, taskScopePredicate.value);
|
|
329
139
|
|
|
@@ -334,7 +144,7 @@ export async function executeCompleteTask(
|
|
|
334
144
|
// ── New CRUD tools ──
|
|
335
145
|
|
|
336
146
|
const UPDATE_TASK_FIELDS_HINT =
|
|
337
|
-
'Accepted fields: taskId (or id), endDate (or dueDate, ISO), name, description, priority, startDate, listId, estimationPoints.';
|
|
147
|
+
'Accepted fields: taskId (or id), endDate (or dueDate, ISO), name, description, priority, startDate, listId, estimationPoints, completed.';
|
|
338
148
|
|
|
339
149
|
export async function executeUpdateTask(
|
|
340
150
|
args: Record<string, unknown>,
|
|
@@ -360,6 +170,11 @@ export async function executeUpdateTask(
|
|
|
360
170
|
})
|
|
361
171
|
: null;
|
|
362
172
|
}
|
|
173
|
+
if (args.completed !== undefined) {
|
|
174
|
+
updates.completed = args.completed as boolean;
|
|
175
|
+
updates.completed_at = args.completed ? new Date().toISOString() : null;
|
|
176
|
+
updates.closed_at = updates.completed_at;
|
|
177
|
+
}
|
|
363
178
|
if (args.priority !== undefined)
|
|
364
179
|
updates.priority = args.priority as Enums<'task_priority'>;
|
|
365
180
|
if (args.startDate !== undefined) {
|
|
@@ -371,7 +186,7 @@ export async function executeUpdateTask(
|
|
|
371
186
|
? parseTaskDateToUTCISO(v, ctx.timezone, false)
|
|
372
187
|
: v;
|
|
373
188
|
}
|
|
374
|
-
const endDateValue = args.endDate
|
|
189
|
+
const endDateValue = args.endDate !== undefined ? args.endDate : args.dueDate;
|
|
375
190
|
if (endDateValue !== undefined) {
|
|
376
191
|
const v = endDateValue as string | null;
|
|
377
192
|
updates.end_date =
|
|
@@ -420,14 +235,43 @@ export async function executeUpdateTask(
|
|
|
420
235
|
return { error: 'Task not found in current workspace' };
|
|
421
236
|
}
|
|
422
237
|
|
|
423
|
-
|
|
238
|
+
if (args.completed === false) {
|
|
239
|
+
const listId = (args.listId as string | undefined) ?? taskScope.listId;
|
|
240
|
+
const { data: list, error } = await ctx.supabase
|
|
241
|
+
.from('task_lists')
|
|
242
|
+
.select('id, status')
|
|
243
|
+
.eq('id', listId ?? '')
|
|
244
|
+
.single();
|
|
245
|
+
if (
|
|
246
|
+
error ||
|
|
247
|
+
!list ||
|
|
248
|
+
!['not_started', 'active'].includes(list.status ?? '')
|
|
249
|
+
)
|
|
250
|
+
return {
|
|
251
|
+
error:
|
|
252
|
+
'To reopen this task, provide an active listId from list_task_lists. No changes were saved.',
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const { data: saved, error } = await ctx.supabase
|
|
424
256
|
.from('tasks')
|
|
425
257
|
.update(updates)
|
|
426
258
|
.eq('id', taskId)
|
|
427
|
-
.eq(taskScopePredicate.column, taskScopePredicate.value)
|
|
259
|
+
.eq(taskScopePredicate.column, taskScopePredicate.value)
|
|
260
|
+
.select('id, name, priority, start_date, end_date, completed, list_id')
|
|
261
|
+
.maybeSingle();
|
|
428
262
|
|
|
429
|
-
if (error
|
|
430
|
-
|
|
263
|
+
if (error || !saved)
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
error:
|
|
267
|
+
error?.message ??
|
|
268
|
+
'Task was not updated; it may have moved or been removed.',
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
success: true,
|
|
272
|
+
task: saved,
|
|
273
|
+
workspaceId: getWorkspaceContextWorkspaceId(ctx),
|
|
274
|
+
};
|
|
431
275
|
}
|
|
432
276
|
|
|
433
277
|
export async function executeDeleteTask(
|
|
@@ -559,9 +403,10 @@ export async function executeListTaskLists(
|
|
|
559
403
|
|
|
560
404
|
const { data, error } = await ctx.supabase
|
|
561
405
|
.from('task_lists')
|
|
562
|
-
.select('id, name, board_id, color, position, archived')
|
|
406
|
+
.select('id, name, board_id, status, color, position, archived')
|
|
563
407
|
.eq('board_id', boardId)
|
|
564
408
|
.eq('archived', false)
|
|
409
|
+
.eq('deleted', false)
|
|
565
410
|
.order('position', { ascending: true });
|
|
566
411
|
|
|
567
412
|
if (error) return { error: error.message };
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
manageWorkspaceSchema,
|
|
3
|
+
showWorkspaceArtifactSchema,
|
|
4
|
+
} from '../../workspace-artifacts';
|
|
1
5
|
import type { MiraToolContext } from '../mira-tools';
|
|
6
|
+
import { getWorkspaceContextWorkspaceId } from '../workspace-context';
|
|
2
7
|
|
|
3
8
|
export async function executeSetTheme(
|
|
4
9
|
args: Record<string, unknown>,
|
|
@@ -21,3 +26,46 @@ export async function executeSetTheme(
|
|
|
21
26
|
message: `Theme changed to ${theme}`,
|
|
22
27
|
};
|
|
23
28
|
}
|
|
29
|
+
|
|
30
|
+
export async function executeSetSidebar(args: Record<string, unknown>) {
|
|
31
|
+
const behavior = args.behavior;
|
|
32
|
+
if (
|
|
33
|
+
!['expanded', 'collapsed', 'hover', 'hidden'].includes(String(behavior))
|
|
34
|
+
) {
|
|
35
|
+
return { error: 'Invalid sidebar behavior' };
|
|
36
|
+
}
|
|
37
|
+
return { success: true, action: 'set_sidebar', behavior };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function executeShowWorkspaceArtifact(
|
|
41
|
+
args: Record<string, unknown>,
|
|
42
|
+
ctx: MiraToolContext
|
|
43
|
+
) {
|
|
44
|
+
const parsed = showWorkspaceArtifactSchema.safeParse(args);
|
|
45
|
+
if (!parsed.success)
|
|
46
|
+
return { error: 'Invalid artifact presentation or layout' };
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
action: 'show_workspace_artifact',
|
|
50
|
+
message:
|
|
51
|
+
'Artifact UI command completed. This UI command did not fetch product data; the panel fetches its own data separately. Do not repeat this successful command. Continue only with other requested actions, then answer.',
|
|
52
|
+
...parsed.data,
|
|
53
|
+
wsId: getWorkspaceContextWorkspaceId(ctx),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function executeManageWorkspace(
|
|
58
|
+
args: Record<string, unknown>,
|
|
59
|
+
ctx: MiraToolContext
|
|
60
|
+
) {
|
|
61
|
+
const parsed = manageWorkspaceSchema.safeParse(args);
|
|
62
|
+
if (!parsed.success) return { error: 'Invalid workspace operation' };
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
action: 'manage_workspace',
|
|
66
|
+
message:
|
|
67
|
+
'Workspace UI command completed. Do not repeat this successful command to verify it. Preserve panels the user asked to keep; once the requested actions are complete, answer without further UI calls.',
|
|
68
|
+
...parsed.data,
|
|
69
|
+
wsId: getWorkspaceContextWorkspaceId(ctx),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { PermissionId } from '@tuturuuu/types';
|
|
2
|
+
import { type ToolSet, toolSearch } from 'ai';
|
|
3
|
+
import { MIRA_TOOL_PERMISSIONS } from './mira-tool-metadata';
|
|
4
|
+
import type { MiraToolName } from './mira-tool-names';
|
|
5
|
+
import type { MiraToolContext } from './mira-tool-types';
|
|
6
|
+
import { getWorkspaceContextWorkspaceId } from './workspace-context';
|
|
7
|
+
|
|
8
|
+
type StepPolicy = {
|
|
9
|
+
activeTools: string[];
|
|
10
|
+
toolChoice?: 'required' | 'none';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Each generation owns discovery state; execution retains the original guards. */
|
|
14
|
+
export function createMiraSdkToolSearch(
|
|
15
|
+
originalTools: ToolSet,
|
|
16
|
+
ctx: MiraToolContext,
|
|
17
|
+
withoutPermission?: (permission: PermissionId) => boolean
|
|
18
|
+
) {
|
|
19
|
+
const tools: ToolSet = Object.fromEntries(
|
|
20
|
+
Object.entries(originalTools).map(([name, definition]) => [
|
|
21
|
+
name,
|
|
22
|
+
name === 'search_tools'
|
|
23
|
+
? toolSearch()
|
|
24
|
+
: { ...definition, deferLoading: true },
|
|
25
|
+
])
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
async function prepareStep(policy: StepPolicy) {
|
|
29
|
+
if (policy.toolChoice === 'none') {
|
|
30
|
+
return { ...policy, activeTools: [] };
|
|
31
|
+
}
|
|
32
|
+
const workspaceId = getWorkspaceContextWorkspaceId(ctx);
|
|
33
|
+
const allowed = new Set<string>();
|
|
34
|
+
for (const name of Object.keys(tools)) {
|
|
35
|
+
const permission = MIRA_TOOL_PERMISSIONS[name as MiraToolName];
|
|
36
|
+
const required = permission
|
|
37
|
+
? Array.isArray(permission)
|
|
38
|
+
? permission
|
|
39
|
+
: [permission]
|
|
40
|
+
: [];
|
|
41
|
+
let authorized = required.length === 0;
|
|
42
|
+
if (!authorized) {
|
|
43
|
+
try {
|
|
44
|
+
authorized = ctx.authorizeWorkspaceTools
|
|
45
|
+
? await ctx.authorizeWorkspaceTools(workspaceId, required)
|
|
46
|
+
: Boolean(withoutPermission) &&
|
|
47
|
+
!required.some((item) => withoutPermission!(item));
|
|
48
|
+
} catch {
|
|
49
|
+
authorized = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (authorized) allowed.add(name);
|
|
53
|
+
}
|
|
54
|
+
// Never publish a catalog authorized for a context that changed meanwhile.
|
|
55
|
+
if (workspaceId !== getWorkspaceContextWorkspaceId(ctx)) {
|
|
56
|
+
return { toolChoice: 'none' as const, activeTools: [] };
|
|
57
|
+
}
|
|
58
|
+
const selected = new Set(policy.activeTools);
|
|
59
|
+
const canDiscover = selected.has('search_tools');
|
|
60
|
+
const activeTools = [...allowed].filter(
|
|
61
|
+
(name) => canDiscover || selected.has(name)
|
|
62
|
+
);
|
|
63
|
+
// prepareStep supports eligibility overrides, not replacement ToolSets.
|
|
64
|
+
// These definitions belong only to this generation, never to the registry.
|
|
65
|
+
for (const [name, definition] of Object.entries(tools)) {
|
|
66
|
+
if (name !== 'search_tools') {
|
|
67
|
+
tools[name] = { ...definition, deferLoading: !selected.has(name) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { ...policy, activeTools };
|
|
71
|
+
}
|
|
72
|
+
return { tools, prepareStep };
|
|
73
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const object = (value: unknown): value is Record<string, unknown> =>
|
|
2
|
+
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
const dateKey = (value: unknown) => {
|
|
4
|
+
if (typeof value !== 'string') return null;
|
|
5
|
+
const date = new Date(value);
|
|
6
|
+
return Number.isNaN(date.getTime()) ? value : date.toISOString();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** Per-turn protection, retaining receipts whenever a write may have happened. */
|
|
10
|
+
export function createTaskWriteGuard() {
|
|
11
|
+
const pending = new Map<string, Promise<unknown>>();
|
|
12
|
+
return async (
|
|
13
|
+
workspaceId: string,
|
|
14
|
+
args: Record<string, unknown>,
|
|
15
|
+
execute: () => Promise<unknown>
|
|
16
|
+
) => {
|
|
17
|
+
const { dueDate, ...fields } = args;
|
|
18
|
+
const normalized = {
|
|
19
|
+
...fields,
|
|
20
|
+
description: args.description || null,
|
|
21
|
+
priority: args.priority ?? null,
|
|
22
|
+
startDate: dateKey(args.startDate),
|
|
23
|
+
endDate: dateKey(args.endDate ?? dueDate),
|
|
24
|
+
estimationPoints: args.estimationPoints ?? null,
|
|
25
|
+
assignToSelf: args.assignToSelf !== false,
|
|
26
|
+
};
|
|
27
|
+
const key = JSON.stringify([
|
|
28
|
+
workspaceId,
|
|
29
|
+
Object.entries(normalized).sort(([a], [b]) => a.localeCompare(b)),
|
|
30
|
+
]);
|
|
31
|
+
const previous = pending.get(key);
|
|
32
|
+
if (previous) {
|
|
33
|
+
const result = await previous;
|
|
34
|
+
return {
|
|
35
|
+
...(object(result) ? result : { result }),
|
|
36
|
+
reusedResult: true,
|
|
37
|
+
instruction:
|
|
38
|
+
'This is the prior creation result. No second task was created. Explain the saved result or error and stop retrying creation.',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const result = Promise.resolve()
|
|
42
|
+
.then(execute)
|
|
43
|
+
.catch(() => ({
|
|
44
|
+
success: false,
|
|
45
|
+
writeUncertain: true,
|
|
46
|
+
error:
|
|
47
|
+
'Task creation was interrupted. Verify the saved task before retrying.',
|
|
48
|
+
}));
|
|
49
|
+
pending.set(key, result);
|
|
50
|
+
const output = await result;
|
|
51
|
+
// Only an explicit pre-write failure is safe to retry. A transport failure
|
|
52
|
+
// may occur after insertion and must retain its recovery receipt.
|
|
53
|
+
if (
|
|
54
|
+
object(output) &&
|
|
55
|
+
output.created === false &&
|
|
56
|
+
output.writeUncertain !== true
|
|
57
|
+
)
|
|
58
|
+
pending.delete(key);
|
|
59
|
+
return output;
|
|
60
|
+
};
|
|
61
|
+
}
|