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 +12 -0
- package/bin/clawvault.js +167 -62
- package/dist/{chunk-4XJDHIKE.js → chunk-42MXU7A6.js} +42 -14
- package/dist/chunk-4VQTUVH7.js +154 -0
- package/dist/chunk-7766SIJP.js +22 -0
- package/dist/chunk-7ZRP733D.js +16 -0
- package/dist/chunk-MXNXWOPL.js +696 -0
- package/dist/chunk-MZZJLQNQ.js +152 -0
- package/dist/chunk-QYJI73KF.js +97 -0
- package/dist/chunk-VJIFT5T5.js +354 -0
- package/dist/commands/checkpoint.d.ts +15 -2
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/link.d.ts +3 -0
- package/dist/commands/link.js +136 -10
- package/dist/commands/recover.d.ts +7 -2
- package/dist/commands/recover.js +41 -4
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.js +7 -0
- package/dist/commands/status.d.ts +38 -0
- package/dist/commands/status.js +248 -0
- package/dist/commands/template.d.ts +27 -0
- package/dist/commands/template.js +147 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +34 -1217
- package/dist/lib/auto-linker.d.ts +9 -1
- package/dist/lib/auto-linker.js +5 -3
- package/dist/lib/template-engine.d.ts +10 -0
- package/dist/lib/template-engine.js +8 -0
- package/package.json +8 -18
- package/templates/checkpoint.md +19 -0
- package/templates/daily-note.md +19 -0
- package/templates/daily.md +8 -11
- package/templates/decision.md +5 -11
- package/templates/handoff.md +8 -12
- package/templates/lesson.md +7 -9
- package/templates/person.md +11 -26
- package/templates/project.md +10 -22
- package/dist/chunk-ALIUE5KY.js +0 -99
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
|
-
|
|
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',
|
|
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:
|
|
75
|
-
console.log(chalk.dim(`Install
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
|
562
|
-
|
|
563
|
-
|
|
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
|
|
612
|
-
|
|
613
|
-
|
|
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:
|
|
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, {
|
|
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),
|
|
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
|
|
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
|
+
};
|