@velvetmonkey/flywheel-crank 0.10.1 → 0.11.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/index.js +429 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -141,42 +141,90 @@ function findSection(content, sectionName) {
|
|
|
141
141
|
contentStartLine
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
|
+
function isInsideCodeBlock(lines, currentIndex) {
|
|
145
|
+
let fenceCount = 0;
|
|
146
|
+
for (let i = 0; i < currentIndex; i++) {
|
|
147
|
+
if (lines[i].trim().startsWith("```")) {
|
|
148
|
+
fenceCount++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return fenceCount % 2 === 1;
|
|
152
|
+
}
|
|
153
|
+
function isStructuredLine(line) {
|
|
154
|
+
const trimmed = line.trimStart();
|
|
155
|
+
return trimmed.startsWith("|") || // Table row
|
|
156
|
+
trimmed.startsWith(">") || // Blockquote
|
|
157
|
+
trimmed.startsWith("```") || // Code fence
|
|
158
|
+
/^-{3,}$/.test(trimmed) || // Horizontal rule (dashes)
|
|
159
|
+
/^\*{3,}$/.test(trimmed) || // Horizontal rule (asterisks)
|
|
160
|
+
/^_{3,}$/.test(trimmed);
|
|
161
|
+
}
|
|
162
|
+
function isPreformattedList(content) {
|
|
163
|
+
const trimmed = content.trim();
|
|
164
|
+
if (!trimmed)
|
|
165
|
+
return false;
|
|
166
|
+
const firstLine = trimmed.split("\n")[0];
|
|
167
|
+
return /^[-*+]\s/.test(firstLine) || // Bullet
|
|
168
|
+
/^\d+\.\s/.test(firstLine) || // Numbered
|
|
169
|
+
/^[-*+]\s*\[[ xX]\]/.test(firstLine);
|
|
170
|
+
}
|
|
144
171
|
function formatContent(content, format) {
|
|
145
172
|
const trimmed = content.trim();
|
|
146
173
|
switch (format) {
|
|
147
174
|
case "plain":
|
|
148
175
|
return trimmed;
|
|
149
176
|
case "bullet": {
|
|
177
|
+
if (isPreformattedList(trimmed)) {
|
|
178
|
+
return trimmed;
|
|
179
|
+
}
|
|
150
180
|
const lines = trimmed.split("\n");
|
|
151
181
|
return lines.map((line, i) => {
|
|
152
182
|
if (i === 0)
|
|
153
183
|
return `- ${line}`;
|
|
154
184
|
if (line === "")
|
|
155
185
|
return "";
|
|
186
|
+
if (isInsideCodeBlock(lines, i) || isStructuredLine(line)) {
|
|
187
|
+
return line;
|
|
188
|
+
}
|
|
156
189
|
return ` ${line}`;
|
|
157
190
|
}).join("\n");
|
|
158
191
|
}
|
|
159
192
|
case "task": {
|
|
193
|
+
if (isPreformattedList(trimmed)) {
|
|
194
|
+
return trimmed;
|
|
195
|
+
}
|
|
160
196
|
const lines = trimmed.split("\n");
|
|
161
197
|
return lines.map((line, i) => {
|
|
162
198
|
if (i === 0)
|
|
163
199
|
return `- [ ] ${line}`;
|
|
164
200
|
if (line === "")
|
|
165
201
|
return "";
|
|
202
|
+
if (isInsideCodeBlock(lines, i) || isStructuredLine(line)) {
|
|
203
|
+
return line;
|
|
204
|
+
}
|
|
166
205
|
return ` ${line}`;
|
|
167
206
|
}).join("\n");
|
|
168
207
|
}
|
|
169
208
|
case "numbered": {
|
|
209
|
+
if (isPreformattedList(trimmed)) {
|
|
210
|
+
return trimmed;
|
|
211
|
+
}
|
|
170
212
|
const lines = trimmed.split("\n");
|
|
171
213
|
return lines.map((line, i) => {
|
|
172
214
|
if (i === 0)
|
|
173
215
|
return `1. ${line}`;
|
|
174
216
|
if (line === "")
|
|
175
217
|
return "";
|
|
218
|
+
if (isInsideCodeBlock(lines, i) || isStructuredLine(line)) {
|
|
219
|
+
return line;
|
|
220
|
+
}
|
|
176
221
|
return ` ${line}`;
|
|
177
222
|
}).join("\n");
|
|
178
223
|
}
|
|
179
224
|
case "timestamp-bullet": {
|
|
225
|
+
if (isPreformattedList(trimmed)) {
|
|
226
|
+
return trimmed;
|
|
227
|
+
}
|
|
180
228
|
const now = /* @__PURE__ */ new Date();
|
|
181
229
|
const hours = String(now.getHours()).padStart(2, "0");
|
|
182
230
|
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
@@ -188,6 +236,9 @@ function formatContent(content, format) {
|
|
|
188
236
|
return `${prefix}${line}`;
|
|
189
237
|
if (line === "")
|
|
190
238
|
return "";
|
|
239
|
+
if (isInsideCodeBlock(lines, i) || isStructuredLine(line)) {
|
|
240
|
+
return line;
|
|
241
|
+
}
|
|
191
242
|
return `${indent}${line}`;
|
|
192
243
|
}).join("\n");
|
|
193
244
|
}
|
|
@@ -231,7 +282,13 @@ function insertInSection(content, section, newContent, position, options) {
|
|
|
231
282
|
break;
|
|
232
283
|
}
|
|
233
284
|
if (indent) {
|
|
234
|
-
const
|
|
285
|
+
const contentLines = formattedContent.split("\n");
|
|
286
|
+
const indentedContent = contentLines.map((line, i) => {
|
|
287
|
+
if (line === "" || isInsideCodeBlock(contentLines, i) || isStructuredLine(line)) {
|
|
288
|
+
return line;
|
|
289
|
+
}
|
|
290
|
+
return indent + line;
|
|
291
|
+
}).join("\n");
|
|
235
292
|
lines.splice(section.contentStartLine, 0, indentedContent);
|
|
236
293
|
} else {
|
|
237
294
|
lines.splice(section.contentStartLine, 0, formattedContent);
|
|
@@ -250,7 +307,13 @@ function insertInSection(content, section, newContent, position, options) {
|
|
|
250
307
|
if (lastContentLineIdx >= section.contentStartLine && isEmptyPlaceholder(lines[lastContentLineIdx])) {
|
|
251
308
|
if (options?.preserveListNesting) {
|
|
252
309
|
const indent = detectSectionBaseIndentation(lines, section.contentStartLine, section.endLine);
|
|
253
|
-
const
|
|
310
|
+
const contentLines = formattedContent.split("\n");
|
|
311
|
+
const indentedContent = contentLines.map((line, i) => {
|
|
312
|
+
if (line === "" || isInsideCodeBlock(contentLines, i) || isStructuredLine(line)) {
|
|
313
|
+
return line;
|
|
314
|
+
}
|
|
315
|
+
return indent + line;
|
|
316
|
+
}).join("\n");
|
|
254
317
|
lines[lastContentLineIdx] = indentedContent;
|
|
255
318
|
} else {
|
|
256
319
|
lines[lastContentLineIdx] = formattedContent;
|
|
@@ -269,7 +332,13 @@ function insertInSection(content, section, newContent, position, options) {
|
|
|
269
332
|
}
|
|
270
333
|
if (options?.preserveListNesting) {
|
|
271
334
|
const indent = detectSectionBaseIndentation(lines, section.contentStartLine, section.endLine);
|
|
272
|
-
const
|
|
335
|
+
const contentLines = formattedContent.split("\n");
|
|
336
|
+
const indentedContent = contentLines.map((line, i) => {
|
|
337
|
+
if (line === "" || isInsideCodeBlock(contentLines, i) || isStructuredLine(line)) {
|
|
338
|
+
return line;
|
|
339
|
+
}
|
|
340
|
+
return indent + line;
|
|
341
|
+
}).join("\n");
|
|
273
342
|
lines.splice(insertLine, 0, indentedContent);
|
|
274
343
|
} else {
|
|
275
344
|
lines.splice(insertLine, 0, formattedContent);
|
|
@@ -454,9 +523,236 @@ function replaceInSection(content, section, search, replacement, mode = "first",
|
|
|
454
523
|
};
|
|
455
524
|
}
|
|
456
525
|
|
|
526
|
+
// src/core/validator.ts
|
|
527
|
+
var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
|
|
528
|
+
var NON_MARKDOWN_BULLET_PATTERNS = [
|
|
529
|
+
/^[\s]*\u2022/,
|
|
530
|
+
// • (bullet)
|
|
531
|
+
/^[\s]*\u25E6/,
|
|
532
|
+
// ◦ (hollow bullet)
|
|
533
|
+
/^[\s]*\u25AA/,
|
|
534
|
+
// ▪ (small square)
|
|
535
|
+
/^[\s]*\u25AB/,
|
|
536
|
+
// ▫ (hollow small square)
|
|
537
|
+
/^[\s]*\u2023/,
|
|
538
|
+
// ‣ (triangular bullet)
|
|
539
|
+
/^[\s]*\u2043/
|
|
540
|
+
// ⁃ (hyphen bullet)
|
|
541
|
+
];
|
|
542
|
+
var EMBEDDED_HEADING_PATTERN = /^#{1,6}\s+/m;
|
|
543
|
+
function validateInput(content, format) {
|
|
544
|
+
const warnings = [];
|
|
545
|
+
if (format === "timestamp-bullet" && TIMESTAMP_PATTERN.test(content.trim())) {
|
|
546
|
+
warnings.push({
|
|
547
|
+
type: "double-timestamp",
|
|
548
|
+
message: "Content already contains a timestamp prefix, and format is timestamp-bullet",
|
|
549
|
+
suggestion: 'Use format: "plain" or "bullet" instead to avoid duplicate timestamps'
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const lines = content.split("\n");
|
|
553
|
+
for (const line of lines) {
|
|
554
|
+
for (const pattern of NON_MARKDOWN_BULLET_PATTERNS) {
|
|
555
|
+
if (pattern.test(line)) {
|
|
556
|
+
warnings.push({
|
|
557
|
+
type: "non-markdown-bullets",
|
|
558
|
+
message: `Non-markdown bullet character detected: "${line.trim().charAt(0)}"`,
|
|
559
|
+
suggestion: "Use markdown bullets (-) for proper rendering"
|
|
560
|
+
});
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (EMBEDDED_HEADING_PATTERN.test(content)) {
|
|
566
|
+
warnings.push({
|
|
567
|
+
type: "embedded-heading",
|
|
568
|
+
message: "Content contains markdown heading syntax (##)",
|
|
569
|
+
suggestion: "Use bold (**text**) instead of headings inside list items"
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
const fenceCount = (content.match(/^```/gm) || []).length;
|
|
573
|
+
if (fenceCount % 2 !== 0) {
|
|
574
|
+
warnings.push({
|
|
575
|
+
type: "orphaned-fence",
|
|
576
|
+
message: "Odd number of code fence markers (```) detected",
|
|
577
|
+
suggestion: "Ensure code blocks are properly closed"
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
isValid: warnings.length === 0,
|
|
582
|
+
warnings
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function normalizeInput(content, format) {
|
|
586
|
+
let normalized = content;
|
|
587
|
+
const changes = [];
|
|
588
|
+
if (format === "timestamp-bullet" && TIMESTAMP_PATTERN.test(normalized.trim())) {
|
|
589
|
+
normalized = normalized.trim().replace(TIMESTAMP_PATTERN, "").trim();
|
|
590
|
+
changes.push("Removed duplicate timestamp prefix");
|
|
591
|
+
}
|
|
592
|
+
const lines = normalized.split("\n");
|
|
593
|
+
let bulletReplaced = false;
|
|
594
|
+
const normalizedLines = lines.map((line) => {
|
|
595
|
+
for (const pattern of NON_MARKDOWN_BULLET_PATTERNS) {
|
|
596
|
+
if (pattern.test(line)) {
|
|
597
|
+
bulletReplaced = true;
|
|
598
|
+
return line.replace(/^([\s]*)[•◦▪▫‣⁃]\s*/, "$1- ");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return line;
|
|
602
|
+
});
|
|
603
|
+
if (bulletReplaced) {
|
|
604
|
+
normalized = normalizedLines.join("\n");
|
|
605
|
+
changes.push('Replaced non-markdown bullets with "-"');
|
|
606
|
+
}
|
|
607
|
+
const trimmed = normalized.replace(/\n{3,}/g, "\n\n");
|
|
608
|
+
if (trimmed !== normalized) {
|
|
609
|
+
normalized = trimmed;
|
|
610
|
+
changes.push("Trimmed excessive blank lines");
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
content: normalized,
|
|
614
|
+
normalized: changes.length > 0,
|
|
615
|
+
changes
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function validateOutput(formatted) {
|
|
619
|
+
const issues = [];
|
|
620
|
+
const tableRows = formatted.match(/^\s*\|.*\|/gm);
|
|
621
|
+
if (tableRows && tableRows.length > 1) {
|
|
622
|
+
const pipeCounts = tableRows.map((row) => (row.match(/\|/g) || []).length);
|
|
623
|
+
const firstPipeCount = pipeCounts[0];
|
|
624
|
+
const hasInconsistentPipes = pipeCounts.some((count) => count !== firstPipeCount);
|
|
625
|
+
if (hasInconsistentPipes) {
|
|
626
|
+
issues.push({
|
|
627
|
+
type: "broken-table",
|
|
628
|
+
severity: "error",
|
|
629
|
+
message: "Table rows have inconsistent pipe counts - table alignment may be broken"
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const fenceCount = (formatted.match(/^```/gm) || []).length;
|
|
634
|
+
if (fenceCount % 2 !== 0) {
|
|
635
|
+
issues.push({
|
|
636
|
+
type: "orphaned-fence",
|
|
637
|
+
severity: "error",
|
|
638
|
+
message: "Odd number of code fence markers - code block may be unclosed"
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
const indentedFences = formatted.match(/^[ \t]+```/gm);
|
|
642
|
+
if (indentedFences && indentedFences.length > 0) {
|
|
643
|
+
const lines = formatted.split("\n");
|
|
644
|
+
for (let i = 0; i < lines.length; i++) {
|
|
645
|
+
if (/^[ \t]+```/.test(lines[i])) {
|
|
646
|
+
issues.push({
|
|
647
|
+
type: "indented-fence",
|
|
648
|
+
severity: "warning",
|
|
649
|
+
message: "Code fence marker is indented - this may break the code block",
|
|
650
|
+
line: i + 1
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const blockquoteLines = formatted.match(/^[ \t]*>/gm);
|
|
656
|
+
if (blockquoteLines) {
|
|
657
|
+
const lines = formatted.split("\n");
|
|
658
|
+
let inBlockquote = false;
|
|
659
|
+
for (let i = 0; i < lines.length; i++) {
|
|
660
|
+
const line = lines[i];
|
|
661
|
+
const isBlockquoteLine = /^[ \t]*>/.test(line);
|
|
662
|
+
if (isBlockquoteLine) {
|
|
663
|
+
inBlockquote = true;
|
|
664
|
+
} else if (inBlockquote && line.trim() !== "" && !/^[ \t]*>/.test(line)) {
|
|
665
|
+
if (/^[ \t]+[^-*>\d]/.test(line)) {
|
|
666
|
+
issues.push({
|
|
667
|
+
type: "broken-blockquote",
|
|
668
|
+
severity: "warning",
|
|
669
|
+
message: "Blockquote structure may be broken - continuation line not prefixed with >",
|
|
670
|
+
line: i + 1
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
inBlockquote = false;
|
|
674
|
+
} else if (line.trim() === "") {
|
|
675
|
+
inBlockquote = false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
681
|
+
issues
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function runValidationPipeline(content, format, options = {}) {
|
|
685
|
+
const {
|
|
686
|
+
validate = true,
|
|
687
|
+
normalize = true,
|
|
688
|
+
guardrails = "warn"
|
|
689
|
+
} = options;
|
|
690
|
+
let processedContent = content;
|
|
691
|
+
let inputWarnings = [];
|
|
692
|
+
let normalizationChanges = [];
|
|
693
|
+
let outputIssues = [];
|
|
694
|
+
let blocked = false;
|
|
695
|
+
let blockReason;
|
|
696
|
+
if (validate) {
|
|
697
|
+
const inputResult = validateInput(content, format);
|
|
698
|
+
inputWarnings = inputResult.warnings;
|
|
699
|
+
}
|
|
700
|
+
if (normalize) {
|
|
701
|
+
const normResult = normalizeInput(processedContent, format);
|
|
702
|
+
processedContent = normResult.content;
|
|
703
|
+
normalizationChanges = normResult.changes;
|
|
704
|
+
}
|
|
705
|
+
if (guardrails !== "off") {
|
|
706
|
+
const outputResult = validateOutput(processedContent);
|
|
707
|
+
outputIssues = outputResult.issues;
|
|
708
|
+
if (guardrails === "strict" && !outputResult.valid) {
|
|
709
|
+
blocked = true;
|
|
710
|
+
const errors = outputIssues.filter((i) => i.severity === "error");
|
|
711
|
+
blockReason = `Output validation failed: ${errors.map((e) => e.message).join("; ")}`;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
content: processedContent,
|
|
716
|
+
inputWarnings,
|
|
717
|
+
outputIssues,
|
|
718
|
+
normalizationChanges,
|
|
719
|
+
blocked,
|
|
720
|
+
blockReason
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
457
724
|
// src/core/git.ts
|
|
458
725
|
import { simpleGit, CheckRepoActions } from "simple-git";
|
|
459
726
|
import path2 from "path";
|
|
727
|
+
import fs2 from "fs/promises";
|
|
728
|
+
var LAST_COMMIT_FILE = ".claude/last-crank-commit.json";
|
|
729
|
+
async function saveLastCrankCommit(vaultPath2, hash, message) {
|
|
730
|
+
const filePath = path2.join(vaultPath2, LAST_COMMIT_FILE);
|
|
731
|
+
const dirPath = path2.dirname(filePath);
|
|
732
|
+
await fs2.mkdir(dirPath, { recursive: true });
|
|
733
|
+
const data = {
|
|
734
|
+
hash,
|
|
735
|
+
message,
|
|
736
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
737
|
+
};
|
|
738
|
+
await fs2.writeFile(filePath, JSON.stringify(data, null, 2));
|
|
739
|
+
}
|
|
740
|
+
async function getLastCrankCommit(vaultPath2) {
|
|
741
|
+
try {
|
|
742
|
+
const filePath = path2.join(vaultPath2, LAST_COMMIT_FILE);
|
|
743
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
744
|
+
return JSON.parse(content);
|
|
745
|
+
} catch {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
async function clearLastCrankCommit(vaultPath2) {
|
|
750
|
+
try {
|
|
751
|
+
const filePath = path2.join(vaultPath2, LAST_COMMIT_FILE);
|
|
752
|
+
await fs2.unlink(filePath);
|
|
753
|
+
} catch {
|
|
754
|
+
}
|
|
755
|
+
}
|
|
460
756
|
async function isGitRepo(vaultPath2) {
|
|
461
757
|
try {
|
|
462
758
|
const git = simpleGit(vaultPath2);
|
|
@@ -480,6 +776,9 @@ async function commitChange(vaultPath2, filePath, messagePrefix) {
|
|
|
480
776
|
const fileName = path2.basename(filePath);
|
|
481
777
|
const commitMessage = `${messagePrefix} Update ${fileName}`;
|
|
482
778
|
const result = await git.commit(commitMessage);
|
|
779
|
+
if (result.commit) {
|
|
780
|
+
await saveLastCrankCommit(vaultPath2, result.commit, commitMessage);
|
|
781
|
+
}
|
|
483
782
|
return {
|
|
484
783
|
success: true,
|
|
485
784
|
hash: result.commit
|
|
@@ -1755,7 +2054,7 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
1755
2054
|
}
|
|
1756
2055
|
|
|
1757
2056
|
// src/tools/mutations.ts
|
|
1758
|
-
import
|
|
2057
|
+
import fs3 from "fs/promises";
|
|
1759
2058
|
import path5 from "path";
|
|
1760
2059
|
function registerMutationTools(server2, vaultPath2) {
|
|
1761
2060
|
server2.tool(
|
|
@@ -1771,13 +2070,16 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1771
2070
|
skipWikilinks: z.boolean().default(false).describe("If true, skip auto-wikilink application (wikilinks are applied by default)"),
|
|
1772
2071
|
preserveListNesting: z.boolean().default(true).describe("Detect and preserve the indentation level of surrounding list items. Set false to disable."),
|
|
1773
2072
|
suggestOutgoingLinks: z.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.'),
|
|
1774
|
-
maxSuggestions: z.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)")
|
|
2073
|
+
maxSuggestions: z.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)"),
|
|
2074
|
+
validate: z.boolean().default(true).describe("Check input for common issues (double timestamps, non-markdown bullets, etc.)"),
|
|
2075
|
+
normalize: z.boolean().default(true).describe("Auto-fix common issues before formatting (replace \u2022 with -, trim excessive whitespace, etc.)"),
|
|
2076
|
+
guardrails: z.enum(["warn", "strict", "off"]).default("warn").describe('Output validation mode: "warn" returns issues but proceeds, "strict" blocks on errors, "off" disables')
|
|
1775
2077
|
},
|
|
1776
|
-
async ({ path: notePath, section, content, position, format, commit, skipWikilinks, preserveListNesting, suggestOutgoingLinks, maxSuggestions }) => {
|
|
2078
|
+
async ({ path: notePath, section, content, position, format, commit, skipWikilinks, preserveListNesting, suggestOutgoingLinks, maxSuggestions, validate, normalize, guardrails }) => {
|
|
1777
2079
|
try {
|
|
1778
2080
|
const fullPath = path5.join(vaultPath2, notePath);
|
|
1779
2081
|
try {
|
|
1780
|
-
await
|
|
2082
|
+
await fs3.access(fullPath);
|
|
1781
2083
|
} catch {
|
|
1782
2084
|
const result2 = {
|
|
1783
2085
|
success: false,
|
|
@@ -1800,7 +2102,25 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1800
2102
|
result2.tokensEstimate = estimateTokens(result2);
|
|
1801
2103
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
1802
2104
|
}
|
|
1803
|
-
|
|
2105
|
+
const validationResult = runValidationPipeline(content, format, {
|
|
2106
|
+
validate,
|
|
2107
|
+
normalize,
|
|
2108
|
+
guardrails
|
|
2109
|
+
});
|
|
2110
|
+
if (validationResult.blocked) {
|
|
2111
|
+
const result2 = {
|
|
2112
|
+
success: false,
|
|
2113
|
+
message: validationResult.blockReason || "Output validation failed",
|
|
2114
|
+
path: notePath,
|
|
2115
|
+
warnings: validationResult.inputWarnings,
|
|
2116
|
+
outputIssues: validationResult.outputIssues,
|
|
2117
|
+
tokensEstimate: 0
|
|
2118
|
+
};
|
|
2119
|
+
result2.tokensEstimate = estimateTokens(result2);
|
|
2120
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2121
|
+
}
|
|
2122
|
+
let workingContent = validationResult.content;
|
|
2123
|
+
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(workingContent, skipWikilinks);
|
|
1804
2124
|
let suggestInfo;
|
|
1805
2125
|
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
1806
2126
|
const result2 = suggestRelatedLinks(processedContent, { maxSuggestions });
|
|
@@ -1838,8 +2158,12 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1838
2158
|
preview,
|
|
1839
2159
|
gitCommit,
|
|
1840
2160
|
gitError,
|
|
1841
|
-
tokensEstimate: 0
|
|
2161
|
+
tokensEstimate: 0,
|
|
1842
2162
|
// Will be set below
|
|
2163
|
+
// Include validation info if present
|
|
2164
|
+
...validationResult.inputWarnings.length > 0 && { warnings: validationResult.inputWarnings },
|
|
2165
|
+
...validationResult.outputIssues.length > 0 && { outputIssues: validationResult.outputIssues },
|
|
2166
|
+
...validationResult.normalizationChanges.length > 0 && { normalizationChanges: validationResult.normalizationChanges }
|
|
1843
2167
|
};
|
|
1844
2168
|
result.tokensEstimate = estimateTokens(result);
|
|
1845
2169
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -1870,7 +2194,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1870
2194
|
try {
|
|
1871
2195
|
const fullPath = path5.join(vaultPath2, notePath);
|
|
1872
2196
|
try {
|
|
1873
|
-
await
|
|
2197
|
+
await fs3.access(fullPath);
|
|
1874
2198
|
} catch {
|
|
1875
2199
|
const result2 = {
|
|
1876
2200
|
success: false,
|
|
@@ -1957,13 +2281,16 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1957
2281
|
commit: z.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
|
|
1958
2282
|
skipWikilinks: z.boolean().default(false).describe("If true, skip auto-wikilink application on replacement text"),
|
|
1959
2283
|
suggestOutgoingLinks: z.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.'),
|
|
1960
|
-
maxSuggestions: z.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)")
|
|
2284
|
+
maxSuggestions: z.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)"),
|
|
2285
|
+
validate: z.boolean().default(true).describe("Check input for common issues (double timestamps, non-markdown bullets, etc.)"),
|
|
2286
|
+
normalize: z.boolean().default(true).describe("Auto-fix common issues before formatting (replace \u2022 with -, trim excessive whitespace, etc.)"),
|
|
2287
|
+
guardrails: z.enum(["warn", "strict", "off"]).default("warn").describe('Output validation mode: "warn" returns issues but proceeds, "strict" blocks on errors, "off" disables')
|
|
1961
2288
|
},
|
|
1962
|
-
async ({ path: notePath, section, search, replacement, mode, useRegex, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions }) => {
|
|
2289
|
+
async ({ path: notePath, section, search, replacement, mode, useRegex, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions, validate, normalize, guardrails }) => {
|
|
1963
2290
|
try {
|
|
1964
2291
|
const fullPath = path5.join(vaultPath2, notePath);
|
|
1965
2292
|
try {
|
|
1966
|
-
await
|
|
2293
|
+
await fs3.access(fullPath);
|
|
1967
2294
|
} catch {
|
|
1968
2295
|
const result2 = {
|
|
1969
2296
|
success: false,
|
|
@@ -1986,7 +2313,25 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1986
2313
|
result2.tokensEstimate = estimateTokens(result2);
|
|
1987
2314
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
1988
2315
|
}
|
|
1989
|
-
|
|
2316
|
+
const validationResult = runValidationPipeline(replacement, "plain", {
|
|
2317
|
+
validate,
|
|
2318
|
+
normalize,
|
|
2319
|
+
guardrails
|
|
2320
|
+
});
|
|
2321
|
+
if (validationResult.blocked) {
|
|
2322
|
+
const result2 = {
|
|
2323
|
+
success: false,
|
|
2324
|
+
message: validationResult.blockReason || "Output validation failed",
|
|
2325
|
+
path: notePath,
|
|
2326
|
+
warnings: validationResult.inputWarnings,
|
|
2327
|
+
outputIssues: validationResult.outputIssues,
|
|
2328
|
+
tokensEstimate: 0
|
|
2329
|
+
};
|
|
2330
|
+
result2.tokensEstimate = estimateTokens(result2);
|
|
2331
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2332
|
+
}
|
|
2333
|
+
let workingReplacement = validationResult.content;
|
|
2334
|
+
let { content: processedReplacement, wikilinkInfo } = maybeApplyWikilinks(workingReplacement, skipWikilinks);
|
|
1990
2335
|
let suggestInfo;
|
|
1991
2336
|
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
1992
2337
|
const result2 = suggestRelatedLinks(processedReplacement, { maxSuggestions });
|
|
@@ -2035,7 +2380,11 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
2035
2380
|
preview: previewLines.join("\n"),
|
|
2036
2381
|
gitCommit,
|
|
2037
2382
|
gitError,
|
|
2038
|
-
tokensEstimate: 0
|
|
2383
|
+
tokensEstimate: 0,
|
|
2384
|
+
// Include validation info if present
|
|
2385
|
+
...validationResult.inputWarnings.length > 0 && { warnings: validationResult.inputWarnings },
|
|
2386
|
+
...validationResult.outputIssues.length > 0 && { outputIssues: validationResult.outputIssues },
|
|
2387
|
+
...validationResult.normalizationChanges.length > 0 && { normalizationChanges: validationResult.normalizationChanges }
|
|
2039
2388
|
};
|
|
2040
2389
|
result.tokensEstimate = estimateTokens(result);
|
|
2041
2390
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -2056,7 +2405,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
2056
2405
|
|
|
2057
2406
|
// src/tools/tasks.ts
|
|
2058
2407
|
import { z as z2 } from "zod";
|
|
2059
|
-
import
|
|
2408
|
+
import fs4 from "fs/promises";
|
|
2060
2409
|
import path6 from "path";
|
|
2061
2410
|
var TASK_REGEX = /^(\s*)-\s*\[([ xX])\]\s*(.*)$/;
|
|
2062
2411
|
function findTasks(content, section) {
|
|
@@ -2115,7 +2464,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
2115
2464
|
try {
|
|
2116
2465
|
const fullPath = path6.join(vaultPath2, notePath);
|
|
2117
2466
|
try {
|
|
2118
|
-
await
|
|
2467
|
+
await fs4.access(fullPath);
|
|
2119
2468
|
} catch {
|
|
2120
2469
|
const result2 = {
|
|
2121
2470
|
success: false,
|
|
@@ -2217,13 +2566,16 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
2217
2566
|
skipWikilinks: z2.boolean().default(false).describe("If true, skip auto-wikilink application (wikilinks are applied by default)"),
|
|
2218
2567
|
suggestOutgoingLinks: z2.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.'),
|
|
2219
2568
|
maxSuggestions: z2.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)"),
|
|
2220
|
-
preserveListNesting: z2.boolean().default(true).describe("Preserve indentation when inserting into nested lists. Default: true")
|
|
2569
|
+
preserveListNesting: z2.boolean().default(true).describe("Preserve indentation when inserting into nested lists. Default: true"),
|
|
2570
|
+
validate: z2.boolean().default(true).describe("Check input for common issues"),
|
|
2571
|
+
normalize: z2.boolean().default(true).describe("Auto-fix common issues before formatting"),
|
|
2572
|
+
guardrails: z2.enum(["warn", "strict", "off"]).default("warn").describe("Output validation mode")
|
|
2221
2573
|
},
|
|
2222
|
-
async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions, preserveListNesting }) => {
|
|
2574
|
+
async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions, preserveListNesting, validate, normalize, guardrails }) => {
|
|
2223
2575
|
try {
|
|
2224
2576
|
const fullPath = path6.join(vaultPath2, notePath);
|
|
2225
2577
|
try {
|
|
2226
|
-
await
|
|
2578
|
+
await fs4.access(fullPath);
|
|
2227
2579
|
} catch {
|
|
2228
2580
|
const result2 = {
|
|
2229
2581
|
success: false,
|
|
@@ -2246,7 +2598,25 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
2246
2598
|
result2.tokensEstimate = estimateTokens(result2);
|
|
2247
2599
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2248
2600
|
}
|
|
2249
|
-
|
|
2601
|
+
const validationResult = runValidationPipeline(task.trim(), "task", {
|
|
2602
|
+
validate,
|
|
2603
|
+
normalize,
|
|
2604
|
+
guardrails
|
|
2605
|
+
});
|
|
2606
|
+
if (validationResult.blocked) {
|
|
2607
|
+
const result2 = {
|
|
2608
|
+
success: false,
|
|
2609
|
+
message: validationResult.blockReason || "Output validation failed",
|
|
2610
|
+
path: notePath,
|
|
2611
|
+
warnings: validationResult.inputWarnings,
|
|
2612
|
+
outputIssues: validationResult.outputIssues,
|
|
2613
|
+
tokensEstimate: 0
|
|
2614
|
+
};
|
|
2615
|
+
result2.tokensEstimate = estimateTokens(result2);
|
|
2616
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2617
|
+
}
|
|
2618
|
+
let workingTask = validationResult.content;
|
|
2619
|
+
let { content: processedTask, wikilinkInfo } = maybeApplyWikilinks(workingTask, skipWikilinks);
|
|
2250
2620
|
let suggestInfo;
|
|
2251
2621
|
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
2252
2622
|
const result2 = suggestRelatedLinks(processedTask, { maxSuggestions });
|
|
@@ -2284,7 +2654,11 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
2284
2654
|
(${infoLines.join("; ")})` : ""),
|
|
2285
2655
|
gitCommit,
|
|
2286
2656
|
gitError,
|
|
2287
|
-
tokensEstimate: 0
|
|
2657
|
+
tokensEstimate: 0,
|
|
2658
|
+
// Include validation info if present
|
|
2659
|
+
...validationResult.inputWarnings.length > 0 && { warnings: validationResult.inputWarnings },
|
|
2660
|
+
...validationResult.outputIssues.length > 0 && { outputIssues: validationResult.outputIssues },
|
|
2661
|
+
...validationResult.normalizationChanges.length > 0 && { normalizationChanges: validationResult.normalizationChanges }
|
|
2288
2662
|
};
|
|
2289
2663
|
result.tokensEstimate = estimateTokens(result);
|
|
2290
2664
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -2305,7 +2679,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
2305
2679
|
|
|
2306
2680
|
// src/tools/frontmatter.ts
|
|
2307
2681
|
import { z as z3 } from "zod";
|
|
2308
|
-
import
|
|
2682
|
+
import fs5 from "fs/promises";
|
|
2309
2683
|
import path7 from "path";
|
|
2310
2684
|
function registerFrontmatterTools(server2, vaultPath2) {
|
|
2311
2685
|
server2.tool(
|
|
@@ -2320,7 +2694,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
2320
2694
|
try {
|
|
2321
2695
|
const fullPath = path7.join(vaultPath2, notePath);
|
|
2322
2696
|
try {
|
|
2323
|
-
await
|
|
2697
|
+
await fs5.access(fullPath);
|
|
2324
2698
|
} catch {
|
|
2325
2699
|
const result2 = {
|
|
2326
2700
|
success: false,
|
|
@@ -2376,7 +2750,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
2376
2750
|
try {
|
|
2377
2751
|
const fullPath = path7.join(vaultPath2, notePath);
|
|
2378
2752
|
try {
|
|
2379
|
-
await
|
|
2753
|
+
await fs5.access(fullPath);
|
|
2380
2754
|
} catch {
|
|
2381
2755
|
const result2 = {
|
|
2382
2756
|
success: false,
|
|
@@ -2430,7 +2804,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
2430
2804
|
|
|
2431
2805
|
// src/tools/notes.ts
|
|
2432
2806
|
import { z as z4 } from "zod";
|
|
2433
|
-
import
|
|
2807
|
+
import fs6 from "fs/promises";
|
|
2434
2808
|
import path8 from "path";
|
|
2435
2809
|
function registerNoteTools(server2, vaultPath2) {
|
|
2436
2810
|
server2.tool(
|
|
@@ -2455,7 +2829,7 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
2455
2829
|
}
|
|
2456
2830
|
const fullPath = path8.join(vaultPath2, notePath);
|
|
2457
2831
|
try {
|
|
2458
|
-
await
|
|
2832
|
+
await fs6.access(fullPath);
|
|
2459
2833
|
if (!overwrite) {
|
|
2460
2834
|
const result2 = {
|
|
2461
2835
|
success: false,
|
|
@@ -2467,7 +2841,7 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
2467
2841
|
} catch {
|
|
2468
2842
|
}
|
|
2469
2843
|
const dir = path8.dirname(fullPath);
|
|
2470
|
-
await
|
|
2844
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
2471
2845
|
await writeVaultFile(vaultPath2, notePath, content, frontmatter);
|
|
2472
2846
|
let gitCommit;
|
|
2473
2847
|
let gitError;
|
|
@@ -2527,7 +2901,7 @@ Content length: ${content.length} chars`,
|
|
|
2527
2901
|
}
|
|
2528
2902
|
const fullPath = path8.join(vaultPath2, notePath);
|
|
2529
2903
|
try {
|
|
2530
|
-
await
|
|
2904
|
+
await fs6.access(fullPath);
|
|
2531
2905
|
} catch {
|
|
2532
2906
|
const result2 = {
|
|
2533
2907
|
success: false,
|
|
@@ -2536,7 +2910,7 @@ Content length: ${content.length} chars`,
|
|
|
2536
2910
|
};
|
|
2537
2911
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2538
2912
|
}
|
|
2539
|
-
await
|
|
2913
|
+
await fs6.unlink(fullPath);
|
|
2540
2914
|
let gitCommit;
|
|
2541
2915
|
let gitError;
|
|
2542
2916
|
if (commit) {
|
|
@@ -2570,7 +2944,7 @@ Content length: ${content.length} chars`,
|
|
|
2570
2944
|
|
|
2571
2945
|
// src/tools/system.ts
|
|
2572
2946
|
import { z as z5 } from "zod";
|
|
2573
|
-
import
|
|
2947
|
+
import fs7 from "fs/promises";
|
|
2574
2948
|
import path9 from "path";
|
|
2575
2949
|
function registerSystemTools(server2, vaultPath2) {
|
|
2576
2950
|
server2.tool(
|
|
@@ -2593,7 +2967,7 @@ function registerSystemTools(server2, vaultPath2) {
|
|
|
2593
2967
|
}
|
|
2594
2968
|
const fullPath = path9.join(vaultPath2, notePath);
|
|
2595
2969
|
try {
|
|
2596
|
-
await
|
|
2970
|
+
await fs7.access(fullPath);
|
|
2597
2971
|
} catch {
|
|
2598
2972
|
const result2 = {
|
|
2599
2973
|
success: false,
|
|
@@ -2639,8 +3013,8 @@ function registerSystemTools(server2, vaultPath2) {
|
|
|
2639
3013
|
async ({ confirm }) => {
|
|
2640
3014
|
try {
|
|
2641
3015
|
if (!confirm) {
|
|
2642
|
-
const
|
|
2643
|
-
if (!
|
|
3016
|
+
const lastCommit2 = await getLastCommit(vaultPath2);
|
|
3017
|
+
if (!lastCommit2) {
|
|
2644
3018
|
const result3 = {
|
|
2645
3019
|
success: false,
|
|
2646
3020
|
message: "No commits found to undo",
|
|
@@ -2650,11 +3024,11 @@ function registerSystemTools(server2, vaultPath2) {
|
|
|
2650
3024
|
}
|
|
2651
3025
|
const result2 = {
|
|
2652
3026
|
success: false,
|
|
2653
|
-
message: `Undo requires confirmation (confirm=true). Would undo: "${
|
|
3027
|
+
message: `Undo requires confirmation (confirm=true). Would undo: "${lastCommit2.message}"`,
|
|
2654
3028
|
path: "",
|
|
2655
|
-
preview: `Commit: ${
|
|
2656
|
-
Message: ${
|
|
2657
|
-
Date: ${
|
|
3029
|
+
preview: `Commit: ${lastCommit2.hash.substring(0, 7)}
|
|
3030
|
+
Message: ${lastCommit2.message}
|
|
3031
|
+
Date: ${lastCommit2.date}`
|
|
2658
3032
|
};
|
|
2659
3033
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2660
3034
|
}
|
|
@@ -2667,6 +3041,20 @@ Date: ${lastCommit.date}`
|
|
|
2667
3041
|
};
|
|
2668
3042
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2669
3043
|
}
|
|
3044
|
+
const lastCrankCommit = await getLastCrankCommit(vaultPath2);
|
|
3045
|
+
const lastCommit = await getLastCommit(vaultPath2);
|
|
3046
|
+
if (lastCrankCommit && lastCommit) {
|
|
3047
|
+
if (lastCommit.hash !== lastCrankCommit.hash) {
|
|
3048
|
+
const result2 = {
|
|
3049
|
+
success: false,
|
|
3050
|
+
message: `Cannot undo: HEAD (${lastCommit.hash.substring(0, 7)}) doesn't match last Crank commit (${lastCrankCommit.hash.substring(0, 7)}). Another process may have committed since your mutation.`,
|
|
3051
|
+
path: "",
|
|
3052
|
+
preview: `Expected: ${lastCrankCommit.hash.substring(0, 7)} "${lastCrankCommit.message}"
|
|
3053
|
+
Actual HEAD: ${lastCommit.hash.substring(0, 7)} "${lastCommit.message}"`
|
|
3054
|
+
};
|
|
3055
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
2670
3058
|
const undoResult = await undoLastCommit(vaultPath2);
|
|
2671
3059
|
if (!undoResult.success) {
|
|
2672
3060
|
const result2 = {
|
|
@@ -2676,6 +3064,7 @@ Date: ${lastCommit.date}`
|
|
|
2676
3064
|
};
|
|
2677
3065
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2678
3066
|
}
|
|
3067
|
+
await clearLastCrankCommit(vaultPath2);
|
|
2679
3068
|
const result = {
|
|
2680
3069
|
success: true,
|
|
2681
3070
|
message: undoResult.message,
|
|
@@ -2698,7 +3087,7 @@ Message: ${undoResult.undoneCommit.message}` : void 0
|
|
|
2698
3087
|
}
|
|
2699
3088
|
|
|
2700
3089
|
// src/core/vaultRoot.ts
|
|
2701
|
-
import * as
|
|
3090
|
+
import * as fs8 from "fs";
|
|
2702
3091
|
import * as path10 from "path";
|
|
2703
3092
|
var VAULT_MARKERS = [".obsidian", ".claude"];
|
|
2704
3093
|
function findVaultRoot(startPath) {
|
|
@@ -2706,7 +3095,7 @@ function findVaultRoot(startPath) {
|
|
|
2706
3095
|
while (true) {
|
|
2707
3096
|
for (const marker of VAULT_MARKERS) {
|
|
2708
3097
|
const markerPath = path10.join(current, marker);
|
|
2709
|
-
if (
|
|
3098
|
+
if (fs8.existsSync(markerPath) && fs8.statSync(markerPath).isDirectory()) {
|
|
2710
3099
|
return current;
|
|
2711
3100
|
}
|
|
2712
3101
|
}
|