@velvetmonkey/flywheel-crank 0.10.1 → 0.11.2
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 +456 -43
- 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,
|
|
@@ -1791,16 +2093,42 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1791
2093
|
const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
|
|
1792
2094
|
const sectionBoundary = findSection(fileContent, section);
|
|
1793
2095
|
if (!sectionBoundary) {
|
|
2096
|
+
const headings = extractHeadings(fileContent);
|
|
2097
|
+
let message;
|
|
2098
|
+
if (headings.length === 0) {
|
|
2099
|
+
message = `Section '${section}' not found. This file has no headings. Use vault_append_to_note for files without section structure.`;
|
|
2100
|
+
} else {
|
|
2101
|
+
const availableSections = headings.map((h) => h.text).join(", ");
|
|
2102
|
+
message = `Section '${section}' not found. Available sections: ${availableSections}`;
|
|
2103
|
+
}
|
|
1794
2104
|
const result2 = {
|
|
1795
2105
|
success: false,
|
|
1796
|
-
message
|
|
2106
|
+
message,
|
|
2107
|
+
path: notePath,
|
|
2108
|
+
tokensEstimate: 0
|
|
2109
|
+
};
|
|
2110
|
+
result2.tokensEstimate = estimateTokens(result2);
|
|
2111
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2112
|
+
}
|
|
2113
|
+
const validationResult = runValidationPipeline(content, format, {
|
|
2114
|
+
validate,
|
|
2115
|
+
normalize,
|
|
2116
|
+
guardrails
|
|
2117
|
+
});
|
|
2118
|
+
if (validationResult.blocked) {
|
|
2119
|
+
const result2 = {
|
|
2120
|
+
success: false,
|
|
2121
|
+
message: validationResult.blockReason || "Output validation failed",
|
|
1797
2122
|
path: notePath,
|
|
2123
|
+
warnings: validationResult.inputWarnings,
|
|
2124
|
+
outputIssues: validationResult.outputIssues,
|
|
1798
2125
|
tokensEstimate: 0
|
|
1799
2126
|
};
|
|
1800
2127
|
result2.tokensEstimate = estimateTokens(result2);
|
|
1801
2128
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
1802
2129
|
}
|
|
1803
|
-
let
|
|
2130
|
+
let workingContent = validationResult.content;
|
|
2131
|
+
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(workingContent, skipWikilinks);
|
|
1804
2132
|
let suggestInfo;
|
|
1805
2133
|
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
1806
2134
|
const result2 = suggestRelatedLinks(processedContent, { maxSuggestions });
|
|
@@ -1838,8 +2166,12 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1838
2166
|
preview,
|
|
1839
2167
|
gitCommit,
|
|
1840
2168
|
gitError,
|
|
1841
|
-
tokensEstimate: 0
|
|
2169
|
+
tokensEstimate: 0,
|
|
1842
2170
|
// Will be set below
|
|
2171
|
+
// Include validation info if present
|
|
2172
|
+
...validationResult.inputWarnings.length > 0 && { warnings: validationResult.inputWarnings },
|
|
2173
|
+
...validationResult.outputIssues.length > 0 && { outputIssues: validationResult.outputIssues },
|
|
2174
|
+
...validationResult.normalizationChanges.length > 0 && { normalizationChanges: validationResult.normalizationChanges }
|
|
1843
2175
|
};
|
|
1844
2176
|
result.tokensEstimate = estimateTokens(result);
|
|
1845
2177
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -1870,7 +2202,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1870
2202
|
try {
|
|
1871
2203
|
const fullPath = path5.join(vaultPath2, notePath);
|
|
1872
2204
|
try {
|
|
1873
|
-
await
|
|
2205
|
+
await fs3.access(fullPath);
|
|
1874
2206
|
} catch {
|
|
1875
2207
|
const result2 = {
|
|
1876
2208
|
success: false,
|
|
@@ -1884,9 +2216,17 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1884
2216
|
const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
|
|
1885
2217
|
const sectionBoundary = findSection(fileContent, section);
|
|
1886
2218
|
if (!sectionBoundary) {
|
|
2219
|
+
const headings = extractHeadings(fileContent);
|
|
2220
|
+
let message;
|
|
2221
|
+
if (headings.length === 0) {
|
|
2222
|
+
message = `Section '${section}' not found. This file has no headings. Use vault_append_to_note for files without section structure.`;
|
|
2223
|
+
} else {
|
|
2224
|
+
const availableSections = headings.map((h) => h.text).join(", ");
|
|
2225
|
+
message = `Section '${section}' not found. Available sections: ${availableSections}`;
|
|
2226
|
+
}
|
|
1887
2227
|
const result2 = {
|
|
1888
2228
|
success: false,
|
|
1889
|
-
message
|
|
2229
|
+
message,
|
|
1890
2230
|
path: notePath,
|
|
1891
2231
|
tokensEstimate: 0
|
|
1892
2232
|
};
|
|
@@ -1957,13 +2297,16 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1957
2297
|
commit: z.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
|
|
1958
2298
|
skipWikilinks: z.boolean().default(false).describe("If true, skip auto-wikilink application on replacement text"),
|
|
1959
2299
|
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)")
|
|
2300
|
+
maxSuggestions: z.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)"),
|
|
2301
|
+
validate: z.boolean().default(true).describe("Check input for common issues (double timestamps, non-markdown bullets, etc.)"),
|
|
2302
|
+
normalize: z.boolean().default(true).describe("Auto-fix common issues before formatting (replace \u2022 with -, trim excessive whitespace, etc.)"),
|
|
2303
|
+
guardrails: z.enum(["warn", "strict", "off"]).default("warn").describe('Output validation mode: "warn" returns issues but proceeds, "strict" blocks on errors, "off" disables')
|
|
1961
2304
|
},
|
|
1962
|
-
async ({ path: notePath, section, search, replacement, mode, useRegex, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions }) => {
|
|
2305
|
+
async ({ path: notePath, section, search, replacement, mode, useRegex, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions, validate, normalize, guardrails }) => {
|
|
1963
2306
|
try {
|
|
1964
2307
|
const fullPath = path5.join(vaultPath2, notePath);
|
|
1965
2308
|
try {
|
|
1966
|
-
await
|
|
2309
|
+
await fs3.access(fullPath);
|
|
1967
2310
|
} catch {
|
|
1968
2311
|
const result2 = {
|
|
1969
2312
|
success: false,
|
|
@@ -1977,16 +2320,42 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1977
2320
|
const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
|
|
1978
2321
|
const sectionBoundary = findSection(fileContent, section);
|
|
1979
2322
|
if (!sectionBoundary) {
|
|
2323
|
+
const headings = extractHeadings(fileContent);
|
|
2324
|
+
let message;
|
|
2325
|
+
if (headings.length === 0) {
|
|
2326
|
+
message = `Section '${section}' not found. This file has no headings. Use vault_append_to_note for files without section structure.`;
|
|
2327
|
+
} else {
|
|
2328
|
+
const availableSections = headings.map((h) => h.text).join(", ");
|
|
2329
|
+
message = `Section '${section}' not found. Available sections: ${availableSections}`;
|
|
2330
|
+
}
|
|
1980
2331
|
const result2 = {
|
|
1981
2332
|
success: false,
|
|
1982
|
-
message
|
|
2333
|
+
message,
|
|
1983
2334
|
path: notePath,
|
|
1984
2335
|
tokensEstimate: 0
|
|
1985
2336
|
};
|
|
1986
2337
|
result2.tokensEstimate = estimateTokens(result2);
|
|
1987
2338
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
1988
2339
|
}
|
|
1989
|
-
|
|
2340
|
+
const validationResult = runValidationPipeline(replacement, "plain", {
|
|
2341
|
+
validate,
|
|
2342
|
+
normalize,
|
|
2343
|
+
guardrails
|
|
2344
|
+
});
|
|
2345
|
+
if (validationResult.blocked) {
|
|
2346
|
+
const result2 = {
|
|
2347
|
+
success: false,
|
|
2348
|
+
message: validationResult.blockReason || "Output validation failed",
|
|
2349
|
+
path: notePath,
|
|
2350
|
+
warnings: validationResult.inputWarnings,
|
|
2351
|
+
outputIssues: validationResult.outputIssues,
|
|
2352
|
+
tokensEstimate: 0
|
|
2353
|
+
};
|
|
2354
|
+
result2.tokensEstimate = estimateTokens(result2);
|
|
2355
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2356
|
+
}
|
|
2357
|
+
let workingReplacement = validationResult.content;
|
|
2358
|
+
let { content: processedReplacement, wikilinkInfo } = maybeApplyWikilinks(workingReplacement, skipWikilinks);
|
|
1990
2359
|
let suggestInfo;
|
|
1991
2360
|
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
1992
2361
|
const result2 = suggestRelatedLinks(processedReplacement, { maxSuggestions });
|
|
@@ -2035,7 +2404,11 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
2035
2404
|
preview: previewLines.join("\n"),
|
|
2036
2405
|
gitCommit,
|
|
2037
2406
|
gitError,
|
|
2038
|
-
tokensEstimate: 0
|
|
2407
|
+
tokensEstimate: 0,
|
|
2408
|
+
// Include validation info if present
|
|
2409
|
+
...validationResult.inputWarnings.length > 0 && { warnings: validationResult.inputWarnings },
|
|
2410
|
+
...validationResult.outputIssues.length > 0 && { outputIssues: validationResult.outputIssues },
|
|
2411
|
+
...validationResult.normalizationChanges.length > 0 && { normalizationChanges: validationResult.normalizationChanges }
|
|
2039
2412
|
};
|
|
2040
2413
|
result.tokensEstimate = estimateTokens(result);
|
|
2041
2414
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -2056,7 +2429,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
2056
2429
|
|
|
2057
2430
|
// src/tools/tasks.ts
|
|
2058
2431
|
import { z as z2 } from "zod";
|
|
2059
|
-
import
|
|
2432
|
+
import fs4 from "fs/promises";
|
|
2060
2433
|
import path6 from "path";
|
|
2061
2434
|
var TASK_REGEX = /^(\s*)-\s*\[([ xX])\]\s*(.*)$/;
|
|
2062
2435
|
function findTasks(content, section) {
|
|
@@ -2115,7 +2488,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
2115
2488
|
try {
|
|
2116
2489
|
const fullPath = path6.join(vaultPath2, notePath);
|
|
2117
2490
|
try {
|
|
2118
|
-
await
|
|
2491
|
+
await fs4.access(fullPath);
|
|
2119
2492
|
} catch {
|
|
2120
2493
|
const result2 = {
|
|
2121
2494
|
success: false,
|
|
@@ -2217,13 +2590,16 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
2217
2590
|
skipWikilinks: z2.boolean().default(false).describe("If true, skip auto-wikilink application (wikilinks are applied by default)"),
|
|
2218
2591
|
suggestOutgoingLinks: z2.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.'),
|
|
2219
2592
|
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")
|
|
2593
|
+
preserveListNesting: z2.boolean().default(true).describe("Preserve indentation when inserting into nested lists. Default: true"),
|
|
2594
|
+
validate: z2.boolean().default(true).describe("Check input for common issues"),
|
|
2595
|
+
normalize: z2.boolean().default(true).describe("Auto-fix common issues before formatting"),
|
|
2596
|
+
guardrails: z2.enum(["warn", "strict", "off"]).default("warn").describe("Output validation mode")
|
|
2221
2597
|
},
|
|
2222
|
-
async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions, preserveListNesting }) => {
|
|
2598
|
+
async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions, preserveListNesting, validate, normalize, guardrails }) => {
|
|
2223
2599
|
try {
|
|
2224
2600
|
const fullPath = path6.join(vaultPath2, notePath);
|
|
2225
2601
|
try {
|
|
2226
|
-
await
|
|
2602
|
+
await fs4.access(fullPath);
|
|
2227
2603
|
} catch {
|
|
2228
2604
|
const result2 = {
|
|
2229
2605
|
success: false,
|
|
@@ -2246,7 +2622,25 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
2246
2622
|
result2.tokensEstimate = estimateTokens(result2);
|
|
2247
2623
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2248
2624
|
}
|
|
2249
|
-
|
|
2625
|
+
const validationResult = runValidationPipeline(task.trim(), "task", {
|
|
2626
|
+
validate,
|
|
2627
|
+
normalize,
|
|
2628
|
+
guardrails
|
|
2629
|
+
});
|
|
2630
|
+
if (validationResult.blocked) {
|
|
2631
|
+
const result2 = {
|
|
2632
|
+
success: false,
|
|
2633
|
+
message: validationResult.blockReason || "Output validation failed",
|
|
2634
|
+
path: notePath,
|
|
2635
|
+
warnings: validationResult.inputWarnings,
|
|
2636
|
+
outputIssues: validationResult.outputIssues,
|
|
2637
|
+
tokensEstimate: 0
|
|
2638
|
+
};
|
|
2639
|
+
result2.tokensEstimate = estimateTokens(result2);
|
|
2640
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2641
|
+
}
|
|
2642
|
+
let workingTask = validationResult.content;
|
|
2643
|
+
let { content: processedTask, wikilinkInfo } = maybeApplyWikilinks(workingTask, skipWikilinks);
|
|
2250
2644
|
let suggestInfo;
|
|
2251
2645
|
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
2252
2646
|
const result2 = suggestRelatedLinks(processedTask, { maxSuggestions });
|
|
@@ -2284,7 +2678,11 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
2284
2678
|
(${infoLines.join("; ")})` : ""),
|
|
2285
2679
|
gitCommit,
|
|
2286
2680
|
gitError,
|
|
2287
|
-
tokensEstimate: 0
|
|
2681
|
+
tokensEstimate: 0,
|
|
2682
|
+
// Include validation info if present
|
|
2683
|
+
...validationResult.inputWarnings.length > 0 && { warnings: validationResult.inputWarnings },
|
|
2684
|
+
...validationResult.outputIssues.length > 0 && { outputIssues: validationResult.outputIssues },
|
|
2685
|
+
...validationResult.normalizationChanges.length > 0 && { normalizationChanges: validationResult.normalizationChanges }
|
|
2288
2686
|
};
|
|
2289
2687
|
result.tokensEstimate = estimateTokens(result);
|
|
2290
2688
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -2305,7 +2703,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
2305
2703
|
|
|
2306
2704
|
// src/tools/frontmatter.ts
|
|
2307
2705
|
import { z as z3 } from "zod";
|
|
2308
|
-
import
|
|
2706
|
+
import fs5 from "fs/promises";
|
|
2309
2707
|
import path7 from "path";
|
|
2310
2708
|
function registerFrontmatterTools(server2, vaultPath2) {
|
|
2311
2709
|
server2.tool(
|
|
@@ -2320,7 +2718,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
2320
2718
|
try {
|
|
2321
2719
|
const fullPath = path7.join(vaultPath2, notePath);
|
|
2322
2720
|
try {
|
|
2323
|
-
await
|
|
2721
|
+
await fs5.access(fullPath);
|
|
2324
2722
|
} catch {
|
|
2325
2723
|
const result2 = {
|
|
2326
2724
|
success: false,
|
|
@@ -2376,7 +2774,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
2376
2774
|
try {
|
|
2377
2775
|
const fullPath = path7.join(vaultPath2, notePath);
|
|
2378
2776
|
try {
|
|
2379
|
-
await
|
|
2777
|
+
await fs5.access(fullPath);
|
|
2380
2778
|
} catch {
|
|
2381
2779
|
const result2 = {
|
|
2382
2780
|
success: false,
|
|
@@ -2430,7 +2828,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
2430
2828
|
|
|
2431
2829
|
// src/tools/notes.ts
|
|
2432
2830
|
import { z as z4 } from "zod";
|
|
2433
|
-
import
|
|
2831
|
+
import fs6 from "fs/promises";
|
|
2434
2832
|
import path8 from "path";
|
|
2435
2833
|
function registerNoteTools(server2, vaultPath2) {
|
|
2436
2834
|
server2.tool(
|
|
@@ -2455,7 +2853,7 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
2455
2853
|
}
|
|
2456
2854
|
const fullPath = path8.join(vaultPath2, notePath);
|
|
2457
2855
|
try {
|
|
2458
|
-
await
|
|
2856
|
+
await fs6.access(fullPath);
|
|
2459
2857
|
if (!overwrite) {
|
|
2460
2858
|
const result2 = {
|
|
2461
2859
|
success: false,
|
|
@@ -2467,7 +2865,7 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
2467
2865
|
} catch {
|
|
2468
2866
|
}
|
|
2469
2867
|
const dir = path8.dirname(fullPath);
|
|
2470
|
-
await
|
|
2868
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
2471
2869
|
await writeVaultFile(vaultPath2, notePath, content, frontmatter);
|
|
2472
2870
|
let gitCommit;
|
|
2473
2871
|
let gitError;
|
|
@@ -2527,7 +2925,7 @@ Content length: ${content.length} chars`,
|
|
|
2527
2925
|
}
|
|
2528
2926
|
const fullPath = path8.join(vaultPath2, notePath);
|
|
2529
2927
|
try {
|
|
2530
|
-
await
|
|
2928
|
+
await fs6.access(fullPath);
|
|
2531
2929
|
} catch {
|
|
2532
2930
|
const result2 = {
|
|
2533
2931
|
success: false,
|
|
@@ -2536,7 +2934,7 @@ Content length: ${content.length} chars`,
|
|
|
2536
2934
|
};
|
|
2537
2935
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2538
2936
|
}
|
|
2539
|
-
await
|
|
2937
|
+
await fs6.unlink(fullPath);
|
|
2540
2938
|
let gitCommit;
|
|
2541
2939
|
let gitError;
|
|
2542
2940
|
if (commit) {
|
|
@@ -2570,7 +2968,7 @@ Content length: ${content.length} chars`,
|
|
|
2570
2968
|
|
|
2571
2969
|
// src/tools/system.ts
|
|
2572
2970
|
import { z as z5 } from "zod";
|
|
2573
|
-
import
|
|
2971
|
+
import fs7 from "fs/promises";
|
|
2574
2972
|
import path9 from "path";
|
|
2575
2973
|
function registerSystemTools(server2, vaultPath2) {
|
|
2576
2974
|
server2.tool(
|
|
@@ -2593,7 +2991,7 @@ function registerSystemTools(server2, vaultPath2) {
|
|
|
2593
2991
|
}
|
|
2594
2992
|
const fullPath = path9.join(vaultPath2, notePath);
|
|
2595
2993
|
try {
|
|
2596
|
-
await
|
|
2994
|
+
await fs7.access(fullPath);
|
|
2597
2995
|
} catch {
|
|
2598
2996
|
const result2 = {
|
|
2599
2997
|
success: false,
|
|
@@ -2639,8 +3037,8 @@ function registerSystemTools(server2, vaultPath2) {
|
|
|
2639
3037
|
async ({ confirm }) => {
|
|
2640
3038
|
try {
|
|
2641
3039
|
if (!confirm) {
|
|
2642
|
-
const
|
|
2643
|
-
if (!
|
|
3040
|
+
const lastCommit2 = await getLastCommit(vaultPath2);
|
|
3041
|
+
if (!lastCommit2) {
|
|
2644
3042
|
const result3 = {
|
|
2645
3043
|
success: false,
|
|
2646
3044
|
message: "No commits found to undo",
|
|
@@ -2650,11 +3048,11 @@ function registerSystemTools(server2, vaultPath2) {
|
|
|
2650
3048
|
}
|
|
2651
3049
|
const result2 = {
|
|
2652
3050
|
success: false,
|
|
2653
|
-
message: `Undo requires confirmation (confirm=true). Would undo: "${
|
|
3051
|
+
message: `Undo requires confirmation (confirm=true). Would undo: "${lastCommit2.message}"`,
|
|
2654
3052
|
path: "",
|
|
2655
|
-
preview: `Commit: ${
|
|
2656
|
-
Message: ${
|
|
2657
|
-
Date: ${
|
|
3053
|
+
preview: `Commit: ${lastCommit2.hash.substring(0, 7)}
|
|
3054
|
+
Message: ${lastCommit2.message}
|
|
3055
|
+
Date: ${lastCommit2.date}`
|
|
2658
3056
|
};
|
|
2659
3057
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2660
3058
|
}
|
|
@@ -2667,6 +3065,20 @@ Date: ${lastCommit.date}`
|
|
|
2667
3065
|
};
|
|
2668
3066
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2669
3067
|
}
|
|
3068
|
+
const lastCrankCommit = await getLastCrankCommit(vaultPath2);
|
|
3069
|
+
const lastCommit = await getLastCommit(vaultPath2);
|
|
3070
|
+
if (lastCrankCommit && lastCommit) {
|
|
3071
|
+
if (lastCommit.hash !== lastCrankCommit.hash) {
|
|
3072
|
+
const result2 = {
|
|
3073
|
+
success: false,
|
|
3074
|
+
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.`,
|
|
3075
|
+
path: "",
|
|
3076
|
+
preview: `Expected: ${lastCrankCommit.hash.substring(0, 7)} "${lastCrankCommit.message}"
|
|
3077
|
+
Actual HEAD: ${lastCommit.hash.substring(0, 7)} "${lastCommit.message}"`
|
|
3078
|
+
};
|
|
3079
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
2670
3082
|
const undoResult = await undoLastCommit(vaultPath2);
|
|
2671
3083
|
if (!undoResult.success) {
|
|
2672
3084
|
const result2 = {
|
|
@@ -2676,6 +3088,7 @@ Date: ${lastCommit.date}`
|
|
|
2676
3088
|
};
|
|
2677
3089
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
2678
3090
|
}
|
|
3091
|
+
await clearLastCrankCommit(vaultPath2);
|
|
2679
3092
|
const result = {
|
|
2680
3093
|
success: true,
|
|
2681
3094
|
message: undoResult.message,
|
|
@@ -2698,7 +3111,7 @@ Message: ${undoResult.undoneCommit.message}` : void 0
|
|
|
2698
3111
|
}
|
|
2699
3112
|
|
|
2700
3113
|
// src/core/vaultRoot.ts
|
|
2701
|
-
import * as
|
|
3114
|
+
import * as fs8 from "fs";
|
|
2702
3115
|
import * as path10 from "path";
|
|
2703
3116
|
var VAULT_MARKERS = [".obsidian", ".claude"];
|
|
2704
3117
|
function findVaultRoot(startPath) {
|
|
@@ -2706,7 +3119,7 @@ function findVaultRoot(startPath) {
|
|
|
2706
3119
|
while (true) {
|
|
2707
3120
|
for (const marker of VAULT_MARKERS) {
|
|
2708
3121
|
const markerPath = path10.join(current, marker);
|
|
2709
|
-
if (
|
|
3122
|
+
if (fs8.existsSync(markerPath) && fs8.statSync(markerPath).isDirectory()) {
|
|
2710
3123
|
return current;
|
|
2711
3124
|
}
|
|
2712
3125
|
}
|