@cad0p/napkin 0.8.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +342 -0
  3. package/dist/commands/aliases.d.ts +7 -0
  4. package/dist/commands/aliases.js +25 -0
  5. package/dist/commands/bases.d.ts +23 -0
  6. package/dist/commands/bases.js +139 -0
  7. package/dist/commands/bookmarks.d.ts +15 -0
  8. package/dist/commands/bookmarks.js +51 -0
  9. package/dist/commands/canvas.d.ts +49 -0
  10. package/dist/commands/canvas.js +186 -0
  11. package/dist/commands/config.d.ts +13 -0
  12. package/dist/commands/config.js +48 -0
  13. package/dist/commands/crud.d.ts +40 -0
  14. package/dist/commands/crud.js +195 -0
  15. package/dist/commands/daily.d.ts +20 -0
  16. package/dist/commands/daily.js +58 -0
  17. package/dist/commands/files.d.ts +23 -0
  18. package/dist/commands/files.js +132 -0
  19. package/dist/commands/graph.d.ts +4 -0
  20. package/dist/commands/graph.js +461 -0
  21. package/dist/commands/init.d.ts +7 -0
  22. package/dist/commands/init.js +52 -0
  23. package/dist/commands/links.d.ts +26 -0
  24. package/dist/commands/links.js +119 -0
  25. package/dist/commands/outline.d.ts +7 -0
  26. package/dist/commands/outline.js +48 -0
  27. package/dist/commands/overview.d.ts +6 -0
  28. package/dist/commands/overview.js +40 -0
  29. package/dist/commands/properties.d.ts +24 -0
  30. package/dist/commands/properties.js +115 -0
  31. package/dist/commands/search.d.ts +13 -0
  32. package/dist/commands/search.js +48 -0
  33. package/dist/commands/tags.d.ts +13 -0
  34. package/dist/commands/tags.js +51 -0
  35. package/dist/commands/tasks.d.ts +22 -0
  36. package/dist/commands/tasks.js +106 -0
  37. package/dist/commands/templates.d.ts +16 -0
  38. package/dist/commands/templates.js +70 -0
  39. package/dist/commands/vault.d.ts +4 -0
  40. package/dist/commands/vault.js +17 -0
  41. package/dist/commands/wordcount.d.ts +7 -0
  42. package/dist/commands/wordcount.js +43 -0
  43. package/dist/core/aliases.d.ts +5 -0
  44. package/dist/core/aliases.js +26 -0
  45. package/dist/core/bases.d.ts +29 -0
  46. package/dist/core/bases.js +67 -0
  47. package/dist/core/bookmarks.d.ts +14 -0
  48. package/dist/core/bookmarks.js +34 -0
  49. package/dist/core/canvas.d.ts +74 -0
  50. package/dist/core/canvas.js +125 -0
  51. package/dist/core/config.d.ts +7 -0
  52. package/dist/core/config.js +35 -0
  53. package/dist/core/crud.d.ts +32 -0
  54. package/dist/core/crud.js +119 -0
  55. package/dist/core/daily.d.ts +12 -0
  56. package/dist/core/daily.js +102 -0
  57. package/dist/core/files.d.ts +15 -0
  58. package/dist/core/files.js +30 -0
  59. package/dist/core/init.d.ts +31 -0
  60. package/dist/core/init.js +119 -0
  61. package/dist/core/links.d.ts +11 -0
  62. package/dist/core/links.js +66 -0
  63. package/dist/core/outline.d.ts +3 -0
  64. package/dist/core/outline.js +12 -0
  65. package/dist/core/overview.d.ts +15 -0
  66. package/dist/core/overview.js +384 -0
  67. package/dist/core/properties.d.ts +14 -0
  68. package/dist/core/properties.js +60 -0
  69. package/dist/core/search.d.ts +17 -0
  70. package/dist/core/search.js +153 -0
  71. package/dist/core/tags.d.ts +11 -0
  72. package/dist/core/tags.js +40 -0
  73. package/dist/core/tasks.d.ts +35 -0
  74. package/dist/core/tasks.js +97 -0
  75. package/dist/core/templates.d.ts +14 -0
  76. package/dist/core/templates.js +55 -0
  77. package/dist/core/vault.d.ts +10 -0
  78. package/dist/core/vault.js +37 -0
  79. package/dist/core/wordcount.d.ts +5 -0
  80. package/dist/core/wordcount.js +16 -0
  81. package/dist/index.d.ts +17 -0
  82. package/dist/index.js +1 -0
  83. package/dist/main.d.ts +2 -0
  84. package/dist/main.js +715 -0
  85. package/dist/sdk.d.ts +179 -0
  86. package/dist/sdk.js +232 -0
  87. package/dist/templates/coding.d.ts +2 -0
  88. package/dist/templates/coding.js +104 -0
  89. package/dist/templates/company.d.ts +2 -0
  90. package/dist/templates/company.js +121 -0
  91. package/dist/templates/index.d.ts +4 -0
  92. package/dist/templates/index.js +15 -0
  93. package/dist/templates/personal.d.ts +2 -0
  94. package/dist/templates/personal.js +91 -0
  95. package/dist/templates/product.d.ts +2 -0
  96. package/dist/templates/product.js +123 -0
  97. package/dist/templates/research.d.ts +2 -0
  98. package/dist/templates/research.js +114 -0
  99. package/dist/templates/types.d.ts +7 -0
  100. package/dist/templates/types.js +1 -0
  101. package/dist/utils/bases.d.ts +61 -0
  102. package/dist/utils/bases.js +661 -0
  103. package/dist/utils/config.d.ts +42 -0
  104. package/dist/utils/config.js +112 -0
  105. package/dist/utils/exit-codes.d.ts +5 -0
  106. package/dist/utils/exit-codes.js +5 -0
  107. package/dist/utils/files.d.ts +135 -0
  108. package/dist/utils/files.js +299 -0
  109. package/dist/utils/formula.d.ts +28 -0
  110. package/dist/utils/formula.js +462 -0
  111. package/dist/utils/frontmatter.d.ts +17 -0
  112. package/dist/utils/frontmatter.js +34 -0
  113. package/dist/utils/markdown.d.ts +31 -0
  114. package/dist/utils/markdown.js +80 -0
  115. package/dist/utils/output.d.ts +28 -0
  116. package/dist/utils/output.js +48 -0
  117. package/dist/utils/search-cache.d.ts +29 -0
  118. package/dist/utils/search-cache.js +41 -0
  119. package/dist/utils/test-helpers.d.ts +13 -0
  120. package/dist/utils/test-helpers.js +40 -0
  121. package/dist/utils/vault.d.ts +21 -0
  122. package/dist/utils/vault.js +144 -0
  123. package/package.json +76 -0
@@ -0,0 +1,112 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ export const DEFAULT_CONFIG = {
4
+ overview: {
5
+ depth: 3,
6
+ keywords: 8,
7
+ },
8
+ search: {
9
+ limit: 30,
10
+ snippetLines: 0,
11
+ },
12
+ daily: {
13
+ folder: "daily",
14
+ format: "YYYY-MM-DD",
15
+ },
16
+ templates: {
17
+ folder: "Templates",
18
+ },
19
+ graph: {
20
+ renderer: "auto",
21
+ },
22
+ };
23
+ /**
24
+ * Load napkin config from config.json in the .napkin/ directory.
25
+ * Missing fields fall back to defaults.
26
+ */
27
+ export function loadConfig(napkinDir) {
28
+ const configPath = path.join(napkinDir, "config.json");
29
+ if (!fs.existsSync(configPath))
30
+ return { ...DEFAULT_CONFIG };
31
+ try {
32
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
33
+ return deepMerge(DEFAULT_CONFIG, raw);
34
+ }
35
+ catch {
36
+ return { ...DEFAULT_CONFIG };
37
+ }
38
+ }
39
+ /**
40
+ * Save napkin config to config.json in the .napkin/ directory and sync to .obsidian/.
41
+ * If obsidianDir is not provided, resolves it from config.vault or defaults to .napkin/.obsidian/.
42
+ */
43
+ export function saveConfig(napkinDir, config, obsidianDir) {
44
+ const configPath = path.join(napkinDir, "config.json");
45
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
46
+ const resolvedObsidian = obsidianDir ||
47
+ (config.vault?.obsidian
48
+ ? path.resolve(napkinDir, config.vault.obsidian)
49
+ : path.join(napkinDir, ".obsidian"));
50
+ syncObsidianConfig(resolvedObsidian, config);
51
+ }
52
+ /**
53
+ * Update specific config fields, save, and sync.
54
+ */
55
+ export function updateConfig(napkinDir, partial) {
56
+ const current = loadConfig(napkinDir);
57
+ const updated = deepMerge(current, partial);
58
+ saveConfig(napkinDir, updated);
59
+ return updated;
60
+ }
61
+ /**
62
+ * Write .obsidian/ config files derived from napkin config.
63
+ * napkin is the source of truth — Obsidian reads from these.
64
+ */
65
+ function syncObsidianConfig(obsidianDir, config) {
66
+ if (!fs.existsSync(obsidianDir)) {
67
+ fs.mkdirSync(obsidianDir, { recursive: true });
68
+ }
69
+ // daily-notes.json
70
+ fs.writeFileSync(path.join(obsidianDir, "daily-notes.json"), JSON.stringify({
71
+ folder: config.daily.folder,
72
+ format: config.daily.format,
73
+ template: `${config.templates.folder}/Daily Note`,
74
+ }, null, 2));
75
+ // templates.json
76
+ fs.writeFileSync(path.join(obsidianDir, "templates.json"), JSON.stringify({
77
+ folder: config.templates.folder,
78
+ }, null, 2));
79
+ // app.json
80
+ const appPath = path.join(obsidianDir, "app.json");
81
+ let app = {};
82
+ if (fs.existsSync(appPath)) {
83
+ try {
84
+ app = JSON.parse(fs.readFileSync(appPath, "utf-8"));
85
+ }
86
+ catch {
87
+ // ignore
88
+ }
89
+ }
90
+ app.alwaysUpdateLinks = true;
91
+ fs.writeFileSync(appPath, JSON.stringify(app, null, 2));
92
+ }
93
+ // biome-ignore lint/suspicious/noExplicitAny: deep merge requires flexible types
94
+ function deepMerge(target, source) {
95
+ const result = { ...target };
96
+ for (const key of Object.keys(source)) {
97
+ const srcVal = source[key];
98
+ const tgtVal = target[key];
99
+ if (srcVal &&
100
+ typeof srcVal === "object" &&
101
+ !Array.isArray(srcVal) &&
102
+ tgtVal &&
103
+ typeof tgtVal === "object" &&
104
+ !Array.isArray(tgtVal)) {
105
+ result[key] = deepMerge(tgtVal, srcVal);
106
+ }
107
+ else {
108
+ result[key] = srcVal;
109
+ }
110
+ }
111
+ return result;
112
+ }
@@ -0,0 +1,5 @@
1
+ export declare const EXIT_SUCCESS = 0;
2
+ export declare const EXIT_ERROR = 1;
3
+ export declare const EXIT_USER_ERROR = 2;
4
+ export declare const EXIT_NOT_FOUND = 3;
5
+ export declare const EXIT_NO_VAULT = 4;
@@ -0,0 +1,5 @@
1
+ export const EXIT_SUCCESS = 0;
2
+ export const EXIT_ERROR = 1;
3
+ export const EXIT_USER_ERROR = 2;
4
+ export const EXIT_NOT_FOUND = 3;
5
+ export const EXIT_NO_VAULT = 4;
@@ -0,0 +1,135 @@
1
+ import * as fs from "node:fs";
2
+ export interface FileInfo {
3
+ path: string;
4
+ name: string;
5
+ extension: string;
6
+ size: number;
7
+ created: number;
8
+ modified: number;
9
+ }
10
+ export interface ListFilesOptions {
11
+ folder?: string;
12
+ ext?: string;
13
+ }
14
+ /**
15
+ * Directory names that walkers skip unconditionally (internal Obsidian/napkin
16
+ * state, VCS metadata, and package managers).
17
+ *
18
+ * Shared by listFiles, listFolders, walkMd (graph), and getVaultSize so all
19
+ * four walkers agree on what to exclude. This matters especially when a vault
20
+ * contains symlinks into external trees (e.g. Brazil workspace packages) whose
21
+ * node_modules/ would otherwise balloon results.
22
+ */
23
+ export declare const SKIP_DIRS: ReadonlySet<string>;
24
+ /** What a Dirent resolves to, following symlinks to their target. */
25
+ export type EntryKind = "dir" | "file" | null;
26
+ /**
27
+ * Classify a Dirent. For symlinks, follows to the target and reports the
28
+ * target kind. Returns null for broken symlinks, unreadable targets, and
29
+ * non-regular entries (sockets, FIFOs, devices).
30
+ *
31
+ * Using this lets walkers treat symlinks to directories/files as first-class
32
+ * entries while still gracefully skipping unreadable targets.
33
+ */
34
+ export declare function direntKind(fullPath: string, entry: fs.Dirent): EntryKind;
35
+ /**
36
+ * Resolve a directory's real path for cycle detection. Returns null on
37
+ * failure (permission errors, broken intermediate path, etc.) so callers
38
+ * can fail closed rather than risk an unbounded walk.
39
+ */
40
+ export declare function safeRealpath(dir: string): string | null;
41
+ /**
42
+ * Options for {@link walkDir}.
43
+ */
44
+ export interface WalkDirOptions {
45
+ /**
46
+ * Called once per visited file or directory entry (after skip-dir
47
+ * filtering and symlink resolution). Never invoked with kind=null;
48
+ * non-regular and broken entries are filtered out by the walker.
49
+ */
50
+ onEntry: (fullPath: string, entry: fs.Dirent, kind: "dir" | "file") => void;
51
+ /**
52
+ * Optional predicate to prune directories. Called for each directory
53
+ * entry (including symlinked dirs) AFTER the SKIP_DIRS check. Return
54
+ * false to skip the directory entirely: it is neither reported via
55
+ * onEntry nor walked. Not consulted for files.
56
+ *
57
+ * Use this to apply stricter pruning than SKIP_DIRS (e.g. walkMd and
58
+ * listFolders prune all dot-prefixed directories).
59
+ */
60
+ shouldEnter?: (fullPath: string, entry: fs.Dirent) => boolean;
61
+ }
62
+ /**
63
+ * Walk a directory tree, invoking onEntry for each file and directory.
64
+ *
65
+ * Skip semantics:
66
+ * - Directory names in SKIP_DIRS are never entered or reported.
67
+ * - If shouldEnter is provided, directories for which it returns false
68
+ * are also neither entered nor reported. This lets callers prune
69
+ * subtrees (e.g. dotdirs) symmetrically with SKIP_DIRS — filtering
70
+ * inside onEntry would only hide the report while still walking the
71
+ * subtree.
72
+ *
73
+ * Symlink semantics:
74
+ * - Symlinks to regular files/directories are classified via direntKind
75
+ * and reported with kind "dir" or "file".
76
+ * - A symlink that resolves to a directory is walked recursively.
77
+ * - Broken symlinks, sockets, FIFOs, and devices are silently skipped.
78
+ *
79
+ * Cycle detection: uses an on-stack set of realpaths for the current
80
+ * recursion path. A symlink that resolves to an ancestor in the descent
81
+ * is skipped exactly once; two sibling symlinks to the same target are
82
+ * both walked. realpathSync is only invoked when crossing a symlink
83
+ * boundary (or at the root), so symlink-free vaults pay no extra cost
84
+ * versus the pre-fix baseline.
85
+ *
86
+ * Fail-closed: if realpathSync fails on a subtree entered via a symlink,
87
+ * that subtree is skipped to avoid potential unbounded recursion.
88
+ */
89
+ export declare function walkDir(root: string, opts: WalkDirOptions): void;
90
+ /**
91
+ * Recursively list files in a vault.
92
+ *
93
+ * - Skips the directories in SKIP_DIRS (".obsidian", ".git", etc.).
94
+ * - Follows symlinks to files and directories. Note that symlinked content
95
+ * may physically live outside vaultPath; callers that feed results to
96
+ * downstream consumers (e.g. LLM prompts via the SDK) should be aware
97
+ * that `readFile` of a returned path may access external data.
98
+ * - Detects symlink cycles via realpath tracking on the current recursion
99
+ * path, so a symlink pointing back to an ancestor is entered at most
100
+ * once. Sibling symlinks to the same target are both walked (each
101
+ * contributes its own prefixed entries).
102
+ * - Broken symlinks, sockets, FIFOs, and unreadable entries are silently
103
+ * skipped.
104
+ */
105
+ export declare function listFiles(vaultPath: string, opts?: ListFilesOptions): string[];
106
+ /**
107
+ * List folders in a vault. Same symlink semantics as listFiles.
108
+ */
109
+ export declare function listFolders(vaultPath: string, parentFolder?: string): string[];
110
+ /**
111
+ * Resolve a file reference (wikilink-style name or exact path) to a relative path in the vault.
112
+ * Throws on ambiguous matches so the user can disambiguate.
113
+ */
114
+ export declare function resolveFile(vaultPath: string, fileRef: string): string | null;
115
+ /**
116
+ * Like resolveFile but never throws on ambiguous matches.
117
+ * Returns the shallowest match (fewest path segments), matching Obsidian's behavior.
118
+ */
119
+ export declare function resolveFileLoose(vaultPath: string, fileRef: string): string | null;
120
+ /**
121
+ * Suggest similar filenames when a file isn't found.
122
+ * Returns up to 3 suggestions sorted by similarity.
123
+ */
124
+ export declare function suggestFile(vaultPath: string, fileRef: string): string[];
125
+ /**
126
+ * Read a file's contents, resolving by name or path.
127
+ */
128
+ export declare function readFile(vaultPath: string, fileRef: string): {
129
+ path: string;
130
+ content: string;
131
+ };
132
+ /**
133
+ * Get file info for a resolved file path.
134
+ */
135
+ export declare function getFileInfo(vaultPath: string, relativePath: string): FileInfo;
@@ -0,0 +1,299 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ /**
4
+ * Directory names that walkers skip unconditionally (internal Obsidian/napkin
5
+ * state, VCS metadata, and package managers).
6
+ *
7
+ * Shared by listFiles, listFolders, walkMd (graph), and getVaultSize so all
8
+ * four walkers agree on what to exclude. This matters especially when a vault
9
+ * contains symlinks into external trees (e.g. Brazil workspace packages) whose
10
+ * node_modules/ would otherwise balloon results.
11
+ */
12
+ export const SKIP_DIRS = new Set([
13
+ ".obsidian",
14
+ ".git",
15
+ ".trash",
16
+ ".nanny",
17
+ ".napkin",
18
+ "node_modules",
19
+ ]);
20
+ /**
21
+ * Classify a Dirent. For symlinks, follows to the target and reports the
22
+ * target kind. Returns null for broken symlinks, unreadable targets, and
23
+ * non-regular entries (sockets, FIFOs, devices).
24
+ *
25
+ * Using this lets walkers treat symlinks to directories/files as first-class
26
+ * entries while still gracefully skipping unreadable targets.
27
+ */
28
+ export function direntKind(fullPath, entry) {
29
+ if (entry.isDirectory())
30
+ return "dir";
31
+ if (entry.isFile())
32
+ return "file";
33
+ if (entry.isSymbolicLink()) {
34
+ try {
35
+ const stat = fs.statSync(fullPath); // follows symlinks
36
+ if (stat.isDirectory())
37
+ return "dir";
38
+ if (stat.isFile())
39
+ return "file";
40
+ return null; // socket / FIFO / device
41
+ }
42
+ catch {
43
+ // Broken symlink or target inaccessible.
44
+ return null;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Resolve a directory's real path for cycle detection. Returns null on
51
+ * failure (permission errors, broken intermediate path, etc.) so callers
52
+ * can fail closed rather than risk an unbounded walk.
53
+ */
54
+ export function safeRealpath(dir) {
55
+ try {
56
+ return fs.realpathSync(dir);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /**
63
+ * Walk a directory tree, invoking onEntry for each file and directory.
64
+ *
65
+ * Skip semantics:
66
+ * - Directory names in SKIP_DIRS are never entered or reported.
67
+ * - If shouldEnter is provided, directories for which it returns false
68
+ * are also neither entered nor reported. This lets callers prune
69
+ * subtrees (e.g. dotdirs) symmetrically with SKIP_DIRS — filtering
70
+ * inside onEntry would only hide the report while still walking the
71
+ * subtree.
72
+ *
73
+ * Symlink semantics:
74
+ * - Symlinks to regular files/directories are classified via direntKind
75
+ * and reported with kind "dir" or "file".
76
+ * - A symlink that resolves to a directory is walked recursively.
77
+ * - Broken symlinks, sockets, FIFOs, and devices are silently skipped.
78
+ *
79
+ * Cycle detection: uses an on-stack set of realpaths for the current
80
+ * recursion path. A symlink that resolves to an ancestor in the descent
81
+ * is skipped exactly once; two sibling symlinks to the same target are
82
+ * both walked. realpathSync is only invoked when crossing a symlink
83
+ * boundary (or at the root), so symlink-free vaults pay no extra cost
84
+ * versus the pre-fix baseline.
85
+ *
86
+ * Fail-closed: if realpathSync fails on a subtree entered via a symlink,
87
+ * that subtree is skipped to avoid potential unbounded recursion.
88
+ */
89
+ export function walkDir(root, opts) {
90
+ const onStack = new Set();
91
+ // Seed with the root's real path so a symlink back to the root is
92
+ // detected even though we don't mark the top-level descent as
93
+ // "via symlink".
94
+ const rootReal = safeRealpath(root);
95
+ if (rootReal !== null)
96
+ onStack.add(rootReal);
97
+ function walk(dir, viaSymlink) {
98
+ let stackKey = null;
99
+ if (viaSymlink) {
100
+ stackKey = safeRealpath(dir);
101
+ if (stackKey === null)
102
+ return; // inaccessible symlinked subtree
103
+ if (onStack.has(stackKey))
104
+ return; // cycle
105
+ onStack.add(stackKey);
106
+ }
107
+ try {
108
+ let entries;
109
+ try {
110
+ entries = fs.readdirSync(dir, { withFileTypes: true });
111
+ }
112
+ catch {
113
+ return;
114
+ }
115
+ for (const entry of entries) {
116
+ if (SKIP_DIRS.has(entry.name))
117
+ continue;
118
+ const fullPath = path.join(dir, entry.name);
119
+ const kind = direntKind(fullPath, entry);
120
+ if (kind === null)
121
+ continue;
122
+ if (kind === "dir") {
123
+ // Check shouldEnter BEFORE reporting so that a pruned
124
+ // directory is neither emitted nor walked, matching the
125
+ // pre-consolidation semantics of listFolders/walkMd.
126
+ if (opts.shouldEnter && !opts.shouldEnter(fullPath, entry))
127
+ continue;
128
+ opts.onEntry(fullPath, entry, kind);
129
+ walk(fullPath, viaSymlink || entry.isSymbolicLink());
130
+ }
131
+ else {
132
+ opts.onEntry(fullPath, entry, kind);
133
+ }
134
+ }
135
+ }
136
+ finally {
137
+ if (stackKey !== null)
138
+ onStack.delete(stackKey);
139
+ }
140
+ }
141
+ walk(root, false);
142
+ }
143
+ /**
144
+ * Recursively list files in a vault.
145
+ *
146
+ * - Skips the directories in SKIP_DIRS (".obsidian", ".git", etc.).
147
+ * - Follows symlinks to files and directories. Note that symlinked content
148
+ * may physically live outside vaultPath; callers that feed results to
149
+ * downstream consumers (e.g. LLM prompts via the SDK) should be aware
150
+ * that `readFile` of a returned path may access external data.
151
+ * - Detects symlink cycles via realpath tracking on the current recursion
152
+ * path, so a symlink pointing back to an ancestor is entered at most
153
+ * once. Sibling symlinks to the same target are both walked (each
154
+ * contributes its own prefixed entries).
155
+ * - Broken symlinks, sockets, FIFOs, and unreadable entries are silently
156
+ * skipped.
157
+ */
158
+ export function listFiles(vaultPath, opts) {
159
+ const results = [];
160
+ // Internal napkin files that shouldn't appear in vault content listings
161
+ const skipFiles = new Set(["config.json", "search-cache.json"]);
162
+ const baseDir = opts?.folder ? path.join(vaultPath, opts.folder) : vaultPath;
163
+ if (!fs.existsSync(baseDir))
164
+ return results;
165
+ walkDir(baseDir, {
166
+ onEntry: (fullPath, _entry, kind) => {
167
+ if (kind !== "file")
168
+ return;
169
+ // Skip internal config files at vault root
170
+ if (path.dirname(fullPath) === vaultPath &&
171
+ skipFiles.has(path.basename(fullPath)))
172
+ return;
173
+ const rel = path.relative(vaultPath, fullPath);
174
+ if (opts?.ext) {
175
+ if (path.extname(fullPath).slice(1) === opts.ext) {
176
+ results.push(rel);
177
+ }
178
+ }
179
+ else {
180
+ results.push(rel);
181
+ }
182
+ },
183
+ });
184
+ return results.sort();
185
+ }
186
+ /**
187
+ * List folders in a vault. Same symlink semantics as listFiles.
188
+ */
189
+ export function listFolders(vaultPath, parentFolder) {
190
+ const results = [];
191
+ const baseDir = parentFolder ? path.join(vaultPath, parentFolder) : vaultPath;
192
+ if (!fs.existsSync(baseDir))
193
+ return results;
194
+ walkDir(baseDir, {
195
+ onEntry: (fullPath, _entry, kind) => {
196
+ if (kind !== "dir")
197
+ return;
198
+ results.push(path.relative(vaultPath, fullPath));
199
+ },
200
+ // Prune dotdirs at descent time so their subtree is not walked
201
+ // (matches pre-consolidation listFolders behavior). walkDir already
202
+ // filters SKIP_DIRS; this is the additional dotdir policy.
203
+ shouldEnter: (_fullPath, entry) => !entry.name.startsWith("."),
204
+ });
205
+ return results.sort();
206
+ }
207
+ /**
208
+ * Find all .md files matching a wikilink-style name or exact path.
209
+ */
210
+ function findMatches(vaultPath, fileRef) {
211
+ // Exact path
212
+ if (fileRef.includes("/") || fileRef.endsWith(".md")) {
213
+ const ref = fileRef.endsWith(".md") ? fileRef : `${fileRef}.md`;
214
+ const fullPath = path.join(vaultPath, ref);
215
+ return fs.existsSync(fullPath) ? [ref] : [];
216
+ }
217
+ // Wikilink-style: search by basename
218
+ const target = fileRef.toLowerCase();
219
+ const allFiles = listFiles(vaultPath, { ext: "md" });
220
+ return allFiles.filter((file) => path.basename(file, ".md").toLowerCase() === target);
221
+ }
222
+ /**
223
+ * Resolve a file reference (wikilink-style name or exact path) to a relative path in the vault.
224
+ * Throws on ambiguous matches so the user can disambiguate.
225
+ */
226
+ export function resolveFile(vaultPath, fileRef) {
227
+ const matches = findMatches(vaultPath, fileRef);
228
+ if (matches.length > 1) {
229
+ throw new Error(`Ambiguous file reference "${fileRef}" matches ${matches.length} files: ${matches.join(", ")}. Use the full path to disambiguate.`);
230
+ }
231
+ return matches[0] ?? null;
232
+ }
233
+ /**
234
+ * Like resolveFile but never throws on ambiguous matches.
235
+ * Returns the shallowest match (fewest path segments), matching Obsidian's behavior.
236
+ */
237
+ export function resolveFileLoose(vaultPath, fileRef) {
238
+ const matches = findMatches(vaultPath, fileRef);
239
+ if (matches.length > 1) {
240
+ matches.sort((a, b) => a.split("/").length - b.split("/").length);
241
+ }
242
+ return matches[0] ?? null;
243
+ }
244
+ /**
245
+ * Suggest similar filenames when a file isn't found.
246
+ * Returns up to 3 suggestions sorted by similarity.
247
+ */
248
+ export function suggestFile(vaultPath, fileRef) {
249
+ const target = fileRef.toLowerCase();
250
+ const allFiles = listFiles(vaultPath, { ext: "md" });
251
+ const scored = allFiles
252
+ .map((f) => {
253
+ const basename = path.basename(f, ".md").toLowerCase();
254
+ // Simple substring match scoring
255
+ let score = 0;
256
+ if (basename.includes(target) || target.includes(basename))
257
+ score += 3;
258
+ // Shared prefix
259
+ let prefix = 0;
260
+ while (prefix < basename.length &&
261
+ prefix < target.length &&
262
+ basename[prefix] === target[prefix])
263
+ prefix++;
264
+ score += prefix;
265
+ return { file: f, score };
266
+ })
267
+ .filter((s) => s.score > 0)
268
+ .sort((a, b) => b.score - a.score)
269
+ .slice(0, 3);
270
+ return scored.map((s) => s.file);
271
+ }
272
+ /**
273
+ * Read a file's contents, resolving by name or path.
274
+ */
275
+ export function readFile(vaultPath, fileRef) {
276
+ const resolved = resolveFile(vaultPath, fileRef);
277
+ if (!resolved) {
278
+ throw new Error(`File not found: ${fileRef}`);
279
+ }
280
+ const fullPath = path.join(vaultPath, resolved);
281
+ const content = fs.readFileSync(fullPath, "utf-8");
282
+ return { path: resolved, content };
283
+ }
284
+ /**
285
+ * Get file info for a resolved file path.
286
+ */
287
+ export function getFileInfo(vaultPath, relativePath) {
288
+ const fullPath = path.join(vaultPath, relativePath);
289
+ const stat = fs.statSync(fullPath);
290
+ const ext = path.extname(relativePath);
291
+ return {
292
+ path: relativePath,
293
+ name: path.basename(relativePath, ext),
294
+ extension: ext.slice(1),
295
+ size: stat.size,
296
+ created: stat.birthtimeMs,
297
+ modified: stat.mtimeMs,
298
+ };
299
+ }
@@ -0,0 +1,28 @@
1
+ import Jexl from "jexl";
2
+ /**
3
+ * Transform Obsidian expression syntax to jexl syntax.
4
+ * Converts .method(args) to |method(args) for known transforms.
5
+ * Also remaps if() to _if() since if is reserved.
6
+ */
7
+ export declare function obsidianToJexl(expr: string): string;
8
+ /**
9
+ * Create a configured jexl instance with all Obsidian Bases functions.
10
+ */
11
+ export declare function createFormulaEngine(): InstanceType<typeof Jexl.Jexl>;
12
+ /**
13
+ * Build a context object for formula evaluation from a database row.
14
+ */
15
+ export declare function buildFormulaContext(columns: string[], row: unknown[], formulaResults?: Record<string, unknown>, thisFile?: {
16
+ name: string;
17
+ path: string;
18
+ folder: string;
19
+ }): Record<string, unknown>;
20
+ /**
21
+ * Evaluate all formulas for a single row.
22
+ * Handles formula dependencies (formula referencing another formula).
23
+ */
24
+ export declare function evaluateFormulas(engine: InstanceType<typeof Jexl.Jexl>, formulas: Record<string, string>, columns: string[], row: unknown[], thisFile?: {
25
+ name: string;
26
+ path: string;
27
+ folder: string;
28
+ }): Promise<Record<string, unknown>>;