@tuturuuu/ai 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +76 -0
  2. package/package.json +106 -0
  3. package/src/api-key-hash.ts +28 -0
  4. package/src/calendar/events.ts +34 -0
  5. package/src/calendar/route.ts +114 -0
  6. package/src/chat/credit-source.ts +1 -0
  7. package/src/chat/google/chat-request-schema.ts +150 -0
  8. package/src/chat/google/default-system-instruction.ts +198 -0
  9. package/src/chat/google/message-file-processing.ts +212 -0
  10. package/src/chat/google/mira-step-preparation.ts +221 -0
  11. package/src/chat/google/new/route.ts +368 -0
  12. package/src/chat/google/route-auth.ts +81 -0
  13. package/src/chat/google/route-chat-resolution.ts +98 -0
  14. package/src/chat/google/route-credits.ts +61 -0
  15. package/src/chat/google/route-message-preparation.ts +331 -0
  16. package/src/chat/google/route-mira-runtime.ts +206 -0
  17. package/src/chat/google/route.ts +632 -0
  18. package/src/chat/google/stream-finish-persistence.ts +722 -0
  19. package/src/chat/google/summary/route.ts +153 -0
  20. package/src/chat/mira-render-ui-policy.ts +540 -0
  21. package/src/chat/mira-system-instruction.ts +484 -0
  22. package/src/chat-sdk/adapters.ts +389 -0
  23. package/src/chat-sdk/registry.ts +197 -0
  24. package/src/chat-sdk.ts +33 -0
  25. package/src/core.ts +3 -0
  26. package/src/credits/cap-output-tokens.ts +90 -0
  27. package/src/credits/check-credits.ts +232 -0
  28. package/src/credits/constants.ts +30 -0
  29. package/src/credits/index.ts +46 -0
  30. package/src/credits/model-mapping.ts +92 -0
  31. package/src/credits/reservations.ts +514 -0
  32. package/src/credits/resolve-plan-model.ts +219 -0
  33. package/src/credits/sync-gateway-models.ts +351 -0
  34. package/src/credits/types.ts +109 -0
  35. package/src/credits/use-ai-credits.ts +3 -0
  36. package/src/embeddings/metered.ts +283 -0
  37. package/src/executions/route.ts +137 -0
  38. package/src/generate/route.ts +411 -0
  39. package/src/hooks.ts +7 -0
  40. package/src/meetings/summary/route.ts +7 -0
  41. package/src/meetings/transcription/route.ts +134 -0
  42. package/src/memory/client.ts +158 -0
  43. package/src/memory/config.ts +38 -0
  44. package/src/memory/index.ts +32 -0
  45. package/src/memory/ingest.ts +51 -0
  46. package/src/memory/middleware.ts +35 -0
  47. package/src/memory/operations.ts +480 -0
  48. package/src/memory/scope.ts +102 -0
  49. package/src/memory/settings.ts +121 -0
  50. package/src/memory/types.ts +101 -0
  51. package/src/memory/workspace.ts +36 -0
  52. package/src/memory.ts +1 -0
  53. package/src/mind/patch.ts +146 -0
  54. package/src/mind/route.ts +687 -0
  55. package/src/mind/tools.ts +1500 -0
  56. package/src/mind/types.ts +20 -0
  57. package/src/object/core.ts +3 -0
  58. package/src/object/flashcards/route.ts +140 -0
  59. package/src/object/quizzes/explanation/route.ts +145 -0
  60. package/src/object/quizzes/route.ts +142 -0
  61. package/src/object/types.ts +187 -0
  62. package/src/object/year-plan/route.ts +196 -0
  63. package/src/react.ts +1 -0
  64. package/src/scheduling/algorithm.ts +791 -0
  65. package/src/scheduling/default.ts +36 -0
  66. package/src/scheduling/duration-optimizer.ts +689 -0
  67. package/src/scheduling/index.ts +79 -0
  68. package/src/scheduling/priority-calculator.ts +187 -0
  69. package/src/scheduling/recurrence-calculator.ts +621 -0
  70. package/src/scheduling/templates.ts +892 -0
  71. package/src/scheduling/types.ts +136 -0
  72. package/src/scheduling/web-adapter.ts +308 -0
  73. package/src/scheduling.ts +6 -0
  74. package/src/supported-actions.ts +1 -0
  75. package/src/supported-providers.ts +6 -0
  76. package/src/tools/context-builder.ts +372 -0
  77. package/src/tools/core.ts +1 -0
  78. package/src/tools/definitions/calendar.ts +106 -0
  79. package/src/tools/definitions/finance.ts +197 -0
  80. package/src/tools/definitions/image.ts +74 -0
  81. package/src/tools/definitions/memory.ts +83 -0
  82. package/src/tools/definitions/meta.ts +154 -0
  83. package/src/tools/definitions/render-ui.ts +81 -0
  84. package/src/tools/definitions/tasks.ts +343 -0
  85. package/src/tools/definitions/time-tracking.ts +381 -0
  86. package/src/tools/definitions/workspace-context.ts +45 -0
  87. package/src/tools/definitions/workspace-user-chat.ts +111 -0
  88. package/src/tools/executors/calendar.ts +371 -0
  89. package/src/tools/executors/chat.ts +15 -0
  90. package/src/tools/executors/finance.ts +638 -0
  91. package/src/tools/executors/helpers/encryption.ts +107 -0
  92. package/src/tools/executors/image.ts +247 -0
  93. package/src/tools/executors/markitdown.ts +684 -0
  94. package/src/tools/executors/memory.ts +277 -0
  95. package/src/tools/executors/parallel-checks.ts +176 -0
  96. package/src/tools/executors/qr.ts +170 -0
  97. package/src/tools/executors/scope-helpers.ts +192 -0
  98. package/src/tools/executors/search.ts +149 -0
  99. package/src/tools/executors/settings.ts +40 -0
  100. package/src/tools/executors/tasks.ts +1087 -0
  101. package/src/tools/executors/theme.ts +23 -0
  102. package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
  103. package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
  104. package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
  105. package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
  106. package/src/tools/executors/timer/timer-helpers.ts +372 -0
  107. package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
  108. package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
  109. package/src/tools/executors/timer/timer-mutations.ts +19 -0
  110. package/src/tools/executors/timer/timer-queries.ts +18 -0
  111. package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
  112. package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
  113. package/src/tools/executors/timer/timer-session-queries.ts +153 -0
  114. package/src/tools/executors/timer/timer-session-updates.ts +200 -0
  115. package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
  116. package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
  117. package/src/tools/executors/timer.ts +22 -0
  118. package/src/tools/executors/user.ts +60 -0
  119. package/src/tools/executors/workspace.ts +135 -0
  120. package/src/tools/json-render-catalog.ts +875 -0
  121. package/src/tools/mira-tool-definitions.ts +55 -0
  122. package/src/tools/mira-tool-dispatcher.ts +265 -0
  123. package/src/tools/mira-tool-metadata.ts +164 -0
  124. package/src/tools/mira-tool-names.ts +95 -0
  125. package/src/tools/mira-tool-render-ui.ts +54 -0
  126. package/src/tools/mira-tool-types.ts +17 -0
  127. package/src/tools/mira-tools.ts +167 -0
  128. package/src/tools/normalize-render-ui-input.ts +321 -0
  129. package/src/tools/workspace-context.ts +233 -0
  130. package/src/types.ts +38 -0
@@ -0,0 +1,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
+ }