triliumnext-mcp 0.3.11 → 0.3.12

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.
@@ -221,6 +221,38 @@ function format_attribute_response(result, noteId, operation) {
221
221
  text: `šŸ“‹ Error details:\n${result.errors.map((err, i) => `${i + 1}. ${err}`).join('\n')}`
222
222
  });
223
223
  }
224
+ // Add specific guidance for common errors
225
+ if (result.errors && result.errors.some(err => err.includes("already exists"))) {
226
+ content.push({
227
+ type: "text",
228
+ text: `šŸ’” **Attribute Already Exists**
229
+
230
+ The attribute you're trying to create already exists on this note. Here are your options:
231
+
232
+ 1. **Update the existing attribute** (recommended):
233
+ - Use operation: "update" instead of "create"
234
+ - Only the value and position can be updated for labels
235
+ - Only the position can be updated for relations
236
+
237
+ 2. **Delete and recreate** (for inheritable changes):
238
+ - First delete with operation: "delete"
239
+ - Then recreate with operation: "create"
240
+ - Use this when you need to change the 'isInheritable' property
241
+
242
+ 3. **View current attributes**:
243
+ - Use read_attributes to see all existing attributes
244
+ - Check current values, positions, and inheritable settings
245
+
246
+ šŸ“‹ **Example update operation**:
247
+ \`\`\`
248
+ {
249
+ "noteId": "${noteId}",
250
+ "operation": "update",
251
+ "attributes": [{"type": "label", "name": "your_attribute", "value": "new_value"}]
252
+ }
253
+ \`\`\``
254
+ });
255
+ }
224
256
  }
225
257
  // Add attribute data for successful operations
226
258
  if (result.success && result.attributes && result.attributes.length > 0) {
@@ -246,6 +278,28 @@ function format_attribute_response(result, noteId, operation) {
246
278
  });
247
279
  }
248
280
  }
281
+ // Add guidance for batch operations with conflicts
282
+ if (result.success && operation === "batch_create" && result.errors && result.errors.length > 0) {
283
+ const hasConflicts = result.errors.some(err => err.includes("Skipping duplicate") || err.includes("already exist"));
284
+ if (hasConflicts) {
285
+ content.push({
286
+ type: "text",
287
+ text: `āš ļø **Batch Operation Summary**
288
+
289
+ Some attributes were skipped due to conflicts (already existing). This is normal behavior for batch operations:
290
+
291
+ āœ… **Successfully created**: ${result.attributes?.length || 0} attributes
292
+ āŒ **Skipped duplicates**: ${result.errors?.filter(err => err.includes("Skipping duplicate") || err.includes("already exist")).length || 0} attributes
293
+
294
+ šŸ’” **To manage skipped attributes**:
295
+ - Use \`read_attributes\` to view current attributes
296
+ - Use \`update\` operation to modify existing attributes
297
+ - Use \`delete\` operation to remove unwanted attributes first
298
+
299
+ This approach prevents accidental overwrites while allowing partial success for batch operations.`
300
+ });
301
+ }
302
+ }
249
303
  return { content };
250
304
  }
251
305
  /**
@@ -316,5 +370,11 @@ export function get_attributes_help() {
316
370
  - Template relations require the target note to exist in your Trilium instance
317
371
  - Position values control display order (lower numbers appear first)
318
372
  - Use read_attributes to view existing attributes before making changes
373
+
374
+ šŸ›”ļø **Validation & Conflict Handling**:
375
+ - Create operations now validate against existing attributes to prevent duplicates
376
+ - If an attribute already exists, you'll get detailed error messages with guidance
377
+ - Batch operations skip duplicates and continue with valid attributes
378
+ - Use "update" to modify existing attributes, "create" for new ones only
319
379
  `;
320
380
  }
@@ -5,6 +5,7 @@
5
5
  import axios from 'axios';
6
6
  import { logVerbose, logVerboseApi, logVerboseAxiosError } from "../utils/verboseUtils.js";
7
7
  import { validateAndTranslateTemplate, createTemplateRelationError } from "../utils/templateMapper.js";
8
+ import { cleanAttributeName, generateCleaningMessage } from "../utils/attributeNameCleaner.js";
8
9
  /**
9
10
  * Manage note attributes with write operations (create, update, delete)
10
11
  * This function provides write-only access to note attributes
@@ -36,12 +37,38 @@ export async function manage_attributes(params, axiosInstance) {
36
37
  };
37
38
  }
38
39
  }
40
+ /**
41
+ * Check if an attribute already exists on a note
42
+ */
43
+ async function check_attribute_exists(noteId, attribute, axiosInstance) {
44
+ try {
45
+ const response = await axiosInstance.get(`/notes/${noteId}`);
46
+ const existingAttributes = response.data.attributes.map((attr) => ({
47
+ type: attr.type,
48
+ name: attr.name,
49
+ value: attr.value,
50
+ position: attr.position,
51
+ isInheritable: attr.isInheritable
52
+ }));
53
+ // Find matching attribute by name and type
54
+ const matchingAttribute = existingAttributes.find(attr => attr.name === attribute.name && attr.type === attribute.type);
55
+ return {
56
+ exists: !!matchingAttribute,
57
+ existingAttribute: matchingAttribute,
58
+ allAttributes: existingAttributes
59
+ };
60
+ }
61
+ catch (error) {
62
+ // If we can't read attributes, assume it doesn't exist and let the create operation handle the error
63
+ return { exists: false };
64
+ }
65
+ }
39
66
  /**
40
67
  * Create a single attribute on a note
41
68
  */
42
69
  async function create_single_attribute(noteId, attribute, axiosInstance) {
43
70
  try {
44
- // Validate attribute
71
+ // Validate attribute with auto-correction
45
72
  const validation = validate_attribute(attribute);
46
73
  if (!validation.valid) {
47
74
  return {
@@ -50,20 +77,42 @@ async function create_single_attribute(noteId, attribute, axiosInstance) {
50
77
  errors: validation.errors
51
78
  };
52
79
  }
80
+ // Use cleaned attribute if correction was made
81
+ const attributeToUse = validation.cleanedAttribute || attribute;
82
+ // Check if attribute already exists (use cleaned name)
83
+ const existenceCheck = await check_attribute_exists(noteId, attributeToUse, axiosInstance);
84
+ if (existenceCheck.exists && existenceCheck.existingAttribute) {
85
+ const existing = existenceCheck.existingAttribute;
86
+ const availableAttrs = existenceCheck.allAttributes?.map((attr) => `${attr.type}:${attr.name}`).join(', ') || 'none';
87
+ let message = `Attribute '${attributeToUse.name}' of type '${attributeToUse.type}' already exists on note ${noteId}. Available attributes: ${availableAttrs}`;
88
+ // Add cleaning information if name was corrected
89
+ if (validation.cleaningResult && validation.cleaningResult.wasCleaned) {
90
+ message += `\n\nšŸ”§ **Note**: Attribute name was auto-corrected from "${validation.cleaningResult.originalName}" to "${attributeToUse.name}"`;
91
+ }
92
+ return {
93
+ success: false,
94
+ message,
95
+ errors: [
96
+ "Attribute already exists",
97
+ `Existing ${attributeToUse.type} '${attributeToUse.name}' has value: ${existing.value || 'none'}, position: ${existing.position || 'default'}, inheritable: ${existing.isInheritable || false}`,
98
+ "To modify the existing attribute, use operation: 'update' instead of 'create'"
99
+ ]
100
+ };
101
+ }
53
102
  // Translate template names to note IDs for template relations
54
- let processedValue = attribute.value || "";
55
- if (attribute.type === "relation" && attribute.name === "template" && attribute.value) {
103
+ let processedValue = attributeToUse.value || "";
104
+ if (attributeToUse.type === "relation" && attributeToUse.name === "template" && attributeToUse.value) {
56
105
  try {
57
- processedValue = validateAndTranslateTemplate(attribute.value);
106
+ processedValue = validateAndTranslateTemplate(attributeToUse.value);
58
107
  logVerbose("create_single_attribute", `Translated template relation`, {
59
- from: attribute.value,
108
+ from: attributeToUse.value,
60
109
  to: processedValue
61
110
  });
62
111
  }
63
112
  catch (error) {
64
113
  return {
65
114
  success: false,
66
- message: createTemplateRelationError(attribute.value),
115
+ message: createTemplateRelationError(attributeToUse.value),
67
116
  errors: [error instanceof Error ? error.message : 'Template validation failed']
68
117
  };
69
118
  }
@@ -71,17 +120,22 @@ async function create_single_attribute(noteId, attribute, axiosInstance) {
71
120
  // Prepare attribute data for ETAPI
72
121
  const attributeData = {
73
122
  noteId: noteId,
74
- type: attribute.type,
75
- name: attribute.name,
123
+ type: attributeToUse.type,
124
+ name: attributeToUse.name,
76
125
  value: processedValue,
77
- position: attribute.position || 10,
78
- isInheritable: attribute.isInheritable || false
126
+ position: attributeToUse.position || 10,
127
+ isInheritable: attributeToUse.isInheritable || false
79
128
  };
80
129
  // Make API call to create attribute
81
130
  const response = await axiosInstance.post(`/attributes`, attributeData);
131
+ // Build success message with cleaning information
132
+ let successMessage = `Successfully created ${attributeToUse.type} '${attributeToUse.name}' on note ${noteId}`;
133
+ if (validation.cleaningResult && validation.cleaningResult.wasCleaned) {
134
+ successMessage += `\n\n${generateCleaningMessage([validation.cleaningResult])}`;
135
+ }
82
136
  return {
83
137
  success: true,
84
- message: `Successfully created ${attribute.type} '${attribute.name}' on note ${noteId}`,
138
+ message: successMessage,
85
139
  attributes: [response.data]
86
140
  };
87
141
  }
@@ -93,6 +147,50 @@ async function create_single_attribute(noteId, attribute, axiosInstance) {
93
147
  };
94
148
  }
95
149
  }
150
+ /**
151
+ * Validate batch attributes for conflicts and duplicates
152
+ */
153
+ async function validate_batch_attributes(noteId, attributes, axiosInstance) {
154
+ try {
155
+ // Get all existing attributes once
156
+ const response = await axiosInstance.get(`/notes/${noteId}`);
157
+ const existingAttributes = response.data.attributes.map((attr) => ({
158
+ type: attr.type,
159
+ name: attr.name,
160
+ value: attr.value,
161
+ position: attr.position,
162
+ isInheritable: attr.isInheritable
163
+ }));
164
+ const conflicts = [];
165
+ const validAttributes = [];
166
+ // Check each new attribute against existing ones
167
+ for (const attribute of attributes) {
168
+ const existingMatch = existingAttributes.find(attr => attr.name === attribute.name && attr.type === attribute.type);
169
+ if (existingMatch) {
170
+ conflicts.push({
171
+ attribute,
172
+ existingAttribute: existingMatch
173
+ });
174
+ }
175
+ else {
176
+ validAttributes.push(attribute);
177
+ }
178
+ }
179
+ return {
180
+ conflicts,
181
+ validAttributes,
182
+ allExistingAttributes: existingAttributes
183
+ };
184
+ }
185
+ catch (error) {
186
+ // If we can't read attributes, assume no conflicts and proceed
187
+ return {
188
+ conflicts: [],
189
+ validAttributes: attributes,
190
+ allExistingAttributes: []
191
+ };
192
+ }
193
+ }
96
194
  /**
97
195
  * Create multiple attributes in batch
98
196
  */
@@ -104,38 +202,58 @@ async function create_batch_attributes(noteId, attributes, axiosInstance) {
104
202
  attributes: []
105
203
  };
106
204
  }
205
+ // Validate batch for conflicts first
206
+ const batchValidation = await validate_batch_attributes(noteId, attributes, axiosInstance);
107
207
  const results = [];
108
208
  const errors = [];
109
- // Create attributes in parallel for better performance
110
- const promises = attributes.map(async (attribute) => {
209
+ // Add conflict warnings if any
210
+ if (batchValidation.conflicts.length > 0) {
211
+ const conflictMessages = batchValidation.conflicts.map(({ attribute, existingAttribute }) => {
212
+ return `Skipping duplicate ${attribute.type} '${attribute.name}' (already exists with value: ${existingAttribute.value || 'none'})`;
213
+ });
214
+ errors.push(...conflictMessages);
215
+ }
216
+ // If all attributes are conflicts, return early
217
+ if (batchValidation.validAttributes.length === 0) {
218
+ return {
219
+ success: false,
220
+ message: `All ${attributes.length} attributes already exist on note ${noteId}`,
221
+ errors,
222
+ attributes: []
223
+ };
224
+ }
225
+ // Create only valid (non-conflicting) attributes in parallel
226
+ const promises = batchValidation.validAttributes.map(async (attribute) => {
111
227
  try {
112
228
  const validation = validate_attribute(attribute);
113
229
  if (!validation.valid) {
114
230
  errors.push(`Validation failed for ${attribute.type} '${attribute.name}': ${validation.errors.join(', ')}`);
115
231
  return null;
116
232
  }
233
+ // Use cleaned attribute if correction was made
234
+ const attributeToUse = validation.cleanedAttribute || attribute;
117
235
  // Translate template names to note IDs for template relations
118
- let processedValue = attribute.value || "";
119
- if (attribute.type === "relation" && attribute.name === "template" && attribute.value) {
236
+ let processedValue = attributeToUse.value || "";
237
+ if (attributeToUse.type === "relation" && attributeToUse.name === "template" && attributeToUse.value) {
120
238
  try {
121
- processedValue = validateAndTranslateTemplate(attribute.value);
239
+ processedValue = validateAndTranslateTemplate(attributeToUse.value);
122
240
  logVerbose("create_batch_attributes", `Translated template relation`, {
123
- from: attribute.value,
241
+ from: attributeToUse.value,
124
242
  to: processedValue
125
243
  });
126
244
  }
127
245
  catch (error) {
128
- errors.push(createTemplateRelationError(attribute.value));
246
+ errors.push(createTemplateRelationError(attributeToUse.value));
129
247
  return null;
130
248
  }
131
249
  }
132
250
  const attributeData = {
133
251
  noteId: noteId,
134
- type: attribute.type,
135
- name: attribute.name,
252
+ type: attributeToUse.type,
253
+ name: attributeToUse.name,
136
254
  value: processedValue,
137
- position: attribute.position || 10,
138
- isInheritable: attribute.isInheritable || false
255
+ position: attributeToUse.position || 10,
256
+ isInheritable: attributeToUse.isInheritable || false
139
257
  };
140
258
  const response = await axiosInstance.post(`/attributes`, attributeData);
141
259
  results.push(response.data);
@@ -148,7 +266,7 @@ async function create_batch_attributes(noteId, attributes, axiosInstance) {
148
266
  }
149
267
  });
150
268
  await Promise.all(promises);
151
- if (errors.length === attributes.length) {
269
+ if (errors.length === batchValidation.validAttributes.length) {
152
270
  return {
153
271
  success: false,
154
272
  message: "All attribute creation operations failed",
@@ -156,10 +274,19 @@ async function create_batch_attributes(noteId, attributes, axiosInstance) {
156
274
  };
157
275
  }
158
276
  const successCount = results.length;
277
+ const validCount = batchValidation.validAttributes.length;
278
+ const conflictCount = batchValidation.conflicts.length;
159
279
  const totalCount = attributes.length;
280
+ let message = `Created ${successCount}/${validCount} valid attributes successfully`;
281
+ if (conflictCount > 0) {
282
+ message += ` (${conflictCount} skipped due to conflicts, ${totalCount} total requested)`;
283
+ }
284
+ if (errors.length > 0) {
285
+ message += ` with ${errors.length} errors`;
286
+ }
160
287
  return {
161
288
  success: successCount > 0,
162
- message: `Created ${successCount}/${totalCount} attributes successfully${errors.length > 0 ? ` with ${errors.length} errors` : ''}`,
289
+ message,
163
290
  attributes: results,
164
291
  errors: errors.length > 0 ? errors : undefined
165
292
  };
@@ -169,17 +296,28 @@ async function create_batch_attributes(noteId, attributes, axiosInstance) {
169
296
  */
170
297
  async function update_attribute(noteId, attribute, axiosInstance) {
171
298
  try {
299
+ // Clean attribute name first
300
+ const cleaningResult = cleanAttributeName(attribute.name, attribute.type);
301
+ const attributeToUse = {
302
+ ...attribute,
303
+ name: cleaningResult.cleanedName
304
+ };
172
305
  // For update, we need the attribute ID, which requires finding it first
173
306
  const noteResponse = await axiosInstance.get(`/notes/${noteId}`);
174
307
  // Debug: Log available attributes
175
308
  logVerbose("update_attribute", `Available attributes on note ${noteId}`, noteResponse.data.attributes);
176
- // Find the attribute to update by name and type
177
- const targetAttribute = noteResponse.data.attributes.find((attr) => attr.name === attribute.name && attr.type === attribute.type);
309
+ // Find the attribute to update by name and type (use cleaned name)
310
+ const targetAttribute = noteResponse.data.attributes.find((attr) => attr.name === attributeToUse.name && attr.type === attributeToUse.type);
178
311
  if (!targetAttribute) {
179
312
  const availableAttrs = noteResponse.data.attributes.map((attr) => `${attr.type}:${attr.name}`).join(', ');
313
+ let message = `Attribute '${attributeToUse.name}' of type '${attributeToUse.type}' not found on note ${noteId}. Available attributes: ${availableAttrs || 'none'}`;
314
+ // Add cleaning information if name was corrected
315
+ if (cleaningResult.wasCleaned) {
316
+ message += `\n\nšŸ”§ **Note**: Attribute name was auto-corrected from "${cleaningResult.originalName}" to "${attributeToUse.name}"`;
317
+ }
180
318
  return {
181
319
  success: false,
182
- message: `Attribute '${attribute.name}' of type '${attribute.type}' not found on note ${noteId}. Available attributes: ${availableAttrs || 'none'}`,
320
+ message,
183
321
  errors: ["Attribute not found"]
184
322
  };
185
323
  }
@@ -189,20 +327,20 @@ async function update_attribute(noteId, attribute, axiosInstance) {
189
327
  // - For relations: only position can be updated
190
328
  // - isInheritable cannot be updated via PATCH, requires delete+recreate
191
329
  const updateData = {};
192
- if (attribute.type === "label") {
193
- updateData.value = attribute.value !== undefined ? attribute.value : targetAttribute.value;
330
+ if (attributeToUse.type === "label") {
331
+ updateData.value = attributeToUse.value !== undefined ? attributeToUse.value : targetAttribute.value;
194
332
  }
195
- if (attribute.position !== undefined) {
196
- updateData.position = attribute.position;
333
+ if (attributeToUse.position !== undefined) {
334
+ updateData.position = attributeToUse.position;
197
335
  }
198
336
  else if (targetAttribute.position !== undefined) {
199
337
  updateData.position = targetAttribute.position;
200
338
  }
201
339
  // Check if isInheritable is being changed (not allowed via PATCH)
202
- if (attribute.isInheritable !== undefined && attribute.isInheritable !== targetAttribute.isInheritable) {
340
+ if (attributeToUse.isInheritable !== undefined && attributeToUse.isInheritable !== targetAttribute.isInheritable) {
203
341
  logVerbose("update_attribute", "isInheritable change detected, requires delete+recreate", {
204
342
  current: targetAttribute.isInheritable,
205
- requested: attribute.isInheritable
343
+ requested: attributeToUse.isInheritable
206
344
  });
207
345
  return {
208
346
  success: false,
@@ -213,9 +351,14 @@ async function update_attribute(noteId, attribute, axiosInstance) {
213
351
  logVerbose("update_attribute", "Update data", updateData);
214
352
  logVerboseApi("PATCH", `/attributes/${targetAttribute.attributeId}`, updateData);
215
353
  const response = await axiosInstance.patch(`/attributes/${targetAttribute.attributeId}`, updateData);
354
+ // Build success message with cleaning information
355
+ let successMessage = `Successfully updated ${attributeToUse.type} '${attributeToUse.name}' on note ${noteId}`;
356
+ if (cleaningResult.wasCleaned) {
357
+ successMessage += `\n\n${generateCleaningMessage([cleaningResult])}`;
358
+ }
216
359
  return {
217
360
  success: true,
218
- message: `Successfully updated ${attribute.type} '${attribute.name}' on note ${noteId}`,
361
+ message: successMessage,
219
362
  attributes: [response.data]
220
363
  };
221
364
  }
@@ -241,21 +384,37 @@ async function update_attribute(noteId, attribute, axiosInstance) {
241
384
  */
242
385
  async function delete_attribute(noteId, attribute, axiosInstance) {
243
386
  try {
387
+ // Clean attribute name first
388
+ const cleaningResult = cleanAttributeName(attribute.name, attribute.type);
389
+ const attributeToUse = {
390
+ ...attribute,
391
+ name: cleaningResult.cleanedName
392
+ };
244
393
  // For delete, we need the attribute ID, which requires finding it first
245
394
  const noteResponse = await axiosInstance.get(`/notes/${noteId}`);
246
- // Find the attribute to delete by name and type
247
- const targetAttribute = noteResponse.data.attributes.find((attr) => attr.name === attribute.name && attr.type === attribute.type);
395
+ // Find the attribute to delete by name and type (use cleaned name)
396
+ const targetAttribute = noteResponse.data.attributes.find((attr) => attr.name === attributeToUse.name && attr.type === attributeToUse.type);
248
397
  if (!targetAttribute) {
398
+ let message = `Attribute '${attributeToUse.name}' of type '${attributeToUse.type}' not found on note ${noteId}`;
399
+ // Add cleaning information if name was corrected
400
+ if (cleaningResult.wasCleaned) {
401
+ message += `\n\nšŸ”§ **Note**: Attribute name was auto-corrected from "${cleaningResult.originalName}" to "${attributeToUse.name}"`;
402
+ }
249
403
  return {
250
404
  success: false,
251
- message: `Attribute '${attribute.name}' of type '${attribute.type}' not found on note ${noteId}`,
405
+ message,
252
406
  errors: ["Attribute not found"]
253
407
  };
254
408
  }
255
409
  await axiosInstance.delete(`/attributes/${targetAttribute.attributeId}`);
410
+ // Build success message with cleaning information
411
+ let successMessage = `Successfully deleted ${attributeToUse.type} '${attributeToUse.name}' from note ${noteId}`;
412
+ if (cleaningResult.wasCleaned) {
413
+ successMessage += `\n\n${generateCleaningMessage([cleaningResult])}`;
414
+ }
256
415
  return {
257
416
  success: true,
258
- message: `Successfully deleted ${attribute.type} '${attribute.name}' from note ${noteId}`
417
+ message: successMessage
259
418
  };
260
419
  }
261
420
  catch (error) {
@@ -267,7 +426,7 @@ async function delete_attribute(noteId, attribute, axiosInstance) {
267
426
  }
268
427
  }
269
428
  /**
270
- * Validate attribute data
429
+ * Validate attribute data with auto-correction
271
430
  */
272
431
  function validate_attribute(attribute) {
273
432
  const errors = [];
@@ -275,8 +434,14 @@ function validate_attribute(attribute) {
275
434
  if (!["label", "relation"].includes(attribute.type)) {
276
435
  errors.push("Attribute type must be either 'label' or 'relation'");
277
436
  }
278
- // Validate name
279
- if (!attribute.name || typeof attribute.name !== 'string' || attribute.name.trim() === '') {
437
+ // Clean attribute name first
438
+ const cleaningResult = cleanAttributeName(attribute.name, attribute.type);
439
+ const cleanedAttribute = {
440
+ ...attribute,
441
+ name: cleaningResult.cleanedName
442
+ };
443
+ // Validate name (using cleaned name)
444
+ if (!cleanedAttribute.name || typeof cleanedAttribute.name !== 'string' || cleanedAttribute.name.trim() === '') {
280
445
  errors.push("Attribute name is required and must be a non-empty string");
281
446
  }
282
447
  // Validate position
@@ -293,7 +458,9 @@ function validate_attribute(attribute) {
293
458
  }
294
459
  return {
295
460
  valid: errors.length === 0,
296
- errors
461
+ errors,
462
+ cleanedAttribute: cleaningResult.wasCleaned ? cleanedAttribute : undefined,
463
+ cleaningResult: cleaningResult.wasCleaned ? cleaningResult : undefined
297
464
  };
298
465
  }
299
466
  /**
@@ -6,6 +6,7 @@ import { processContentArray } from '../utils/contentProcessor.js';
6
6
  import { logVerbose, logVerboseError, logVerboseApi } from '../utils/verboseUtils.js';
7
7
  import { getContentRequirements, validateContentForNoteType, extractTemplateRelation } from '../utils/contentRules.js';
8
8
  import { validateAndTranslateTemplate, createTemplateRelationError } from '../utils/templateMapper.js';
9
+ import { cleanAttributeName, generateCleaningMessage } from '../utils/attributeNameCleaner.js';
9
10
  /**
10
11
  * Strip HTML tags from content for text notes
11
12
  */
@@ -207,8 +208,15 @@ export async function handleCreateNote(args, axiosInstance) {
207
208
  if (attributes && attributes.length > 0) {
208
209
  try {
209
210
  logVerbose("handleCreateNote", `Creating ${attributes.length} attributes for note ${noteId}`, attributes);
210
- await createNoteAttributes(noteId, attributes, axiosInstance);
211
+ const attributeResult = await createNoteAttributes(noteId, attributes, axiosInstance);
211
212
  logVerbose("handleCreateNote", `Successfully created all attributes for note ${noteId}`);
213
+ // Add cleaning information to response if any corrections were made
214
+ if (attributeResult.cleaningResults && attributeResult.cleaningResults.length > 0) {
215
+ const cleaningMessage = generateCleaningMessage(attributeResult.cleaningResults);
216
+ if (cleaningMessage) {
217
+ response.data.attributeCleaningMessage = cleaningMessage;
218
+ }
219
+ }
212
220
  }
213
221
  catch (attributeError) {
214
222
  const errorMsg = `Note created but attributes failed to apply: ${attributeError instanceof Error ? attributeError.message : attributeError}`;
@@ -226,37 +234,49 @@ export async function handleCreateNote(args, axiosInstance) {
226
234
  * Create attributes for a note (helper function)
227
235
  */
228
236
  async function createNoteAttributes(noteId, attributes, axiosInstance) {
237
+ const cleaningResults = [];
229
238
  const attributePromises = attributes.map(async (attr) => {
239
+ // Clean attribute name first
240
+ const cleaningResult = cleanAttributeName(attr.name, attr.type);
241
+ if (cleaningResult.wasCleaned) {
242
+ cleaningResults.push(cleaningResult);
243
+ }
244
+ // Use cleaned attribute name
245
+ const cleanedAttr = {
246
+ ...attr,
247
+ name: cleaningResult.cleanedName
248
+ };
230
249
  // Translate template names to note IDs for template relations
231
- let processedValue = attr.value || '';
232
- if (attr.type === "relation" && attr.name === "template" && attr.value) {
250
+ let processedValue = cleanedAttr.value || '';
251
+ if (cleanedAttr.type === "relation" && cleanedAttr.name === "template" && cleanedAttr.value) {
233
252
  try {
234
- processedValue = validateAndTranslateTemplate(attr.value);
253
+ processedValue = validateAndTranslateTemplate(cleanedAttr.value);
235
254
  logVerbose("createNoteAttributes", `Translated template relation`, {
236
- from: attr.value,
255
+ from: cleanedAttr.value,
237
256
  to: processedValue
238
257
  });
239
258
  }
240
259
  catch (error) {
241
260
  logVerboseError("createNoteAttributes", error);
242
- throw new Error(createTemplateRelationError(attr.value));
261
+ throw new Error(createTemplateRelationError(cleanedAttr.value));
243
262
  }
244
263
  }
245
264
  const attributeData = {
246
265
  noteId: noteId,
247
- type: attr.type,
248
- name: attr.name,
266
+ type: cleanedAttr.type,
267
+ name: cleanedAttr.name,
249
268
  value: processedValue,
250
- position: attr.position || 10,
251
- isInheritable: attr.isInheritable || false
269
+ position: cleanedAttr.position || 10,
270
+ isInheritable: cleanedAttr.isInheritable || false
252
271
  };
253
272
  logVerboseApi("POST", `/attributes`, attributeData);
254
273
  const response = await axiosInstance.post(`/attributes`, attributeData);
255
- logVerbose("createNoteAttributes", `Created ${attr.type} '${attr.name}' for note ${noteId}`, response.data);
274
+ logVerbose("createNoteAttributes", `Created ${cleanedAttr.type} '${cleanedAttr.name}' for note ${noteId}`, response.data);
256
275
  return response;
257
276
  });
258
277
  const results = await Promise.all(attributePromises);
259
278
  logVerbose("createNoteAttributes", `Successfully created ${results.length} attributes for note ${noteId}`);
279
+ return { results, cleaningResults };
260
280
  }
261
281
  /**
262
282
  * Handle update note operation
@@ -298,6 +318,103 @@ export async function handleUpdateNote(args, axiosInstance) {
298
318
  };
299
319
  }
300
320
  }
321
+ // Step 2.5: Type change validation if provided
322
+ if (type && type !== currentNote.data.type) {
323
+ const currentType = currentNote.data.type;
324
+ const newType = type;
325
+ // Check for incompatible type changes with template relations
326
+ const existingAttributes = currentNote.data.attributes || [];
327
+ const templateRelation = existingAttributes.find((attr) => attr.type === 'relation' && attr.name === 'template')?.value;
328
+ if (templateRelation) {
329
+ // Container templates require 'book' type
330
+ const containerTemplates = [
331
+ 'board', '_template_board',
332
+ 'grid view', '_template_grid_view',
333
+ 'list view', '_template_list_view',
334
+ 'geo map', '_template_geo_map',
335
+ 'calendar', '_template_calendar'
336
+ ];
337
+ const isContainerTemplate = containerTemplates.includes(templateRelation.toLowerCase());
338
+ if (isContainerTemplate && newType !== 'book') {
339
+ return {
340
+ noteId,
341
+ message: `TYPE CHANGE CONFLICT: Cannot change note type from "${currentType}" to "${newType}" while it has a "${templateRelation}" template relation.
342
+
343
+ šŸ“‹ **Template Compatibility Rules**:
344
+ Container templates (Board, Calendar, Grid View, List View, Geo Map) require type "book" to function properly.
345
+
346
+ šŸ› ļø **Solutions**:
347
+ 1. **Keep current type**: Use type "book" to maintain template functionality
348
+ 2. **Remove template**: Delete the ~template relation first, then change type
349
+ 3. **Choose different template**: Switch to a template compatible with type "${newType}"
350
+
351
+ šŸ’” **For most use cases**: If you want a regular note with "${templateRelation}" features, consider:
352
+ - Using type "text" with the ~template relation removed
353
+ - Creating a child note inside this container for your content
354
+
355
+ Please remove the template relation first or keep type as "book".`,
356
+ revisionCreated: false,
357
+ conflict: true
358
+ };
359
+ }
360
+ // Book type can only be used with container templates
361
+ if (newType === 'book' && !isContainerTemplate) {
362
+ return {
363
+ noteId,
364
+ message: `TYPE CHANGE CONFLICT: Cannot change note type to "book" while it has a "${templateRelation}" template relation.
365
+
366
+ šŸ“‹ **Book Type Requirements**:
367
+ Book type should only be used with container templates (Board, Calendar, Grid View, List View, Geo Map) that provide specialized layouts.
368
+
369
+ šŸ› ļø **Solutions**:
370
+ 1. **Use type "text"**: Best for regular notes with content
371
+ 2. **Switch to container template**: Change ~template to "Board", "Calendar", etc. for specialized layouts
372
+ 3. **Keep current type**: Use "${currentType}" for optimal functionality
373
+
374
+ šŸ’” **Recommendation**: For regular notes with template relations, type "text" usually provides the best experience.
375
+
376
+ Please choose a compatible template or use type "text".`,
377
+ revisionCreated: false,
378
+ conflict: true
379
+ };
380
+ }
381
+ }
382
+ // Validate content compatibility for type changes with content updates
383
+ if (rawContent && currentContent.data) {
384
+ try {
385
+ // Test if existing content is compatible with new type
386
+ await validateContentForNoteType(currentContent.data, newType, currentContent.data, templateRelation);
387
+ }
388
+ catch (validationError) {
389
+ const errorMessage = validationError instanceof Error ? validationError.message : String(validationError);
390
+ return {
391
+ noteId,
392
+ message: `CONTENT COMPATIBILITY ERROR: Cannot change note type from "${currentType}" to "${newType}" with current content.
393
+
394
+ šŸ“‹ **Content Requirements for ${newType}**:
395
+ ${errorMessage}
396
+
397
+ šŸ› ļø **Solutions**:
398
+ 1. **Clean content**: Remove HTML, formatting, or incompatible elements
399
+ 2. **Keep current type**: Use "${currentType}" for existing content
400
+ 3. **Manual conversion**: Convert content manually to match ${newType} requirements
401
+
402
+ šŸ’” **Content Guidelines**:
403
+ - **Code notes**: Plain text only (no HTML tags)
404
+ - **Text notes**: HTML content (plain text auto-wrapped in <p> tags)
405
+ - **Mermaid notes**: Plain text diagram definitions only
406
+
407
+ Please modify the content to be compatible with type "${newType}" or keep the current type.`,
408
+ revisionCreated: false,
409
+ conflict: true
410
+ };
411
+ }
412
+ }
413
+ logVerbose("handleUpdateNote", `Type change validation passed: ${currentType} → ${newType}`, {
414
+ templateRelation,
415
+ hasContent: !!rawContent
416
+ });
417
+ }
301
418
  // Handle title-only update (efficient PATCH operation)
302
419
  if (isTitleOnlyUpdate) {
303
420
  // For title-only updates, skip revision creation for efficiency
@@ -306,6 +423,10 @@ export async function handleUpdateNote(args, axiosInstance) {
306
423
  if (mime) {
307
424
  patchData.mime = mime;
308
425
  }
426
+ // Add note type if provided (new capability)
427
+ if (type) {
428
+ patchData.type = type;
429
+ }
309
430
  logVerboseApi("PATCH", `/notes/${noteId}`, patchData);
310
431
  const response = await axiosInstance.patch(`/notes/${noteId}`, patchData, {
311
432
  headers: {
@@ -315,10 +436,11 @@ export async function handleUpdateNote(args, axiosInstance) {
315
436
  if (response.status !== 200) {
316
437
  throw new Error(`Unexpected response status: ${response.status}`);
317
438
  }
439
+ const typeMessage = type ? ` and type updated to "${type}"` : "";
318
440
  const mimeMessage = mime ? ` and MIME type updated to "${mime}"` : "";
319
441
  return {
320
442
  noteId,
321
- message: `Note ${noteId} title updated successfully to "${title}"${mimeMessage}`,
443
+ message: `Note ${noteId} title updated successfully to "${title}"${typeMessage}${mimeMessage}`,
322
444
  revisionCreated: false,
323
445
  conflict: false
324
446
  };
@@ -391,12 +513,15 @@ export async function handleUpdateNote(args, axiosInstance) {
391
513
  if (contentResponse.status !== 204) {
392
514
  throw new Error(`Unexpected response status: ${contentResponse.status}`);
393
515
  }
394
- // Step 7: Update title and MIME type if provided (multi-parameter update)
395
- if (isMultiParamUpdate && (title || mime)) {
516
+ // Step 7: Update title, type, and MIME type if provided (multi-parameter update)
517
+ if (isMultiParamUpdate && (title || type || mime)) {
396
518
  const patchData = {};
397
519
  if (title) {
398
520
  patchData.title = title;
399
521
  }
522
+ if (type) {
523
+ patchData.type = type;
524
+ }
400
525
  if (mime) {
401
526
  patchData.mime = mime;
402
527
  }
@@ -417,10 +542,11 @@ export async function handleUpdateNote(args, axiosInstance) {
417
542
  const correctionMsg = (finalContent !== rawContent) ? " (content auto-corrected)" : "";
418
543
  const modeMsg = mode === 'append' ? " (content appended)" : " (content overwritten)";
419
544
  const titleMsg = (isMultiParamUpdate && title) ? ` (title updated to "${title}")` : "";
545
+ const typeMsg = (isMultiParamUpdate && type) ? ` (type updated to "${type}")` : "";
420
546
  const mimeMsg = (isMultiParamUpdate && mime) ? ` (MIME type updated to "${mime}")` : "";
421
547
  return {
422
548
  noteId,
423
- message: `Note ${noteId} updated successfully${revisionMsg}${correctionMsg}${modeMsg}${titleMsg}${mimeMsg}`,
549
+ message: `Note ${noteId} updated successfully${revisionMsg}${correctionMsg}${modeMsg}${titleMsg}${typeMsg}${mimeMsg}`,
424
550
  revisionCreated,
425
551
  conflict: false
426
552
  };
@@ -9,13 +9,13 @@ export function createWriteTools() {
9
9
  return [
10
10
  {
11
11
  name: "create_note",
12
- description: "Create a new note in TriliumNext with duplicate title detection. When a note with the same title already exists in the same directory, you'll be presented with choices: skip creation or update the existing note. ONLY use this tool when the user explicitly requests note creation (e.g., 'create a note', 'make a new note'). DO NOT use this tool proactively or when the user is only asking questions about their notes. TIP: For code notes, content is plain text (no HTML processing).",
12
+ description: "Create a new note in TriliumNext with duplicate title detection. When a note with the same title already exists in the same directory, you'll be presented with choices: skip creation or update the existing note. āš ļø **PARENT FOLDER SELECTION**: If user references a parent folder by name (e.g., 'create meeting note in Projects folder') and there are multiple folders with that name, ALWAYS use resolve_note_id first to find the exact parent note ID and let user choose the correct location. ONLY use this tool when the user explicitly requests note creation (e.g., 'create a note', 'make a new note'). DO NOT use this tool proactively or when the user is only asking questions about their notes. TIP: For code notes, content is plain text (no HTML processing).",
13
13
  inputSchema: {
14
14
  type: "object",
15
15
  properties: {
16
16
  parentNoteId: {
17
17
  type: "string",
18
- description: "ID of the parent note. āš ļø IMPORTANT: When creating child notes for special note types, ensure proper requirements:\n• RENDER parents: child must be type='code', mime='text/html' (HTML content for rendering)\n• CALENDAR parents: child must have #startDate, #endDate, #startTime, #endTime labels\n• BOARD parents: child must have #status label (e.g., 'To Do', 'In Progress', 'Done')\n• Missing required labels/relations will cause child notes to not display properly",
18
+ description: "ID of the parent note. āš ļø CRITICAL: If you have multiple folders/notes with identical names (e.g., multiple 'Projects' folders, multiple 'Grid View' notes), you MUST use the exact note ID - do NOT guess or auto-select! When multiple potential parents exist, the system will detect conflicts and ask you to choose the specific parent note ID.\n\nšŸ” **FINDING PARENT NOTE ID**: Use resolve_note_id to find the exact note ID when you only know the folder name. Example: resolve_note_id({noteName: 'Projects'}) will return the note ID and show alternatives if multiple matches exist.\n\nšŸ“‹ **PARENT NOTE REQUIREMENTS**:\n• RENDER parents: child must be type='code', mime='text/html' (HTML content for rendering)\n• CALENDAR parents: child must have #startDate, #endDate, #startTime, #endTime labels\n• BOARD parents: child must have #status label (e.g., 'To Do', 'In Progress', 'Done')\n• Missing required labels/relations will cause child notes to not display properly\n\nāš ļø **MULTIPLE PARENTS WITH SAME NAME**: When users say 'put note under Projects folder' but there are multiple 'Projects' folders, ALWAYS use resolve_note_id first to let users choose the correct parent, then use the returned note ID for parentNoteId.",
19
19
  default: "root"
20
20
  },
21
21
  title: {
@@ -74,7 +74,7 @@ export function createWriteTools() {
74
74
  },
75
75
  {
76
76
  name: "update_note",
77
- description: "Update note with support for title-only updates, content overwrite, or content append. āš ļø REQUIRED: ALWAYS call get_note first to obtain current hash. MODE SELECTION: Use 'append' when user wants to add/insert content (e.g., 'append to note', 'add to the end', 'insert content', 'add more content', 'continue writing', 'add to bottom'). Use 'overwrite' when replacing entire content (e.g., 'replace content', 'overwrite note', 'update the whole note', 'completely replace'). TITLE-ONLY: Efficient title changes without content modification. PREVENTS: Overwriting changes made by other users (hash mismatch) or content type violations. ONLY use when user explicitly requests note update. WORKFLOW: get_note → review content → update_note with returned hash",
77
+ description: "Update note with support for title-only updates, content operations, note type changes, and MIME type updates. āš ļø REQUIRED: ALWAYS call get_note first to obtain current hash and validate current note type. MODE SELECTION: Use 'append' when adding content, 'overwrite' when replacing content, or title-only for efficient title changes. TYPE CHANGES: Convert between note types (e.g., 'text' to 'code') with automatic validation for template compatibility and content requirements. MIME UPDATES: Change content type for code notes (e.g., JavaScript to Python). PREVENTS: Overwriting changes made by other users (hash mismatch) or invalid type conversions. WORKFLOW: get_note → review current state → update_note with hash",
78
78
  inputSchema: {
79
79
  type: "object",
80
80
  properties: {
@@ -84,16 +84,16 @@ export function createWriteTools() {
84
84
  },
85
85
  title: {
86
86
  type: "string",
87
- description: "New title for the note. If provided alone (without content), performs efficient title-only update without affecting note content or blobId."
87
+ description: "New title for the note. If provided alone (without content, type, or mime), performs efficient title-only update without affecting note content or blobId."
88
88
  },
89
89
  type: {
90
90
  type: "string",
91
91
  enum: ["text", "code", "render", "search", "relationMap", "book", "noteMap", "mermaid", "webView"],
92
- description: "Type of note (aligned with TriliumNext ETAPI specification). This determines content validation requirements. Required when updating content, optional for title-only updates."
92
+ description: "NEW: Change note type with validation (e.g., convert 'text' to 'code' note). āš ļø IMPORTANT: Type changes are validated for template compatibility - notes with ~template='Board' must remain type='book', ~template='Calendar' must remain type='book', etc. Content compatibility is also validated (e.g., HTML content may not be suitable for code notes). Use this when converting between different note formats (e.g., changing a text note to a code note for better syntax highlighting, or converting a generic note to a Mermaid diagram). Optional - omit if not changing note type."
93
93
  },
94
94
  mime: {
95
95
  type: "string",
96
- description: "MIME type for code/file/image notes. Helps with syntax highlighting and content type identification."
96
+ description: "NEW: Update MIME type for code notes (e.g., change 'text/javascript' to 'text/x-python'). Only valid for code-type notes. Use this when switching programming languages or content types. Optional - omit if not changing MIME type."
97
97
  },
98
98
  content: {
99
99
  type: "string",
@@ -354,7 +354,7 @@ export function createWriteAttributeTools() {
354
354
  return [
355
355
  {
356
356
  name: "manage_attributes",
357
- description: "Manage note attributes with write operations (create, update, delete). Create labels (#tags), template relations (~template), update existing attributes, and organize notes with metadata. āš ļø PRIORITY: Use create_note with attributes parameter for template relations when possible - only use this tool for post-creation modifications or complex scenarios.\n\nāœ… BEST PRACTICE: Most template relations (~template = 'Board', 'Calendar', etc.) should be added during create_note\nāŒ USE THIS TOOL FOR: ~renderNote relations, custom note-to-note relations, post-creation label updates\n\nIMPORTANT: This tool only provides write access - use read_attributes to view existing attributes. Relations require values pointing to existing notes (e.g., template relations use human-readable names like 'Board', 'Calendar' which are automatically translated to system note IDs; author relations use target note titles or IDs). UPDATE LIMITATIONS: For labels, only value and position can be updated. For relations, only position can be updated. The isInheritable property cannot be changed via update - delete and recreate to modify inheritability. Supports single operations and efficient batch creation for better performance.",
357
+ description: "Manage note attributes with write operations (create, update, delete). Create labels (#tags), template relations (~template), update existing attributes, and organize notes with metadata. āš ļø PRIORITY: Use create_note with attributes parameter for template relations when possible - only use this tool for post-creation modifications or complex scenarios.\n\nāœ… BEST PRACTICE: Most template relations (~template = 'Board', 'Calendar', etc.) should be added during create_note\nāŒ USE THIS TOOL FOR: ~renderNote relations, custom note-to-note relations, post-creation label updates\n\nIMPORTANT: This tool only provides write access - use read_attributes to view existing attributes. Relations require values pointing to existing notes (e.g., template relations use human-readable names like 'Board', 'Calendar' which are automatically translated to system note IDs; author relations use target note titles or IDs). UPDATE LIMITATIONS: For labels, only value and position can be updated. For relations, only position can be updated. The isInheritable property cannot be changed via update - delete and recreate to modify inheritability. Supports single operations and efficient batch creation for better performance.\n\nšŸ›”ļø VALIDATION: Create operations automatically check for existing attributes to prevent duplicates. If an attribute already exists, you'll receive detailed error messages with guidance on using 'update' instead. Batch operations skip duplicates and continue processing valid attributes.",
358
358
  inputSchema: {
359
359
  type: "object",
360
360
  properties: {
@@ -365,7 +365,7 @@ export function createWriteAttributeTools() {
365
365
  operation: {
366
366
  type: "string",
367
367
  enum: ["create", "update", "delete", "batch_create"],
368
- description: "Operation type: 'create' (new attribute), 'update' (modify existing - limited to label value/position and relation position only), 'delete' (remove attribute), 'batch_create' (multiple new attributes efficiently)"
368
+ description: "Operation type: 'create' (new attribute - validates against existing attributes to prevent duplicates), 'update' (modify existing - limited to label value/position and relation position only), 'delete' (remove attribute), 'batch_create' (multiple new attributes efficiently - skips duplicates and continues with valid attributes)"
369
369
  },
370
370
  attributes: {
371
371
  type: "array",
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Attribute Name Cleaner Utility
3
+ * Auto-corrects common LLM mistakes in attribute names by removing
4
+ * leading/trailing # and ~ symbols that don't belong in attribute names.
5
+ */
6
+ import { logVerbose } from './verboseUtils.js';
7
+ /**
8
+ * Clean attribute name by removing invalid leading/trailing symbols
9
+ *
10
+ * Trilium ETAPI requires attribute names to be clean without # or ~ symbols.
11
+ * LLMs often mistakenly add these symbols to attribute names.
12
+ *
13
+ * @param attributeName The attribute name to clean
14
+ * @param attributeType The type of attribute (label or relation)
15
+ * @returns CleaningResult with details about any corrections made
16
+ */
17
+ export function cleanAttributeName(attributeName, attributeType) {
18
+ const originalName = attributeName;
19
+ const corrections = [];
20
+ if (!attributeName || typeof attributeName !== 'string') {
21
+ return {
22
+ cleanedName: attributeName,
23
+ wasCleaned: false,
24
+ originalName,
25
+ corrections: []
26
+ };
27
+ }
28
+ let cleanedName = attributeName.trim();
29
+ // Remove leading # symbol (common LLM mistake for labels)
30
+ if (attributeType === 'label' && cleanedName.startsWith('#')) {
31
+ cleanedName = cleanedName.substring(1).trim();
32
+ corrections.push(`Removed leading '#' symbol`);
33
+ }
34
+ // Remove leading ~ symbol (common LLM mistake for relations)
35
+ if (attributeType === 'relation' && cleanedName.startsWith('~')) {
36
+ cleanedName = cleanedName.substring(1).trim();
37
+ corrections.push(`Removed leading '~' symbol`);
38
+ }
39
+ const wasCleaned = corrections.length > 0;
40
+ // Log corrections if any were made
41
+ if (wasCleaned) {
42
+ logVerbose('cleanAttributeName', `Auto-corrected ${attributeType} name`, {
43
+ original: originalName,
44
+ corrected: cleanedName,
45
+ corrections,
46
+ attributeType
47
+ });
48
+ }
49
+ return {
50
+ cleanedName,
51
+ wasCleaned,
52
+ originalName,
53
+ corrections
54
+ };
55
+ }
56
+ /**
57
+ * Clean an array of attributes
58
+ *
59
+ * @param attributes Array of attributes to clean
60
+ * @returns Array of cleaned attributes with cleaning information
61
+ */
62
+ export function cleanAttributeNames(attributes) {
63
+ return attributes.map(attribute => {
64
+ const cleaningResult = cleanAttributeName(attribute.name, attribute.type);
65
+ return {
66
+ ...attribute,
67
+ name: cleaningResult.cleanedName,
68
+ cleaningResult: cleaningResult.wasCleaned ? cleaningResult : undefined
69
+ };
70
+ });
71
+ }
72
+ /**
73
+ * Generate user-friendly message about corrections
74
+ *
75
+ * @param cleaningResults Array of cleaning results
76
+ * @returns User-friendly message describing corrections
77
+ */
78
+ export function generateCleaningMessage(cleaningResults) {
79
+ const totalCorrections = cleaningResults.filter(r => r.wasCleaned).length;
80
+ if (totalCorrections === 0) {
81
+ return '';
82
+ }
83
+ const correctionsByType = cleaningResults.reduce((acc, result) => {
84
+ if (result.wasCleaned) {
85
+ result.corrections.forEach(correction => {
86
+ acc[correction] = (acc[correction] || 0) + 1;
87
+ });
88
+ }
89
+ return acc;
90
+ }, {});
91
+ const correctionSummary = Object.entries(correctionsByType)
92
+ .map(([correction, count]) => `${count}Ɨ ${correction}`)
93
+ .join(', ');
94
+ return `šŸ”§ **Auto-Corrections Applied**\n` +
95
+ `Fixed ${totalCorrections} attribute name(s): ${correctionSummary}\n\n` +
96
+ `šŸ’” **Common LLM Mistakes Fixed**:\n` +
97
+ `• Attribute names should NOT start with # or ~ symbols\n` +
98
+ `• # symbols are for attribute values in search queries (e.g., #book)\n` +
99
+ `• ~ symbols are for attribute values in search queries (e.g., ~template)\n` +
100
+ `• Attribute names should be plain text (e.g., "startDate", "template")`;
101
+ }
102
+ /**
103
+ * Check if an attribute name needs cleaning
104
+ *
105
+ * @param attributeName The attribute name to check
106
+ * @returns True if the name needs cleaning
107
+ */
108
+ export function needsCleaning(attributeName, attributeType = 'label') {
109
+ if (!attributeName || typeof attributeName !== 'string') {
110
+ return false;
111
+ }
112
+ const trimmed = attributeName.trim();
113
+ return (attributeType === 'label' && trimmed.startsWith('#')) ||
114
+ (attributeType === 'relation' && trimmed.startsWith('~'));
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triliumnext-mcp",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "A model context protocol server for TriliumNext Notes",
5
5
  "type": "module",
6
6
  "bin": {