@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,1500 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MindAiPatch,
|
|
3
|
+
MindAiPatchRecord,
|
|
4
|
+
MindBoardSnapshot,
|
|
5
|
+
MindBoardSummary,
|
|
6
|
+
MindJsonObject,
|
|
7
|
+
MindNode,
|
|
8
|
+
MindPatchOperation,
|
|
9
|
+
} from '@tuturuuu/types/db';
|
|
10
|
+
import type { ToolSet } from 'ai';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { tool } from '../tools/core';
|
|
13
|
+
import {
|
|
14
|
+
buildRenderUiFailsafeSpec,
|
|
15
|
+
isRenderableRenderUiSpec,
|
|
16
|
+
} from '../tools/mira-tool-render-ui';
|
|
17
|
+
import { normalizeRenderUiInputForTool } from '../tools/normalize-render-ui-input';
|
|
18
|
+
|
|
19
|
+
export type MindAiWriteMode = 'direct' | 'review';
|
|
20
|
+
|
|
21
|
+
const MIND_HORIZONS = [
|
|
22
|
+
'day',
|
|
23
|
+
'week',
|
|
24
|
+
'month',
|
|
25
|
+
'quarter',
|
|
26
|
+
'year',
|
|
27
|
+
'five_year',
|
|
28
|
+
'ten_year',
|
|
29
|
+
'fifty_year',
|
|
30
|
+
'long_arc',
|
|
31
|
+
] as const;
|
|
32
|
+
const MIND_NODE_TYPES = [
|
|
33
|
+
'decision',
|
|
34
|
+
'goal',
|
|
35
|
+
'idea',
|
|
36
|
+
'milestone',
|
|
37
|
+
'plan',
|
|
38
|
+
'question',
|
|
39
|
+
'resource',
|
|
40
|
+
'risk',
|
|
41
|
+
'system',
|
|
42
|
+
] as const;
|
|
43
|
+
const MIND_NODE_STATUSES = [
|
|
44
|
+
'backlog',
|
|
45
|
+
'planned',
|
|
46
|
+
'in_progress',
|
|
47
|
+
'in_review',
|
|
48
|
+
'blocked',
|
|
49
|
+
'completed',
|
|
50
|
+
'deferred',
|
|
51
|
+
'cancelled',
|
|
52
|
+
] as const;
|
|
53
|
+
const MIND_EDGE_TYPES = [
|
|
54
|
+
'blocks',
|
|
55
|
+
'contains',
|
|
56
|
+
'contradicts',
|
|
57
|
+
'custom',
|
|
58
|
+
'depends_on',
|
|
59
|
+
'reference',
|
|
60
|
+
'relates_to',
|
|
61
|
+
'sequence',
|
|
62
|
+
'supports',
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
const mindHorizonSchema = z.enum(MIND_HORIZONS);
|
|
66
|
+
const mindNodeTypeSchema = z.enum(MIND_NODE_TYPES);
|
|
67
|
+
const mindNodeStatusSchema = z.enum(MIND_NODE_STATUSES);
|
|
68
|
+
const mindEdgeTypeSchema = z.enum(MIND_EDGE_TYPES);
|
|
69
|
+
const jsonPrimitiveSchema = z.union([
|
|
70
|
+
z.string(),
|
|
71
|
+
z.number(),
|
|
72
|
+
z.boolean(),
|
|
73
|
+
z.null(),
|
|
74
|
+
]);
|
|
75
|
+
const jsonSchema = z.union([
|
|
76
|
+
jsonPrimitiveSchema,
|
|
77
|
+
z.array(jsonPrimitiveSchema),
|
|
78
|
+
z.record(z.string(), jsonPrimitiveSchema),
|
|
79
|
+
z.array(z.record(z.string(), jsonPrimitiveSchema)),
|
|
80
|
+
]);
|
|
81
|
+
const mindMetadataSchema = z
|
|
82
|
+
.record(z.string(), jsonSchema)
|
|
83
|
+
.describe(
|
|
84
|
+
'Flat JSON metadata. Use primitive values, primitive arrays, or shallow objects.'
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
export type MindToolCallbacks = {
|
|
88
|
+
applyPatch(input: {
|
|
89
|
+
patchId: string;
|
|
90
|
+
userId: string;
|
|
91
|
+
wsId: string;
|
|
92
|
+
}): Promise<MindAiPatchRecord | null>;
|
|
93
|
+
createPatch(input: {
|
|
94
|
+
boardId: string;
|
|
95
|
+
patch: MindAiPatch;
|
|
96
|
+
summary: string;
|
|
97
|
+
threadId?: string | null;
|
|
98
|
+
userId: string;
|
|
99
|
+
wsId: string;
|
|
100
|
+
}): Promise<MindAiPatchRecord | null>;
|
|
101
|
+
getSnapshot(wsId: string, boardId: string): Promise<MindBoardSnapshot | null>;
|
|
102
|
+
listBoards(wsId: string): Promise<MindBoardSummary[]>;
|
|
103
|
+
searchNodes(input: {
|
|
104
|
+
boardId?: string;
|
|
105
|
+
q?: string;
|
|
106
|
+
wsId: string;
|
|
107
|
+
}): Promise<MindNode[]>;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type MindToolContext = {
|
|
111
|
+
boardId?: string | null;
|
|
112
|
+
threadId?: string | null;
|
|
113
|
+
userId: string;
|
|
114
|
+
writeMode: MindAiWriteMode;
|
|
115
|
+
wsId: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const patchOperationSchema = z.discriminatedUnion('kind', [
|
|
119
|
+
z.object({
|
|
120
|
+
id: z.string().min(1),
|
|
121
|
+
kind: z.literal('create_node'),
|
|
122
|
+
node: z.object({
|
|
123
|
+
body: z.string().nullable().optional(),
|
|
124
|
+
color: z.string().nullable().optional(),
|
|
125
|
+
height: z.number().positive().optional(),
|
|
126
|
+
horizon: mindHorizonSchema.optional(),
|
|
127
|
+
id: z.string().trim().min(1).max(120).optional(),
|
|
128
|
+
metadata: mindMetadataSchema.optional(),
|
|
129
|
+
nodeType: mindNodeTypeSchema.optional(),
|
|
130
|
+
parentNodeId: z.string().trim().min(1).max(120).nullable().optional(),
|
|
131
|
+
positionX: z.number(),
|
|
132
|
+
positionY: z.number(),
|
|
133
|
+
status: mindNodeStatusSchema.optional(),
|
|
134
|
+
title: z.string().min(1).max(240),
|
|
135
|
+
width: z.number().positive().optional(),
|
|
136
|
+
}),
|
|
137
|
+
}),
|
|
138
|
+
z.object({
|
|
139
|
+
body: z.string().nullable().optional(),
|
|
140
|
+
color: z.string().nullable().optional(),
|
|
141
|
+
height: z.number().positive().optional(),
|
|
142
|
+
horizon: mindHorizonSchema.optional(),
|
|
143
|
+
id: z.string().min(1),
|
|
144
|
+
kind: z.literal('update_node'),
|
|
145
|
+
metadata: mindMetadataSchema.optional(),
|
|
146
|
+
nodeId: z.guid(),
|
|
147
|
+
nodeType: mindNodeTypeSchema.optional(),
|
|
148
|
+
parentNodeId: z.guid().nullable().optional(),
|
|
149
|
+
positionX: z.number().optional(),
|
|
150
|
+
positionY: z.number().optional(),
|
|
151
|
+
status: mindNodeStatusSchema.optional(),
|
|
152
|
+
title: z.string().min(1).max(240).optional(),
|
|
153
|
+
width: z.number().positive().optional(),
|
|
154
|
+
}),
|
|
155
|
+
z.object({
|
|
156
|
+
id: z.string().min(1),
|
|
157
|
+
kind: z.literal('delete_node'),
|
|
158
|
+
nodeId: z.guid(),
|
|
159
|
+
}),
|
|
160
|
+
z.object({
|
|
161
|
+
edge: z.object({
|
|
162
|
+
color: z.string().nullable().optional(),
|
|
163
|
+
edgeType: mindEdgeTypeSchema.optional(),
|
|
164
|
+
id: z.string().trim().min(1).max(120).optional(),
|
|
165
|
+
label: z.string().nullable().optional(),
|
|
166
|
+
metadata: mindMetadataSchema.optional(),
|
|
167
|
+
sourceNodeId: z.string().trim().min(1).max(120),
|
|
168
|
+
targetNodeId: z.string().trim().min(1).max(120),
|
|
169
|
+
weight: z.number().nonnegative().optional(),
|
|
170
|
+
}),
|
|
171
|
+
id: z.string().min(1),
|
|
172
|
+
kind: z.literal('create_edge'),
|
|
173
|
+
}),
|
|
174
|
+
z.object({
|
|
175
|
+
color: z.string().nullable().optional(),
|
|
176
|
+
edgeId: z.guid(),
|
|
177
|
+
edgeType: mindEdgeTypeSchema.optional(),
|
|
178
|
+
id: z.string().min(1),
|
|
179
|
+
kind: z.literal('update_edge'),
|
|
180
|
+
label: z.string().nullable().optional(),
|
|
181
|
+
metadata: mindMetadataSchema.optional(),
|
|
182
|
+
sourceNodeId: z.guid().optional(),
|
|
183
|
+
targetNodeId: z.guid().optional(),
|
|
184
|
+
weight: z.number().nonnegative().optional(),
|
|
185
|
+
}),
|
|
186
|
+
z.object({
|
|
187
|
+
edgeId: z.guid(),
|
|
188
|
+
id: z.string().min(1),
|
|
189
|
+
kind: z.literal('delete_edge'),
|
|
190
|
+
}),
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
const patchSchema = z.object({
|
|
194
|
+
operations: z.array(patchOperationSchema).min(1).max(100),
|
|
195
|
+
summary: z.string().min(1).max(2000),
|
|
196
|
+
});
|
|
197
|
+
const loosePatchOperationSchema = z
|
|
198
|
+
.object({
|
|
199
|
+
body: z.string().nullable().optional(),
|
|
200
|
+
color: z.string().nullable().optional(),
|
|
201
|
+
edge: z.record(z.string(), z.unknown()).optional(),
|
|
202
|
+
edgeId: z.string().optional(),
|
|
203
|
+
edgeType: z.string().optional(),
|
|
204
|
+
height: z.number().optional(),
|
|
205
|
+
horizon: z.string().optional(),
|
|
206
|
+
id: z.string().optional(),
|
|
207
|
+
kind: z.string(),
|
|
208
|
+
label: z.string().nullable().optional(),
|
|
209
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
210
|
+
node: z.record(z.string(), z.unknown()).optional(),
|
|
211
|
+
nodeId: z.string().optional(),
|
|
212
|
+
nodeType: z.string().optional(),
|
|
213
|
+
parentNodeId: z.string().nullable().optional(),
|
|
214
|
+
positionX: z.number().optional(),
|
|
215
|
+
positionY: z.number().optional(),
|
|
216
|
+
sourceNodeId: z.string().optional(),
|
|
217
|
+
status: z.string().optional(),
|
|
218
|
+
targetNodeId: z.string().optional(),
|
|
219
|
+
title: z.string().optional(),
|
|
220
|
+
weight: z.number().optional(),
|
|
221
|
+
width: z.number().optional(),
|
|
222
|
+
})
|
|
223
|
+
.passthrough();
|
|
224
|
+
const loosePatchSchema = z
|
|
225
|
+
.object({
|
|
226
|
+
operations: z.array(loosePatchOperationSchema).min(1).max(100),
|
|
227
|
+
summary: z.string().min(1).max(2000),
|
|
228
|
+
})
|
|
229
|
+
.passthrough();
|
|
230
|
+
const looseRenderLeafSchema = z
|
|
231
|
+
.object({
|
|
232
|
+
children: z.array(z.unknown()).optional(),
|
|
233
|
+
content: z.string().optional(),
|
|
234
|
+
description: z.string().optional(),
|
|
235
|
+
props: mindMetadataSchema.optional(),
|
|
236
|
+
subtitle: z.string().optional(),
|
|
237
|
+
title: z.string().optional(),
|
|
238
|
+
type: z.string().optional(),
|
|
239
|
+
})
|
|
240
|
+
.passthrough();
|
|
241
|
+
const looseRenderElementSchema = z
|
|
242
|
+
.object({
|
|
243
|
+
children: z.array(z.union([z.string(), looseRenderLeafSchema])).optional(),
|
|
244
|
+
content: z.string().optional(),
|
|
245
|
+
description: z.string().optional(),
|
|
246
|
+
props: mindMetadataSchema.optional(),
|
|
247
|
+
subtitle: z.string().optional(),
|
|
248
|
+
title: z.string().optional(),
|
|
249
|
+
type: z.string().optional(),
|
|
250
|
+
})
|
|
251
|
+
.passthrough();
|
|
252
|
+
const looseRenderMindUiSchema = z
|
|
253
|
+
.object({
|
|
254
|
+
elements: z
|
|
255
|
+
.union([
|
|
256
|
+
z.array(looseRenderElementSchema),
|
|
257
|
+
z.record(
|
|
258
|
+
z.string(),
|
|
259
|
+
z.union([looseRenderElementSchema, z.array(looseRenderElementSchema)])
|
|
260
|
+
),
|
|
261
|
+
])
|
|
262
|
+
.optional(),
|
|
263
|
+
items: z.array(looseRenderElementSchema).optional(),
|
|
264
|
+
root: z.string().optional(),
|
|
265
|
+
title: z.string().optional(),
|
|
266
|
+
})
|
|
267
|
+
.passthrough();
|
|
268
|
+
|
|
269
|
+
const toolBoardIdSchema = z.string().trim().min(1).max(120).optional();
|
|
270
|
+
|
|
271
|
+
function resolveBoardId(ctx: MindToolContext, boardId?: string | null) {
|
|
272
|
+
if (boardId && isUuid(boardId)) return boardId;
|
|
273
|
+
return ctx.boardId ?? boardId ?? null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isUuid(value: string) {
|
|
277
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
|
|
278
|
+
value
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function readRecordValue(value: unknown): Record<string, unknown> {
|
|
283
|
+
return isRecord(value) ? value : {};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function mergeRecordValues(...values: unknown[]): Record<string, unknown> {
|
|
287
|
+
const merged: Record<string, unknown> = {};
|
|
288
|
+
for (const value of values) {
|
|
289
|
+
Object.assign(merged, readRecordValue(value));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return merged;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function readMindMetadata(value: unknown): MindJsonObject | undefined {
|
|
296
|
+
const parsed = mindMetadataSchema.safeParse(value);
|
|
297
|
+
return parsed.success ? (parsed.data as MindJsonObject) : undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function readStringValue(value: unknown) {
|
|
301
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readNullableStringValue(value: unknown) {
|
|
305
|
+
if (value === null) return null;
|
|
306
|
+
return readStringValue(value);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function readNumberValue(value: unknown) {
|
|
310
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
311
|
+
? value
|
|
312
|
+
: undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function readPositiveNumberValue(value: unknown) {
|
|
316
|
+
const number = readNumberValue(value);
|
|
317
|
+
return number && number > 0 ? number : undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function pickEnumValue<T extends readonly string[]>(
|
|
321
|
+
value: unknown,
|
|
322
|
+
allowed: T,
|
|
323
|
+
fallback: T[number],
|
|
324
|
+
aliases: Record<string, T[number]> = {}
|
|
325
|
+
): T[number] {
|
|
326
|
+
const raw = readStringValue(value)
|
|
327
|
+
?.toLowerCase()
|
|
328
|
+
.replaceAll('-', '_')
|
|
329
|
+
.replace(/\s+/gu, '_');
|
|
330
|
+
if (!raw) return fallback;
|
|
331
|
+
|
|
332
|
+
const candidates = [
|
|
333
|
+
raw,
|
|
334
|
+
...raw
|
|
335
|
+
.split(/[,:;|/]+/u)
|
|
336
|
+
.map((part) => part.trim())
|
|
337
|
+
.filter(Boolean),
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
for (const candidate of candidates) {
|
|
341
|
+
if ((allowed as readonly string[]).includes(candidate)) {
|
|
342
|
+
return candidate as T[number];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const alias = aliases[candidate];
|
|
346
|
+
if (alias) return alias;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return fallback;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function coercePatchOperation(
|
|
353
|
+
operation: z.infer<typeof loosePatchOperationSchema>,
|
|
354
|
+
index: number
|
|
355
|
+
): MindPatchOperation | null {
|
|
356
|
+
const nestedNode = readRecordValue(operation.node);
|
|
357
|
+
const nestedEdge = mergeRecordValues(nestedNode.edge, operation.edge);
|
|
358
|
+
const id = readStringValue(operation.id) ?? `op_${index + 1}`;
|
|
359
|
+
const kind = operation.kind.trim();
|
|
360
|
+
|
|
361
|
+
if (kind === 'create_node') {
|
|
362
|
+
const nodeId =
|
|
363
|
+
readStringValue(nestedNode.id) ??
|
|
364
|
+
readStringValue(nestedNode.nodeId) ??
|
|
365
|
+
id;
|
|
366
|
+
const title =
|
|
367
|
+
readStringValue(nestedNode.title) ??
|
|
368
|
+
readStringValue(operation.title) ??
|
|
369
|
+
'Untitled node';
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
id,
|
|
373
|
+
kind,
|
|
374
|
+
node: {
|
|
375
|
+
body:
|
|
376
|
+
readNullableStringValue(nestedNode.body) ??
|
|
377
|
+
readNullableStringValue(operation.body) ??
|
|
378
|
+
null,
|
|
379
|
+
color:
|
|
380
|
+
readNullableStringValue(nestedNode.color) ??
|
|
381
|
+
readNullableStringValue(operation.color),
|
|
382
|
+
height:
|
|
383
|
+
readPositiveNumberValue(nestedNode.height) ??
|
|
384
|
+
readPositiveNumberValue(operation.height),
|
|
385
|
+
horizon: pickEnumValue(
|
|
386
|
+
nestedNode.horizon ?? operation.horizon,
|
|
387
|
+
MIND_HORIZONS,
|
|
388
|
+
'month'
|
|
389
|
+
),
|
|
390
|
+
id: nodeId,
|
|
391
|
+
metadata: readMindMetadata(nestedNode.metadata ?? operation.metadata),
|
|
392
|
+
nodeType: pickEnumValue(
|
|
393
|
+
nestedNode.nodeType ?? operation.nodeType,
|
|
394
|
+
MIND_NODE_TYPES,
|
|
395
|
+
'idea',
|
|
396
|
+
{
|
|
397
|
+
action: 'idea',
|
|
398
|
+
task: 'idea',
|
|
399
|
+
review: 'milestone',
|
|
400
|
+
review_point: 'milestone',
|
|
401
|
+
}
|
|
402
|
+
),
|
|
403
|
+
parentNodeId:
|
|
404
|
+
readNullableStringValue(nestedNode.parentNodeId) ??
|
|
405
|
+
readNullableStringValue(operation.parentNodeId),
|
|
406
|
+
positionX:
|
|
407
|
+
readNumberValue(nestedNode.positionX) ??
|
|
408
|
+
readNumberValue(operation.positionX) ??
|
|
409
|
+
index * 320,
|
|
410
|
+
positionY:
|
|
411
|
+
readNumberValue(nestedNode.positionY) ??
|
|
412
|
+
readNumberValue(operation.positionY) ??
|
|
413
|
+
240,
|
|
414
|
+
status: pickEnumValue(
|
|
415
|
+
nestedNode.status ?? operation.status,
|
|
416
|
+
MIND_NODE_STATUSES,
|
|
417
|
+
'planned'
|
|
418
|
+
),
|
|
419
|
+
title,
|
|
420
|
+
width:
|
|
421
|
+
readPositiveNumberValue(nestedNode.width) ??
|
|
422
|
+
readPositiveNumberValue(operation.width),
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (kind === 'update_node') {
|
|
428
|
+
const nodeId =
|
|
429
|
+
readStringValue(operation.nodeId) ??
|
|
430
|
+
readStringValue(nestedNode.nodeId) ??
|
|
431
|
+
readStringValue(nestedNode.id);
|
|
432
|
+
if (!nodeId) return null;
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
body:
|
|
436
|
+
readNullableStringValue(nestedNode.body) ??
|
|
437
|
+
readNullableStringValue(operation.body),
|
|
438
|
+
color:
|
|
439
|
+
readNullableStringValue(nestedNode.color) ??
|
|
440
|
+
readNullableStringValue(operation.color),
|
|
441
|
+
height:
|
|
442
|
+
readPositiveNumberValue(nestedNode.height) ??
|
|
443
|
+
readPositiveNumberValue(operation.height),
|
|
444
|
+
horizon:
|
|
445
|
+
nestedNode.horizon || operation.horizon
|
|
446
|
+
? pickEnumValue(
|
|
447
|
+
nestedNode.horizon ?? operation.horizon,
|
|
448
|
+
MIND_HORIZONS,
|
|
449
|
+
'month'
|
|
450
|
+
)
|
|
451
|
+
: undefined,
|
|
452
|
+
id,
|
|
453
|
+
kind,
|
|
454
|
+
metadata:
|
|
455
|
+
nestedNode.metadata || operation.metadata
|
|
456
|
+
? readMindMetadata(nestedNode.metadata ?? operation.metadata)
|
|
457
|
+
: undefined,
|
|
458
|
+
nodeId,
|
|
459
|
+
nodeType:
|
|
460
|
+
nestedNode.nodeType || operation.nodeType
|
|
461
|
+
? pickEnumValue(
|
|
462
|
+
nestedNode.nodeType ?? operation.nodeType,
|
|
463
|
+
MIND_NODE_TYPES,
|
|
464
|
+
'idea',
|
|
465
|
+
{ action: 'idea', task: 'idea' }
|
|
466
|
+
)
|
|
467
|
+
: undefined,
|
|
468
|
+
parentNodeId:
|
|
469
|
+
readNullableStringValue(nestedNode.parentNodeId) ??
|
|
470
|
+
readNullableStringValue(operation.parentNodeId),
|
|
471
|
+
positionX:
|
|
472
|
+
readNumberValue(nestedNode.positionX) ??
|
|
473
|
+
readNumberValue(operation.positionX),
|
|
474
|
+
positionY:
|
|
475
|
+
readNumberValue(nestedNode.positionY) ??
|
|
476
|
+
readNumberValue(operation.positionY),
|
|
477
|
+
status:
|
|
478
|
+
nestedNode.status || operation.status
|
|
479
|
+
? pickEnumValue(
|
|
480
|
+
nestedNode.status ?? operation.status,
|
|
481
|
+
MIND_NODE_STATUSES,
|
|
482
|
+
'planned'
|
|
483
|
+
)
|
|
484
|
+
: undefined,
|
|
485
|
+
title:
|
|
486
|
+
readStringValue(nestedNode.title) ?? readStringValue(operation.title),
|
|
487
|
+
width:
|
|
488
|
+
readPositiveNumberValue(nestedNode.width) ??
|
|
489
|
+
readPositiveNumberValue(operation.width),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (kind === 'delete_node') {
|
|
494
|
+
const nodeId =
|
|
495
|
+
readStringValue(operation.nodeId) ??
|
|
496
|
+
readStringValue(nestedNode.nodeId) ??
|
|
497
|
+
readStringValue(nestedNode.id);
|
|
498
|
+
return nodeId ? { id, kind, nodeId } : null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (kind === 'create_edge') {
|
|
502
|
+
const edgeId =
|
|
503
|
+
readStringValue(nestedEdge.id) ??
|
|
504
|
+
readStringValue(nestedEdge.edgeId) ??
|
|
505
|
+
id;
|
|
506
|
+
const sourceNodeId =
|
|
507
|
+
readStringValue(nestedEdge.sourceNodeId) ??
|
|
508
|
+
readStringValue(operation.sourceNodeId);
|
|
509
|
+
const targetNodeId =
|
|
510
|
+
readStringValue(nestedEdge.targetNodeId) ??
|
|
511
|
+
readStringValue(operation.targetNodeId);
|
|
512
|
+
if (!sourceNodeId || !targetNodeId) return null;
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
edge: {
|
|
516
|
+
color:
|
|
517
|
+
readNullableStringValue(nestedEdge.color) ??
|
|
518
|
+
readNullableStringValue(operation.color),
|
|
519
|
+
edgeType: pickEnumValue(
|
|
520
|
+
nestedEdge.edgeType ?? operation.edgeType,
|
|
521
|
+
MIND_EDGE_TYPES,
|
|
522
|
+
'relates_to',
|
|
523
|
+
{
|
|
524
|
+
prerequisite: 'depends_on',
|
|
525
|
+
requires: 'depends_on',
|
|
526
|
+
validates: 'supports',
|
|
527
|
+
}
|
|
528
|
+
),
|
|
529
|
+
id: edgeId,
|
|
530
|
+
label:
|
|
531
|
+
readNullableStringValue(nestedEdge.label) ??
|
|
532
|
+
readNullableStringValue(operation.label),
|
|
533
|
+
metadata: readMindMetadata(nestedEdge.metadata ?? operation.metadata),
|
|
534
|
+
sourceNodeId,
|
|
535
|
+
targetNodeId,
|
|
536
|
+
weight:
|
|
537
|
+
readNumberValue(nestedEdge.weight) ??
|
|
538
|
+
readNumberValue(operation.weight),
|
|
539
|
+
},
|
|
540
|
+
id,
|
|
541
|
+
kind,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (kind === 'update_edge') {
|
|
546
|
+
const edgeId =
|
|
547
|
+
readStringValue(operation.edgeId) ??
|
|
548
|
+
readStringValue(nestedEdge.edgeId) ??
|
|
549
|
+
readStringValue(nestedEdge.id);
|
|
550
|
+
if (!edgeId) return null;
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
color:
|
|
554
|
+
readNullableStringValue(nestedEdge.color) ??
|
|
555
|
+
readNullableStringValue(operation.color),
|
|
556
|
+
edgeId,
|
|
557
|
+
edgeType:
|
|
558
|
+
nestedEdge.edgeType || operation.edgeType
|
|
559
|
+
? pickEnumValue(
|
|
560
|
+
nestedEdge.edgeType ?? operation.edgeType,
|
|
561
|
+
MIND_EDGE_TYPES,
|
|
562
|
+
'relates_to',
|
|
563
|
+
{
|
|
564
|
+
prerequisite: 'depends_on',
|
|
565
|
+
requires: 'depends_on',
|
|
566
|
+
validates: 'supports',
|
|
567
|
+
}
|
|
568
|
+
)
|
|
569
|
+
: undefined,
|
|
570
|
+
id,
|
|
571
|
+
kind,
|
|
572
|
+
label:
|
|
573
|
+
readNullableStringValue(nestedEdge.label) ??
|
|
574
|
+
readNullableStringValue(operation.label),
|
|
575
|
+
metadata:
|
|
576
|
+
nestedEdge.metadata || operation.metadata
|
|
577
|
+
? readMindMetadata(nestedEdge.metadata ?? operation.metadata)
|
|
578
|
+
: undefined,
|
|
579
|
+
sourceNodeId:
|
|
580
|
+
readStringValue(nestedEdge.sourceNodeId) ??
|
|
581
|
+
readStringValue(operation.sourceNodeId),
|
|
582
|
+
targetNodeId:
|
|
583
|
+
readStringValue(nestedEdge.targetNodeId) ??
|
|
584
|
+
readStringValue(operation.targetNodeId),
|
|
585
|
+
weight:
|
|
586
|
+
readNumberValue(nestedEdge.weight) ?? readNumberValue(operation.weight),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (kind === 'delete_edge') {
|
|
591
|
+
const edgeId =
|
|
592
|
+
readStringValue(operation.edgeId) ??
|
|
593
|
+
readStringValue(nestedEdge.edgeId) ??
|
|
594
|
+
readStringValue(nestedEdge.id);
|
|
595
|
+
return edgeId ? { edgeId, id, kind } : null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function coerceMindAiPatch(
|
|
602
|
+
patch: z.infer<typeof loosePatchSchema>
|
|
603
|
+
): MindAiPatch | { issues: string[] } {
|
|
604
|
+
const operations = patch.operations.flatMap((operation, index) => {
|
|
605
|
+
const coerced = coercePatchOperation(operation, index);
|
|
606
|
+
return coerced ? [coerced] : [];
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const parsed = patchSchema.safeParse({
|
|
610
|
+
operations,
|
|
611
|
+
summary: patch.summary,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
if (!parsed.success) {
|
|
615
|
+
return {
|
|
616
|
+
issues: parsed.error.issues.map(
|
|
617
|
+
(issue) => `${issue.path.join('.') || 'patch'}: ${issue.message}`
|
|
618
|
+
),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return parsed.data as MindAiPatch;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function normalizeGeneratedPatchIds(
|
|
626
|
+
patch: MindAiPatch | z.infer<typeof patchSchema>
|
|
627
|
+
) {
|
|
628
|
+
const idMap = new Map<string, string>();
|
|
629
|
+
|
|
630
|
+
const assignGeneratedId = (key: string) => {
|
|
631
|
+
const existing = idMap.get(key);
|
|
632
|
+
if (existing) return existing;
|
|
633
|
+
const nextId = crypto.randomUUID();
|
|
634
|
+
idMap.set(key, nextId);
|
|
635
|
+
return nextId;
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const resolveId = (value?: string | null, fallback?: string) => {
|
|
639
|
+
const key = value?.trim() || fallback?.trim();
|
|
640
|
+
if (!key) return crypto.randomUUID();
|
|
641
|
+
if (isUuid(key)) return key;
|
|
642
|
+
return assignGeneratedId(key);
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
patch.operations.forEach((operation, index) => {
|
|
646
|
+
if (operation.kind !== 'create_node') return;
|
|
647
|
+
|
|
648
|
+
const nodeId = resolveId(
|
|
649
|
+
operation.node.id,
|
|
650
|
+
operation.id || `create_node_${index + 1}`
|
|
651
|
+
);
|
|
652
|
+
idMap.set(operation.id, nodeId);
|
|
653
|
+
if (operation.node.id) idMap.set(operation.node.id, nodeId);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const operations = patch.operations.map((operation, index) => {
|
|
657
|
+
if (operation.kind === 'create_node') {
|
|
658
|
+
const nodeId = resolveId(
|
|
659
|
+
operation.node.id,
|
|
660
|
+
operation.id || `create_node_${index + 1}`
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
...operation,
|
|
665
|
+
node: {
|
|
666
|
+
...operation.node,
|
|
667
|
+
id: nodeId,
|
|
668
|
+
parentNodeId: operation.node.parentNodeId
|
|
669
|
+
? resolveId(operation.node.parentNodeId)
|
|
670
|
+
: operation.node.parentNodeId,
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (operation.kind === 'create_edge') {
|
|
676
|
+
const edgeId = resolveId(
|
|
677
|
+
operation.edge.id,
|
|
678
|
+
operation.id || `create_edge_${index + 1}`
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
...operation,
|
|
683
|
+
edge: {
|
|
684
|
+
...operation.edge,
|
|
685
|
+
id: edgeId,
|
|
686
|
+
sourceNodeId: resolveId(operation.edge.sourceNodeId),
|
|
687
|
+
targetNodeId: resolveId(operation.edge.targetNodeId),
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (operation.kind === 'update_node') {
|
|
693
|
+
return {
|
|
694
|
+
...operation,
|
|
695
|
+
parentNodeId: operation.parentNodeId
|
|
696
|
+
? resolveId(operation.parentNodeId)
|
|
697
|
+
: operation.parentNodeId,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (operation.kind === 'update_edge') {
|
|
702
|
+
return {
|
|
703
|
+
...operation,
|
|
704
|
+
sourceNodeId: operation.sourceNodeId
|
|
705
|
+
? resolveId(operation.sourceNodeId)
|
|
706
|
+
: operation.sourceNodeId,
|
|
707
|
+
targetNodeId: operation.targetNodeId
|
|
708
|
+
? resolveId(operation.targetNodeId)
|
|
709
|
+
: operation.targetNodeId,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return operation;
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
...patch,
|
|
718
|
+
operations: orderMindPatchOperationsForApply(operations),
|
|
719
|
+
} satisfies MindAiPatch;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function orderMindPatchOperationsForApply(
|
|
723
|
+
operations: MindPatchOperation[]
|
|
724
|
+
) {
|
|
725
|
+
const createNodeOperations = operations.filter(
|
|
726
|
+
(
|
|
727
|
+
operation
|
|
728
|
+
): operation is Extract<MindPatchOperation, { kind: 'create_node' }> =>
|
|
729
|
+
operation.kind === 'create_node'
|
|
730
|
+
);
|
|
731
|
+
const createNodeByRef = new Map<
|
|
732
|
+
string,
|
|
733
|
+
(typeof createNodeOperations)[number]
|
|
734
|
+
>();
|
|
735
|
+
|
|
736
|
+
for (const operation of createNodeOperations) {
|
|
737
|
+
createNodeByRef.set(operation.id, operation);
|
|
738
|
+
createNodeByRef.set(operation.node.id, operation);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const orderedCreateNodes: typeof createNodeOperations = [];
|
|
742
|
+
const visiting = new Set<string>();
|
|
743
|
+
const visited = new Set<string>();
|
|
744
|
+
|
|
745
|
+
const visitCreateNode = (
|
|
746
|
+
operation: (typeof createNodeOperations)[number]
|
|
747
|
+
) => {
|
|
748
|
+
if (visited.has(operation.id)) return;
|
|
749
|
+
if (visiting.has(operation.id)) {
|
|
750
|
+
orderedCreateNodes.push(operation);
|
|
751
|
+
visited.add(operation.id);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
visiting.add(operation.id);
|
|
756
|
+
const parentOperation = operation.node.parentNodeId
|
|
757
|
+
? createNodeByRef.get(operation.node.parentNodeId)
|
|
758
|
+
: undefined;
|
|
759
|
+
if (parentOperation && parentOperation.id !== operation.id) {
|
|
760
|
+
visitCreateNode(parentOperation);
|
|
761
|
+
}
|
|
762
|
+
visiting.delete(operation.id);
|
|
763
|
+
|
|
764
|
+
if (!visited.has(operation.id)) {
|
|
765
|
+
orderedCreateNodes.push(operation);
|
|
766
|
+
visited.add(operation.id);
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
for (const operation of createNodeOperations) {
|
|
771
|
+
visitCreateNode(operation);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const byKind = <Kind extends MindPatchOperation['kind']>(kind: Kind) =>
|
|
775
|
+
operations.filter(
|
|
776
|
+
(operation): operation is Extract<MindPatchOperation, { kind: Kind }> =>
|
|
777
|
+
operation.kind === kind
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
return [
|
|
781
|
+
...orderedCreateNodes,
|
|
782
|
+
...byKind('update_node'),
|
|
783
|
+
...byKind('create_edge'),
|
|
784
|
+
...byKind('update_edge'),
|
|
785
|
+
...byKind('delete_edge'),
|
|
786
|
+
...byKind('delete_node'),
|
|
787
|
+
] satisfies MindPatchOperation[];
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function describePatchOperation(operation: MindPatchOperation) {
|
|
791
|
+
if (operation.kind === 'create_node') {
|
|
792
|
+
return operation.node.title || operation.id;
|
|
793
|
+
}
|
|
794
|
+
if (operation.kind === 'update_node') {
|
|
795
|
+
return operation.title || operation.nodeId;
|
|
796
|
+
}
|
|
797
|
+
if (operation.kind === 'create_edge') {
|
|
798
|
+
return operation.edge.label || operation.id;
|
|
799
|
+
}
|
|
800
|
+
if (operation.kind === 'update_edge') {
|
|
801
|
+
return operation.label || operation.edgeId;
|
|
802
|
+
}
|
|
803
|
+
if (operation.kind === 'delete_node') return operation.nodeId;
|
|
804
|
+
return operation.edgeId;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function validateMindPatchReferences({
|
|
808
|
+
patch,
|
|
809
|
+
snapshot,
|
|
810
|
+
}: {
|
|
811
|
+
patch: MindAiPatch;
|
|
812
|
+
snapshot: MindBoardSnapshot;
|
|
813
|
+
}) {
|
|
814
|
+
const existingNodeIds = new Set(snapshot.nodes.map((node) => node.id));
|
|
815
|
+
const existingEdgeIds = new Set(snapshot.edges.map((edge) => edge.id));
|
|
816
|
+
const createdNodeRefs = new Set<string>();
|
|
817
|
+
const issues: string[] = [];
|
|
818
|
+
|
|
819
|
+
for (const operation of patch.operations) {
|
|
820
|
+
if (operation.kind !== 'create_node') continue;
|
|
821
|
+
createdNodeRefs.add(operation.id);
|
|
822
|
+
if (operation.node.id) createdNodeRefs.add(operation.node.id);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const nodeRefExists = (nodeId: string) =>
|
|
826
|
+
existingNodeIds.has(nodeId) || createdNodeRefs.has(nodeId);
|
|
827
|
+
const existingNodeRefExists = (nodeId: string) => existingNodeIds.has(nodeId);
|
|
828
|
+
|
|
829
|
+
const pushMissingNode = (
|
|
830
|
+
operation: MindPatchOperation,
|
|
831
|
+
nodeId: string,
|
|
832
|
+
role: string
|
|
833
|
+
) => {
|
|
834
|
+
issues.push(
|
|
835
|
+
`Missing node reference for ${role} in ${operation.kind} "${describePatchOperation(
|
|
836
|
+
operation
|
|
837
|
+
)}": ${nodeId}`
|
|
838
|
+
);
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
const pushMissingEdge = (
|
|
842
|
+
operation: MindPatchOperation,
|
|
843
|
+
edgeId: string,
|
|
844
|
+
role: string
|
|
845
|
+
) => {
|
|
846
|
+
issues.push(
|
|
847
|
+
`Missing edge reference for ${role} in ${operation.kind} "${describePatchOperation(
|
|
848
|
+
operation
|
|
849
|
+
)}": ${edgeId}`
|
|
850
|
+
);
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
for (const operation of patch.operations) {
|
|
854
|
+
if (operation.kind === 'create_node') {
|
|
855
|
+
const parentNodeId = operation.node.parentNodeId;
|
|
856
|
+
if (parentNodeId && !nodeRefExists(parentNodeId)) {
|
|
857
|
+
pushMissingNode(operation, parentNodeId, 'parentNodeId');
|
|
858
|
+
}
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (operation.kind === 'update_node') {
|
|
863
|
+
if (!existingNodeRefExists(operation.nodeId)) {
|
|
864
|
+
pushMissingNode(operation, operation.nodeId, 'nodeId');
|
|
865
|
+
}
|
|
866
|
+
if (operation.parentNodeId && !nodeRefExists(operation.parentNodeId)) {
|
|
867
|
+
pushMissingNode(operation, operation.parentNodeId, 'parentNodeId');
|
|
868
|
+
}
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (operation.kind === 'delete_node') {
|
|
873
|
+
if (!existingNodeRefExists(operation.nodeId)) {
|
|
874
|
+
pushMissingNode(operation, operation.nodeId, 'nodeId');
|
|
875
|
+
}
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (operation.kind === 'create_edge') {
|
|
880
|
+
if (!nodeRefExists(operation.edge.sourceNodeId)) {
|
|
881
|
+
pushMissingNode(operation, operation.edge.sourceNodeId, 'sourceNodeId');
|
|
882
|
+
}
|
|
883
|
+
if (!nodeRefExists(operation.edge.targetNodeId)) {
|
|
884
|
+
pushMissingNode(operation, operation.edge.targetNodeId, 'targetNodeId');
|
|
885
|
+
}
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (operation.kind === 'update_edge') {
|
|
890
|
+
if (!existingEdgeIds.has(operation.edgeId)) {
|
|
891
|
+
pushMissingEdge(operation, operation.edgeId, 'edgeId');
|
|
892
|
+
}
|
|
893
|
+
if (operation.sourceNodeId && !nodeRefExists(operation.sourceNodeId)) {
|
|
894
|
+
pushMissingNode(operation, operation.sourceNodeId, 'sourceNodeId');
|
|
895
|
+
}
|
|
896
|
+
if (operation.targetNodeId && !nodeRefExists(operation.targetNodeId)) {
|
|
897
|
+
pushMissingNode(operation, operation.targetNodeId, 'targetNodeId');
|
|
898
|
+
}
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (!existingEdgeIds.has(operation.edgeId)) {
|
|
903
|
+
pushMissingEdge(operation, operation.edgeId, 'edgeId');
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return issues;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function validateMindPatchGraphHealth({
|
|
911
|
+
patch,
|
|
912
|
+
snapshot,
|
|
913
|
+
}: {
|
|
914
|
+
patch: MindAiPatch;
|
|
915
|
+
snapshot: MindBoardSnapshot;
|
|
916
|
+
}) {
|
|
917
|
+
const createNodeOperations = patch.operations.filter(
|
|
918
|
+
(
|
|
919
|
+
operation
|
|
920
|
+
): operation is Extract<MindPatchOperation, { kind: 'create_node' }> =>
|
|
921
|
+
operation.kind === 'create_node'
|
|
922
|
+
);
|
|
923
|
+
const issues: string[] = [];
|
|
924
|
+
if (createNodeOperations.length === 0) return issues;
|
|
925
|
+
|
|
926
|
+
const relationshipRefs = new Set<string>();
|
|
927
|
+
for (const operation of patch.operations) {
|
|
928
|
+
if (operation.kind === 'create_edge') {
|
|
929
|
+
relationshipRefs.add(operation.edge.sourceNodeId);
|
|
930
|
+
relationshipRefs.add(operation.edge.targetNodeId);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (operation.kind === 'update_edge') {
|
|
935
|
+
if (operation.sourceNodeId) relationshipRefs.add(operation.sourceNodeId);
|
|
936
|
+
if (operation.targetNodeId) relationshipRefs.add(operation.targetNodeId);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const shouldRequireConnections =
|
|
941
|
+
snapshot.nodes.length > 0 || createNodeOperations.length > 1;
|
|
942
|
+
|
|
943
|
+
if (!shouldRequireConnections) return issues;
|
|
944
|
+
|
|
945
|
+
for (const operation of createNodeOperations) {
|
|
946
|
+
const refs = [operation.id, operation.node.id].filter(Boolean);
|
|
947
|
+
const hasRelationship = refs.some((ref) => relationshipRefs.has(ref));
|
|
948
|
+
if (hasRelationship) continue;
|
|
949
|
+
|
|
950
|
+
if (operation.node.parentNodeId) {
|
|
951
|
+
issues.push(
|
|
952
|
+
`Patch draft sets parentNodeId for new node "${operation.node.title}" but does not connect it with an explicit edge. Add a contains edge to the parent when possible.`
|
|
953
|
+
);
|
|
954
|
+
} else {
|
|
955
|
+
issues.push(
|
|
956
|
+
`Patch draft leaves new node "${operation.node.title}" isolated. Add a relationship edge to a parent, sibling, or existing anchor when possible.`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return issues;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
965
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function sanitizeElementId(value: string, fallback: string) {
|
|
969
|
+
const normalized = value
|
|
970
|
+
.trim()
|
|
971
|
+
.toLowerCase()
|
|
972
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
973
|
+
.replace(/^_+|_+$/g, '')
|
|
974
|
+
.slice(0, 48);
|
|
975
|
+
|
|
976
|
+
return normalized || fallback;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function readText(value: unknown) {
|
|
980
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function toMindRenderSpec(input: unknown) {
|
|
984
|
+
const normalized = normalizeRenderUiInputForTool(input);
|
|
985
|
+
if (isRecord(normalized) && isRenderableRenderUiSpec(normalized)) {
|
|
986
|
+
return normalized;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const source = isRecord(normalized) ? normalized : {};
|
|
990
|
+
const elements = source.elements;
|
|
991
|
+
const title =
|
|
992
|
+
readText(source.root) ?? readText(source.title) ?? 'Generated plan';
|
|
993
|
+
const convertedElements: Record<string, unknown> = {};
|
|
994
|
+
const rootChildren: string[] = [];
|
|
995
|
+
|
|
996
|
+
const addElement = (id: string, element: Record<string, unknown>) => {
|
|
997
|
+
convertedElements[id] = element;
|
|
998
|
+
return id;
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
const convertOutlineItem = (
|
|
1002
|
+
raw: unknown,
|
|
1003
|
+
fallbackId: string,
|
|
1004
|
+
topLevel = false
|
|
1005
|
+
): string => {
|
|
1006
|
+
if (!isRecord(raw)) {
|
|
1007
|
+
return addElement(fallbackId, {
|
|
1008
|
+
children: [],
|
|
1009
|
+
props: { content: String(raw) },
|
|
1010
|
+
type: 'Text',
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const rawChildren = Array.isArray(raw.children) ? raw.children : [];
|
|
1015
|
+
const itemTitle =
|
|
1016
|
+
readText(raw.title) ??
|
|
1017
|
+
readText(raw.content) ??
|
|
1018
|
+
readText(raw.name) ??
|
|
1019
|
+
'Generated item';
|
|
1020
|
+
const id = sanitizeElementId(readText(raw.id) ?? itemTitle, fallbackId);
|
|
1021
|
+
|
|
1022
|
+
if (readText(raw.type)) {
|
|
1023
|
+
const childIds = rawChildren.map((child, index) =>
|
|
1024
|
+
typeof child === 'string'
|
|
1025
|
+
? child
|
|
1026
|
+
: convertOutlineItem(child, `${id}_${index + 1}`)
|
|
1027
|
+
);
|
|
1028
|
+
return addElement(id, {
|
|
1029
|
+
...raw,
|
|
1030
|
+
children: childIds,
|
|
1031
|
+
props: isRecord(raw.props) ? raw.props : {},
|
|
1032
|
+
type: readText(raw.type),
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (!rawChildren.length && !topLevel) {
|
|
1037
|
+
return addElement(id, {
|
|
1038
|
+
children: [],
|
|
1039
|
+
props: {
|
|
1040
|
+
subtitle: readText(raw.subtitle) ?? readText(raw.description),
|
|
1041
|
+
title: itemTitle,
|
|
1042
|
+
},
|
|
1043
|
+
type: 'ListItem',
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const childIds = rawChildren.map((child, index) =>
|
|
1048
|
+
typeof child === 'string'
|
|
1049
|
+
? child
|
|
1050
|
+
: convertOutlineItem(child, `${id}_${index + 1}`)
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
return addElement(id, {
|
|
1054
|
+
children: childIds,
|
|
1055
|
+
props: {
|
|
1056
|
+
description: readText(raw.description),
|
|
1057
|
+
title: itemTitle,
|
|
1058
|
+
},
|
|
1059
|
+
type: 'Card',
|
|
1060
|
+
});
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
if (Array.isArray(elements)) {
|
|
1064
|
+
elements.forEach((item, index) => {
|
|
1065
|
+
rootChildren.push(convertOutlineItem(item, `item_${index + 1}`, true));
|
|
1066
|
+
});
|
|
1067
|
+
} else if (isRecord(elements)) {
|
|
1068
|
+
for (const [key, value] of Object.entries(elements)) {
|
|
1069
|
+
if (Array.isArray(value)) {
|
|
1070
|
+
value.forEach((item, index) => {
|
|
1071
|
+
rootChildren.push(
|
|
1072
|
+
convertOutlineItem(
|
|
1073
|
+
item,
|
|
1074
|
+
`${sanitizeElementId(key, 'item')}_${index + 1}`,
|
|
1075
|
+
true
|
|
1076
|
+
)
|
|
1077
|
+
);
|
|
1078
|
+
});
|
|
1079
|
+
} else {
|
|
1080
|
+
rootChildren.push(
|
|
1081
|
+
convertOutlineItem(value, sanitizeElementId(key, 'item'), true)
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
} else if (Array.isArray(source.items)) {
|
|
1086
|
+
source.items.forEach((item, index) => {
|
|
1087
|
+
rootChildren.push(convertOutlineItem(item, `item_${index + 1}`, true));
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const rootId = 'mind_generated_ui';
|
|
1092
|
+
return {
|
|
1093
|
+
elements: {
|
|
1094
|
+
[rootId]: {
|
|
1095
|
+
children: rootChildren.length ? rootChildren : ['mind_generated_empty'],
|
|
1096
|
+
props: { title },
|
|
1097
|
+
type: 'Card',
|
|
1098
|
+
},
|
|
1099
|
+
...convertedElements,
|
|
1100
|
+
...(rootChildren.length
|
|
1101
|
+
? {}
|
|
1102
|
+
: {
|
|
1103
|
+
mind_generated_empty: {
|
|
1104
|
+
children: [],
|
|
1105
|
+
props: {
|
|
1106
|
+
content:
|
|
1107
|
+
'Mind could not infer a visual structure from this draft.',
|
|
1108
|
+
title: 'Draft view unavailable',
|
|
1109
|
+
variant: 'warning',
|
|
1110
|
+
},
|
|
1111
|
+
type: 'Callout',
|
|
1112
|
+
},
|
|
1113
|
+
}),
|
|
1114
|
+
},
|
|
1115
|
+
root: rootId,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function countBy<T extends string>(values: T[]) {
|
|
1120
|
+
return values.reduce<Record<T, number>>(
|
|
1121
|
+
(acc, value) => {
|
|
1122
|
+
acc[value] = (acc[value] ?? 0) + 1;
|
|
1123
|
+
return acc;
|
|
1124
|
+
},
|
|
1125
|
+
{} as Record<T, number>
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function getGraphDegree(snapshot: MindBoardSnapshot) {
|
|
1130
|
+
const degree = new Map<string, number>();
|
|
1131
|
+
for (const edge of snapshot.edges) {
|
|
1132
|
+
degree.set(edge.sourceNodeId, (degree.get(edge.sourceNodeId) ?? 0) + 1);
|
|
1133
|
+
degree.set(edge.targetNodeId, (degree.get(edge.targetNodeId) ?? 0) + 1);
|
|
1134
|
+
}
|
|
1135
|
+
return degree;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function createStructureInspection(snapshot: MindBoardSnapshot) {
|
|
1139
|
+
const degree = getGraphDegree(snapshot);
|
|
1140
|
+
const highDegreeNodes = [...snapshot.nodes]
|
|
1141
|
+
.sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0))
|
|
1142
|
+
.slice(0, 12)
|
|
1143
|
+
.map((node) => ({
|
|
1144
|
+
degree: degree.get(node.id) ?? 0,
|
|
1145
|
+
horizon: node.horizon,
|
|
1146
|
+
id: node.id,
|
|
1147
|
+
status: node.status,
|
|
1148
|
+
title: node.title,
|
|
1149
|
+
type: node.nodeType,
|
|
1150
|
+
}));
|
|
1151
|
+
const allIsolatedNodes = snapshot.nodes.filter(
|
|
1152
|
+
(node) => (degree.get(node.id) ?? 0) === 0
|
|
1153
|
+
);
|
|
1154
|
+
const isolatedNodes = allIsolatedNodes.slice(0, 25).map((node) => ({
|
|
1155
|
+
horizon: node.horizon,
|
|
1156
|
+
id: node.id,
|
|
1157
|
+
status: node.status,
|
|
1158
|
+
title: node.title,
|
|
1159
|
+
type: node.nodeType,
|
|
1160
|
+
}));
|
|
1161
|
+
|
|
1162
|
+
return {
|
|
1163
|
+
board: snapshot.board,
|
|
1164
|
+
chunkStrategy: {
|
|
1165
|
+
recommendedLimit: snapshot.nodes.length > 500 ? 80 : 120,
|
|
1166
|
+
recommendedOrder:
|
|
1167
|
+
'Inspect structure first, search for the user topic, load the neighborhood around relevant nodes, then use paged chunks only for broad audits.',
|
|
1168
|
+
totalChunksAt80: Math.ceil(snapshot.nodes.length / 80),
|
|
1169
|
+
},
|
|
1170
|
+
counts: {
|
|
1171
|
+
byHorizon: countBy(snapshot.nodes.map((node) => node.horizon)),
|
|
1172
|
+
byStatus: countBy(snapshot.nodes.map((node) => node.status)),
|
|
1173
|
+
byType: countBy(snapshot.nodes.map((node) => node.nodeType)),
|
|
1174
|
+
edges: snapshot.edges.length,
|
|
1175
|
+
groups: snapshot.groups.length,
|
|
1176
|
+
isolatedNodes: allIsolatedNodes.length,
|
|
1177
|
+
nodes: snapshot.nodes.length,
|
|
1178
|
+
tags: snapshot.tags.length,
|
|
1179
|
+
},
|
|
1180
|
+
highDegreeNodes,
|
|
1181
|
+
isolatedNodes,
|
|
1182
|
+
tags: snapshot.tags.slice(0, 50).map((tag) => ({
|
|
1183
|
+
id: tag.id,
|
|
1184
|
+
name: tag.name,
|
|
1185
|
+
nodeCount: tag.nodeIds.length,
|
|
1186
|
+
})),
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function loadNeighborhood({
|
|
1191
|
+
depth,
|
|
1192
|
+
limit,
|
|
1193
|
+
nodeId,
|
|
1194
|
+
snapshot,
|
|
1195
|
+
}: {
|
|
1196
|
+
depth: number;
|
|
1197
|
+
limit: number;
|
|
1198
|
+
nodeId: string;
|
|
1199
|
+
snapshot: MindBoardSnapshot;
|
|
1200
|
+
}) {
|
|
1201
|
+
const visited = new Set([nodeId]);
|
|
1202
|
+
let frontier = new Set([nodeId]);
|
|
1203
|
+
|
|
1204
|
+
for (let level = 0; level < depth; level += 1) {
|
|
1205
|
+
const next = new Set<string>();
|
|
1206
|
+
for (const edge of snapshot.edges) {
|
|
1207
|
+
if (frontier.has(edge.sourceNodeId)) next.add(edge.targetNodeId);
|
|
1208
|
+
if (frontier.has(edge.targetNodeId)) next.add(edge.sourceNodeId);
|
|
1209
|
+
}
|
|
1210
|
+
for (const id of next) visited.add(id);
|
|
1211
|
+
frontier = next;
|
|
1212
|
+
if (visited.size >= limit) break;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const limitedIds = new Set([...visited].slice(0, limit));
|
|
1216
|
+
const nodes = snapshot.nodes.filter((node) => limitedIds.has(node.id));
|
|
1217
|
+
const edges = snapshot.edges.filter(
|
|
1218
|
+
(edge) =>
|
|
1219
|
+
limitedIds.has(edge.sourceNodeId) && limitedIds.has(edge.targetNodeId)
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
return {
|
|
1223
|
+
edges,
|
|
1224
|
+
hasMore: visited.size > limitedIds.size,
|
|
1225
|
+
nodes,
|
|
1226
|
+
requestedNodeId: nodeId,
|
|
1227
|
+
totalVisited: visited.size,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
export function createMindStreamTools(
|
|
1232
|
+
ctx: MindToolContext,
|
|
1233
|
+
callbacks: MindToolCallbacks
|
|
1234
|
+
): ToolSet {
|
|
1235
|
+
return {
|
|
1236
|
+
apply_mind_patch: tool({
|
|
1237
|
+
description:
|
|
1238
|
+
'Apply a previously proposed Mind patch. Only succeeds when the current chat is in Implement mode.',
|
|
1239
|
+
inputSchema: z.object({
|
|
1240
|
+
patchId: z.string().trim().min(1).max(120),
|
|
1241
|
+
}),
|
|
1242
|
+
execute: async ({ patchId }) => {
|
|
1243
|
+
if (ctx.writeMode !== 'direct') {
|
|
1244
|
+
return {
|
|
1245
|
+
ok: false,
|
|
1246
|
+
reason:
|
|
1247
|
+
'This chat is in Draft mode. The user must apply the patch manually.',
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
if (!isUuid(patchId)) {
|
|
1251
|
+
return { ok: false, reason: 'Patch id must be a UUID.' };
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const patch = await callbacks
|
|
1255
|
+
.applyPatch({
|
|
1256
|
+
patchId,
|
|
1257
|
+
userId: ctx.userId,
|
|
1258
|
+
wsId: ctx.wsId,
|
|
1259
|
+
})
|
|
1260
|
+
.catch((error) => ({
|
|
1261
|
+
error:
|
|
1262
|
+
error instanceof Error && error.message
|
|
1263
|
+
? error.message
|
|
1264
|
+
: 'Patch application failed.',
|
|
1265
|
+
}));
|
|
1266
|
+
|
|
1267
|
+
if (patch && 'error' in patch) {
|
|
1268
|
+
return { ok: false, reason: patch.error };
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
return patch
|
|
1272
|
+
? { ok: true, patch }
|
|
1273
|
+
: { ok: false, reason: 'Patch was not found.' };
|
|
1274
|
+
},
|
|
1275
|
+
}),
|
|
1276
|
+
get_mindboard_snapshot: tool({
|
|
1277
|
+
description:
|
|
1278
|
+
'Load a complete Mind board snapshot including nodes, edges, tags, groups, links, and recent AI patches.',
|
|
1279
|
+
inputSchema: z.object({
|
|
1280
|
+
boardId: toolBoardIdSchema,
|
|
1281
|
+
}),
|
|
1282
|
+
execute: async ({ boardId }) => {
|
|
1283
|
+
const resolvedBoardId = resolveBoardId(ctx, boardId);
|
|
1284
|
+
if (!resolvedBoardId) {
|
|
1285
|
+
return { ok: false, reason: 'No Mind board is selected.' };
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const snapshot = await callbacks.getSnapshot(ctx.wsId, resolvedBoardId);
|
|
1289
|
+
return snapshot
|
|
1290
|
+
? { ok: true, snapshot }
|
|
1291
|
+
: { ok: false, reason: 'Mind board was not found.' };
|
|
1292
|
+
},
|
|
1293
|
+
}),
|
|
1294
|
+
list_mindboards: tool({
|
|
1295
|
+
description:
|
|
1296
|
+
'List active Mind boards in the current workspace with graph counts.',
|
|
1297
|
+
inputSchema: z.object({}),
|
|
1298
|
+
execute: async () => ({
|
|
1299
|
+
boards: await callbacks.listBoards(ctx.wsId),
|
|
1300
|
+
ok: true,
|
|
1301
|
+
}),
|
|
1302
|
+
}),
|
|
1303
|
+
load_mind_chunk: tool({
|
|
1304
|
+
description:
|
|
1305
|
+
'Load a chunk of nodes and all edges connected to those nodes from a Mind board.',
|
|
1306
|
+
inputSchema: z.object({
|
|
1307
|
+
boardId: toolBoardIdSchema,
|
|
1308
|
+
limit: z.number().int().min(1).max(200).default(80),
|
|
1309
|
+
offset: z.number().int().min(0).default(0),
|
|
1310
|
+
}),
|
|
1311
|
+
execute: async ({ boardId, limit, offset }) => {
|
|
1312
|
+
const resolvedBoardId = resolveBoardId(ctx, boardId);
|
|
1313
|
+
if (!resolvedBoardId) {
|
|
1314
|
+
return { ok: false, reason: 'No Mind board is selected.' };
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const snapshot = await callbacks.getSnapshot(ctx.wsId, resolvedBoardId);
|
|
1318
|
+
if (!snapshot)
|
|
1319
|
+
return { ok: false, reason: 'Mind board was not found.' };
|
|
1320
|
+
|
|
1321
|
+
const nodes = snapshot.nodes.slice(offset, offset + limit);
|
|
1322
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
1323
|
+
const edges = snapshot.edges.filter(
|
|
1324
|
+
(edge) =>
|
|
1325
|
+
nodeIds.has(edge.sourceNodeId) || nodeIds.has(edge.targetNodeId)
|
|
1326
|
+
);
|
|
1327
|
+
|
|
1328
|
+
return {
|
|
1329
|
+
board: snapshot.board,
|
|
1330
|
+
edges,
|
|
1331
|
+
hasMore: offset + limit < snapshot.nodes.length,
|
|
1332
|
+
nodes,
|
|
1333
|
+
ok: true,
|
|
1334
|
+
totalNodes: snapshot.nodes.length,
|
|
1335
|
+
};
|
|
1336
|
+
},
|
|
1337
|
+
}),
|
|
1338
|
+
load_mind_neighborhood: tool({
|
|
1339
|
+
description:
|
|
1340
|
+
'Load the local graph neighborhood around a specific node. Prefer this over full snapshots when refining one idea in a large board.',
|
|
1341
|
+
inputSchema: z.object({
|
|
1342
|
+
boardId: toolBoardIdSchema,
|
|
1343
|
+
depth: z.number().int().min(1).max(3).default(1),
|
|
1344
|
+
limit: z.number().int().min(5).max(150).default(80),
|
|
1345
|
+
nodeId: z.guid(),
|
|
1346
|
+
}),
|
|
1347
|
+
execute: async ({ boardId, depth, limit, nodeId }) => {
|
|
1348
|
+
const resolvedBoardId = resolveBoardId(ctx, boardId);
|
|
1349
|
+
if (!resolvedBoardId) {
|
|
1350
|
+
return { ok: false, reason: 'No Mind board is selected.' };
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const snapshot = await callbacks.getSnapshot(ctx.wsId, resolvedBoardId);
|
|
1354
|
+
if (!snapshot)
|
|
1355
|
+
return { ok: false, reason: 'Mind board was not found.' };
|
|
1356
|
+
|
|
1357
|
+
return {
|
|
1358
|
+
ok: true,
|
|
1359
|
+
...loadNeighborhood({ depth, limit, nodeId, snapshot }),
|
|
1360
|
+
};
|
|
1361
|
+
},
|
|
1362
|
+
}),
|
|
1363
|
+
inspect_mind_structure: tool({
|
|
1364
|
+
description:
|
|
1365
|
+
'Inspect board organization before planning changes. Returns counts by horizon, status, and type plus high-degree and isolated nodes so large boards can be navigated in chunks and relationship gaps can be repaired.',
|
|
1366
|
+
inputSchema: z.object({
|
|
1367
|
+
boardId: toolBoardIdSchema,
|
|
1368
|
+
}),
|
|
1369
|
+
execute: async ({ boardId }) => {
|
|
1370
|
+
const resolvedBoardId = resolveBoardId(ctx, boardId);
|
|
1371
|
+
if (!resolvedBoardId) {
|
|
1372
|
+
return { ok: false, reason: 'No Mind board is selected.' };
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const snapshot = await callbacks.getSnapshot(ctx.wsId, resolvedBoardId);
|
|
1376
|
+
if (!snapshot)
|
|
1377
|
+
return { ok: false, reason: 'Mind board was not found.' };
|
|
1378
|
+
|
|
1379
|
+
return { ok: true, structure: createStructureInspection(snapshot) };
|
|
1380
|
+
},
|
|
1381
|
+
}),
|
|
1382
|
+
propose_mind_patch: tool({
|
|
1383
|
+
description:
|
|
1384
|
+
'Create a structured applyable Mind draft patch for user review or implementation. Use kind=create_node with node fields for nodes. Use kind=create_edge with a top-level edge object containing sourceNodeId, targetNodeId, and edgeType; sourceNodeId and targetNodeId may reference IDs of nodes created earlier in the same patch. When the board or patch has more than one possible node, every new node must participate in at least one explicit edge; use contains edges for parent/child structure even when parentNodeId is also set.',
|
|
1385
|
+
inputSchema: z.object({
|
|
1386
|
+
boardId: toolBoardIdSchema,
|
|
1387
|
+
patch: loosePatchSchema,
|
|
1388
|
+
}),
|
|
1389
|
+
execute: async ({ boardId, patch }) => {
|
|
1390
|
+
const resolvedBoardId = resolveBoardId(ctx, boardId);
|
|
1391
|
+
if (!resolvedBoardId) {
|
|
1392
|
+
return { ok: false, reason: 'No Mind board is selected.' };
|
|
1393
|
+
}
|
|
1394
|
+
const coercedPatch = coerceMindAiPatch(patch);
|
|
1395
|
+
if ('issues' in coercedPatch) {
|
|
1396
|
+
return {
|
|
1397
|
+
ok: false,
|
|
1398
|
+
reason: `Patch draft was not applyable: ${coercedPatch.issues
|
|
1399
|
+
.slice(0, 6)
|
|
1400
|
+
.join('; ')}`,
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const snapshot = await callbacks.getSnapshot(ctx.wsId, resolvedBoardId);
|
|
1405
|
+
if (!snapshot) {
|
|
1406
|
+
return { ok: false, reason: 'Mind board was not found.' };
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const referenceIssues = validateMindPatchReferences({
|
|
1410
|
+
patch: coercedPatch,
|
|
1411
|
+
snapshot,
|
|
1412
|
+
});
|
|
1413
|
+
if (referenceIssues.length > 0) {
|
|
1414
|
+
return {
|
|
1415
|
+
ok: false,
|
|
1416
|
+
reason: `Patch draft referenced graph items that are not available: ${referenceIssues
|
|
1417
|
+
.slice(0, 6)
|
|
1418
|
+
.join('; ')}`,
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
const graphHealthIssues = validateMindPatchGraphHealth({
|
|
1422
|
+
patch: coercedPatch,
|
|
1423
|
+
snapshot,
|
|
1424
|
+
});
|
|
1425
|
+
if (graphHealthIssues.length > 0) {
|
|
1426
|
+
return {
|
|
1427
|
+
ok: false,
|
|
1428
|
+
reason: `Patch draft needs graph-health fixes: ${graphHealthIssues
|
|
1429
|
+
.slice(0, 6)
|
|
1430
|
+
.join('; ')}`,
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const record = await callbacks
|
|
1435
|
+
.createPatch({
|
|
1436
|
+
boardId: resolvedBoardId,
|
|
1437
|
+
patch: normalizeGeneratedPatchIds(coercedPatch),
|
|
1438
|
+
summary: coercedPatch.summary,
|
|
1439
|
+
threadId: ctx.threadId,
|
|
1440
|
+
userId: ctx.userId,
|
|
1441
|
+
wsId: ctx.wsId,
|
|
1442
|
+
})
|
|
1443
|
+
.catch((error) => ({
|
|
1444
|
+
error:
|
|
1445
|
+
error instanceof Error && error.message
|
|
1446
|
+
? error.message
|
|
1447
|
+
: 'Patch creation failed.',
|
|
1448
|
+
}));
|
|
1449
|
+
|
|
1450
|
+
if (record && 'error' in record) {
|
|
1451
|
+
return { ok: false, reason: record.error };
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
return record
|
|
1455
|
+
? {
|
|
1456
|
+
directWriteAvailable: ctx.writeMode === 'direct',
|
|
1457
|
+
ok: true,
|
|
1458
|
+
patch: record,
|
|
1459
|
+
}
|
|
1460
|
+
: { ok: false, reason: 'Failed to create Mind patch.' };
|
|
1461
|
+
},
|
|
1462
|
+
}),
|
|
1463
|
+
render_mind_ui: tool({
|
|
1464
|
+
description:
|
|
1465
|
+
'Render compact native UI for Mind planning responses. Use this for visual draft summaries, patch previews, timelines, comparisons, metrics, and structured planning cards instead of pasting large JSON blocks.',
|
|
1466
|
+
inputSchema: looseRenderMindUiSchema,
|
|
1467
|
+
execute: async (input) => {
|
|
1468
|
+
const spec = toMindRenderSpec(input);
|
|
1469
|
+
if (isRenderableRenderUiSpec(spec)) {
|
|
1470
|
+
return { ok: true, spec };
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
return {
|
|
1474
|
+
ok: false,
|
|
1475
|
+
spec: buildRenderUiFailsafeSpec(
|
|
1476
|
+
isRecord(spec) ? spec : { root: 'mind_generated_ui' }
|
|
1477
|
+
),
|
|
1478
|
+
warning:
|
|
1479
|
+
'The generated UI spec needed recovery. Ensure root exists in elements and every element has type, props, and children.',
|
|
1480
|
+
};
|
|
1481
|
+
},
|
|
1482
|
+
}),
|
|
1483
|
+
search_mind_nodes: tool({
|
|
1484
|
+
description:
|
|
1485
|
+
'Search Mind nodes in the current workspace or selected board by title and body.',
|
|
1486
|
+
inputSchema: z.object({
|
|
1487
|
+
boardId: toolBoardIdSchema,
|
|
1488
|
+
q: z.string().trim().min(1).max(200),
|
|
1489
|
+
}),
|
|
1490
|
+
execute: async ({ boardId, q }) => ({
|
|
1491
|
+
nodes: await callbacks.searchNodes({
|
|
1492
|
+
boardId: resolveBoardId(ctx, boardId) ?? undefined,
|
|
1493
|
+
q,
|
|
1494
|
+
wsId: ctx.wsId,
|
|
1495
|
+
}),
|
|
1496
|
+
ok: true,
|
|
1497
|
+
}),
|
|
1498
|
+
}),
|
|
1499
|
+
};
|
|
1500
|
+
}
|