@henrikogard/auroradocs-mcp 0.1.1

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/dist/tools.js ADDED
@@ -0,0 +1,640 @@
1
+ /**
2
+ * tools.ts — Tool execution logic for the MCP server.
3
+ *
4
+ * SECURITY: Every tool call receives the workspaceId and passes it through
5
+ * to AuroraCloud client helpers which verify ownership before any read or write.
6
+ * No cross-workspace or cross-user data leakage is possible.
7
+ */
8
+ import { listObjects, getObject, getContent, setContent, appendContentText, listProperties, listMembers, listTaskLists, listTaskStatuses, createObject, updateObjectTitle, deleteObject, upsertProperty, getTaskProps, updateTaskProps, getWorkspaceKnowledgeObjectServer, listWorkspaceRecentKnowledgeServer, listWorkspaceRelatedKnowledgeServer, listPlanningTasks, createPlanningTimeBlock, readCanvasContent, searchWorkspaceKnowledgeServer, } from './auroraClient.js';
9
+ import { buildWeekPlan, normalizeCanvasContent } from './planningTools.js';
10
+ import { getMcpToolCoverageAudit, getMcpWorkflowRecipes } from './toolCatalog.js';
11
+ // ── Helpers ──────────────────────────────────────────────────────────────────
12
+ function readString(value) {
13
+ if (typeof value !== 'string')
14
+ return null;
15
+ const trimmed = value.trim();
16
+ return trimmed.length ? trimmed : null;
17
+ }
18
+ function readStringArray(value) {
19
+ if (Array.isArray(value))
20
+ return value.map((v) => String(v).trim()).filter(Boolean);
21
+ if (typeof value === 'string')
22
+ return value.split(',').map((v) => v.trim()).filter(Boolean);
23
+ return [];
24
+ }
25
+ function todayDateKey() {
26
+ const now = new Date();
27
+ const year = now.getFullYear();
28
+ const month = String(now.getMonth() + 1).padStart(2, '0');
29
+ const day = String(now.getDate()).padStart(2, '0');
30
+ return `${year}-${month}-${day}`;
31
+ }
32
+ function isDateKey(value) {
33
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
34
+ if (!match)
35
+ return false;
36
+ const [, yearRaw, monthRaw, dayRaw] = match;
37
+ const year = Number(yearRaw);
38
+ const month = Number(monthRaw);
39
+ const day = Number(dayRaw);
40
+ const date = new Date(year, month - 1, day);
41
+ return date.getFullYear() === year
42
+ && date.getMonth() === month - 1
43
+ && date.getDate() === day;
44
+ }
45
+ function isTimeKey(value) {
46
+ return /^([01]\d|2[0-3]):[0-5]\d$/.test(value);
47
+ }
48
+ function weekPlanningDueDateForDay(dateKey, existingDueDate) {
49
+ const time = existingDueDate?.match(/^\d{4}-\d{2}-\d{2}T(\d{2}:\d{2})/)?.[1];
50
+ return time ? `${dateKey}T${time}` : dateKey;
51
+ }
52
+ function buildWeekPlanningTimeBlockTaskInput(input) {
53
+ const title = input.title.trim() || 'Time block';
54
+ const durationMinutes = Number.isFinite(input.durationMinutes) && input.durationMinutes > 0
55
+ ? Math.round(input.durationMinutes)
56
+ : 30;
57
+ return {
58
+ title,
59
+ due_date: `${input.dateKey}T${input.startTime}`,
60
+ labels: ['time-block', `duration:${durationMinutes}m`],
61
+ description: `Duration: ${durationMinutes} minutes`,
62
+ };
63
+ }
64
+ function matchTaskList(query, lists) {
65
+ const q = query.trim().toLowerCase();
66
+ if (!q)
67
+ return null;
68
+ const exact = lists.find((l) => l.id.toLowerCase() === q || l.name.trim().toLowerCase() === q);
69
+ if (exact)
70
+ return exact;
71
+ const partial = lists.filter((l) => l.name.toLowerCase().includes(q));
72
+ return partial.length === 1 ? partial[0] : null;
73
+ }
74
+ function matchMember(query, members) {
75
+ const q = query.trim().toLowerCase();
76
+ if (!q)
77
+ return null;
78
+ const exact = members.find((m) => {
79
+ const name = m.name?.trim().toLowerCase() ?? '';
80
+ const email = m.email.trim().toLowerCase();
81
+ const local = email.split('@')[0] ?? '';
82
+ return m.id.toLowerCase() === q || email === q || local === q || name === q;
83
+ });
84
+ if (exact)
85
+ return exact;
86
+ const partial = members.filter((m) => {
87
+ const name = m.name?.trim().toLowerCase() ?? '';
88
+ const email = m.email.trim().toLowerCase();
89
+ return name.includes(q) || email.includes(q);
90
+ });
91
+ return partial.length === 1 ? partial[0] : null;
92
+ }
93
+ function normalizePriority(value) {
94
+ const v = value.toLowerCase();
95
+ if (v === 'low')
96
+ return 'Low';
97
+ if (v === 'medium')
98
+ return 'Medium';
99
+ if (v === 'high')
100
+ return 'High';
101
+ if (v === 'urgent')
102
+ return 'Urgent';
103
+ return null;
104
+ }
105
+ // ── Tool execution ───────────────────────────────────────────────────────────
106
+ export async function executeToolCall(name, input, workspaceId) {
107
+ try {
108
+ switch (name) {
109
+ // ── Read tools ─────────────────────────────────────────────────────
110
+ case 'search':
111
+ case 'search_objects': {
112
+ const query = String(input['query'] ?? '').toLowerCase();
113
+ const typeFilter = input['type'] ? String(input['type']) : undefined;
114
+ const all = await listObjects(workspaceId, typeFilter);
115
+ const matches = all
116
+ .filter((o) => (o.title ?? '').toLowerCase().includes(query))
117
+ .slice(0, 10)
118
+ .map((o) => ({ id: o.id, title: o.title, type: o.type, icon: o.icon }));
119
+ return { type: 'objects', objects: matches };
120
+ }
121
+ case 'list_objects': {
122
+ const typeFilter = input['type'] ? String(input['type']) : undefined;
123
+ const limit = typeof input['limit'] === 'number' ? input['limit'] : 20;
124
+ const all = await listObjects(workspaceId, typeFilter);
125
+ const results = all
126
+ .filter((o) => !o.is_template)
127
+ .slice(0, limit)
128
+ .map((o) => ({ id: o.id, title: o.title, type: o.type, icon: o.icon }));
129
+ return { type: 'objects', objects: results };
130
+ }
131
+ case 'list_recent': {
132
+ const typeFilter = input['type'] ? String(input['type']) : undefined;
133
+ const limit = typeof input['limit'] === 'number' ? input['limit'] : 20;
134
+ const all = await listObjects(workspaceId, typeFilter);
135
+ const results = all
136
+ .filter((o) => !o.is_template)
137
+ .slice(0, limit)
138
+ .map((o) => ({ id: o.id, title: o.title, type: o.type, icon: o.icon }));
139
+ return { type: 'objects', objects: results };
140
+ }
141
+ case 'wiki_search': {
142
+ const query = readString(input['query']);
143
+ if (!query)
144
+ return { type: 'error', message: 'Query is required' };
145
+ const limit = typeof input['limit'] === 'number' ? input['limit'] : 20;
146
+ const sources = await searchWorkspaceKnowledgeServer(workspaceId, query, limit);
147
+ return { type: 'knowledge_sources', sources };
148
+ }
149
+ case 'wiki_get_page': {
150
+ const id = readString(input['id']);
151
+ if (!id)
152
+ return { type: 'error', message: 'Object ID is required' };
153
+ const includeFullText = Boolean(input['includeFullText']);
154
+ const source = await getWorkspaceKnowledgeObjectServer(workspaceId, id, includeFullText);
155
+ if (!source)
156
+ return { type: 'error', message: `Object ${id} not found in this workspace` };
157
+ return { type: 'knowledge_sources', sources: [source] };
158
+ }
159
+ case 'wiki_related': {
160
+ const id = readString(input['id']);
161
+ if (!id)
162
+ return { type: 'error', message: 'Object ID is required' };
163
+ const limit = typeof input['limit'] === 'number' ? input['limit'] : 6;
164
+ const sources = await listWorkspaceRelatedKnowledgeServer(workspaceId, id, limit);
165
+ return { type: 'knowledge_sources', sources };
166
+ }
167
+ case 'wiki_recent': {
168
+ const limit = typeof input['limit'] === 'number' ? input['limit'] : 6;
169
+ const sources = await listWorkspaceRecentKnowledgeServer(workspaceId, limit);
170
+ return { type: 'knowledge_sources', sources };
171
+ }
172
+ case 'get_object': {
173
+ const id = String(input['id'] ?? '');
174
+ const obj = await getObject(id, workspaceId);
175
+ if (!obj)
176
+ return { type: 'error', message: `Object ${id} not found in this workspace` };
177
+ const content = await getContent(id, workspaceId);
178
+ // Detect E2EE content
179
+ const displayContent = content && (content.startsWith('v1:') || content.startsWith('v2:'))
180
+ ? '[Content is end-to-end encrypted and cannot be read by the MCP server]'
181
+ : content;
182
+ // Include properties
183
+ const rawProps = await listProperties([id], workspaceId);
184
+ const properties = {};
185
+ for (const p of rawProps) {
186
+ properties[p.key] =
187
+ p.value_text
188
+ ?? p.value_date
189
+ ?? (p.value_num != null ? String(p.value_num) : null)
190
+ ?? (p.value_bool != null ? String(p.value_bool) : null)
191
+ ?? p.value_ref
192
+ ?? '';
193
+ }
194
+ return {
195
+ type: 'object',
196
+ object: { id: obj.id, title: obj.title, type: obj.type, icon: obj.icon },
197
+ content: displayContent ?? null,
198
+ properties,
199
+ };
200
+ }
201
+ case 'list_workspace_members': {
202
+ const members = await listMembers(workspaceId);
203
+ return { type: 'members', members };
204
+ }
205
+ case 'list_task_lists': {
206
+ const lists = await listTaskLists(workspaceId);
207
+ return { type: 'task_lists', task_lists: lists.map((l) => ({ id: l.id, name: l.name })) };
208
+ }
209
+ case 'list_task_statuses': {
210
+ const statuses = await listTaskStatuses(workspaceId);
211
+ return { type: 'task_statuses', statuses };
212
+ }
213
+ case 'get_mcp_tool_coverage':
214
+ return { type: 'mcp_tool_coverage', audit: getMcpToolCoverageAudit() };
215
+ case 'get_mcp_workflow_recipes':
216
+ return { type: 'mcp_workflow_recipes', recipes: getMcpWorkflowRecipes() };
217
+ case 'list_week_plan': {
218
+ const anchorDate = readString(input['anchor_date']) ?? todayDateKey();
219
+ if (!isDateKey(anchorDate)) {
220
+ return { type: 'error', message: 'anchor_date must be a valid date formatted YYYY-MM-DD' };
221
+ }
222
+ const includeUnscheduled = input['include_unscheduled'] !== false;
223
+ const unscheduledLimit = typeof input['unscheduled_limit'] === 'number' ? input['unscheduled_limit'] : 12;
224
+ const tasks = await listPlanningTasks(workspaceId);
225
+ return {
226
+ type: 'week_plan',
227
+ plan: buildWeekPlan(tasks.map((task) => ({
228
+ id: task.id,
229
+ title: task.title,
230
+ status: task.status,
231
+ due_date: task.due_date,
232
+ updated_at: task.updated_at,
233
+ labels: task.labels,
234
+ })), {
235
+ anchorDate,
236
+ includeUnscheduled,
237
+ unscheduledLimit,
238
+ }),
239
+ };
240
+ }
241
+ case 'read_canvas': {
242
+ const id = readString(input['id']);
243
+ if (!id)
244
+ return { type: 'error', message: 'Canvas object ID is required' };
245
+ const result = await readCanvasContent(workspaceId, id);
246
+ if (!result)
247
+ return { type: 'error', message: `Canvas ${id} not found in this workspace` };
248
+ const includeText = input['include_text'] !== false;
249
+ return {
250
+ type: 'canvas',
251
+ canvas: normalizeCanvasContent({ id: result.object.id, title: result.object.title, type: result.object.type }, result.contentJson, { includeText }),
252
+ };
253
+ }
254
+ case 'schedule_task_block': {
255
+ const mode = readString(input['mode']);
256
+ const date = readString(input['date']);
257
+ if (mode !== 'schedule_existing_task' && mode !== 'create_time_block') {
258
+ return { type: 'error', message: 'mode must be schedule_existing_task or create_time_block' };
259
+ }
260
+ if (!date || !isDateKey(date)) {
261
+ return { type: 'error', message: 'date must be a valid date formatted YYYY-MM-DD' };
262
+ }
263
+ const startTime = readString(input['start_time']);
264
+ if (startTime && !isTimeKey(startTime)) {
265
+ return { type: 'error', message: 'start_time must be a valid time formatted HH:mm' };
266
+ }
267
+ if (mode === 'schedule_existing_task') {
268
+ const taskId = readString(input['task_id']);
269
+ if (!taskId)
270
+ return { type: 'error', message: 'task_id is required for schedule_existing_task' };
271
+ const object = await getObject(taskId, workspaceId);
272
+ if (!object)
273
+ return { type: 'error', message: `Object ${taskId} not found in this workspace` };
274
+ if (object.type !== 'task')
275
+ return { type: 'error', message: `${taskId} is not a task` };
276
+ const props = await getTaskProps(taskId, workspaceId);
277
+ if (props.status === 'Done')
278
+ return { type: 'error', message: 'Completed tasks cannot be scheduled' };
279
+ const dueDate = startTime ? `${date}T${startTime}` : weekPlanningDueDateForDay(date, props.due_date);
280
+ await updateTaskProps(taskId, workspaceId, { due_date: dueDate });
281
+ return { type: 'scheduled_task_block', id: taskId, title: object.title, due_date: dueDate, mode };
282
+ }
283
+ const title = readString(input['title']);
284
+ if (!title)
285
+ return { type: 'error', message: 'title is required for create_time_block' };
286
+ const durationRaw = input['duration_minutes'];
287
+ if (durationRaw !== undefined
288
+ && (typeof durationRaw !== 'number' || !Number.isFinite(durationRaw) || durationRaw <= 0)) {
289
+ return { type: 'error', message: 'duration_minutes must be a positive number' };
290
+ }
291
+ const blockInput = buildWeekPlanningTimeBlockTaskInput({
292
+ title,
293
+ dateKey: date,
294
+ startTime: startTime ?? '09:00',
295
+ durationMinutes: typeof durationRaw === 'number' ? durationRaw : 30,
296
+ });
297
+ const object = await createPlanningTimeBlock(workspaceId, {
298
+ title: blockInput.title,
299
+ dueDate: blockInput.due_date,
300
+ labels: blockInput.labels,
301
+ description: blockInput.description,
302
+ });
303
+ return { type: 'scheduled_task_block', id: object.id, title: object.title, due_date: blockInput.due_date, mode };
304
+ }
305
+ // ── Write tools ────────────────────────────────────────────────────
306
+ case 'create_object': {
307
+ const type = String(input['type'] ?? 'page');
308
+ if (type === 'task')
309
+ return { type: 'error', message: 'Use create_task for tasks' };
310
+ const title = String(input['title'] ?? 'Untitled');
311
+ const obj = await createObject(workspaceId, type, title);
312
+ return { type: 'created', id: obj.id, title: obj.title ?? title };
313
+ }
314
+ case 'create_task': {
315
+ const title = readString(input['title']);
316
+ if (!title)
317
+ return { type: 'error', message: 'Task title is required' };
318
+ const obj = await createObject(workspaceId, 'task', title);
319
+ // Resolve task fields
320
+ const patch = await resolveTaskInput(input, workspaceId);
321
+ if (Object.keys(patch).length > 0) {
322
+ await updateTaskProps(obj.id, workspaceId, patch);
323
+ }
324
+ const taskListName = patch.task_list_id
325
+ ? (await listTaskLists(workspaceId)).find((l) => l.id === patch.task_list_id)?.name ?? null
326
+ : null;
327
+ return { type: 'task_created', id: obj.id, title, status: patch.status ?? null, task_list_name: taskListName };
328
+ }
329
+ case 'update_task': {
330
+ const id = readString(input['id']);
331
+ if (!id)
332
+ return { type: 'error', message: 'Task ID is required' };
333
+ const obj = await getObject(id, workspaceId);
334
+ if (!obj)
335
+ return { type: 'error', message: `Object ${id} not found in this workspace` };
336
+ if (obj.type !== 'task')
337
+ return { type: 'error', message: `${id} is not a task` };
338
+ const changedFields = [];
339
+ if ('title' in input) {
340
+ const newTitle = readString(input['title']);
341
+ if (!newTitle)
342
+ return { type: 'error', message: 'Task title cannot be empty' };
343
+ await updateObjectTitle(id, newTitle, workspaceId);
344
+ changedFields.push('title');
345
+ }
346
+ const patch = await resolveTaskInput(input, workspaceId);
347
+ if (Object.keys(patch).length > 0) {
348
+ await updateTaskProps(id, workspaceId, patch);
349
+ if (patch.status !== undefined)
350
+ changedFields.push('status');
351
+ if (patch.priority !== undefined)
352
+ changedFields.push('priority');
353
+ if (patch.due_date !== undefined)
354
+ changedFields.push('due_date');
355
+ if (patch.assignees !== undefined)
356
+ changedFields.push('assignees');
357
+ if (patch.labels !== undefined)
358
+ changedFields.push('labels');
359
+ if (patch.description !== undefined)
360
+ changedFields.push('description');
361
+ if (patch.task_list_id !== undefined)
362
+ changedFields.push('task_list');
363
+ }
364
+ return { type: 'task_updated', id, title: obj.title ?? id, changed_fields: changedFields };
365
+ }
366
+ case 'update_object_title': {
367
+ const id = String(input['id'] ?? '');
368
+ const title = String(input['title'] ?? '');
369
+ await updateObjectTitle(id, title, workspaceId);
370
+ return { type: 'updated', id, title };
371
+ }
372
+ case 'update_object': {
373
+ const id = readString(input['id']);
374
+ if (!id)
375
+ return { type: 'error', message: 'Object ID is required' };
376
+ const obj = await getObject(id, workspaceId);
377
+ if (!obj)
378
+ return { type: 'error', message: `Object ${id} not found in this workspace` };
379
+ const changedFields = [];
380
+ if ('title' in input) {
381
+ const title = readString(input['title']);
382
+ if (!title)
383
+ return { type: 'error', message: 'Object title cannot be empty' };
384
+ await updateObjectTitle(id, title, workspaceId);
385
+ changedFields.push('title');
386
+ }
387
+ if ('text' in input || 'content' in input) {
388
+ const text = readString(input['text'] ?? input['content']);
389
+ if (!text)
390
+ return { type: 'error', message: 'Object content cannot be empty' };
391
+ await setContent(id, workspaceId, textToTipTapDoc(text));
392
+ changedFields.push('content');
393
+ }
394
+ return changedFields.length
395
+ ? { type: 'object_updated', id, changed_fields: changedFields }
396
+ : { type: 'no_op', message: `No updates supplied for ${id}` };
397
+ }
398
+ case 'set_content': {
399
+ const id = String(input['id'] ?? '');
400
+ const text = String(input['text'] ?? '');
401
+ // Build a minimal TipTap document from plain text
402
+ const doc = textToTipTapDoc(text);
403
+ await setContent(id, workspaceId, doc);
404
+ return { type: 'content_set', id };
405
+ }
406
+ case 'append_block': {
407
+ const id = readString(input['id']);
408
+ const text = readString(input['text'] ?? input['content']);
409
+ if (!id)
410
+ return { type: 'error', message: 'Object ID is required' };
411
+ if (!text)
412
+ return { type: 'error', message: 'Text is required' };
413
+ await appendContentText(id, workspaceId, text);
414
+ return { type: 'content_appended', id };
415
+ }
416
+ case 'delete_object': {
417
+ const id = String(input['id'] ?? '');
418
+ await deleteObject(id, workspaceId);
419
+ return { type: 'deleted', id };
420
+ }
421
+ case 'set_property': {
422
+ const objectId = String(input['object_id'] ?? '');
423
+ const key = String(input['key'] ?? '');
424
+ const valueType = String(input['value_type'] ?? 'text');
425
+ const value = String(input['value'] ?? '');
426
+ await upsertProperty(objectId, workspaceId, key, valueType, value);
427
+ return { type: 'property_set', objectId, key, value };
428
+ }
429
+ case 'navigate_to':
430
+ return { type: 'no_op', message: 'Navigation is not supported in the MCP server context.' };
431
+ default:
432
+ return { type: 'error', message: `Unknown tool: ${name}` };
433
+ }
434
+ }
435
+ catch (err) {
436
+ return { type: 'error', message: err instanceof Error ? err.message : String(err) };
437
+ }
438
+ }
439
+ // ── Task input resolution ────────────────────────────────────────────────────
440
+ async function resolveTaskInput(input, workspaceId) {
441
+ const patch = {};
442
+ if ('status' in input) {
443
+ const raw = readString(input['status']);
444
+ if (raw) {
445
+ const statuses = await listTaskStatuses(workspaceId);
446
+ const match = statuses.find((s) => s.toLowerCase() === raw.toLowerCase());
447
+ patch.status = match ?? raw;
448
+ }
449
+ }
450
+ if ('priority' in input) {
451
+ const raw = readString(input['priority']);
452
+ if (raw === null)
453
+ patch.priority = null;
454
+ else {
455
+ const normalized = normalizePriority(raw);
456
+ if (!normalized)
457
+ throw new Error(`Unknown priority: ${raw}`);
458
+ patch.priority = normalized;
459
+ }
460
+ }
461
+ if ('due_date' in input) {
462
+ const raw = readString(input['due_date']);
463
+ // Normalize space-separated datetime to ISO
464
+ patch.due_date = raw && /^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}$/.test(raw) ? raw.replace(/\s+/, 'T') : raw;
465
+ }
466
+ if ('description' in input) {
467
+ patch.description = readString(input['description']);
468
+ }
469
+ if ('labels' in input || 'tags' in input) {
470
+ patch.labels = readStringArray(input['labels'] ?? input['tags']);
471
+ }
472
+ if ('task_list' in input) {
473
+ const query = readString(input['task_list']);
474
+ if (query === null) {
475
+ patch.task_list_id = null;
476
+ }
477
+ else {
478
+ const lists = await listTaskLists(workspaceId);
479
+ const match = matchTaskList(query, lists);
480
+ if (!match)
481
+ throw new Error(`Could not resolve task list: ${query}`);
482
+ patch.task_list_id = match.id;
483
+ // Apply default status if not explicitly set
484
+ if (patch.status === undefined && match.default_status) {
485
+ patch.status = match.default_status;
486
+ }
487
+ }
488
+ }
489
+ if ('assignees' in input || 'assignee' in input) {
490
+ const queries = [...readStringArray(input['assignees']), ...readStringArray(input['assignee'])];
491
+ if (!queries.length) {
492
+ patch.assignees = [];
493
+ }
494
+ else {
495
+ const members = await listMembers(workspaceId);
496
+ const resolved = queries.map((q) => {
497
+ const m = matchMember(q, members);
498
+ if (!m)
499
+ throw new Error(`Could not resolve assignee: ${q}`);
500
+ return m.id;
501
+ });
502
+ patch.assignees = [...new Set(resolved)];
503
+ }
504
+ }
505
+ return patch;
506
+ }
507
+ // ── Content helpers ──────────────────────────────────────────────────────────
508
+ function textToTipTapDoc(text) {
509
+ const paragraphs = text.split('\n').map((line) => ({
510
+ type: 'paragraph',
511
+ content: line ? [{ type: 'text', text: line }] : [],
512
+ }));
513
+ return { type: 'doc', content: paragraphs };
514
+ }
515
+ // ── Format result for MCP text response ──────────────────────────────────────
516
+ export function formatToolResult(result) {
517
+ switch (result.type) {
518
+ case 'objects':
519
+ if (!result.objects.length)
520
+ return 'No objects found.';
521
+ return result.objects.map((o) => `- [${o.type}] ${o.title ?? '(Untitled)'} (${o.id})`).join('\n');
522
+ case 'object': {
523
+ const propLines = Object.entries(result.properties)
524
+ .map(([k, v]) => ` ${k}: ${v}`)
525
+ .join('\n');
526
+ return [
527
+ `**${result.object.title ?? 'Untitled'}** (${result.object.type}, ${result.object.id})`,
528
+ propLines ? `\nProperties:\n${propLines}` : '',
529
+ result.content ? `\n\n${result.content}` : '\n*(no content)*',
530
+ ].join('');
531
+ }
532
+ case 'created':
533
+ return `Created: "${result.title}" (${result.id})`;
534
+ case 'task_created':
535
+ return `Task created: "${result.title}" (${result.id})${result.status ? ` [${result.status}]` : ''}${result.task_list_name ? ` in ${result.task_list_name}` : ''}`;
536
+ case 'task_updated':
537
+ return `Task updated: "${result.title}" (${result.id}) — changed: ${result.changed_fields.join(', ') || 'nothing'}`;
538
+ case 'object_updated':
539
+ return `Object updated: ${result.id} — changed: ${result.changed_fields.join(', ') || 'nothing'}`;
540
+ case 'updated':
541
+ return `Updated: "${result.title}" (${result.id})`;
542
+ case 'deleted':
543
+ return `Deleted: ${result.id} (soft-deleted)`;
544
+ case 'content_set':
545
+ return `Content set on ${result.id}`;
546
+ case 'content_appended':
547
+ return `Content appended to ${result.id}`;
548
+ case 'property_set':
549
+ return `Set property "${result.key}" = "${result.value}" on ${result.objectId}`;
550
+ case 'knowledge_sources':
551
+ if (!result.sources.length)
552
+ return 'No matching workspace sources.';
553
+ return result.sources.map((source, index) => {
554
+ const header = `[${index + 1}] ${source.title ?? '(Untitled)'} (${source.objectType}, ${source.objectId})`;
555
+ const lines = [
556
+ header,
557
+ ` sourceId: ${source.sourceId}`,
558
+ ` deepLink: ${source.deepLink}`,
559
+ ` updatedAt: ${source.updatedAt}`,
560
+ ` availability: ${source.availability}`,
561
+ ];
562
+ if (source.snippet)
563
+ lines.push(` snippet: ${source.snippet}`);
564
+ if (source.matchedFields.length)
565
+ lines.push(` matchedFields: ${source.matchedFields.join(', ')}`);
566
+ if (source.plainText) {
567
+ lines.push(' content:');
568
+ lines.push(...source.plainText.split('\n').map((line) => ` ${line}`));
569
+ }
570
+ return lines.join('\n');
571
+ }).join('\n\n');
572
+ case 'mcp_tool_coverage':
573
+ return JSON.stringify(result.audit, null, 2);
574
+ case 'mcp_workflow_recipes':
575
+ return result.recipes.map((recipe) => [
576
+ `## ${recipe.id}: ${recipe.title}`,
577
+ recipe.goal,
578
+ `Scopes: ${recipe.requiredScopes.join(', ')}`,
579
+ `Tools: ${recipe.toolSteps.join(' -> ')}`,
580
+ `Prompt: ${recipe.prompt}`,
581
+ ].join('\n')).join('\n\n');
582
+ case 'week_plan':
583
+ return [
584
+ `Week ${result.plan.range.start} to ${result.plan.range.end}`,
585
+ ...result.plan.days.map((day) => {
586
+ const tasks = day.tasks.length
587
+ ? day.tasks.map((task) => {
588
+ const block = task.timeBlock.isTimeBlock
589
+ ? ` [time block${task.timeBlock.durationMinutes ? `, ${task.timeBlock.durationMinutes}m` : ''}]`
590
+ : '';
591
+ return ` - ${task.title ?? '(Untitled)'} (${task.id})${task.due_date ? ` due ${task.due_date}` : ''}${block}`;
592
+ }).join('\n')
593
+ : ' - No scheduled tasks';
594
+ return `${day.date}\n${tasks}`;
595
+ }),
596
+ result.plan.unscheduled.length
597
+ ? `Unscheduled\n${result.plan.unscheduled.map((task) => ` - ${task.title ?? '(Untitled)'} (${task.id})`).join('\n')}`
598
+ : 'Unscheduled\n - None',
599
+ ].join('\n\n');
600
+ case 'canvas': {
601
+ const cardLines = result.canvas.cards.length
602
+ ? result.canvas.cards.map((card) => {
603
+ const ref = card.objectId ? ` object=${card.objectId}${card.objectTitle ? ` (${card.objectTitle})` : ''}` : '';
604
+ const text = card.text ? ` text="${card.text}"` : '';
605
+ return `- ${card.id} [${card.type}] x=${card.x ?? '?'} y=${card.y ?? '?'} w=${card.width ?? '?'} h=${card.height ?? '?'}${ref}${text}`;
606
+ })
607
+ : ['- No cards'];
608
+ const edgeLines = result.canvas.edges.length
609
+ ? result.canvas.edges.map((edge) => `- ${edge.id}: ${edge.fromCard ?? '?'} -> ${edge.toCard ?? '?'}${edge.label ? ` (${edge.label})` : ''}`)
610
+ : ['- No edges'];
611
+ const warningLines = result.canvas.warnings.length
612
+ ? ['Warnings:', ...result.canvas.warnings.map((warning) => `- ${warning}`)]
613
+ : [];
614
+ return [
615
+ `Canvas: ${result.canvas.canvas.title ?? '(Untitled)'} (${result.canvas.canvas.id})`,
616
+ 'Cards:',
617
+ ...cardLines,
618
+ 'Edges:',
619
+ ...edgeLines,
620
+ ...warningLines,
621
+ ].join('\n');
622
+ }
623
+ case 'scheduled_task_block':
624
+ return `Scheduled: ${result.title ?? '(Untitled)'} (${result.id}) due ${result.due_date} via ${result.mode}`;
625
+ case 'members':
626
+ if (!result.members.length)
627
+ return 'No members found.';
628
+ return result.members.map((m) => `- ${m.name ?? m.email} (${m.role}, ${m.id})`).join('\n');
629
+ case 'task_lists':
630
+ if (!result.task_lists.length)
631
+ return 'No task lists found.';
632
+ return result.task_lists.map((l) => `- ${l.name} (${l.id})`).join('\n');
633
+ case 'task_statuses':
634
+ return result.statuses.join(', ');
635
+ case 'no_op':
636
+ return result.message;
637
+ case 'error':
638
+ return `Error: ${result.message}`;
639
+ }
640
+ }
@@ -0,0 +1 @@
1
+ export declare const SERVER_VERSION: string;
@@ -0,0 +1,3 @@
1
+ import { readFileSync } from 'node:fs';
2
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
3
+ export const SERVER_VERSION = pkg.version;