clawvault 3.4.1 → 3.5.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +543 -0
  2. package/LICENSE +21 -0
  3. package/SKILL.md +369 -0
  4. package/dist/{chunk-X3SPPUFG.js → chunk-JI7VUQV7.js} +118 -132
  5. package/dist/{chunk-PLNK37JD.js → chunk-QUFQBAHP.js} +114 -217
  6. package/dist/cli/index.js +1 -1
  7. package/dist/commands/compat.js +1 -1
  8. package/dist/commands/observe.js +1 -1
  9. package/dist/commands/status.js +4 -4
  10. package/dist/index.js +11 -8
  11. package/dist/openclaw-plugin.js +6 -1
  12. package/docs/clawhub-security-release-playbook.md +75 -0
  13. package/docs/getting-started/installation.md +99 -0
  14. package/docs/openclaw-plugin-usage.md +152 -0
  15. package/openclaw.plugin.json +1 -1
  16. package/package.json +26 -8
  17. package/bin/command-registration.test.js +0 -179
  18. package/bin/command-runtime.test.js +0 -154
  19. package/bin/help-contract.test.js +0 -55
  20. package/bin/register-config-route-commands.test.js +0 -121
  21. package/bin/register-core-commands.test.js +0 -80
  22. package/bin/register-kanban-commands.test.js +0 -83
  23. package/bin/register-project-commands.test.js +0 -206
  24. package/bin/register-query-commands.test.js +0 -80
  25. package/bin/register-resilience-commands.test.js +0 -81
  26. package/bin/register-task-commands.test.js +0 -69
  27. package/bin/register-template-commands.test.js +0 -87
  28. package/bin/test-helpers/cli-command-fixtures.js +0 -120
  29. package/dashboard/lib/graph-diff.test.js +0 -75
  30. package/dashboard/lib/vault-parser.test.js +0 -254
  31. package/hooks/clawvault/HOOK.md +0 -130
  32. package/hooks/clawvault/handler.js +0 -1696
  33. package/hooks/clawvault/handler.test.js +0 -576
  34. package/hooks/clawvault/integrity.js +0 -112
  35. package/hooks/clawvault/integrity.test.js +0 -32
  36. package/hooks/clawvault/openclaw.plugin.json +0 -190
@@ -1,81 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { Command } from 'commander';
3
- import { registerResilienceCommands } from './register-resilience-commands.js';
4
- import { chalkStub } from './test-helpers/cli-command-fixtures.js';
5
-
6
- const {
7
- recoverMock,
8
- formatRecoveryInfoMock,
9
- checkRecoveryStatusMock,
10
- formatRecoveryCheckStatusMock,
11
- listCheckpointsMock,
12
- formatCheckpointListMock
13
- } = vi.hoisted(() => ({
14
- recoverMock: vi.fn(),
15
- formatRecoveryInfoMock: vi.fn(),
16
- checkRecoveryStatusMock: vi.fn(),
17
- formatRecoveryCheckStatusMock: vi.fn(),
18
- listCheckpointsMock: vi.fn(),
19
- formatCheckpointListMock: vi.fn()
20
- }));
21
-
22
- vi.mock('../dist/commands/recover.js', () => ({
23
- recover: recoverMock,
24
- formatRecoveryInfo: formatRecoveryInfoMock,
25
- checkRecoveryStatus: checkRecoveryStatusMock,
26
- formatRecoveryCheckStatus: formatRecoveryCheckStatusMock,
27
- listCheckpoints: listCheckpointsMock,
28
- formatCheckpointList: formatCheckpointListMock
29
- }));
30
-
31
- function buildProgram() {
32
- const program = new Command();
33
- registerResilienceCommands(program, {
34
- chalk: chalkStub,
35
- resolveVaultPath: (value) => value ?? '/vault'
36
- });
37
- return program;
38
- }
39
-
40
- async function runCommand(args) {
41
- const program = buildProgram();
42
- await program.parseAsync(args, { from: 'user' });
43
- }
44
-
45
- beforeEach(() => {
46
- vi.clearAllMocks();
47
- recoverMock.mockResolvedValue({ died: false });
48
- formatRecoveryInfoMock.mockReturnValue('recover');
49
- checkRecoveryStatusMock.mockResolvedValue({ died: false, deathTime: null, checkpoint: null });
50
- formatRecoveryCheckStatusMock.mockReturnValue('check');
51
- listCheckpointsMock.mockReturnValue([]);
52
- formatCheckpointListMock.mockReturnValue('list');
53
- });
54
-
55
- describe('register-resilience-commands', () => {
56
- it('routes recover --check through check helpers', async () => {
57
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
58
- try {
59
- await runCommand(['recover', '--check']);
60
- expect(checkRecoveryStatusMock).toHaveBeenCalledWith('/vault');
61
- expect(formatRecoveryCheckStatusMock).toHaveBeenCalled();
62
- expect(recoverMock).not.toHaveBeenCalled();
63
- expect(logSpy).toHaveBeenCalledWith('check');
64
- } finally {
65
- logSpy.mockRestore();
66
- }
67
- });
68
-
69
- it('routes recover --list through checkpoint listing helpers', async () => {
70
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
71
- try {
72
- await runCommand(['recover', '--list']);
73
- expect(listCheckpointsMock).toHaveBeenCalledWith('/vault');
74
- expect(formatCheckpointListMock).toHaveBeenCalled();
75
- expect(recoverMock).not.toHaveBeenCalled();
76
- expect(logSpy).toHaveBeenCalledWith('list');
77
- } finally {
78
- logSpy.mockRestore();
79
- }
80
- });
81
- });
@@ -1,69 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Command } from 'commander';
3
- import { registerTaskCommands } from './register-task-commands.js';
4
- import { chalkStub, stubResolveVaultPath } from './test-helpers/cli-command-fixtures.js';
5
-
6
- describe('register-task-commands', () => {
7
- it('adds enriched task add/list/update options', () => {
8
- const program = new Command();
9
- registerTaskCommands(program, {
10
- chalk: chalkStub,
11
- resolveVaultPath: stubResolveVaultPath
12
- });
13
-
14
- const taskCommand = program.commands.find((command) => command.name() === 'task');
15
- expect(taskCommand).toBeDefined();
16
-
17
- const addCommand = taskCommand?.commands.find((command) => command.name() === 'add');
18
- const addFlags = addCommand?.options.map((option) => option.flags) ?? [];
19
- expect(addFlags).toEqual(expect.arrayContaining([
20
- '--due <date>',
21
- '--tags <tags>',
22
- '--description <description>',
23
- '--estimate <estimate>',
24
- '--parent <slug>',
25
- '--depends-on <slugs>'
26
- ]));
27
-
28
- const listCommand = taskCommand?.commands.find((command) => command.name() === 'list');
29
- const listFlags = listCommand?.options.map((option) => option.flags) ?? [];
30
- expect(listFlags).toEqual(expect.arrayContaining([
31
- '--due',
32
- '--tag <tag>',
33
- '--overdue'
34
- ]));
35
-
36
- const updateCommand = taskCommand?.commands.find((command) => command.name() === 'update');
37
- const updateFlags = updateCommand?.options.map((option) => option.flags) ?? [];
38
- expect(updateFlags).toEqual(expect.arrayContaining([
39
- '--tags <tags>',
40
- '--description <description>',
41
- '--estimate <estimate>',
42
- '--parent <slug>',
43
- '--depends-on <slugs>',
44
- '--clear-due',
45
- '--clear-tags',
46
- '--clear-description',
47
- '--clear-estimate',
48
- '--clear-parent',
49
- '--clear-depends-on'
50
- ]));
51
- });
52
-
53
- it('adds simplified canvas flags', () => {
54
- const program = new Command();
55
- registerTaskCommands(program, {
56
- chalk: chalkStub,
57
- resolveVaultPath: stubResolveVaultPath
58
- });
59
-
60
- const canvasCommand = program.commands.find((command) => command.name() === 'canvas');
61
- expect(canvasCommand).toBeDefined();
62
-
63
- const optionFlags = canvasCommand?.options.map((option) => option.flags) ?? [];
64
- expect(optionFlags).toEqual(expect.arrayContaining([
65
- '-v, --vault <path>',
66
- '--output <path>'
67
- ]));
68
- });
69
- });
@@ -1,87 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { Command } from 'commander';
3
- import { registerTemplateCommands } from './register-template-commands.js';
4
- import { chalkStub } from './test-helpers/cli-command-fixtures.js';
5
-
6
- const {
7
- listTemplateDefinitionsMock,
8
- createFromTemplateMock,
9
- addTemplateMock
10
- } = vi.hoisted(() => ({
11
- listTemplateDefinitionsMock: vi.fn(),
12
- createFromTemplateMock: vi.fn(),
13
- addTemplateMock: vi.fn()
14
- }));
15
-
16
- vi.mock('../dist/commands/template.js', () => ({
17
- listTemplateDefinitions: listTemplateDefinitionsMock,
18
- createFromTemplate: createFromTemplateMock,
19
- addTemplate: addTemplateMock
20
- }));
21
-
22
- function buildProgram() {
23
- const program = new Command();
24
- registerTemplateCommands(program, { chalk: chalkStub });
25
- return program;
26
- }
27
-
28
- async function runCommand(args) {
29
- const program = buildProgram();
30
- await program.parseAsync(args, { from: 'user' });
31
- }
32
-
33
- describe('register-template-commands', () => {
34
- beforeEach(() => {
35
- vi.clearAllMocks();
36
- listTemplateDefinitionsMock.mockReturnValue([]);
37
- });
38
-
39
- it('registers list/create/add template subcommands', () => {
40
- const program = buildProgram();
41
- const templateCommand = program.commands.find((command) => command.name() === 'template');
42
- expect(templateCommand).toBeDefined();
43
- expect(templateCommand?.commands.map((command) => command.name())).toEqual(
44
- expect.arrayContaining(['list', 'create', 'add'])
45
- );
46
- });
47
-
48
- it('prints template fields in list output', async () => {
49
- listTemplateDefinitionsMock.mockReturnValue([
50
- { name: 'task', fields: ['status', 'owner'] },
51
- { name: 'project', fields: ['status', 'client'] }
52
- ]);
53
-
54
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
55
- try {
56
- await runCommand(['template', 'list']);
57
- expect(logSpy).toHaveBeenCalledWith('- task (status, owner)');
58
- expect(logSpy).toHaveBeenCalledWith('- project (status, client)');
59
- } finally {
60
- logSpy.mockRestore();
61
- }
62
- });
63
-
64
- it('dispatches create and add operations to template command handlers', async () => {
65
- createFromTemplateMock.mockReturnValue({
66
- outputPath: '/tmp/task.md',
67
- templatePath: '/templates/task.md',
68
- variables: { title: 'Task', type: 'task', date: '2026-02-16', datetime: '2026-02-16T00:00:00.000Z' }
69
- });
70
- addTemplateMock.mockReturnValue({
71
- templatePath: '/vault/templates/custom.md',
72
- name: 'custom'
73
- });
74
-
75
- await runCommand(['template', 'create', 'task', '--title', 'Ship It']);
76
- expect(createFromTemplateMock).toHaveBeenCalledWith('task', {
77
- title: 'Ship It',
78
- vaultPath: undefined
79
- });
80
-
81
- await runCommand(['template', 'add', 'source.md', '--name', 'custom']);
82
- expect(addTemplateMock).toHaveBeenCalledWith('source.md', {
83
- name: 'custom',
84
- vaultPath: undefined
85
- });
86
- });
87
- });
@@ -1,120 +0,0 @@
1
- import { Command } from 'commander';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import { registerCoreCommands } from '../register-core-commands.js';
5
- import { registerMaintenanceCommands } from '../register-maintenance-commands.js';
6
- import { registerQueryCommands } from '../register-query-commands.js';
7
- import { registerResilienceCommands } from '../register-resilience-commands.js';
8
- import { registerSessionLifecycleCommands } from '../register-session-lifecycle-commands.js';
9
- import { registerTemplateCommands } from '../register-template-commands.js';
10
- import { registerVaultOperationsCommands } from '../register-vault-operations-commands.js';
11
- import { registerConfigCommands } from '../register-config-commands.js';
12
- import { registerRouteCommands } from '../register-route-commands.js';
13
- import { registerTaskCommands } from '../register-task-commands.js';
14
- import { registerKanbanCommands } from '../register-kanban-commands.js';
15
- import { registerProjectCommands } from '../register-project-commands.js';
16
-
17
- export const chalkStub = {
18
- cyan: (value) => value,
19
- green: (value) => value,
20
- red: (value) => value,
21
- dim: (value) => value,
22
- yellow: (value) => value,
23
- white: (value) => value
24
- };
25
-
26
- export function stubResolveVaultPath(value) {
27
- return value ?? '/vault';
28
- }
29
-
30
- export function createVaultStub(overrides = {}) {
31
- return {
32
- store: async () => ({}),
33
- patch: async () => ({}),
34
- capture: async () => ({}),
35
- find: async () => [],
36
- vsearch: async () => [],
37
- list: async () => [],
38
- get: async () => null,
39
- stats: async () => ({ tags: [], categories: {} }),
40
- sync: async () => ({ copied: [], deleted: [], unchanged: [], errors: [] }),
41
- reindex: async () => 0,
42
- remember: async () => ({ id: '' }),
43
- getQmdCollection: () => '',
44
- createHandoff: async () => ({ id: '', path: '' }),
45
- generateRecap: async () => ({}),
46
- formatRecap: () => '',
47
- ...overrides
48
- };
49
- }
50
-
51
- export function createGetVaultStub(overrides = {}) {
52
- return async () => createVaultStub(overrides);
53
- }
54
-
55
- export function registerAllCommandModules(program = new Command()) {
56
- const getVault = createGetVaultStub();
57
-
58
- registerCoreCommands(program, {
59
- chalk: chalkStub,
60
- path,
61
- fs,
62
- createVault: async () => ({ getCategories: () => [], getQmdRoot: () => '', getQmdCollection: () => '' }),
63
- getVault,
64
- runQmd: async () => {}
65
- });
66
-
67
- registerQueryCommands(program, {
68
- chalk: chalkStub,
69
- getVault,
70
- resolveVaultPath: stubResolveVaultPath,
71
- QmdUnavailableError: class extends Error {},
72
- printQmdMissing: () => {}
73
- });
74
-
75
- registerVaultOperationsCommands(program, {
76
- chalk: chalkStub,
77
- fs,
78
- getVault,
79
- runQmd: async () => {},
80
- resolveVaultPath: stubResolveVaultPath,
81
- path
82
- });
83
-
84
- registerMaintenanceCommands(program, { chalk: chalkStub });
85
- registerResilienceCommands(program, {
86
- chalk: chalkStub,
87
- resolveVaultPath: stubResolveVaultPath
88
- });
89
- registerSessionLifecycleCommands(program, {
90
- chalk: chalkStub,
91
- resolveVaultPath: stubResolveVaultPath,
92
- QmdUnavailableError: class extends Error {},
93
- printQmdMissing: () => {},
94
- getVault,
95
- runQmd: async () => {}
96
- });
97
- registerTemplateCommands(program, { chalk: chalkStub });
98
- registerConfigCommands(program, {
99
- chalk: chalkStub,
100
- resolveVaultPath: stubResolveVaultPath
101
- });
102
- registerRouteCommands(program, {
103
- chalk: chalkStub,
104
- resolveVaultPath: stubResolveVaultPath
105
- });
106
- registerTaskCommands(program, {
107
- chalk: chalkStub,
108
- resolveVaultPath: stubResolveVaultPath
109
- });
110
- registerKanbanCommands(program, {
111
- chalk: chalkStub,
112
- resolveVaultPath: stubResolveVaultPath
113
- });
114
- registerProjectCommands(program, {
115
- chalk: chalkStub,
116
- resolveVaultPath: stubResolveVaultPath
117
- });
118
-
119
- return program;
120
- }
@@ -1,75 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { diffGraphs } from './graph-diff.js';
3
-
4
- describe('diffGraphs', () => {
5
- it('detects node and edge additions, updates, and removals', () => {
6
- const previous = {
7
- nodes: [
8
- { id: 'a', title: 'A', category: 'root', tags: [], path: 'a.md', missing: false, degree: 1 },
9
- { id: 'b', title: 'B', category: 'root', tags: ['x'], path: 'b.md', missing: false, degree: 1 },
10
- { id: 'c', title: 'C', category: 'root', tags: [], path: null, missing: true, degree: 0 }
11
- ],
12
- edges: [{ source: 'a', target: 'b' }],
13
- stats: { nodeCount: 3, edgeCount: 1 }
14
- };
15
-
16
- const next = {
17
- nodes: [
18
- { id: 'a', title: 'A Updated', category: 'root', tags: [], path: 'a.md', missing: false, degree: 1 },
19
- { id: 'b', title: 'B', category: 'root', tags: ['x'], path: 'b.md', missing: false, degree: 2 },
20
- { id: 'd', title: 'D', category: 'projects', tags: [], path: 'd.md', missing: false, degree: 1 }
21
- ],
22
- edges: [
23
- { source: 'a', target: 'b' },
24
- { source: 'b', target: 'd' }
25
- ],
26
- stats: { nodeCount: 3, edgeCount: 2 }
27
- };
28
-
29
- const patch = diffGraphs(previous, next);
30
-
31
- expect(patch.addedNodes).toEqual([next.nodes[2]]);
32
- expect(patch.updatedNodes).toEqual(expect.arrayContaining([next.nodes[0], next.nodes[1]]));
33
- expect(patch.removedNodeIds).toEqual(['c']);
34
- expect(patch.addedEdges).toEqual([{ source: 'b', target: 'd' }]);
35
- expect(patch.removedEdges).toEqual([]);
36
- expect(patch.changedNodeIds).toEqual(['a', 'b', 'c', 'd']);
37
- expect(patch.hasChanges).toBe(true);
38
- });
39
-
40
- it('returns hasChanges=false for equivalent graphs', () => {
41
- const graph = {
42
- nodes: [{ id: 'a', title: 'A', category: 'root', tags: ['t'], path: 'a.md', missing: false, degree: 0 }],
43
- edges: [],
44
- stats: { nodeCount: 1, edgeCount: 0 }
45
- };
46
-
47
- const patch = diffGraphs(graph, structuredClone(graph));
48
-
49
- expect(patch.hasChanges).toBe(false);
50
- expect(patch.addedNodes).toEqual([]);
51
- expect(patch.updatedNodes).toEqual([]);
52
- expect(patch.removedNodeIds).toEqual([]);
53
- expect(patch.addedEdges).toEqual([]);
54
- expect(patch.removedEdges).toEqual([]);
55
- });
56
-
57
- it('treats edge type changes as edge diff', () => {
58
- const previous = {
59
- nodes: [
60
- { id: 'a', title: 'A', category: 'root', tags: [], path: 'a.md', missing: false, degree: 1 },
61
- { id: 'b', title: 'B', category: 'root', tags: [], path: 'b.md', missing: false, degree: 1 }
62
- ],
63
- edges: [{ source: 'a', target: 'b', type: 'wiki_link' }]
64
- };
65
- const next = {
66
- nodes: previous.nodes,
67
- edges: [{ source: 'a', target: 'b', type: 'frontmatter_relation', label: 'related' }]
68
- };
69
-
70
- const patch = diffGraphs(previous, next);
71
- expect(patch.addedEdges).toEqual([{ source: 'a', target: 'b', type: 'frontmatter_relation', label: 'related' }]);
72
- expect(patch.removedEdges).toEqual([{ source: 'a', target: 'b', type: 'wiki_link' }]);
73
- expect(patch.hasChanges).toBe(true);
74
- });
75
- });
@@ -1,254 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as os from 'node:os';
3
- import * as path from 'node:path';
4
- import { describe, expect, it } from 'vitest';
5
- import { buildVaultGraph } from './vault-parser.js';
6
-
7
- function makeTempVault() {
8
- return fs.mkdtempSync(path.join(os.tmpdir(), 'clawvault-dashboard-'));
9
- }
10
-
11
- function writeVaultFile(root, relativePath, content) {
12
- const fullPath = path.join(root, relativePath);
13
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
14
- fs.writeFileSync(fullPath, content, 'utf8');
15
- }
16
-
17
- describe('buildVaultGraph', () => {
18
- it('builds nodes and edges from markdown wiki-links', async () => {
19
- const vaultPath = makeTempVault();
20
- try {
21
- writeVaultFile(
22
- vaultPath,
23
- 'decisions/use-clawvault.md',
24
- `---
25
- title: Use ClawVault
26
- tags: [architecture, memory]
27
- ---
28
- Linked to [[projects/clawvault|ClawVault Project]] and [[missing-note]].
29
- `
30
- );
31
- writeVaultFile(vaultPath, 'projects/clawvault.md', '# ClawVault');
32
-
33
- const graph = await buildVaultGraph(vaultPath);
34
- const decisionNode = graph.nodes.find((node) => node.id === 'decisions/use-clawvault');
35
- const unresolvedNode = graph.nodes.find((node) => node.id === 'missing-note');
36
-
37
- expect(decisionNode).toMatchObject({
38
- title: 'Use ClawVault',
39
- category: 'decisions',
40
- tags: ['architecture', 'memory'],
41
- type: 'decision'
42
- });
43
- expect(graph.edges).toEqual(expect.arrayContaining([
44
- expect.objectContaining({
45
- source: 'decisions/use-clawvault',
46
- target: 'projects/clawvault',
47
- type: 'wiki_link'
48
- }),
49
- expect.objectContaining({
50
- source: 'decisions/use-clawvault',
51
- target: 'missing-note',
52
- type: 'wiki_link'
53
- }),
54
- expect.objectContaining({
55
- source: 'decisions/use-clawvault',
56
- target: 'tag:architecture',
57
- type: 'tag'
58
- })
59
- ]));
60
- expect(unresolvedNode).toMatchObject({
61
- missing: true,
62
- category: 'unresolved'
63
- });
64
- expect(graph.stats.edgeTypeCounts.wiki_link).toBeGreaterThanOrEqual(2);
65
- expect(graph.stats.edgeTypeCounts.tag).toBeGreaterThanOrEqual(1);
66
- } finally {
67
- fs.rmSync(vaultPath, { recursive: true, force: true });
68
- }
69
- });
70
-
71
- it('resolves basename links when there is a unique match', async () => {
72
- const vaultPath = makeTempVault();
73
- try {
74
- writeVaultFile(vaultPath, 'research/notes.md', 'See [[clawvault]].');
75
- writeVaultFile(vaultPath, 'projects/clawvault.md', '# ClawVault');
76
-
77
- const graph = await buildVaultGraph(vaultPath);
78
-
79
- expect(graph.edges).toEqual(expect.arrayContaining([
80
- expect.objectContaining({
81
- source: 'research/notes',
82
- target: 'projects/clawvault',
83
- type: 'wiki_link'
84
- })
85
- ]));
86
- } finally {
87
- fs.rmSync(vaultPath, { recursive: true, force: true });
88
- }
89
- });
90
-
91
- it('emits frontmatter relation edges with labels', async () => {
92
- const vaultPath = makeTempVault();
93
- try {
94
- writeVaultFile(
95
- vaultPath,
96
- 'decisions/db.md',
97
- `---
98
- related:
99
- - projects/clawvault
100
- owner: people/alice
101
- ---
102
- Decision details`
103
- );
104
- writeVaultFile(vaultPath, 'projects/clawvault.md', '# ClawVault');
105
- writeVaultFile(vaultPath, 'people/alice.md', '# Alice');
106
-
107
- const graph = await buildVaultGraph(vaultPath);
108
- expect(graph.edges).toEqual(expect.arrayContaining([
109
- expect.objectContaining({
110
- source: 'decisions/db',
111
- target: 'projects/clawvault',
112
- type: 'frontmatter_relation',
113
- label: 'related'
114
- }),
115
- expect.objectContaining({
116
- source: 'decisions/db',
117
- target: 'people/alice',
118
- type: 'frontmatter_relation',
119
- label: 'owner'
120
- })
121
- ]));
122
- } finally {
123
- fs.rmSync(vaultPath, { recursive: true, force: true });
124
- }
125
- });
126
-
127
- it('loads graph data from memory graph index when present', async () => {
128
- const vaultPath = makeTempVault();
129
- try {
130
- writeVaultFile(vaultPath, 'decisions/use-clawvault.md', '# Placeholder');
131
- writeVaultFile(vaultPath, 'projects/clawvault.md', '# Placeholder project');
132
- const decisionMtime = fs.statSync(path.join(vaultPath, 'decisions/use-clawvault.md')).mtimeMs;
133
- const projectMtime = fs.statSync(path.join(vaultPath, 'projects/clawvault.md')).mtimeMs;
134
- const indexPath = path.join(vaultPath, '.clawvault', 'graph-index.json');
135
- fs.mkdirSync(path.dirname(indexPath), { recursive: true });
136
- fs.writeFileSync(
137
- indexPath,
138
- JSON.stringify({
139
- schemaVersion: 1,
140
- files: {
141
- 'decisions/use-clawvault.md': {
142
- relativePath: 'decisions/use-clawvault.md',
143
- mtimeMs: decisionMtime
144
- },
145
- 'projects/clawvault.md': {
146
- relativePath: 'projects/clawvault.md',
147
- mtimeMs: projectMtime
148
- }
149
- },
150
- graph: {
151
- nodes: [
152
- {
153
- id: 'note:decisions/use-clawvault',
154
- title: 'Use ClawVault',
155
- type: 'decision',
156
- category: 'decisions',
157
- tags: ['architecture'],
158
- path: 'decisions/use-clawvault.md',
159
- missing: false,
160
- degree: 1
161
- },
162
- {
163
- id: 'note:projects/clawvault',
164
- title: 'ClawVault Project',
165
- type: 'project',
166
- category: 'projects',
167
- tags: [],
168
- path: 'projects/clawvault.md',
169
- missing: false,
170
- degree: 1
171
- }
172
- ],
173
- edges: [
174
- {
175
- source: 'note:decisions/use-clawvault',
176
- target: 'note:projects/clawvault',
177
- type: 'frontmatter_relation',
178
- label: 'related'
179
- }
180
- ],
181
- stats: { generatedAt: '2026-02-13T00:00:00.000Z' }
182
- }
183
- }),
184
- 'utf8'
185
- );
186
-
187
- const graph = await buildVaultGraph(vaultPath);
188
- expect(graph.nodes.find((node) => node.id === 'decisions/use-clawvault')).toBeTruthy();
189
- expect(graph.edges).toEqual([
190
- {
191
- source: 'decisions/use-clawvault',
192
- target: 'projects/clawvault',
193
- type: 'frontmatter_relation',
194
- label: 'related'
195
- }
196
- ]);
197
- expect(graph.stats.fileCount).toBe(2);
198
- } finally {
199
- fs.rmSync(vaultPath, { recursive: true, force: true });
200
- }
201
- });
202
-
203
- it('falls back to markdown parsing when memory graph index is stale', async () => {
204
- const vaultPath = makeTempVault();
205
- try {
206
- writeVaultFile(vaultPath, 'projects/clawvault.md', '# ClawVault');
207
- writeVaultFile(vaultPath, 'decisions/use-clawvault.md', 'See [[projects/clawvault]].');
208
-
209
- const indexPath = path.join(vaultPath, '.clawvault', 'graph-index.json');
210
- fs.mkdirSync(path.dirname(indexPath), { recursive: true });
211
- fs.writeFileSync(
212
- indexPath,
213
- JSON.stringify({
214
- schemaVersion: 1,
215
- generatedAt: '2026-02-13T00:00:00.000Z',
216
- files: {
217
- 'decisions/use-clawvault.md': { relativePath: 'decisions/use-clawvault.md', mtimeMs: 1 },
218
- 'projects/clawvault.md': { relativePath: 'projects/clawvault.md', mtimeMs: 1 }
219
- },
220
- graph: {
221
- nodes: [
222
- {
223
- id: 'note:decisions/use-clawvault',
224
- title: 'Old node',
225
- type: 'decision',
226
- category: 'decisions',
227
- tags: [],
228
- path: 'decisions/use-clawvault.md',
229
- missing: false,
230
- degree: 0
231
- }
232
- ],
233
- edges: [],
234
- stats: { generatedAt: '2026-02-13T00:00:00.000Z' }
235
- }
236
- }),
237
- 'utf8'
238
- );
239
-
240
- const graph = await buildVaultGraph(vaultPath);
241
- const node = graph.nodes.find((candidate) => candidate.id === 'decisions/use-clawvault');
242
- expect(node?.title).not.toBe('Old node');
243
- expect(graph.edges).toEqual(expect.arrayContaining([
244
- expect.objectContaining({
245
- source: 'decisions/use-clawvault',
246
- target: 'projects/clawvault',
247
- type: 'wiki_link'
248
- })
249
- ]));
250
- } finally {
251
- fs.rmSync(vaultPath, { recursive: true, force: true });
252
- }
253
- });
254
- });