@velvetmonkey/flywheel-crank 0.4.1 → 0.5.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 +135 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,6 +16,61 @@ import matter from "gray-matter";
|
|
|
16
16
|
var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
|
|
17
17
|
|
|
18
18
|
// src/core/writer.ts
|
|
19
|
+
var SENSITIVE_PATH_PATTERNS = [
|
|
20
|
+
/\.env($|\..*)/i,
|
|
21
|
+
// .env, .env.local, .env.production, etc.
|
|
22
|
+
/\.git\/config$/i,
|
|
23
|
+
// Git config (may contain tokens)
|
|
24
|
+
/\.git\/credentials$/i,
|
|
25
|
+
// Git credentials
|
|
26
|
+
/\.pem$/i,
|
|
27
|
+
// SSL/TLS certificates
|
|
28
|
+
/\.key$/i,
|
|
29
|
+
// Private keys
|
|
30
|
+
/\.p12$/i,
|
|
31
|
+
// PKCS#12 certificates
|
|
32
|
+
/\.pfx$/i,
|
|
33
|
+
// Windows certificate format
|
|
34
|
+
/\.jks$/i,
|
|
35
|
+
// Java keystore
|
|
36
|
+
/id_rsa/i,
|
|
37
|
+
// SSH private key
|
|
38
|
+
/id_ed25519/i,
|
|
39
|
+
// SSH private key (ed25519)
|
|
40
|
+
/id_ecdsa/i,
|
|
41
|
+
// SSH private key (ecdsa)
|
|
42
|
+
/credentials\.json$/i,
|
|
43
|
+
// Cloud credentials files
|
|
44
|
+
/secrets\.json$/i,
|
|
45
|
+
// Secrets files
|
|
46
|
+
/secrets\.ya?ml$/i,
|
|
47
|
+
// Secrets YAML files
|
|
48
|
+
/\.htpasswd$/i,
|
|
49
|
+
// Apache password file
|
|
50
|
+
/shadow$/,
|
|
51
|
+
// Unix shadow password file
|
|
52
|
+
/passwd$/
|
|
53
|
+
// Unix password file
|
|
54
|
+
];
|
|
55
|
+
function isSensitivePath(filePath) {
|
|
56
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
57
|
+
return SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath));
|
|
58
|
+
}
|
|
59
|
+
function detectLineEnding(content) {
|
|
60
|
+
const crlfCount = (content.match(/\r\n/g) || []).length;
|
|
61
|
+
const lfCount = (content.match(/(?<!\r)\n/g) || []).length;
|
|
62
|
+
return crlfCount > lfCount ? "CRLF" : "LF";
|
|
63
|
+
}
|
|
64
|
+
function normalizeLineEndings(content) {
|
|
65
|
+
return content.replace(/\r\n/g, "\n");
|
|
66
|
+
}
|
|
67
|
+
function convertLineEndings(content, style) {
|
|
68
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
69
|
+
return style === "CRLF" ? normalized.replace(/\n/g, "\r\n") : normalized;
|
|
70
|
+
}
|
|
71
|
+
function normalizeTrailingNewline(content) {
|
|
72
|
+
return content.replace(/[\r\n\s]+$/, "") + "\n";
|
|
73
|
+
}
|
|
19
74
|
var EMPTY_PLACEHOLDER_PATTERNS = [
|
|
20
75
|
/^\d+\.\s*$/,
|
|
21
76
|
// "1. " or "2. " (numbered list placeholder)
|
|
@@ -169,25 +224,88 @@ function validatePath(vaultPath2, notePath) {
|
|
|
169
224
|
const resolvedNote = path.resolve(vaultPath2, notePath);
|
|
170
225
|
return resolvedNote.startsWith(resolvedVault);
|
|
171
226
|
}
|
|
227
|
+
async function validatePathSecure(vaultPath2, notePath) {
|
|
228
|
+
const resolvedVault = path.resolve(vaultPath2);
|
|
229
|
+
const resolvedNote = path.resolve(vaultPath2, notePath);
|
|
230
|
+
if (!resolvedNote.startsWith(resolvedVault)) {
|
|
231
|
+
return {
|
|
232
|
+
valid: false,
|
|
233
|
+
reason: "Path traversal not allowed"
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (isSensitivePath(notePath)) {
|
|
237
|
+
return {
|
|
238
|
+
valid: false,
|
|
239
|
+
reason: "Cannot write to sensitive file (credentials, keys, secrets)"
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const fullPath = path.join(vaultPath2, notePath);
|
|
244
|
+
try {
|
|
245
|
+
await fs.access(fullPath);
|
|
246
|
+
const realPath = await fs.realpath(fullPath);
|
|
247
|
+
const realVaultPath = await fs.realpath(vaultPath2);
|
|
248
|
+
if (!realPath.startsWith(realVaultPath)) {
|
|
249
|
+
return {
|
|
250
|
+
valid: false,
|
|
251
|
+
reason: "Symlink target is outside vault"
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const relativePath = path.relative(realVaultPath, realPath);
|
|
255
|
+
if (isSensitivePath(relativePath)) {
|
|
256
|
+
return {
|
|
257
|
+
valid: false,
|
|
258
|
+
reason: "Symlink target is a sensitive file"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
const parentDir = path.dirname(fullPath);
|
|
263
|
+
try {
|
|
264
|
+
await fs.access(parentDir);
|
|
265
|
+
const realParentPath = await fs.realpath(parentDir);
|
|
266
|
+
const realVaultPath = await fs.realpath(vaultPath2);
|
|
267
|
+
if (!realParentPath.startsWith(realVaultPath)) {
|
|
268
|
+
return {
|
|
269
|
+
valid: false,
|
|
270
|
+
reason: "Parent directory symlink target is outside vault"
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
return {
|
|
278
|
+
valid: false,
|
|
279
|
+
reason: `Path validation error: ${error.message}`
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return { valid: true };
|
|
283
|
+
}
|
|
172
284
|
async function readVaultFile(vaultPath2, notePath) {
|
|
173
285
|
if (!validatePath(vaultPath2, notePath)) {
|
|
174
286
|
throw new Error("Invalid path: path traversal not allowed");
|
|
175
287
|
}
|
|
176
288
|
const fullPath = path.join(vaultPath2, notePath);
|
|
177
289
|
const rawContent = await fs.readFile(fullPath, "utf-8");
|
|
178
|
-
const
|
|
290
|
+
const lineEnding = detectLineEnding(rawContent);
|
|
291
|
+
const normalizedContent = normalizeLineEndings(rawContent);
|
|
292
|
+
const parsed = matter(normalizedContent);
|
|
179
293
|
return {
|
|
180
294
|
content: parsed.content,
|
|
181
295
|
frontmatter: parsed.data,
|
|
182
|
-
rawContent
|
|
296
|
+
rawContent,
|
|
297
|
+
lineEnding
|
|
183
298
|
};
|
|
184
299
|
}
|
|
185
|
-
async function writeVaultFile(vaultPath2, notePath, content, frontmatter) {
|
|
186
|
-
|
|
187
|
-
|
|
300
|
+
async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEnding = "LF") {
|
|
301
|
+
const validation = await validatePathSecure(vaultPath2, notePath);
|
|
302
|
+
if (!validation.valid) {
|
|
303
|
+
throw new Error(`Invalid path: ${validation.reason}`);
|
|
188
304
|
}
|
|
189
305
|
const fullPath = path.join(vaultPath2, notePath);
|
|
190
|
-
|
|
306
|
+
let output = matter.stringify(content, frontmatter);
|
|
307
|
+
output = normalizeTrailingNewline(output);
|
|
308
|
+
output = convertLineEndings(output, lineEnding);
|
|
191
309
|
await fs.writeFile(fullPath, output, "utf-8");
|
|
192
310
|
}
|
|
193
311
|
function removeFromSection(content, section, pattern, mode = "first", useRegex = false) {
|
|
@@ -591,12 +709,15 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
591
709
|
const linkedEntities = excludeLinked ? extractLinkedEntities(content) : /* @__PURE__ */ new Set();
|
|
592
710
|
const scoredEntities = [];
|
|
593
711
|
for (const entity of entities) {
|
|
594
|
-
|
|
712
|
+
const entityName = typeof entity === "string" ? entity : entity.name;
|
|
713
|
+
if (!entityName)
|
|
714
|
+
continue;
|
|
715
|
+
if (linkedEntities.has(entityName.toLowerCase())) {
|
|
595
716
|
continue;
|
|
596
717
|
}
|
|
597
|
-
const score = scoreEntity(
|
|
718
|
+
const score = scoreEntity(entityName, contentTokens);
|
|
598
719
|
if (score > 0) {
|
|
599
|
-
scoredEntities.push({ name:
|
|
720
|
+
scoredEntities.push({ name: entityName, score });
|
|
600
721
|
}
|
|
601
722
|
}
|
|
602
723
|
scoredEntities.sort((a, b) => b.score - a.score);
|
|
@@ -1029,9 +1150,10 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
1029
1150
|
completed: z2.boolean().default(false).describe("Whether the task should start as completed"),
|
|
1030
1151
|
commit: z2.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
|
|
1031
1152
|
skipWikilinks: z2.boolean().default(false).describe("If true, skip auto-wikilink application (wikilinks are applied by default)"),
|
|
1032
|
-
suggestOutgoingLinks: z2.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.')
|
|
1153
|
+
suggestOutgoingLinks: z2.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.'),
|
|
1154
|
+
preserveListNesting: z2.boolean().default(true).describe("Preserve indentation when inserting into nested lists. Default: true")
|
|
1033
1155
|
},
|
|
1034
|
-
async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks }) => {
|
|
1156
|
+
async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks, preserveListNesting }) => {
|
|
1035
1157
|
try {
|
|
1036
1158
|
const fullPath = path5.join(vaultPath2, notePath);
|
|
1037
1159
|
try {
|
|
@@ -1069,7 +1191,8 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
1069
1191
|
fileContent,
|
|
1070
1192
|
sectionBoundary,
|
|
1071
1193
|
taskLine,
|
|
1072
|
-
position
|
|
1194
|
+
position,
|
|
1195
|
+
{ preserveListNesting }
|
|
1073
1196
|
);
|
|
1074
1197
|
await writeVaultFile(vaultPath2, notePath, updatedContent, frontmatter);
|
|
1075
1198
|
let gitCommit;
|