clawvault 1.3.0 → 1.4.0

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/README.md CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  Structured memory system for AI agents. Store, search, and link memories across sessions.
6
6
 
7
+ > **Built for [OpenClaw](https://openclaw.ai)** — the AI agent framework. Works standalone too.
8
+
9
+ ## Install for OpenClaw Agents
10
+
11
+ ```bash
12
+ # Install the skill (recommended for OpenClaw agents)
13
+ clawhub install clawvault
14
+
15
+ # Or install the CLI globally
16
+ npm install -g clawvault
17
+ ```
18
+
7
19
  ## Requirements
8
20
 
9
21
  - **Node.js 18+**
package/bin/clawvault.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  findVault,
17
17
  hasQmd,
18
18
  QmdUnavailableError,
19
- QMD_INSTALL_URL
19
+ QMD_INSTALL_COMMAND
20
20
  } from '../dist/index.js';
21
21
 
22
22
  const program = new Command();
@@ -66,13 +66,19 @@ async function runQmd(args) {
66
66
  if (code === 0) resolve();
67
67
  else reject(new Error(`qmd exited with code ${code}`));
68
68
  });
69
- proc.on('error', reject);
69
+ proc.on('error', (err) => {
70
+ if (err?.code === 'ENOENT') {
71
+ reject(new QmdUnavailableError());
72
+ } else {
73
+ reject(err);
74
+ }
75
+ });
70
76
  });
71
77
  }
72
78
 
73
79
  function printQmdMissing() {
74
- console.error(chalk.red('Error: qmd is not installed or not on PATH.'));
75
- console.log(chalk.dim(`Install qmd: ${QMD_INSTALL_URL}`));
80
+ console.error(chalk.red('Error: ClawVault requires qmd.'));
81
+ console.log(chalk.dim(`Install: ${QMD_INSTALL_COMMAND}`));
76
82
  }
77
83
 
78
84
  program
@@ -100,31 +106,22 @@ program
100
106
  console.log(chalk.green('✓ Vault created'));
101
107
  console.log(chalk.dim(` Categories: ${vault.getCategories().join(', ')}`));
102
108
 
103
- const qmdAvailable = hasQmd();
104
-
105
109
  // Always set up qmd collection (qmd is required)
106
110
  console.log(chalk.cyan('\nSetting up qmd collection...'));
107
- if (!qmdAvailable) {
108
- console.log(chalk.red('✗ qmd not found. Install qmd first:'));
109
- console.log(chalk.dim(` bun install -g qmd`));
110
- console.log(chalk.dim(` # or: npm install -g qmd`));
111
- console.log(chalk.dim(` Then run: qmd collection add ${vault.getQmdRoot()} --name ${vault.getQmdCollection()} --mask "**/*.md"`));
112
- } else {
113
- try {
114
- await runQmd([
115
- 'collection',
116
- 'add',
117
- vault.getQmdRoot(),
118
- '--name',
119
- vault.getQmdCollection(),
120
- '--mask',
121
- '**/*.md'
122
- ]);
123
- console.log(chalk.green('✓ qmd collection created'));
124
- } catch (err) {
125
- // Collection might already exist
126
- console.log(chalk.yellow('⚠ qmd collection may already exist'));
127
- }
111
+ try {
112
+ await runQmd([
113
+ 'collection',
114
+ 'add',
115
+ vault.getQmdRoot(),
116
+ '--name',
117
+ vault.getQmdCollection(),
118
+ '--mask',
119
+ '**/*.md'
120
+ ]);
121
+ console.log(chalk.green('✓ qmd collection created'));
122
+ } catch (err) {
123
+ // Collection might already exist
124
+ console.log(chalk.yellow('⚠ qmd collection may already exist'));
128
125
  }
129
126
 
130
127
  console.log(chalk.green('\n✅ ClawVault ready!\n'));
@@ -138,6 +135,20 @@ program
138
135
  }
139
136
  });
140
137
 
138
+ // === SETUP ===
139
+ program
140
+ .command('setup')
141
+ .description('Auto-discover and configure a ClawVault')
142
+ .action(async () => {
143
+ try {
144
+ const { setupCommand } = await import('../dist/commands/setup.js');
145
+ await setupCommand();
146
+ } catch (err) {
147
+ console.error(chalk.red(`Error: ${err.message}`));
148
+ process.exit(1);
149
+ }
150
+ });
151
+
141
152
  // === STORE ===
142
153
  program
143
154
  .command('store')
@@ -174,14 +185,12 @@ program
174
185
  console.log(chalk.dim(` Path: ${doc.path}`));
175
186
 
176
187
  // Auto-update qmd index unless --no-index
177
- if (options.index !== false && hasQmd()) {
178
- try {
179
- const collection = vault.getQmdCollection();
180
- await runQmd(collection ? ['update', '-c', collection] : ['update']);
181
- if (options.embed) {
182
- await runQmd(['embed']);
183
- }
184
- } catch { /* ignore qmd errors */ }
188
+ if (options.index !== false) {
189
+ const collection = vault.getQmdCollection();
190
+ await runQmd(collection ? ['update', '-c', collection] : ['update']);
191
+ if (options.embed) {
192
+ await runQmd(collection ? ['embed', '-c', collection] : ['embed']);
193
+ }
185
194
  }
186
195
  } catch (err) {
187
196
  console.error(chalk.red(`Error: ${err.message}`));
@@ -203,11 +212,9 @@ program
203
212
  console.log(chalk.green(`✓ Captured: ${doc.id}`));
204
213
 
205
214
  // Auto-update qmd index unless --no-index
206
- if (options.index !== false && hasQmd()) {
207
- try {
208
- const collection = vault.getQmdCollection();
209
- await runQmd(collection ? ['update', '-c', collection] : ['update']);
210
- } catch { /* ignore qmd errors */ }
215
+ if (options.index !== false) {
216
+ const collection = vault.getQmdCollection();
217
+ await runQmd(collection ? ['update', '-c', collection] : ['update']);
211
218
  }
212
219
  } catch (err) {
213
220
  console.error(chalk.red(`Error: ${err.message}`));
@@ -317,7 +324,7 @@ program
317
324
  process.exit(1);
318
325
  }
319
326
  console.error(chalk.red(`Error: ${err.message}`));
320
- console.log(chalk.dim(`\nTip: Install qmd for semantic search: ${QMD_INSTALL_URL}`));
327
+ console.log(chalk.dim(`\nTip: Install qmd: ${QMD_INSTALL_COMMAND}`));
321
328
  process.exit(1);
322
329
  }
323
330
  });
@@ -511,13 +518,9 @@ program
511
518
 
512
519
  if (options.qmd) {
513
520
  console.log(chalk.cyan('Updating qmd embeddings...'));
514
- try {
515
- const collection = vault.getQmdCollection();
516
- await runQmd(collection ? ['update', '-c', collection] : ['update']);
517
- console.log(chalk.green('✓ qmd updated'));
518
- } catch {
519
- console.log(chalk.yellow('⚠ qmd update failed'));
520
- }
521
+ const collection = vault.getQmdCollection();
522
+ await runQmd(collection ? ['update', '-c', collection] : ['update']);
523
+ console.log(chalk.green(' qmd updated'));
521
524
  }
522
525
 
523
526
  console.log();
@@ -558,11 +561,9 @@ program
558
561
  console.log(chalk.green(`✓ Remembered (${type}): ${doc.id}`));
559
562
 
560
563
  // Auto-update qmd index unless --no-index
561
- if (options.index !== false && hasQmd()) {
562
- try {
563
- const collection = vault.getQmdCollection();
564
- await runQmd(collection ? ['update', '-c', collection] : ['update']);
565
- } catch { /* ignore qmd errors */ }
564
+ if (options.index !== false) {
565
+ const collection = vault.getQmdCollection();
566
+ await runQmd(collection ? ['update', '-c', collection] : ['update']);
566
567
  }
567
568
  } catch (err) {
568
569
  console.error(chalk.red(`Error: ${err.message}`));
@@ -608,11 +609,9 @@ program
608
609
  }
609
610
 
610
611
  // Auto-update qmd index unless --no-index
611
- if (options.index !== false && hasQmd()) {
612
- try {
613
- const collection = vault.getQmdCollection();
614
- await runQmd(collection ? ['update', '-c', collection] : ['update']);
615
- } catch { /* ignore qmd errors */ }
612
+ if (options.index !== false) {
613
+ const collection = vault.getQmdCollection();
614
+ await runQmd(collection ? ['update', '-c', collection] : ['update']);
616
615
  }
617
616
  } catch (err) {
618
617
  console.error(chalk.red(`Error: ${err.message}`));
@@ -652,6 +651,74 @@ program
652
651
  }
653
652
  });
654
653
 
654
+ // === TEMPLATE ===
655
+ const template = program
656
+ .command('template')
657
+ .description('Manage templates');
658
+
659
+ template
660
+ .command('list')
661
+ .description('List available templates')
662
+ .option('-v, --vault <path>', 'Vault path')
663
+ .action(async (options) => {
664
+ try {
665
+ const { listTemplates } = await import('../dist/commands/template.js');
666
+ const templates = listTemplates({ vaultPath: options.vault });
667
+ if (templates.length === 0) {
668
+ console.log(chalk.yellow('No templates found.'));
669
+ return;
670
+ }
671
+ console.log(chalk.cyan('\n📄 Templates:\n'));
672
+ for (const name of templates) {
673
+ console.log(`- ${name}`);
674
+ }
675
+ console.log();
676
+ } catch (err) {
677
+ console.error(chalk.red(`Error: ${err.message}`));
678
+ process.exit(1);
679
+ }
680
+ });
681
+
682
+ template
683
+ .command('create <name>')
684
+ .description('Create a file from a template')
685
+ .option('-t, --title <title>', 'Document title')
686
+ .option('-v, --vault <path>', 'Vault path')
687
+ .action(async (name, options) => {
688
+ try {
689
+ const { createFromTemplate } = await import('../dist/commands/template.js');
690
+ const result = createFromTemplate(name, {
691
+ title: options.title,
692
+ vaultPath: options.vault
693
+ });
694
+ console.log(chalk.green(`✓ Created from template: ${name}`));
695
+ console.log(chalk.dim(` Output: ${result.outputPath}`));
696
+ } catch (err) {
697
+ console.error(chalk.red(`Error: ${err.message}`));
698
+ process.exit(1);
699
+ }
700
+ });
701
+
702
+ template
703
+ .command('add <file>')
704
+ .description('Add a custom template')
705
+ .requiredOption('--name <name>', 'Template name')
706
+ .option('-v, --vault <path>', 'Vault path')
707
+ .action(async (file, options) => {
708
+ try {
709
+ const { addTemplate } = await import('../dist/commands/template.js');
710
+ const result = addTemplate(file, {
711
+ name: options.name,
712
+ vaultPath: options.vault
713
+ });
714
+ console.log(chalk.green(`✓ Template added: ${result.name}`));
715
+ console.log(chalk.dim(` Path: ${result.templatePath}`));
716
+ } catch (err) {
717
+ console.error(chalk.red(`Error: ${err.message}`));
718
+ process.exit(1);
719
+ }
720
+ });
721
+
655
722
  // === DOCTOR (health check) ===
656
723
  program
657
724
  .command('doctor')
@@ -667,7 +734,7 @@ program
667
734
  console.log(chalk.green('✓ qmd installed'));
668
735
  } else {
669
736
  console.log(chalk.red('✗ qmd not installed'));
670
- console.log(chalk.dim(` Install: bun install -g qmd`));
737
+ console.log(chalk.dim(` Install: ${QMD_INSTALL_COMMAND}`));
671
738
  issues++;
672
739
  }
673
740
 
@@ -741,7 +808,10 @@ program
741
808
  .command('link [file]')
742
809
  .description('Auto-link entity mentions in markdown files')
743
810
  .option('--all', 'Link all files in vault')
811
+ .option('--backlinks <file>', 'Show backlinks to a file')
744
812
  .option('--dry-run', 'Show what would be linked without changing files')
813
+ .option('--orphans', 'List broken wiki-links')
814
+ .option('--rebuild', 'Rebuild backlinks index')
745
815
  .option('-v, --vault <path>', 'Vault path')
746
816
  .action(async (file, options) => {
747
817
  try {
@@ -752,7 +822,13 @@ program
752
822
  }
753
823
 
754
824
  const { linkCommand } = await import('../dist/commands/link.js');
755
- await linkCommand(file, { all: options.all, dryRun: options.dryRun });
825
+ await linkCommand(file, {
826
+ all: options.all,
827
+ dryRun: options.dryRun,
828
+ backlinks: options.backlinks,
829
+ orphans: options.orphans,
830
+ rebuild: options.rebuild
831
+ });
756
832
  } catch (err) {
757
833
  console.error(chalk.red(`Error: ${err.message}`));
758
834
  process.exit(1);
@@ -766,6 +842,7 @@ program
766
842
  .option('--working-on <text>', 'What you are currently working on')
767
843
  .option('--focus <text>', 'Current focus area')
768
844
  .option('--blocked <text>', 'What is blocking progress')
845
+ .option('--urgent', 'Trigger OpenClaw wake after checkpoint')
769
846
  .option('-v, --vault <path>', 'Vault path')
770
847
  .option('--json', 'Output as JSON')
771
848
  .action(async (options) => {
@@ -782,6 +859,7 @@ program
782
859
  workingOn: options.workingOn,
783
860
  focus: options.focus,
784
861
  blocked: options.blocked,
862
+ urgent: options.urgent
785
863
  });
786
864
 
787
865
  if (options.json) {
@@ -792,6 +870,7 @@ program
792
870
  if (data.workingOn) console.log(chalk.dim(` Working on: ${data.workingOn}`));
793
871
  if (data.focus) console.log(chalk.dim(` Focus: ${data.focus}`));
794
872
  if (data.blocked) console.log(chalk.dim(` Blocked: ${data.blocked}`));
873
+ if (data.urgent) console.log(chalk.dim(' Urgent: yes'));
795
874
  }
796
875
  } catch (err) {
797
876
  console.error(chalk.red(`Error: ${err.message}`));
@@ -804,6 +883,7 @@ program
804
883
  .command('recover')
805
884
  .description('Check for context death and recover state')
806
885
  .option('--clear', 'Clear the dirty death flag after recovery')
886
+ .option('--verbose', 'Show full checkpoint and handoff content')
807
887
  .option('-v, --vault <path>', 'Vault path')
808
888
  .option('--json', 'Output as JSON')
809
889
  .action(async (options) => {
@@ -815,12 +895,15 @@ program
815
895
  }
816
896
 
817
897
  const { recover, formatRecoveryInfo } = await import('../dist/commands/recover.js');
818
- const info = await recover(path.resolve(vaultPath), options.clear);
898
+ const info = await recover(path.resolve(vaultPath), {
899
+ clearFlag: options.clear,
900
+ verbose: options.verbose
901
+ });
819
902
 
820
903
  if (options.json) {
821
904
  console.log(JSON.stringify(info, null, 2));
822
905
  } else {
823
- console.log(formatRecoveryInfo(info));
906
+ console.log(formatRecoveryInfo(info, { verbose: options.verbose }));
824
907
  }
825
908
  } catch (err) {
826
909
  console.error(chalk.red(`Error: ${err.message}`));
@@ -828,6 +911,28 @@ program
828
911
  }
829
912
  });
830
913
 
914
+ // === STATUS ===
915
+ program
916
+ .command('status')
917
+ .description('Show vault health and status')
918
+ .option('-v, --vault <path>', 'Vault path')
919
+ .option('--json', 'Output as JSON')
920
+ .action(async (options) => {
921
+ try {
922
+ const vaultPath = options.vault || process.env.CLAWVAULT_PATH;
923
+ if (!vaultPath) {
924
+ console.error(chalk.red('Error: No vault path. Set CLAWVAULT_PATH or use -v'));
925
+ process.exit(1);
926
+ }
927
+
928
+ const { statusCommand } = await import('../dist/commands/status.js');
929
+ await statusCommand(path.resolve(vaultPath), { json: options.json });
930
+ } catch (err) {
931
+ console.error(chalk.red(`Error: ${err.message}`));
932
+ process.exit(1);
933
+ }
934
+ });
935
+
831
936
  // === CLEAN-EXIT ===
832
937
  program
833
938
  .command('clean-exit')
@@ -31,6 +31,21 @@ function findProtectedRanges(content) {
31
31
  function isProtected(pos, ranges) {
32
32
  return ranges.some((r) => pos >= r.start && pos < r.end);
33
33
  }
34
+ function createLineLookup(content) {
35
+ const lines = content.split("\n");
36
+ let charPos = 0;
37
+ const lineStarts = [];
38
+ for (const line of lines) {
39
+ lineStarts.push(charPos);
40
+ charPos += line.length + 1;
41
+ }
42
+ return (pos) => {
43
+ for (let i = lineStarts.length - 1; i >= 0; i--) {
44
+ if (pos >= lineStarts[i]) return i + 1;
45
+ }
46
+ return 1;
47
+ };
48
+ }
34
49
  function autoLink(content, index) {
35
50
  const protectedRanges = findProtectedRanges(content);
36
51
  const sortedAliases = getSortedAliases(index);
@@ -65,19 +80,7 @@ function dryRunLink(content, index) {
65
80
  const sortedAliases = getSortedAliases(index);
66
81
  const linkedEntities = /* @__PURE__ */ new Set();
67
82
  const matches = [];
68
- const lines = content.split("\n");
69
- let charPos = 0;
70
- const lineStarts = [];
71
- for (const line of lines) {
72
- lineStarts.push(charPos);
73
- charPos += line.length + 1;
74
- }
75
- function getLineNumber(pos) {
76
- for (let i = lineStarts.length - 1; i >= 0; i--) {
77
- if (pos >= lineStarts[i]) return i + 1;
78
- }
79
- return 1;
80
- }
83
+ const getLineNumber = createLineLookup(content);
81
84
  for (const { alias, path } of sortedAliases) {
82
85
  if (linkedEntities.has(path)) continue;
83
86
  const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -96,8 +99,33 @@ function dryRunLink(content, index) {
96
99
  }
97
100
  return matches;
98
101
  }
102
+ function findUnlinkedMentions(content, index) {
103
+ const protectedRanges = findProtectedRanges(content);
104
+ const sortedAliases = getSortedAliases(index);
105
+ const matches = [];
106
+ const seen = /* @__PURE__ */ new Set();
107
+ const getLineNumber = createLineLookup(content);
108
+ for (const { alias, path } of sortedAliases) {
109
+ if (seen.has(path)) continue;
110
+ const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
111
+ const regex = new RegExp(`\\b${escapedAlias}\\b`, "gi");
112
+ let match;
113
+ while ((match = regex.exec(content)) !== null) {
114
+ if (isProtected(match.index, protectedRanges)) continue;
115
+ matches.push({
116
+ alias: match[0],
117
+ path,
118
+ line: getLineNumber(match.index)
119
+ });
120
+ seen.add(path);
121
+ break;
122
+ }
123
+ }
124
+ return matches;
125
+ }
99
126
 
100
127
  export {
101
128
  autoLink,
102
- dryRunLink
129
+ dryRunLink,
130
+ findUnlinkedMentions
103
131
  };
@@ -0,0 +1,154 @@
1
+ import {
2
+ buildEntityIndex
3
+ } from "./chunk-J7ZWCI2C.js";
4
+
5
+ // src/lib/backlinks.ts
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ var CLAWVAULT_DIR = ".clawvault";
9
+ var BACKLINKS_FILE = "backlinks.json";
10
+ var WIKI_LINK_REGEX = /\[\[([^\]]+)\]\]/g;
11
+ function ensureClawvaultDir(vaultPath) {
12
+ const dir = path.join(vaultPath, CLAWVAULT_DIR);
13
+ if (!fs.existsSync(dir)) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+ return dir;
17
+ }
18
+ function toVaultId(vaultPath, filePath) {
19
+ const relative2 = path.relative(vaultPath, filePath).replace(/\.md$/, "");
20
+ return relative2.split(path.sep).join("/");
21
+ }
22
+ function normalizeLinkTarget(raw) {
23
+ let target = raw.trim();
24
+ if (!target) return "";
25
+ if (target.startsWith("[[") && target.endsWith("]]")) {
26
+ target = target.slice(2, -2);
27
+ }
28
+ const pipeIndex = target.indexOf("|");
29
+ if (pipeIndex !== -1) {
30
+ target = target.slice(0, pipeIndex);
31
+ }
32
+ if (target.startsWith("#")) return "";
33
+ const hashIndex = target.indexOf("#");
34
+ if (hashIndex !== -1) {
35
+ target = target.slice(0, hashIndex);
36
+ }
37
+ target = target.trim();
38
+ if (!target) return "";
39
+ if (target.endsWith(".md")) {
40
+ target = target.slice(0, -3);
41
+ }
42
+ if (target.startsWith("/")) {
43
+ target = target.slice(1);
44
+ }
45
+ return target.replace(/\\/g, "/");
46
+ }
47
+ function listMarkdownFiles(vaultPath) {
48
+ const files = [];
49
+ const skipDirs = /* @__PURE__ */ new Set(["archive", "templates", "node_modules"]);
50
+ function walk(dir) {
51
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
52
+ for (const entry of entries) {
53
+ const fullPath = path.join(dir, entry.name);
54
+ if (entry.isDirectory()) {
55
+ if (entry.name.startsWith(".") || skipDirs.has(entry.name)) continue;
56
+ walk(fullPath);
57
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
58
+ files.push(fullPath);
59
+ }
60
+ }
61
+ }
62
+ walk(vaultPath);
63
+ return files;
64
+ }
65
+ function buildKnownIds(vaultPath, files) {
66
+ const ids = /* @__PURE__ */ new Set();
67
+ const idsLower = /* @__PURE__ */ new Map();
68
+ for (const file of files) {
69
+ const id = toVaultId(vaultPath, file);
70
+ ids.add(id);
71
+ const lower = id.toLowerCase();
72
+ if (!idsLower.has(lower)) {
73
+ idsLower.set(lower, id);
74
+ }
75
+ }
76
+ return { ids, idsLower };
77
+ }
78
+ function resolveTarget(target, known, entityIndex) {
79
+ if (!target) return null;
80
+ if (known.ids.has(target)) return target;
81
+ const lower = target.toLowerCase();
82
+ if (known.idsLower.has(lower)) return known.idsLower.get(lower);
83
+ if (entityIndex?.entries.has(lower)) return entityIndex.entries.get(lower);
84
+ return null;
85
+ }
86
+ function scanVaultLinks(vaultPath, options = {}) {
87
+ const files = listMarkdownFiles(vaultPath);
88
+ const known = buildKnownIds(vaultPath, files);
89
+ const entityIndex = options.entityIndex ?? buildEntityIndex(vaultPath);
90
+ const backlinks = /* @__PURE__ */ new Map();
91
+ const orphans = [];
92
+ let linkCount = 0;
93
+ for (const file of files) {
94
+ const sourceId = toVaultId(vaultPath, file);
95
+ const content = fs.readFileSync(file, "utf-8");
96
+ const matches = content.match(WIKI_LINK_REGEX) || [];
97
+ linkCount += matches.length;
98
+ for (const match of matches) {
99
+ const target = normalizeLinkTarget(match);
100
+ if (!target) continue;
101
+ const resolved = resolveTarget(target, known, entityIndex);
102
+ if (!resolved) {
103
+ orphans.push({ source: sourceId, target });
104
+ continue;
105
+ }
106
+ if (!backlinks.has(resolved)) {
107
+ backlinks.set(resolved, /* @__PURE__ */ new Set());
108
+ }
109
+ backlinks.get(resolved).add(sourceId);
110
+ }
111
+ }
112
+ const backlinksMap = /* @__PURE__ */ new Map();
113
+ for (const [target, sources] of backlinks) {
114
+ backlinksMap.set(target, [...sources].sort());
115
+ }
116
+ return { backlinks: backlinksMap, orphans, linkCount };
117
+ }
118
+ function writeBacklinksIndex(vaultPath, backlinks) {
119
+ const dir = ensureClawvaultDir(vaultPath);
120
+ const output = {};
121
+ const targets = [...backlinks.keys()].sort();
122
+ for (const target of targets) {
123
+ const sources = backlinks.get(target) || [];
124
+ output[target] = [...new Set(sources)].sort();
125
+ }
126
+ fs.writeFileSync(path.join(dir, BACKLINKS_FILE), JSON.stringify(output, null, 2));
127
+ }
128
+ function readBacklinksIndex(vaultPath) {
129
+ const filePath = path.join(vaultPath, CLAWVAULT_DIR, BACKLINKS_FILE);
130
+ if (!fs.existsSync(filePath)) return null;
131
+ try {
132
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
133
+ const map = /* @__PURE__ */ new Map();
134
+ for (const [target, sources] of Object.entries(raw)) {
135
+ if (Array.isArray(sources)) {
136
+ map.set(target, sources);
137
+ }
138
+ }
139
+ return map;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+ function rebuildBacklinksIndex(vaultPath, options = {}) {
145
+ const result = scanVaultLinks(vaultPath, options);
146
+ writeBacklinksIndex(vaultPath, result.backlinks);
147
+ return result;
148
+ }
149
+
150
+ export {
151
+ scanVaultLinks,
152
+ readBacklinksIndex,
153
+ rebuildBacklinksIndex
154
+ };
@@ -0,0 +1,22 @@
1
+ // src/lib/template-engine.ts
2
+ function buildTemplateVariables(input = {}, now = /* @__PURE__ */ new Date()) {
3
+ const datetime = input.datetime ?? now.toISOString();
4
+ const date = input.date ?? datetime.split("T")[0];
5
+ return {
6
+ title: input.title ?? "",
7
+ type: input.type ?? "",
8
+ date,
9
+ datetime
10
+ };
11
+ }
12
+ function renderTemplate(template, variables) {
13
+ return template.replace(/\{\{\s*([a-zA-Z0-9_-]+)\s*\}\}/g, (match, key) => {
14
+ const value = variables[key];
15
+ return value !== void 0 ? String(value) : match;
16
+ });
17
+ }
18
+
19
+ export {
20
+ buildTemplateVariables,
21
+ renderTemplate
22
+ };
@@ -0,0 +1,16 @@
1
+ // src/lib/time.ts
2
+ function formatAge(ms) {
3
+ if (!Number.isFinite(ms)) return "unknown";
4
+ const seconds = Math.max(0, Math.floor(ms / 1e3));
5
+ const minutes = Math.floor(seconds / 60);
6
+ const hours = Math.floor(minutes / 60);
7
+ const days = Math.floor(hours / 24);
8
+ if (days > 0) return `${days}d ${hours % 24}h`;
9
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
10
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
11
+ return `${seconds}s`;
12
+ }
13
+
14
+ export {
15
+ formatAge
16
+ };