clawvault 2.6.1 → 2.6.3
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 +2 -0
- package/dist/{chunk-TLGBDTYT.js → chunk-6546Q4OR.js} +1 -1
- package/dist/{chunk-XAVB4GB4.js → chunk-DTEHFAL7.js} +1 -1
- package/dist/{chunk-UEOUADMO.js → chunk-PZ2AUU2W.js} +1 -1
- package/dist/{chunk-KL4NAOMO.js → chunk-RCBMXTWS.js} +28 -3
- package/dist/cli/index.js +3 -3
- package/dist/commands/context.js +2 -2
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/sleep.js +1 -1
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +99 -19
- package/dist/commands/wake.js +1 -1
- package/dist/index.d.ts +87 -1
- package/dist/index.js +159 -5
- package/hooks/clawvault/HOOK.md +34 -4
- package/hooks/clawvault/handler.js +39 -8
- package/hooks/clawvault/handler.test.js +94 -0
- package/openclaw.plugin.json +72 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Structured memory system for AI agents and operators: typed markdown memory, gra
|
|
|
6
6
|
|
|
7
7
|
> Local-first. Markdown-first. Built to survive long-running autonomous work.
|
|
8
8
|
|
|
9
|
+
**$CLAW**: [`5Fjr82MTB8mvxkzi9FYtvrUsPiDGE2M29w3dYcZpump`](https://pump.fun/coin/5Fjr82MTB8mvxkzi9FYtvrUsPiDGE2M29w3dYcZpump)
|
|
10
|
+
|
|
9
11
|
## Requirements
|
|
10
12
|
|
|
11
13
|
- Node.js 18+
|
|
@@ -624,14 +624,15 @@ var ClawVault = class {
|
|
|
624
624
|
md += `## Recent Sessions
|
|
625
625
|
`;
|
|
626
626
|
for (const h of recap.recentHandoffs) {
|
|
627
|
+
const datePart = this.extractDatePart(h.created);
|
|
627
628
|
if (brief) {
|
|
628
|
-
md += `- **${
|
|
629
|
+
md += `- **${datePart}:** ${h.workingOn.slice(0, 2).join(", ")}`;
|
|
629
630
|
if (h.nextSteps.length > 0) md += ` \u2192 ${h.nextSteps[0]}`;
|
|
630
631
|
md += `
|
|
631
632
|
`;
|
|
632
633
|
} else {
|
|
633
634
|
md += `
|
|
634
|
-
### ${
|
|
635
|
+
### ${datePart}
|
|
635
636
|
`;
|
|
636
637
|
md += `**Working on:** ${h.workingOn.join(", ")}
|
|
637
638
|
`;
|
|
@@ -689,7 +690,7 @@ var ClawVault = class {
|
|
|
689
690
|
*/
|
|
690
691
|
parseHandoff(doc) {
|
|
691
692
|
return {
|
|
692
|
-
created: doc.frontmatter.date
|
|
693
|
+
created: this.toDateString(doc.frontmatter.date, doc.modified.toISOString()),
|
|
693
694
|
sessionKey: doc.frontmatter.sessionKey,
|
|
694
695
|
workingOn: doc.frontmatter.workingOn || [],
|
|
695
696
|
blocked: doc.frontmatter.blocked || [],
|
|
@@ -700,6 +701,30 @@ var ClawVault = class {
|
|
|
700
701
|
};
|
|
701
702
|
}
|
|
702
703
|
// === Private helpers ===
|
|
704
|
+
/**
|
|
705
|
+
* Safely convert a date value to ISO string format.
|
|
706
|
+
* Handles Date objects, strings, and undefined values.
|
|
707
|
+
*/
|
|
708
|
+
toDateString(value, fallback) {
|
|
709
|
+
if (value instanceof Date) {
|
|
710
|
+
return value.toISOString();
|
|
711
|
+
}
|
|
712
|
+
if (typeof value === "string" && value.length > 0) {
|
|
713
|
+
return value;
|
|
714
|
+
}
|
|
715
|
+
return fallback || (/* @__PURE__ */ new Date()).toISOString();
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Extract the date portion (YYYY-MM-DD) from an ISO date string or Date object.
|
|
719
|
+
* Provides safe handling for various date formats.
|
|
720
|
+
*/
|
|
721
|
+
extractDatePart(value) {
|
|
722
|
+
const dateStr = this.toDateString(value);
|
|
723
|
+
if (dateStr.includes("T")) {
|
|
724
|
+
return dateStr.split("T")[0];
|
|
725
|
+
}
|
|
726
|
+
return dateStr.slice(0, 10);
|
|
727
|
+
}
|
|
703
728
|
applyQmdConfig(meta) {
|
|
704
729
|
const collection = meta?.qmdCollection || this.config.qmdCollection || this.config.name;
|
|
705
730
|
const root = meta?.qmdRoot || this.config.qmdRoot || this.config.path;
|
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
registerCliCommands
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-6546Q4OR.js";
|
|
4
4
|
import "../chunk-THRJVD4L.js";
|
|
5
5
|
import "../chunk-TIGW564L.js";
|
|
6
6
|
import "../chunk-IVRIKYFE.js";
|
|
@@ -8,12 +8,12 @@ import "../chunk-ME37YNW3.js";
|
|
|
8
8
|
import "../chunk-P5EPF6MB.js";
|
|
9
9
|
import "../chunk-3BTHWPMB.js";
|
|
10
10
|
import "../chunk-T76H47ZS.js";
|
|
11
|
-
import "../chunk-
|
|
11
|
+
import "../chunk-DTEHFAL7.js";
|
|
12
12
|
import "../chunk-IEVLHNLU.js";
|
|
13
13
|
import "../chunk-HRLWZGMA.js";
|
|
14
14
|
import "../chunk-Q2J5YTUF.js";
|
|
15
15
|
import "../chunk-AZYOKJYC.js";
|
|
16
|
-
import "../chunk-
|
|
16
|
+
import "../chunk-RCBMXTWS.js";
|
|
17
17
|
import "../chunk-FHFUXL6G.js";
|
|
18
18
|
import "../chunk-4QYGFWRM.js";
|
|
19
19
|
import "../chunk-MAKNAHAW.js";
|
package/dist/commands/context.js
CHANGED
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
contextCommand,
|
|
4
4
|
formatContextMarkdown,
|
|
5
5
|
registerContextCommand
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-DTEHFAL7.js";
|
|
7
|
+
import "../chunk-RCBMXTWS.js";
|
|
8
8
|
import "../chunk-FHFUXL6G.js";
|
|
9
9
|
import "../chunk-MAKNAHAW.js";
|
|
10
10
|
import "../chunk-2CDEETQN.js";
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
doctor
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-PZ2AUU2W.js";
|
|
4
4
|
import "../chunk-7ZRP733D.js";
|
|
5
5
|
import "../chunk-4VQTUVH7.js";
|
|
6
6
|
import "../chunk-J7ZWCI2C.js";
|
|
@@ -8,7 +8,7 @@ import "../chunk-IEVLHNLU.js";
|
|
|
8
8
|
import "../chunk-HRLWZGMA.js";
|
|
9
9
|
import "../chunk-Q2J5YTUF.js";
|
|
10
10
|
import "../chunk-AZYOKJYC.js";
|
|
11
|
-
import "../chunk-
|
|
11
|
+
import "../chunk-RCBMXTWS.js";
|
|
12
12
|
import "../chunk-FHFUXL6G.js";
|
|
13
13
|
import "../chunk-MAKNAHAW.js";
|
|
14
14
|
import "../chunk-ITPEXLHA.js";
|
package/dist/commands/sleep.js
CHANGED
package/dist/commands/status.js
CHANGED
|
@@ -13,7 +13,7 @@ import "../chunk-Q2J5YTUF.js";
|
|
|
13
13
|
import "../chunk-AZYOKJYC.js";
|
|
14
14
|
import {
|
|
15
15
|
ClawVault
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-RCBMXTWS.js";
|
|
17
17
|
import "../chunk-FHFUXL6G.js";
|
|
18
18
|
import {
|
|
19
19
|
QmdUnavailableError,
|
|
@@ -34,6 +34,80 @@ import "../chunk-7766SIJP.js";
|
|
|
34
34
|
import * as fs from "fs";
|
|
35
35
|
import * as path from "path";
|
|
36
36
|
import { execFileSync } from "child_process";
|
|
37
|
+
|
|
38
|
+
// src/lib/qmd-collections.ts
|
|
39
|
+
var COLLECTION_HEADER_RE = /^(\S+)\s+\(qmd:\/\/([^)]+)\)\s*$/;
|
|
40
|
+
var DETAIL_LINE_RE = /^\s+([A-Za-z][A-Za-z0-9 _-]*):\s*(.+)\s*$/;
|
|
41
|
+
function normalizeDetailKey(value) {
|
|
42
|
+
return value.trim().toLowerCase().replace(/[ -]+/g, "_");
|
|
43
|
+
}
|
|
44
|
+
function parseCount(raw) {
|
|
45
|
+
if (!raw) return void 0;
|
|
46
|
+
const match = raw.match(/-?\d[\d,]*/);
|
|
47
|
+
if (!match) return void 0;
|
|
48
|
+
const parsed = Number.parseInt(match[0].replace(/,/g, ""), 10);
|
|
49
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
50
|
+
}
|
|
51
|
+
function pickDetail(details, keys) {
|
|
52
|
+
for (const key of keys) {
|
|
53
|
+
const value = details[key];
|
|
54
|
+
if (typeof value === "string" && value.trim()) {
|
|
55
|
+
return value.trim();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
function pickCount(details, keys) {
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
const parsed = parseCount(details[key]);
|
|
63
|
+
if (parsed !== void 0) {
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
function parseQmdCollectionList(raw) {
|
|
70
|
+
const collections = [];
|
|
71
|
+
let current = null;
|
|
72
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
73
|
+
const headerMatch = line.match(COLLECTION_HEADER_RE);
|
|
74
|
+
if (headerMatch) {
|
|
75
|
+
current = {
|
|
76
|
+
name: headerMatch[1],
|
|
77
|
+
uri: headerMatch[2],
|
|
78
|
+
details: {}
|
|
79
|
+
};
|
|
80
|
+
collections.push(current);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!current) continue;
|
|
84
|
+
const detailMatch = line.match(DETAIL_LINE_RE);
|
|
85
|
+
if (!detailMatch) continue;
|
|
86
|
+
const key = normalizeDetailKey(detailMatch[1]);
|
|
87
|
+
current.details[key] = detailMatch[2].trim();
|
|
88
|
+
}
|
|
89
|
+
for (const collection of collections) {
|
|
90
|
+
const root = pickDetail(collection.details, ["root", "path", "directory"]);
|
|
91
|
+
if (root) {
|
|
92
|
+
collection.root = root;
|
|
93
|
+
}
|
|
94
|
+
collection.files = pickCount(collection.details, ["files", "documents", "docs"]);
|
|
95
|
+
collection.vectors = pickCount(collection.details, ["vectors", "embeddings", "vector_embeddings"]);
|
|
96
|
+
collection.pendingEmbeddings = pickCount(collection.details, [
|
|
97
|
+
"pending",
|
|
98
|
+
"pending_vectors",
|
|
99
|
+
"pending_embeddings",
|
|
100
|
+
"unembedded",
|
|
101
|
+
"without_embeddings"
|
|
102
|
+
]);
|
|
103
|
+
if (collection.pendingEmbeddings === void 0 && collection.files !== void 0 && collection.vectors !== void 0) {
|
|
104
|
+
collection.pendingEmbeddings = Math.max(collection.files - collection.vectors, 0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return collections;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/commands/status.ts
|
|
37
111
|
var CLAWVAULT_DIR = ".clawvault";
|
|
38
112
|
var CHECKPOINT_FILE = "last-checkpoint.json";
|
|
39
113
|
var DIRTY_DEATH_FLAG = "dirty-death.flag";
|
|
@@ -80,22 +154,18 @@ function getLatestVaultMarkdownMtime(vaultPath) {
|
|
|
80
154
|
walk(vaultPath);
|
|
81
155
|
return latest;
|
|
82
156
|
}
|
|
83
|
-
function parseQmdCollectionsText(raw) {
|
|
84
|
-
const names = [];
|
|
85
|
-
const regex = /^(\S+)\s+\(qmd:\/\/\1\/\)/gm;
|
|
86
|
-
let match;
|
|
87
|
-
while ((match = regex.exec(raw)) !== null) {
|
|
88
|
-
names.push(match[1]);
|
|
89
|
-
}
|
|
90
|
-
return names;
|
|
91
|
-
}
|
|
92
157
|
function getQmdIndexStatus(collection, root, indexName) {
|
|
93
158
|
const output = execFileSync("qmd", withQmdIndexArgs(["collection", "list"], indexName), { encoding: "utf-8" });
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
159
|
+
const collections = parseQmdCollectionList(output);
|
|
160
|
+
const collectionInfo = collections.find((c) => c.name === collection);
|
|
161
|
+
if (collectionInfo) {
|
|
162
|
+
return {
|
|
163
|
+
status: "present",
|
|
164
|
+
files: collectionInfo.files,
|
|
165
|
+
vectors: collectionInfo.vectors
|
|
166
|
+
};
|
|
97
167
|
}
|
|
98
|
-
return "missing";
|
|
168
|
+
return { status: "missing" };
|
|
99
169
|
}
|
|
100
170
|
function loadCheckpoint(vaultPath) {
|
|
101
171
|
const checkpointPath = path.join(vaultPath, CLAWVAULT_DIR, CHECKPOINT_FILE);
|
|
@@ -140,12 +210,12 @@ async function getStatus(vaultPath, options = {}) {
|
|
|
140
210
|
}
|
|
141
211
|
const qmdCollection = vault.getQmdCollection();
|
|
142
212
|
const qmdRoot = vault.getQmdRoot();
|
|
143
|
-
let
|
|
213
|
+
let qmdIndexResult = { status: "missing" };
|
|
144
214
|
let qmdError;
|
|
145
215
|
try {
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
issues.push(`qmd collection ${
|
|
216
|
+
qmdIndexResult = getQmdIndexStatus(qmdCollection, qmdRoot, options.qmdIndexName);
|
|
217
|
+
if (qmdIndexResult.status !== "present") {
|
|
218
|
+
issues.push(`qmd collection ${qmdIndexResult.status.replace("-", " ")}`);
|
|
149
219
|
}
|
|
150
220
|
} catch (err) {
|
|
151
221
|
qmdError = err?.message || "Failed to check qmd index";
|
|
@@ -197,7 +267,9 @@ async function getStatus(vaultPath, options = {}) {
|
|
|
197
267
|
qmd: {
|
|
198
268
|
collection: qmdCollection,
|
|
199
269
|
root: qmdRoot,
|
|
200
|
-
indexStatus:
|
|
270
|
+
indexStatus: qmdIndexResult.status,
|
|
271
|
+
files: qmdIndexResult.files,
|
|
272
|
+
vectors: qmdIndexResult.vectors,
|
|
201
273
|
error: qmdError
|
|
202
274
|
},
|
|
203
275
|
graph: graphStatus,
|
|
@@ -256,6 +328,14 @@ function formatStatus(status) {
|
|
|
256
328
|
`;
|
|
257
329
|
output += ` - Index: ${status.qmd.indexStatus}
|
|
258
330
|
`;
|
|
331
|
+
if (status.qmd.files !== void 0) {
|
|
332
|
+
output += ` - Files: ${status.qmd.files}
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
if (status.qmd.vectors !== void 0) {
|
|
336
|
+
output += ` - Vectors: ${status.qmd.vectors}
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
259
339
|
if (status.qmd.error) {
|
|
260
340
|
output += ` - Error: ${status.qmd.error}
|
|
261
341
|
`;
|
package/dist/commands/wake.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -157,6 +157,16 @@ declare class ClawVault {
|
|
|
157
157
|
* Parse a handoff document back into structured form
|
|
158
158
|
*/
|
|
159
159
|
private parseHandoff;
|
|
160
|
+
/**
|
|
161
|
+
* Safely convert a date value to ISO string format.
|
|
162
|
+
* Handles Date objects, strings, and undefined values.
|
|
163
|
+
*/
|
|
164
|
+
private toDateString;
|
|
165
|
+
/**
|
|
166
|
+
* Extract the date portion (YYYY-MM-DD) from an ISO date string or Date object.
|
|
167
|
+
* Provides safe handling for various date formats.
|
|
168
|
+
*/
|
|
169
|
+
private extractDatePart;
|
|
160
170
|
private applyQmdConfig;
|
|
161
171
|
private slugify;
|
|
162
172
|
private saveIndex;
|
|
@@ -300,6 +310,82 @@ declare function extractWikiLinks(content: string): string[];
|
|
|
300
310
|
*/
|
|
301
311
|
declare function extractTags(content: string): string[];
|
|
302
312
|
|
|
313
|
+
/**
|
|
314
|
+
* ClawVault Hybrid Search — BM25 + Semantic Embeddings + RRF
|
|
315
|
+
*
|
|
316
|
+
* Proven in LongMemEval benchmarks:
|
|
317
|
+
* - v28 pipeline: 57.0% overall (up from 52.6% with BM25-only)
|
|
318
|
+
* - Multi-session: 45.9% (up from 28.6%)
|
|
319
|
+
* - Single-session-user: 85.7% (up from 72.9%)
|
|
320
|
+
*
|
|
321
|
+
* Architecture:
|
|
322
|
+
* 1. BM25 via existing qmd search
|
|
323
|
+
* 2. Semantic via @huggingface/transformers (all-MiniLM-L6-v2)
|
|
324
|
+
* 3. Reciprocal Rank Fusion (k=60)
|
|
325
|
+
*/
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Compute embedding for a text string
|
|
329
|
+
*/
|
|
330
|
+
declare function embed(text: string): Promise<Float32Array>;
|
|
331
|
+
/**
|
|
332
|
+
* Compute embeddings for multiple texts
|
|
333
|
+
*/
|
|
334
|
+
declare function embedBatch(texts: string[]): Promise<Float32Array[]>;
|
|
335
|
+
/**
|
|
336
|
+
* Cosine similarity between two normalized vectors
|
|
337
|
+
*/
|
|
338
|
+
declare function cosineSimilarity(a: Float32Array, b: Float32Array): number;
|
|
339
|
+
/**
|
|
340
|
+
* Embedding cache — stores embeddings on disk alongside vault files
|
|
341
|
+
*/
|
|
342
|
+
declare class EmbeddingCache {
|
|
343
|
+
private cachePath;
|
|
344
|
+
private cache;
|
|
345
|
+
private dirty;
|
|
346
|
+
constructor(vaultPath: string);
|
|
347
|
+
/**
|
|
348
|
+
* Load cache from disk
|
|
349
|
+
*/
|
|
350
|
+
load(): void;
|
|
351
|
+
/**
|
|
352
|
+
* Save cache to disk
|
|
353
|
+
*/
|
|
354
|
+
save(): void;
|
|
355
|
+
get(key: string): Float32Array | undefined;
|
|
356
|
+
set(key: string, embedding: Float32Array): void;
|
|
357
|
+
has(key: string): boolean;
|
|
358
|
+
entries(): IterableIterator<[string, Float32Array]>;
|
|
359
|
+
get size(): number;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Reciprocal Rank Fusion of two ranked lists
|
|
363
|
+
*/
|
|
364
|
+
declare function reciprocalRankFusion(list1: {
|
|
365
|
+
id: string;
|
|
366
|
+
score: number;
|
|
367
|
+
}[], list2: {
|
|
368
|
+
id: string;
|
|
369
|
+
score: number;
|
|
370
|
+
}[], k?: number): {
|
|
371
|
+
id: string;
|
|
372
|
+
score: number;
|
|
373
|
+
}[];
|
|
374
|
+
/**
|
|
375
|
+
* Semantic search against embedding cache
|
|
376
|
+
*/
|
|
377
|
+
declare function semanticSearch(query: string, cache: EmbeddingCache, topK?: number): Promise<{
|
|
378
|
+
id: string;
|
|
379
|
+
score: number;
|
|
380
|
+
}[]>;
|
|
381
|
+
/**
|
|
382
|
+
* Hybrid search: combines BM25 results with semantic results via RRF
|
|
383
|
+
*/
|
|
384
|
+
declare function hybridSearch(query: string, bm25Results: SearchResult[], cache: EmbeddingCache, options?: {
|
|
385
|
+
topK?: number;
|
|
386
|
+
rrfK?: number;
|
|
387
|
+
}): Promise<SearchResult[]>;
|
|
388
|
+
|
|
303
389
|
declare const OBSERVE_PROVIDERS: readonly ["anthropic", "openai", "gemini"];
|
|
304
390
|
declare const OBSERVER_COMPRESSION_PROVIDERS: readonly ["anthropic", "openai", "gemini", "openai-compatible", "ollama"];
|
|
305
391
|
declare const THEMES: readonly ["neural", "minimal", "none"];
|
|
@@ -670,4 +756,4 @@ declare function runReflection(options: ReflectOptions): Promise<ReflectResult>;
|
|
|
670
756
|
declare const VERSION: string;
|
|
671
757
|
declare function registerCommanderCommands(program: Command): Command;
|
|
672
758
|
|
|
673
|
-
export { type ActiveObservationCandidate, type ActiveObservationFailure, type ActiveObserveOptions, type ActiveObserveResult, type ArchiveObservationsOptions, type ArchiveObservationsResult, Category, ClawVault, type CompressionProvider, Compressor, type CompressorOptions, type ContextProfile as ConfigDefaultProfile, Document, HandoffDocument, type ManagedConfigKey, MemoryType, type ObserveCursorEntry, type ObserveCursorStore, type ObserveProvider, Observer, type ObserverCompressionProvider, type ObserverCompressor, type ObserverOptions, type ObserverReflector, type ObserverStalenessResult, QMD_INSTALL_COMMAND, QMD_INSTALL_URL, QmdUnavailableError, type ReflectOptions, type ReflectResult, Reflector, type ReflectorOptions, type RouteRule, SUPPORTED_CONFIG_KEYS, SearchEngine, SearchOptions, SearchResult, SessionRecap, SessionWatcher, type SessionWatcherOptions, StoreOptions, SyncOptions, SyncResult, type Theme, type TransitionEvent, VERSION, VaultConfig, addRouteRule, appendTransition, archiveObservations, buildTransitionEvent, countBlockedTransitions, createVault, extractTags, extractWikiLinks, findVault, formatTransitionsTable, getConfig, getConfigValue, getObserverStaleness, getScaledObservationThresholdBytes, hasQmd, isRegression, listConfig, listRouteRules, matchRouteRule, observeActiveSessions, parseSessionFile, parseSessionSourceLabel, qmdEmbed, qmdUpdate, queryTransitions, readAllTransitions, registerCommanderCommands, removeRouteRule, resetConfig, runReflection, setConfigValue, testRouteRule };
|
|
759
|
+
export { type ActiveObservationCandidate, type ActiveObservationFailure, type ActiveObserveOptions, type ActiveObserveResult, type ArchiveObservationsOptions, type ArchiveObservationsResult, Category, ClawVault, type CompressionProvider, Compressor, type CompressorOptions, type ContextProfile as ConfigDefaultProfile, Document, EmbeddingCache, HandoffDocument, type ManagedConfigKey, MemoryType, type ObserveCursorEntry, type ObserveCursorStore, type ObserveProvider, Observer, type ObserverCompressionProvider, type ObserverCompressor, type ObserverOptions, type ObserverReflector, type ObserverStalenessResult, QMD_INSTALL_COMMAND, QMD_INSTALL_URL, QmdUnavailableError, type ReflectOptions, type ReflectResult, Reflector, type ReflectorOptions, type RouteRule, SUPPORTED_CONFIG_KEYS, SearchEngine, SearchOptions, SearchResult, SessionRecap, SessionWatcher, type SessionWatcherOptions, StoreOptions, SyncOptions, SyncResult, type Theme, type TransitionEvent, VERSION, VaultConfig, addRouteRule, appendTransition, archiveObservations, buildTransitionEvent, cosineSimilarity, countBlockedTransitions, createVault, embed, embedBatch, extractTags, extractWikiLinks, findVault, formatTransitionsTable, getConfig, getConfigValue, getObserverStaleness, getScaledObservationThresholdBytes, hasQmd, hybridSearch, isRegression, listConfig, listRouteRules, matchRouteRule, observeActiveSessions, parseSessionFile, parseSessionSourceLabel, qmdEmbed, qmdUpdate, queryTransitions, readAllTransitions, reciprocalRankFusion, registerCommanderCommands, removeRouteRule, resetConfig, runReflection, semanticSearch, setConfigValue, testRouteRule };
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from "./chunk-R6SXNSFD.js";
|
|
26
26
|
import {
|
|
27
27
|
doctor
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-PZ2AUU2W.js";
|
|
29
29
|
import "./chunk-7ZRP733D.js";
|
|
30
30
|
import {
|
|
31
31
|
graphCommand,
|
|
@@ -45,7 +45,7 @@ import "./chunk-4VQTUVH7.js";
|
|
|
45
45
|
import "./chunk-J7ZWCI2C.js";
|
|
46
46
|
import {
|
|
47
47
|
registerCliCommands
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-6546Q4OR.js";
|
|
49
49
|
import {
|
|
50
50
|
registerTailscaleCommands,
|
|
51
51
|
registerTailscaleDiscoverCommand,
|
|
@@ -102,7 +102,7 @@ import {
|
|
|
102
102
|
normalizeContextProfileInput,
|
|
103
103
|
registerContextCommand,
|
|
104
104
|
resolveContextProfile
|
|
105
|
-
} from "./chunk-
|
|
105
|
+
} from "./chunk-DTEHFAL7.js";
|
|
106
106
|
import {
|
|
107
107
|
getObserverStaleness,
|
|
108
108
|
getScaledObservationThresholdBytes,
|
|
@@ -128,7 +128,7 @@ import {
|
|
|
128
128
|
ClawVault,
|
|
129
129
|
createVault,
|
|
130
130
|
findVault
|
|
131
|
-
} from "./chunk-
|
|
131
|
+
} from "./chunk-RCBMXTWS.js";
|
|
132
132
|
import "./chunk-FHFUXL6G.js";
|
|
133
133
|
import {
|
|
134
134
|
embedCommand,
|
|
@@ -220,11 +220,158 @@ import {
|
|
|
220
220
|
} from "./chunk-QVMXF7FY.js";
|
|
221
221
|
|
|
222
222
|
// src/index.ts
|
|
223
|
+
import * as fs2 from "fs";
|
|
224
|
+
|
|
225
|
+
// src/lib/hybrid-search.ts
|
|
223
226
|
import * as fs from "fs";
|
|
227
|
+
import * as path from "path";
|
|
228
|
+
var embeddingPipeline = null;
|
|
229
|
+
var pipelineLoading = null;
|
|
230
|
+
async function getEmbeddingPipeline() {
|
|
231
|
+
if (embeddingPipeline) return embeddingPipeline;
|
|
232
|
+
if (pipelineLoading) return pipelineLoading;
|
|
233
|
+
pipelineLoading = (async () => {
|
|
234
|
+
const { pipeline } = await import("@huggingface/transformers");
|
|
235
|
+
embeddingPipeline = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
|
|
236
|
+
dtype: "fp32"
|
|
237
|
+
});
|
|
238
|
+
return embeddingPipeline;
|
|
239
|
+
})();
|
|
240
|
+
return pipelineLoading;
|
|
241
|
+
}
|
|
242
|
+
async function embed(text) {
|
|
243
|
+
const pipe = await getEmbeddingPipeline();
|
|
244
|
+
const result = await pipe(text, { pooling: "mean", normalize: true });
|
|
245
|
+
return new Float32Array(result.data);
|
|
246
|
+
}
|
|
247
|
+
async function embedBatch(texts) {
|
|
248
|
+
const pipe = await getEmbeddingPipeline();
|
|
249
|
+
const results = [];
|
|
250
|
+
const batchSize = 32;
|
|
251
|
+
for (let i = 0; i < texts.length; i += batchSize) {
|
|
252
|
+
const batch = texts.slice(i, i + batchSize);
|
|
253
|
+
for (const text of batch) {
|
|
254
|
+
const result = await pipe(text, { pooling: "mean", normalize: true });
|
|
255
|
+
results.push(new Float32Array(result.data));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return results;
|
|
259
|
+
}
|
|
260
|
+
function cosineSimilarity(a, b) {
|
|
261
|
+
let dot = 0;
|
|
262
|
+
for (let i = 0; i < a.length; i++) {
|
|
263
|
+
dot += a[i] * b[i];
|
|
264
|
+
}
|
|
265
|
+
return dot;
|
|
266
|
+
}
|
|
267
|
+
var EmbeddingCache = class {
|
|
268
|
+
cachePath;
|
|
269
|
+
cache = /* @__PURE__ */ new Map();
|
|
270
|
+
dirty = false;
|
|
271
|
+
constructor(vaultPath) {
|
|
272
|
+
this.cachePath = path.join(vaultPath, ".clawvault", "embeddings.bin");
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Load cache from disk
|
|
276
|
+
*/
|
|
277
|
+
load() {
|
|
278
|
+
try {
|
|
279
|
+
if (!fs.existsSync(this.cachePath)) return;
|
|
280
|
+
const data = JSON.parse(fs.readFileSync(this.cachePath + ".json", "utf-8"));
|
|
281
|
+
for (const [key, arr] of Object.entries(data)) {
|
|
282
|
+
this.cache.set(key, new Float32Array(arr));
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Save cache to disk
|
|
289
|
+
*/
|
|
290
|
+
save() {
|
|
291
|
+
if (!this.dirty) return;
|
|
292
|
+
const dir = path.dirname(this.cachePath);
|
|
293
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
294
|
+
const data = {};
|
|
295
|
+
for (const [key, arr] of this.cache.entries()) {
|
|
296
|
+
data[key] = Array.from(arr);
|
|
297
|
+
}
|
|
298
|
+
fs.writeFileSync(this.cachePath + ".json", JSON.stringify(data));
|
|
299
|
+
this.dirty = false;
|
|
300
|
+
}
|
|
301
|
+
get(key) {
|
|
302
|
+
return this.cache.get(key);
|
|
303
|
+
}
|
|
304
|
+
set(key, embedding) {
|
|
305
|
+
this.cache.set(key, embedding);
|
|
306
|
+
this.dirty = true;
|
|
307
|
+
}
|
|
308
|
+
has(key) {
|
|
309
|
+
return this.cache.has(key);
|
|
310
|
+
}
|
|
311
|
+
entries() {
|
|
312
|
+
return this.cache.entries();
|
|
313
|
+
}
|
|
314
|
+
get size() {
|
|
315
|
+
return this.cache.size;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
function reciprocalRankFusion(list1, list2, k = 60) {
|
|
319
|
+
const scores = /* @__PURE__ */ new Map();
|
|
320
|
+
for (let rank = 0; rank < list1.length; rank++) {
|
|
321
|
+
const { id } = list1[rank];
|
|
322
|
+
scores.set(id, (scores.get(id) || 0) + 1 / (k + rank + 1));
|
|
323
|
+
}
|
|
324
|
+
for (let rank = 0; rank < list2.length; rank++) {
|
|
325
|
+
const { id } = list2[rank];
|
|
326
|
+
scores.set(id, (scores.get(id) || 0) + 1 / (k + rank + 1));
|
|
327
|
+
}
|
|
328
|
+
return Array.from(scores.entries()).map(([id, score]) => ({ id, score })).sort((a, b) => b.score - a.score);
|
|
329
|
+
}
|
|
330
|
+
async function semanticSearch(query, cache, topK = 20) {
|
|
331
|
+
const queryEmb = await embed(query);
|
|
332
|
+
const results = [];
|
|
333
|
+
for (const [id, docEmb] of cache.entries()) {
|
|
334
|
+
results.push({ id, score: cosineSimilarity(queryEmb, docEmb) });
|
|
335
|
+
}
|
|
336
|
+
results.sort((a, b) => b.score - a.score);
|
|
337
|
+
return results.slice(0, topK);
|
|
338
|
+
}
|
|
339
|
+
async function hybridSearch(query, bm25Results, cache, options = {}) {
|
|
340
|
+
const { topK = 20, rrfK = 60 } = options;
|
|
341
|
+
const bm25Ranked = bm25Results.map((r) => ({ id: r.document.path || r.document.id, score: r.score }));
|
|
342
|
+
const semanticRanked = await semanticSearch(query, cache, topK);
|
|
343
|
+
const fused = reciprocalRankFusion(bm25Ranked, semanticRanked, rrfK);
|
|
344
|
+
const bm25Map = new Map(bm25Results.map((r) => [r.document.path || r.document.id, r]));
|
|
345
|
+
return fused.slice(0, topK).map(({ id, score }) => {
|
|
346
|
+
const existing = bm25Map.get(id);
|
|
347
|
+
if (existing) {
|
|
348
|
+
return { ...existing, score };
|
|
349
|
+
}
|
|
350
|
+
const minimalDoc = {
|
|
351
|
+
id: id.replace(/\.md$/, ""),
|
|
352
|
+
path: id.endsWith(".md") ? id : id + ".md",
|
|
353
|
+
title: (id.split("/").pop() || id).replace(/\.md$/, ""),
|
|
354
|
+
content: "",
|
|
355
|
+
category: id.split("/")[0] || "root",
|
|
356
|
+
frontmatter: {},
|
|
357
|
+
links: [],
|
|
358
|
+
tags: [],
|
|
359
|
+
modified: /* @__PURE__ */ new Date()
|
|
360
|
+
};
|
|
361
|
+
return {
|
|
362
|
+
document: minimalDoc,
|
|
363
|
+
score,
|
|
364
|
+
snippet: "",
|
|
365
|
+
matchedTerms: []
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/index.ts
|
|
224
371
|
function readPackageVersion() {
|
|
225
372
|
try {
|
|
226
373
|
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
227
|
-
const pkg = JSON.parse(
|
|
374
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgUrl, "utf-8"));
|
|
228
375
|
return pkg.version ?? "0.0.0";
|
|
229
376
|
} catch {
|
|
230
377
|
return "0.0.0";
|
|
@@ -241,6 +388,7 @@ export {
|
|
|
241
388
|
DEFAULT_CATEGORIES,
|
|
242
389
|
DEFAULT_CONFIG,
|
|
243
390
|
DEFAULT_SERVE_PORT,
|
|
391
|
+
EmbeddingCache,
|
|
244
392
|
MEMORY_GRAPH_SCHEMA_VERSION,
|
|
245
393
|
MEMORY_TYPES,
|
|
246
394
|
Observer,
|
|
@@ -273,12 +421,15 @@ export {
|
|
|
273
421
|
completeTask,
|
|
274
422
|
configureTailscaleServe,
|
|
275
423
|
contextCommand,
|
|
424
|
+
cosineSimilarity,
|
|
276
425
|
countBlockedTransitions,
|
|
277
426
|
createProject,
|
|
278
427
|
createVault,
|
|
279
428
|
deterministicInjectMatches,
|
|
280
429
|
discoverClawVaultPeers,
|
|
281
430
|
doctor,
|
|
431
|
+
embed,
|
|
432
|
+
embedBatch,
|
|
282
433
|
embedCommand,
|
|
283
434
|
extractCardSlug,
|
|
284
435
|
extractTags,
|
|
@@ -309,6 +460,7 @@ export {
|
|
|
309
460
|
graphSummary,
|
|
310
461
|
hasQmd,
|
|
311
462
|
hasTailscale,
|
|
463
|
+
hybridSearch,
|
|
312
464
|
importKanbanBoard,
|
|
313
465
|
indexInjectableItems,
|
|
314
466
|
inferContextProfile,
|
|
@@ -337,6 +489,7 @@ export {
|
|
|
337
489
|
readAllTransitions,
|
|
338
490
|
readProject,
|
|
339
491
|
rebuildCommand,
|
|
492
|
+
reciprocalRankFusion,
|
|
340
493
|
reflectCommand,
|
|
341
494
|
registerArchiveCommand,
|
|
342
495
|
registerCliCommands,
|
|
@@ -366,6 +519,7 @@ export {
|
|
|
366
519
|
resolveVaultPath,
|
|
367
520
|
runPromptInjection,
|
|
368
521
|
runReflection,
|
|
522
|
+
semanticSearch,
|
|
369
523
|
serveVault,
|
|
370
524
|
sessionRecapCommand,
|
|
371
525
|
setConfigValue,
|
package/hooks/clawvault/HOOK.md
CHANGED
|
@@ -73,11 +73,41 @@ Injection format:
|
|
|
73
73
|
|
|
74
74
|
The hook accepts canonical OpenClaw events (`gateway:startup`, `gateway:heartbeat`, `command:new`, `session:start`, `compaction:memoryFlush`, `cron.weekly`) and tolerates alias payload shapes (`event`, `eventName`, `name`, `hook`, `trigger`) to remain robust across runtime wrappers.
|
|
75
75
|
|
|
76
|
-
## Configuration
|
|
76
|
+
## Configuration
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
### Plugin Configuration (Recommended)
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
Configure the plugin via OpenClaw's config system:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Set vault path
|
|
84
|
+
openclaw config set plugins.clawvault.config.vaultPath ~/my-vault
|
|
85
|
+
|
|
86
|
+
# View current config
|
|
87
|
+
openclaw config get plugins.clawvault.config
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Available configuration options:
|
|
91
|
+
|
|
92
|
+
| Key | Type | Default | Description |
|
|
93
|
+
|-----|------|---------|-------------|
|
|
94
|
+
| `vaultPath` | string | (auto-detected) | Path to the ClawVault vault directory |
|
|
95
|
+
| `autoCheckpoint` | boolean | `true` | Enable automatic checkpointing on session events |
|
|
96
|
+
| `contextProfile` | string | `"auto"` | Default context profile (`default`, `planning`, `incident`, `handoff`, `auto`) |
|
|
97
|
+
| `maxContextResults` | integer | `4` | Maximum context results to inject on session start |
|
|
98
|
+
| `observeOnHeartbeat` | boolean | `true` | Enable observation threshold checks on heartbeat |
|
|
99
|
+
| `weeklyReflection` | boolean | `true` | Enable weekly reflection on Sunday midnight UTC |
|
|
100
|
+
|
|
101
|
+
### Vault Path Resolution
|
|
102
|
+
|
|
103
|
+
The hook resolves the vault path in this order:
|
|
104
|
+
|
|
105
|
+
1. Plugin config (`plugins.clawvault.config.vaultPath` set via `openclaw config`)
|
|
106
|
+
2. `OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH` environment variable
|
|
107
|
+
3. `CLAWVAULT_PATH` environment variable
|
|
108
|
+
4. Walking up from cwd to find `.clawvault.json`
|
|
109
|
+
5. Checking `memory/` subdirectory (OpenClaw convention)
|
|
110
|
+
|
|
111
|
+
### Troubleshooting
|
|
82
112
|
|
|
83
113
|
If `openclaw hooks enable clawvault` fails with hook-not-found, run `openclaw hooks install clawvault` first and verify discovery with `openclaw hooks list --verbose`.
|
|
@@ -444,9 +444,40 @@ function validateVaultPath(vaultPath) {
|
|
|
444
444
|
return resolved;
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
// Extract plugin config from event context (set via openclaw config)
|
|
448
|
+
function extractPluginConfig(event) {
|
|
449
|
+
const candidates = [
|
|
450
|
+
event?.pluginConfig,
|
|
451
|
+
event?.context?.pluginConfig,
|
|
452
|
+
event?.config?.plugins?.clawvault?.config,
|
|
453
|
+
event?.context?.config?.plugins?.clawvault?.config
|
|
454
|
+
];
|
|
455
|
+
|
|
456
|
+
for (const candidate of candidates) {
|
|
457
|
+
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
|
|
458
|
+
return candidate;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return {};
|
|
463
|
+
}
|
|
464
|
+
|
|
447
465
|
// Find vault by walking up directories
|
|
448
|
-
function findVaultPath() {
|
|
449
|
-
// Check
|
|
466
|
+
function findVaultPath(event) {
|
|
467
|
+
// Check plugin config first (set via openclaw config set plugins.clawvault.config.vaultPath)
|
|
468
|
+
const pluginConfig = extractPluginConfig(event);
|
|
469
|
+
if (pluginConfig.vaultPath) {
|
|
470
|
+
const validated = validateVaultPath(pluginConfig.vaultPath);
|
|
471
|
+
if (validated) return validated;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Check OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH env (injected by OpenClaw from plugin config)
|
|
475
|
+
if (process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH) {
|
|
476
|
+
const validated = validateVaultPath(process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH);
|
|
477
|
+
if (validated) return validated;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Check CLAWVAULT_PATH env
|
|
450
481
|
if (process.env.CLAWVAULT_PATH) {
|
|
451
482
|
return validateVaultPath(process.env.CLAWVAULT_PATH);
|
|
452
483
|
}
|
|
@@ -571,7 +602,7 @@ function isSundayMidnightUtc(date) {
|
|
|
571
602
|
}
|
|
572
603
|
|
|
573
604
|
async function handleWeeklyReflect(event) {
|
|
574
|
-
const vaultPath = findVaultPath();
|
|
605
|
+
const vaultPath = findVaultPath(event);
|
|
575
606
|
if (!vaultPath) {
|
|
576
607
|
console.log('[clawvault] No vault found, skipping weekly reflection');
|
|
577
608
|
return;
|
|
@@ -593,7 +624,7 @@ async function handleWeeklyReflect(event) {
|
|
|
593
624
|
|
|
594
625
|
// Handle gateway startup - check for context death
|
|
595
626
|
async function handleStartup(event) {
|
|
596
|
-
const vaultPath = findVaultPath();
|
|
627
|
+
const vaultPath = findVaultPath(event);
|
|
597
628
|
if (!vaultPath) {
|
|
598
629
|
console.log('[clawvault] No vault found, skipping recovery check');
|
|
599
630
|
return;
|
|
@@ -632,7 +663,7 @@ async function handleStartup(event) {
|
|
|
632
663
|
|
|
633
664
|
// Handle /new command - auto-checkpoint before reset
|
|
634
665
|
async function handleNew(event) {
|
|
635
|
-
const vaultPath = findVaultPath();
|
|
666
|
+
const vaultPath = findVaultPath(event);
|
|
636
667
|
if (!vaultPath) {
|
|
637
668
|
console.log('[clawvault] No vault found, skipping auto-checkpoint');
|
|
638
669
|
return;
|
|
@@ -671,7 +702,7 @@ async function handleNew(event) {
|
|
|
671
702
|
|
|
672
703
|
// Handle session start - inject dynamic context for first prompt
|
|
673
704
|
async function handleSessionStart(event) {
|
|
674
|
-
const vaultPath = findVaultPath();
|
|
705
|
+
const vaultPath = findVaultPath(event);
|
|
675
706
|
if (!vaultPath) {
|
|
676
707
|
console.log('[clawvault] No vault found, skipping context injection');
|
|
677
708
|
return;
|
|
@@ -733,7 +764,7 @@ async function handleSessionStart(event) {
|
|
|
733
764
|
|
|
734
765
|
// Handle heartbeat events - cheap stat-based trigger for active observation
|
|
735
766
|
async function handleHeartbeat(event) {
|
|
736
|
-
const vaultPath = findVaultPath();
|
|
767
|
+
const vaultPath = findVaultPath(event);
|
|
737
768
|
if (!vaultPath) {
|
|
738
769
|
console.log('[clawvault] No vault found, skipping heartbeat observation check');
|
|
739
770
|
return;
|
|
@@ -750,7 +781,7 @@ async function handleHeartbeat(event) {
|
|
|
750
781
|
|
|
751
782
|
// Handle context compaction - force flush any pending session deltas
|
|
752
783
|
async function handleContextCompaction(event) {
|
|
753
|
-
const vaultPath = findVaultPath();
|
|
784
|
+
const vaultPath = findVaultPath(event);
|
|
754
785
|
if (!vaultPath) {
|
|
755
786
|
console.log('[clawvault] No vault found, skipping compaction observation');
|
|
756
787
|
return;
|
|
@@ -260,4 +260,98 @@ describe('clawvault hook handler', () => {
|
|
|
260
260
|
|
|
261
261
|
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
262
262
|
});
|
|
263
|
+
|
|
264
|
+
it('uses vaultPath from plugin config when provided in event', async () => {
|
|
265
|
+
const vaultPath = makeVaultFixture();
|
|
266
|
+
|
|
267
|
+
execFileSyncMock.mockImplementation((_command, args) => {
|
|
268
|
+
if (args[0] === 'recover') {
|
|
269
|
+
return 'Clean startup';
|
|
270
|
+
}
|
|
271
|
+
return '';
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const handler = await loadHandler();
|
|
275
|
+
const event = {
|
|
276
|
+
type: 'gateway',
|
|
277
|
+
action: 'startup',
|
|
278
|
+
pluginConfig: {
|
|
279
|
+
vaultPath
|
|
280
|
+
},
|
|
281
|
+
messages: []
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
await handler(event);
|
|
285
|
+
|
|
286
|
+
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
287
|
+
'clawvault',
|
|
288
|
+
expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
|
|
289
|
+
expect.objectContaining({ shell: false })
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('uses vaultPath from context.pluginConfig when provided', async () => {
|
|
296
|
+
const vaultPath = makeVaultFixture();
|
|
297
|
+
|
|
298
|
+
execFileSyncMock.mockImplementation((_command, args) => {
|
|
299
|
+
if (args[0] === 'recover') {
|
|
300
|
+
return 'Clean startup';
|
|
301
|
+
}
|
|
302
|
+
return '';
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const handler = await loadHandler();
|
|
306
|
+
const event = {
|
|
307
|
+
type: 'gateway',
|
|
308
|
+
action: 'startup',
|
|
309
|
+
context: {
|
|
310
|
+
pluginConfig: {
|
|
311
|
+
vaultPath
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
messages: []
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
await handler(event);
|
|
318
|
+
|
|
319
|
+
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
320
|
+
'clawvault',
|
|
321
|
+
expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
|
|
322
|
+
expect.objectContaining({ shell: false })
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('uses vaultPath from OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH env var', async () => {
|
|
329
|
+
const vaultPath = makeVaultFixture();
|
|
330
|
+
process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH = vaultPath;
|
|
331
|
+
|
|
332
|
+
execFileSyncMock.mockImplementation((_command, args) => {
|
|
333
|
+
if (args[0] === 'recover') {
|
|
334
|
+
return 'Clean startup';
|
|
335
|
+
}
|
|
336
|
+
return '';
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const handler = await loadHandler();
|
|
340
|
+
const event = {
|
|
341
|
+
type: 'gateway',
|
|
342
|
+
action: 'startup',
|
|
343
|
+
messages: []
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
await handler(event);
|
|
347
|
+
|
|
348
|
+
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
349
|
+
'clawvault',
|
|
350
|
+
expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
|
|
351
|
+
expect.objectContaining({ shell: false })
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
delete process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH;
|
|
355
|
+
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
356
|
+
});
|
|
263
357
|
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "clawvault",
|
|
3
|
+
"name": "ClawVault",
|
|
4
|
+
"version": "2.6.1",
|
|
5
|
+
"description": "Structured memory system for AI agents with context death resilience",
|
|
6
|
+
"kind": "memory",
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"vaultPath": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Path to the ClawVault vault directory. If not set, auto-discovered from CLAWVAULT_PATH or by walking up from cwd."
|
|
13
|
+
},
|
|
14
|
+
"autoCheckpoint": {
|
|
15
|
+
"type": "boolean",
|
|
16
|
+
"description": "Enable automatic checkpointing on session events",
|
|
17
|
+
"default": true
|
|
18
|
+
},
|
|
19
|
+
"contextProfile": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"enum": ["default", "planning", "incident", "handoff", "auto"],
|
|
22
|
+
"description": "Default context profile for session start injection",
|
|
23
|
+
"default": "auto"
|
|
24
|
+
},
|
|
25
|
+
"maxContextResults": {
|
|
26
|
+
"type": "integer",
|
|
27
|
+
"minimum": 1,
|
|
28
|
+
"maximum": 20,
|
|
29
|
+
"description": "Maximum number of context results to inject on session start",
|
|
30
|
+
"default": 4
|
|
31
|
+
},
|
|
32
|
+
"observeOnHeartbeat": {
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"description": "Enable observation threshold checks on gateway heartbeat",
|
|
35
|
+
"default": true
|
|
36
|
+
},
|
|
37
|
+
"weeklyReflection": {
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"description": "Enable weekly reflection on Sunday midnight UTC",
|
|
40
|
+
"default": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"additionalProperties": false
|
|
44
|
+
},
|
|
45
|
+
"uiHints": {
|
|
46
|
+
"vaultPath": {
|
|
47
|
+
"label": "Vault Path",
|
|
48
|
+
"placeholder": "~/my-vault",
|
|
49
|
+
"description": "Path to your ClawVault memory vault"
|
|
50
|
+
},
|
|
51
|
+
"autoCheckpoint": {
|
|
52
|
+
"label": "Auto Checkpoint",
|
|
53
|
+
"description": "Automatically checkpoint before session resets"
|
|
54
|
+
},
|
|
55
|
+
"contextProfile": {
|
|
56
|
+
"label": "Context Profile",
|
|
57
|
+
"description": "Profile used for context injection at session start"
|
|
58
|
+
},
|
|
59
|
+
"maxContextResults": {
|
|
60
|
+
"label": "Max Context Results",
|
|
61
|
+
"description": "Number of vault memories to inject"
|
|
62
|
+
},
|
|
63
|
+
"observeOnHeartbeat": {
|
|
64
|
+
"label": "Observe on Heartbeat",
|
|
65
|
+
"description": "Check observation thresholds during heartbeat events"
|
|
66
|
+
},
|
|
67
|
+
"weeklyReflection": {
|
|
68
|
+
"label": "Weekly Reflection",
|
|
69
|
+
"description": "Run weekly reflection on Sunday midnight UTC"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawvault",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.3",
|
|
4
4
|
"description": "Structured memory system for AI agents — typed storage, knowledge graph, context profiles, canvas dashboards, neural graph themes, and Obsidian-native task views. An elephant never forgets. 🐘",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
"bin",
|
|
22
22
|
"dashboard",
|
|
23
23
|
"templates",
|
|
24
|
-
"hooks"
|
|
24
|
+
"hooks",
|
|
25
|
+
"openclaw.plugin.json"
|
|
25
26
|
],
|
|
26
27
|
"openclaw": {
|
|
28
|
+
"plugin": "./openclaw.plugin.json",
|
|
27
29
|
"hooks": [
|
|
28
30
|
"./hooks/clawvault"
|
|
29
31
|
]
|
|
@@ -65,6 +67,7 @@
|
|
|
65
67
|
"node": ">=18"
|
|
66
68
|
},
|
|
67
69
|
"dependencies": {
|
|
70
|
+
"@huggingface/transformers": "^3.8.1",
|
|
68
71
|
"chalk": "^5.3.0",
|
|
69
72
|
"chokidar": "^5.0.0",
|
|
70
73
|
"commander": "^12.0.0",
|