@xdarkicex/openclaw-memory-libravdb 1.4.6 → 1.4.8
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/HOOK.md +14 -0
- package/README.md +32 -2
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +208 -0
- package/dist/context-engine.d.ts +56 -0
- package/dist/context-engine.js +125 -0
- package/dist/dream-promotion.d.ts +47 -0
- package/dist/dream-promotion.js +363 -0
- package/dist/dream-routing.d.ts +6 -0
- package/dist/dream-routing.js +31 -0
- package/dist/durable-namespace.d.ts +6 -0
- package/dist/durable-namespace.js +24 -0
- package/dist/grpc-client.d.ts +23 -0
- package/dist/grpc-client.js +104 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +40 -0
- package/dist/lifecycle-hooks.d.ts +4 -0
- package/dist/lifecycle-hooks.js +64 -0
- package/dist/markdown-hash.d.ts +3 -0
- package/dist/markdown-hash.js +82 -0
- package/dist/markdown-ingest.d.ts +43 -0
- package/dist/markdown-ingest.js +464 -0
- package/dist/memory-provider.d.ts +4 -0
- package/dist/memory-provider.js +13 -0
- package/dist/memory-runtime.d.ts +118 -0
- package/dist/memory-runtime.js +217 -0
- package/dist/plugin-runtime.d.ts +28 -0
- package/dist/plugin-runtime.js +127 -0
- package/dist/proto/intelligence_kernel/v1/kernel.proto +378 -0
- package/dist/recall-cache.d.ts +2 -0
- package/dist/recall-cache.js +30 -0
- package/dist/rpc-protobuf-codecs.d.ts +70 -0
- package/dist/rpc-protobuf-codecs.js +77 -0
- package/dist/rpc.d.ts +14 -0
- package/dist/rpc.js +121 -0
- package/dist/sidecar.d.ts +34 -0
- package/dist/sidecar.js +535 -0
- package/dist/types.d.ts +163 -0
- package/dist/types.js +1 -0
- package/docs/contributing.md +14 -13
- package/docs/install.md +7 -9
- package/docs/installation.md +23 -16
- package/docs/uninstall.md +1 -1
- package/index.js +2 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +39 -16
- package/packaging/README.md +0 -71
- package/packaging/homebrew/libravdbd.rb.tmpl +0 -224
- package/packaging/launchd/com.xdarkicex.libravdbd.plist +0 -32
- package/packaging/systemd/libravdbd.service +0 -12
- package/src/cli.ts +0 -299
- package/src/comparison-experiments.ts +0 -128
- package/src/context-engine.ts +0 -1645
- package/src/continuity.ts +0 -93
- package/src/dream-promotion.ts +0 -492
- package/src/dream-routing.ts +0 -40
- package/src/durable-namespace.ts +0 -34
- package/src/index.ts +0 -47
- package/src/lifecycle-hooks.ts +0 -96
- package/src/markdown-hash.ts +0 -104
- package/src/markdown-ingest.ts +0 -627
- package/src/memory-provider.ts +0 -25
- package/src/memory-runtime.ts +0 -283
- package/src/openclaw-plugin-sdk.d.ts +0 -59
- package/src/plugin-runtime.ts +0 -119
- package/src/recall-cache.ts +0 -34
- package/src/recall-utils.ts +0 -131
- package/src/rpc.ts +0 -92
- package/src/scoring.ts +0 -632
- package/src/sidecar.ts +0 -583
- package/src/temporal.ts +0 -1031
- package/src/tokens.ts +0 -52
- package/src/types.ts +0 -278
- package/tsconfig.json +0 -20
- package/tsconfig.tests.json +0 -12
package/src/markdown-ingest.ts
DELETED
|
@@ -1,627 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import fsp from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
import type { LoggerLike, PluginConfig } from "./types.js";
|
|
6
|
-
import { hashBytes } from "./markdown-hash.js";
|
|
7
|
-
|
|
8
|
-
const DEFAULT_DEBOUNCE_MS = 150;
|
|
9
|
-
const DEFAULT_TOKENIZER_ID = "markdown-ingest:v1";
|
|
10
|
-
const MARKDOWN_INGEST_VERSION = 3;
|
|
11
|
-
const HASH_BACKEND = "wasm-fnv1a64";
|
|
12
|
-
type Disposable = { close(): void };
|
|
13
|
-
|
|
14
|
-
interface RpcLike {
|
|
15
|
-
call<T>(method: string, params: unknown): Promise<T>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type RpcGetterLike = () => Promise<RpcLike>;
|
|
19
|
-
|
|
20
|
-
interface FsDirentLike {
|
|
21
|
-
name: string;
|
|
22
|
-
isDirectory(): boolean;
|
|
23
|
-
isFile(): boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface FsWatcherLike extends Disposable {
|
|
27
|
-
on(event: "error", handler: (error: Error) => void): void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface FsApi {
|
|
31
|
-
readdir(dir: string): Promise<FsDirentLike[]>;
|
|
32
|
-
readFile(file: string): Promise<Uint8Array>;
|
|
33
|
-
stat(file: string): Promise<{ size: number; mtimeMs: number }>;
|
|
34
|
-
watch(dir: string, onChange: (event: string, filename: string | Buffer | null) => void): FsWatcherLike;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface MarkdownSourceAdapter {
|
|
38
|
-
kind: string;
|
|
39
|
-
start(): Promise<void>;
|
|
40
|
-
refresh(): Promise<void>;
|
|
41
|
-
stop(): Promise<void>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface MarkdownIngestionHandle {
|
|
45
|
-
start(): Promise<void>;
|
|
46
|
-
refresh(): Promise<void>;
|
|
47
|
-
stop(): Promise<void>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface MarkdownIngestionSnapshot {
|
|
51
|
-
fileHash: string;
|
|
52
|
-
size: number;
|
|
53
|
-
mtimeMs: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface RootState {
|
|
57
|
-
root: string;
|
|
58
|
-
scanState: {
|
|
59
|
-
scanning: boolean;
|
|
60
|
-
dirty: boolean;
|
|
61
|
-
timer: ReturnType<typeof setTimeout> | null;
|
|
62
|
-
};
|
|
63
|
-
knownFiles: Set<string>;
|
|
64
|
-
directoryWatchers: Map<string, FsWatcherLike>;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface FileState extends MarkdownIngestionSnapshot {
|
|
68
|
-
root: string;
|
|
69
|
-
sourceDoc: string;
|
|
70
|
-
relativePath: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface GenericMarkdownSourceConfig {
|
|
74
|
-
roots: string[];
|
|
75
|
-
include?: string[];
|
|
76
|
-
exclude?: string[];
|
|
77
|
-
debounceMs?: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
interface IngestMarkdownDocumentParams {
|
|
81
|
-
sourceDoc: string;
|
|
82
|
-
text: string;
|
|
83
|
-
tokenizerId: string;
|
|
84
|
-
coreDoc: boolean;
|
|
85
|
-
sourceMeta: {
|
|
86
|
-
sourceRoot: string;
|
|
87
|
-
sourcePath: string;
|
|
88
|
-
sourceKind: string;
|
|
89
|
-
fileHash: string;
|
|
90
|
-
sourceSize: number;
|
|
91
|
-
sourceMtimeMs: number;
|
|
92
|
-
ingestVersion: number;
|
|
93
|
-
hashBackend: string;
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
interface DeleteAuthoredDocumentParams {
|
|
98
|
-
sourceDoc: string;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function createMarkdownIngestionHandle(
|
|
102
|
-
cfg: PluginConfig,
|
|
103
|
-
getRpc: RpcGetterLike,
|
|
104
|
-
logger: LoggerLike = console,
|
|
105
|
-
fsApi: FsApi = createRealFsApi(),
|
|
106
|
-
): MarkdownIngestionHandle {
|
|
107
|
-
const adapters: MarkdownSourceAdapter[] = [];
|
|
108
|
-
|
|
109
|
-
const genericRoots = normalizeMarkdownRoots(cfg.markdownIngestionRoots);
|
|
110
|
-
if (isMarkdownIngestionEnabled(cfg, genericRoots)) {
|
|
111
|
-
adapters.push(
|
|
112
|
-
new DirectoryMarkdownSourceAdapter(
|
|
113
|
-
"generic",
|
|
114
|
-
{
|
|
115
|
-
roots: genericRoots,
|
|
116
|
-
include: cfg.markdownIngestionInclude,
|
|
117
|
-
exclude: cfg.markdownIngestionExclude,
|
|
118
|
-
debounceMs: cfg.markdownIngestionDebounceMs ?? DEFAULT_DEBOUNCE_MS,
|
|
119
|
-
},
|
|
120
|
-
getRpc,
|
|
121
|
-
logger,
|
|
122
|
-
fsApi,
|
|
123
|
-
),
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const obsidianRoots = normalizeMarkdownRoots(cfg.markdownIngestionObsidianRoots);
|
|
128
|
-
if (cfg.markdownIngestionObsidianEnabled !== false && obsidianRoots.length > 0) {
|
|
129
|
-
adapters.push(
|
|
130
|
-
new DirectoryMarkdownSourceAdapter(
|
|
131
|
-
"obsidian",
|
|
132
|
-
{
|
|
133
|
-
roots: obsidianRoots,
|
|
134
|
-
include: cfg.markdownIngestionObsidianInclude,
|
|
135
|
-
exclude: cfg.markdownIngestionObsidianExclude,
|
|
136
|
-
debounceMs: cfg.markdownIngestionObsidianDebounceMs ?? cfg.markdownIngestionDebounceMs ?? DEFAULT_DEBOUNCE_MS,
|
|
137
|
-
},
|
|
138
|
-
getRpc,
|
|
139
|
-
logger,
|
|
140
|
-
fsApi,
|
|
141
|
-
),
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (adapters.length === 0) {
|
|
146
|
-
return {
|
|
147
|
-
async start() {},
|
|
148
|
-
async refresh() {},
|
|
149
|
-
async stop() {},
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const adapter = new CompositeMarkdownSourceAdapter(adapters);
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
start: () => adapter.start(),
|
|
157
|
-
refresh: () => adapter.refresh(),
|
|
158
|
-
stop: () => adapter.stop(),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
class CompositeMarkdownSourceAdapter implements MarkdownSourceAdapter {
|
|
163
|
-
kind = "composite";
|
|
164
|
-
constructor(private readonly adapters: MarkdownSourceAdapter[]) {}
|
|
165
|
-
|
|
166
|
-
async start(): Promise<void> {
|
|
167
|
-
for (const adapter of this.adapters) {
|
|
168
|
-
await adapter.start();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async refresh(): Promise<void> {
|
|
173
|
-
for (const adapter of this.adapters) {
|
|
174
|
-
await adapter.refresh();
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async stop(): Promise<void> {
|
|
179
|
-
for (const adapter of this.adapters) {
|
|
180
|
-
await adapter.stop();
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
class DirectoryMarkdownSourceAdapter implements MarkdownSourceAdapter {
|
|
186
|
-
readonly kind: string;
|
|
187
|
-
private readonly roots: string[];
|
|
188
|
-
private readonly includePatterns: string[];
|
|
189
|
-
private readonly excludePatterns: string[];
|
|
190
|
-
private readonly debounceMs: number;
|
|
191
|
-
private readonly fsApi: FsApi;
|
|
192
|
-
private readonly getRpc: RpcGetterLike;
|
|
193
|
-
private readonly logger: LoggerLike;
|
|
194
|
-
private readonly states = new Map<string, RootState>();
|
|
195
|
-
private readonly fileStates = new Map<string, FileState>();
|
|
196
|
-
private readonly tokenizerId: string;
|
|
197
|
-
private readonly coreDoc: boolean;
|
|
198
|
-
private started = false;
|
|
199
|
-
|
|
200
|
-
constructor(kind: string, config: GenericMarkdownSourceConfig, getRpc: RpcGetterLike, logger: LoggerLike, fsApi: FsApi) {
|
|
201
|
-
this.kind = kind;
|
|
202
|
-
this.roots = config.roots;
|
|
203
|
-
this.includePatterns = config.include?.length ? config.include : [];
|
|
204
|
-
this.excludePatterns = config.exclude?.length ? config.exclude : [];
|
|
205
|
-
this.debounceMs = config.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
206
|
-
this.fsApi = fsApi;
|
|
207
|
-
this.getRpc = getRpc;
|
|
208
|
-
this.logger = logger;
|
|
209
|
-
this.tokenizerId = DEFAULT_TOKENIZER_ID;
|
|
210
|
-
this.coreDoc = true;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async start(): Promise<void> {
|
|
214
|
-
if (this.started) {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
this.started = true;
|
|
218
|
-
await this.refresh();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async refresh(): Promise<void> {
|
|
222
|
-
if (!this.started) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
for (const root of this.roots) {
|
|
226
|
-
await this.scanRoot(root);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async stop(): Promise<void> {
|
|
231
|
-
for (const state of this.states.values()) {
|
|
232
|
-
if (state.scanState.timer) {
|
|
233
|
-
clearTimeout(state.scanState.timer);
|
|
234
|
-
state.scanState.timer = null;
|
|
235
|
-
}
|
|
236
|
-
for (const watcher of state.directoryWatchers.values()) {
|
|
237
|
-
watcher.close();
|
|
238
|
-
}
|
|
239
|
-
state.directoryWatchers.clear();
|
|
240
|
-
}
|
|
241
|
-
this.states.clear();
|
|
242
|
-
this.fileStates.clear();
|
|
243
|
-
this.started = false;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private getRootState(root: string): RootState {
|
|
247
|
-
const resolved = path.resolve(root);
|
|
248
|
-
const existing = this.states.get(resolved);
|
|
249
|
-
if (existing) {
|
|
250
|
-
return existing;
|
|
251
|
-
}
|
|
252
|
-
const created: RootState = {
|
|
253
|
-
root: resolved,
|
|
254
|
-
scanState: {
|
|
255
|
-
scanning: false,
|
|
256
|
-
dirty: false,
|
|
257
|
-
timer: null,
|
|
258
|
-
},
|
|
259
|
-
knownFiles: new Set<string>(),
|
|
260
|
-
directoryWatchers: new Map<string, FsWatcherLike>(),
|
|
261
|
-
};
|
|
262
|
-
this.states.set(resolved, created);
|
|
263
|
-
return created;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private async scanRoot(root: string): Promise<void> {
|
|
267
|
-
const rootState = this.getRootState(root);
|
|
268
|
-
if (rootState.scanState.scanning) {
|
|
269
|
-
rootState.scanState.dirty = true;
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
rootState.scanState.scanning = true;
|
|
274
|
-
try {
|
|
275
|
-
const currentFiles = new Set<string>();
|
|
276
|
-
await this.walkDirectory(rootState, rootState.root, currentFiles);
|
|
277
|
-
await this.pruneDeletedFiles(rootState, currentFiles);
|
|
278
|
-
rootState.knownFiles = currentFiles;
|
|
279
|
-
} finally {
|
|
280
|
-
rootState.scanState.scanning = false;
|
|
281
|
-
if (rootState.scanState.dirty) {
|
|
282
|
-
rootState.scanState.dirty = false;
|
|
283
|
-
this.scheduleRootScan(rootState);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
private scheduleRootScan(rootState: RootState): void {
|
|
289
|
-
if (rootState.scanState.scanning) {
|
|
290
|
-
rootState.scanState.dirty = true;
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
if (rootState.scanState.timer) {
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
rootState.scanState.timer = setTimeout(() => {
|
|
297
|
-
rootState.scanState.timer = null;
|
|
298
|
-
void this.scanRoot(rootState.root).catch((error) => {
|
|
299
|
-
this.logger.warn?.(`[markdown-ingest] root scan failed for ${rootState.root}: ${formatError(error)}`);
|
|
300
|
-
});
|
|
301
|
-
}, this.debounceMs);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
private async walkDirectory(rootState: RootState, dir: string, currentFiles: Set<string>): Promise<void> {
|
|
305
|
-
await this.ensureDirectoryWatcher(rootState, dir);
|
|
306
|
-
|
|
307
|
-
let entries: FsDirentLike[];
|
|
308
|
-
try {
|
|
309
|
-
entries = await this.fsApi.readdir(dir);
|
|
310
|
-
} catch (error) {
|
|
311
|
-
const message = formatError(error);
|
|
312
|
-
if (!message.includes("ENOENT")) {
|
|
313
|
-
this.logger.warn?.(`[markdown-ingest] readdir failed for ${dir}: ${message}`);
|
|
314
|
-
}
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
for (const entry of entries) {
|
|
319
|
-
const child = path.join(dir, entry.name);
|
|
320
|
-
if (entry.isDirectory()) {
|
|
321
|
-
await this.walkDirectory(rootState, child, currentFiles);
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
if (!entry.isFile() || !isMarkdownFile(entry.name)) {
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (!this.shouldIncludeFile(rootState.root, child)) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
currentFiles.add(child);
|
|
331
|
-
try {
|
|
332
|
-
await this.syncMarkdownFile(rootState, child);
|
|
333
|
-
} catch (error) {
|
|
334
|
-
this.logger.warn?.(`[markdown-ingest] sync failed for ${child}: ${formatError(error)}`);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private async ensureDirectoryWatcher(rootState: RootState, dir: string): Promise<void> {
|
|
340
|
-
if (rootState.directoryWatchers.has(dir)) {
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
const watcher = this.fsApi.watch(dir, () => {
|
|
346
|
-
this.scheduleRootScan(rootState);
|
|
347
|
-
});
|
|
348
|
-
watcher.on("error", (error) => {
|
|
349
|
-
this.logger.warn?.(`[markdown-ingest] watch error for ${dir}: ${formatError(error)}`);
|
|
350
|
-
});
|
|
351
|
-
rootState.directoryWatchers.set(dir, watcher);
|
|
352
|
-
} catch (error) {
|
|
353
|
-
this.logger.warn?.(`[markdown-ingest] watch unavailable for ${dir}: ${formatError(error)}`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private shouldIncludeFile(root: string, filePath: string): boolean {
|
|
358
|
-
if (isOpenClawMemoryFile(filePath)) {
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
const relative = toPosixPath(path.relative(root, filePath));
|
|
362
|
-
if (this.excludePatterns.length > 0) {
|
|
363
|
-
for (const pattern of this.excludePatterns) {
|
|
364
|
-
if (matchesGlob(relative, pattern)) {
|
|
365
|
-
return false;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (this.includePatterns.length > 0) {
|
|
370
|
-
for (const pattern of this.includePatterns) {
|
|
371
|
-
if (matchesGlob(relative, pattern)) {
|
|
372
|
-
return true;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return false;
|
|
376
|
-
}
|
|
377
|
-
return true;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
private async pruneDeletedFiles(rootState: RootState, currentFiles: Set<string>): Promise<void> {
|
|
381
|
-
const removed: string[] = [];
|
|
382
|
-
for (const previous of rootState.knownFiles) {
|
|
383
|
-
if (!currentFiles.has(previous)) {
|
|
384
|
-
removed.push(previous);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (removed.length === 0) {
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
for (const filePath of removed) {
|
|
391
|
-
await this.deleteSourceDocument(filePath);
|
|
392
|
-
this.fileStates.delete(filePath);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
private async syncMarkdownFile(rootState: RootState, filePath: string): Promise<void> {
|
|
397
|
-
const sourceDoc = filePath;
|
|
398
|
-
const relativePath = toPosixPath(path.relative(rootState.root, filePath));
|
|
399
|
-
const stat = await this.safeStat(filePath);
|
|
400
|
-
if (!stat) {
|
|
401
|
-
await this.deleteSourceDocument(sourceDoc);
|
|
402
|
-
this.fileStates.delete(sourceDoc);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const cached = this.fileStates.get(sourceDoc);
|
|
407
|
-
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const bytes = await this.safeReadFile(filePath);
|
|
412
|
-
if (!bytes) {
|
|
413
|
-
await this.deleteSourceDocument(sourceDoc);
|
|
414
|
-
this.fileStates.delete(sourceDoc);
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const fileHash = hashBytes(bytes);
|
|
419
|
-
if (cached && cached.fileHash === fileHash) {
|
|
420
|
-
this.fileStates.set(sourceDoc, {
|
|
421
|
-
root: rootState.root,
|
|
422
|
-
sourceDoc,
|
|
423
|
-
relativePath,
|
|
424
|
-
fileHash,
|
|
425
|
-
size: stat.size,
|
|
426
|
-
mtimeMs: stat.mtimeMs,
|
|
427
|
-
});
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const text = textDecoder.decode(bytes);
|
|
432
|
-
if (this.kind === "obsidian" && this.includePatterns.length === 0 && !looksLikeObsidianNote(filePath, text)) {
|
|
433
|
-
await this.deleteSourceDocument(sourceDoc);
|
|
434
|
-
this.fileStates.delete(sourceDoc);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
await this.ingestMarkdownDocument(sourceDoc, text, rootState.root, relativePath, fileHash, stat.size, stat.mtimeMs);
|
|
438
|
-
this.fileStates.set(sourceDoc, {
|
|
439
|
-
root: rootState.root,
|
|
440
|
-
sourceDoc,
|
|
441
|
-
relativePath,
|
|
442
|
-
fileHash,
|
|
443
|
-
size: stat.size,
|
|
444
|
-
mtimeMs: stat.mtimeMs,
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
private async ingestMarkdownDocument(
|
|
449
|
-
sourceDoc: string,
|
|
450
|
-
text: string,
|
|
451
|
-
sourceRoot: string,
|
|
452
|
-
sourcePath: string,
|
|
453
|
-
fileHash: string,
|
|
454
|
-
sourceSize: number,
|
|
455
|
-
sourceMtimeMs: number,
|
|
456
|
-
): Promise<void> {
|
|
457
|
-
const rpc = await this.getRpc();
|
|
458
|
-
const params: IngestMarkdownDocumentParams = {
|
|
459
|
-
sourceDoc,
|
|
460
|
-
text,
|
|
461
|
-
tokenizerId: this.tokenizerId,
|
|
462
|
-
coreDoc: this.coreDoc,
|
|
463
|
-
sourceMeta: {
|
|
464
|
-
sourceRoot,
|
|
465
|
-
sourcePath,
|
|
466
|
-
sourceKind: this.kind,
|
|
467
|
-
fileHash,
|
|
468
|
-
sourceSize,
|
|
469
|
-
sourceMtimeMs,
|
|
470
|
-
ingestVersion: MARKDOWN_INGEST_VERSION,
|
|
471
|
-
hashBackend: HASH_BACKEND,
|
|
472
|
-
},
|
|
473
|
-
};
|
|
474
|
-
await rpc.call("ingest_markdown_document", params);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
private async deleteSourceDocument(sourceDoc: string): Promise<void> {
|
|
478
|
-
const rpc = await this.getRpc();
|
|
479
|
-
const params: DeleteAuthoredDocumentParams = { sourceDoc };
|
|
480
|
-
await rpc.call("delete_authored_document", params);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
private async safeStat(filePath: string): Promise<{ size: number; mtimeMs: number } | null> {
|
|
484
|
-
try {
|
|
485
|
-
return await this.fsApi.stat(filePath);
|
|
486
|
-
} catch {
|
|
487
|
-
return null;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
private async safeReadFile(filePath: string): Promise<Uint8Array | null> {
|
|
492
|
-
try {
|
|
493
|
-
return await this.fsApi.readFile(filePath);
|
|
494
|
-
} catch {
|
|
495
|
-
return null;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function toPosixPath(value: string): string {
|
|
501
|
-
return value.split(path.sep).join("/");
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const textDecoder = new TextDecoder();
|
|
505
|
-
|
|
506
|
-
function normalizeMarkdownRoots(roots?: string[]): string[] {
|
|
507
|
-
if (!roots?.length) {
|
|
508
|
-
return [];
|
|
509
|
-
}
|
|
510
|
-
const resolved = new Set<string>();
|
|
511
|
-
for (const root of roots) {
|
|
512
|
-
const trimmed = root.trim();
|
|
513
|
-
if (!trimmed) {
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
resolved.add(path.resolve(trimmed));
|
|
517
|
-
}
|
|
518
|
-
return [...resolved];
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function isMarkdownIngestionEnabled(cfg: PluginConfig, roots: string[]): boolean {
|
|
522
|
-
if (cfg.markdownIngestionEnabled === false) {
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
return roots.length > 0;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function createRealFsApi(): FsApi {
|
|
529
|
-
return {
|
|
530
|
-
readdir: async (dir: string) => fsp.readdir(dir, { withFileTypes: true }) as Promise<FsDirentLike[]>,
|
|
531
|
-
readFile: async (file: string) => fsp.readFile(file),
|
|
532
|
-
stat: async (file: string) => {
|
|
533
|
-
const stat = await fsp.stat(file);
|
|
534
|
-
return { size: stat.size, mtimeMs: stat.mtimeMs };
|
|
535
|
-
},
|
|
536
|
-
watch: (dir: string, onChange: (event: string, filename: string | Buffer | null) => void) => fs.watch(dir, onChange),
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function isMarkdownFile(fileName: string): boolean {
|
|
541
|
-
const lower = fileName.toLowerCase();
|
|
542
|
-
return lower.endsWith(".md") || lower.endsWith(".markdown");
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function matchesGlob(value: string, pattern: string): boolean {
|
|
546
|
-
const escaped = pattern
|
|
547
|
-
.split("*")
|
|
548
|
-
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
549
|
-
.join(".*");
|
|
550
|
-
return new RegExp(`^${escaped}$`).test(value);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function formatError(error: unknown): string {
|
|
554
|
-
if (error instanceof Error) {
|
|
555
|
-
return error.message;
|
|
556
|
-
}
|
|
557
|
-
return String(error);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function looksLikeObsidianNote(filePath: string, text: string): boolean {
|
|
561
|
-
if (!text.startsWith("---\n")) {
|
|
562
|
-
return hasInlineObsidianTag(text);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const frontmatterEnd = findFrontmatterEnd(text, 4);
|
|
566
|
-
if (frontmatterEnd < 0) {
|
|
567
|
-
return hasInlineObsidianTag(text);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const frontmatter = text.slice(4, frontmatterEnd);
|
|
571
|
-
const lines = frontmatter.split("\n");
|
|
572
|
-
for (const line of lines) {
|
|
573
|
-
const trimmed = line.trimStart();
|
|
574
|
-
if (
|
|
575
|
-
trimmed.startsWith("tags:") ||
|
|
576
|
-
trimmed.startsWith("tag:") ||
|
|
577
|
-
trimmed.startsWith("openclaw:") ||
|
|
578
|
-
trimmed.startsWith("memory:")
|
|
579
|
-
) {
|
|
580
|
-
return true;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return hasInlineObsidianTag(text.slice(frontmatterEnd + 4));
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function findFrontmatterEnd(text: string, offset: number): number {
|
|
588
|
-
for (let i = offset; i < text.length - 3; i++) {
|
|
589
|
-
if (text.charCodeAt(i) !== 45 || text.charCodeAt(i + 1) !== 45 || text.charCodeAt(i + 2) !== 45) {
|
|
590
|
-
continue;
|
|
591
|
-
}
|
|
592
|
-
const next = text.charCodeAt(i + 3);
|
|
593
|
-
if (next === 10) {
|
|
594
|
-
return i;
|
|
595
|
-
}
|
|
596
|
-
if (next === 13 && text.charCodeAt(i + 4) === 10) {
|
|
597
|
-
return i;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
return -1;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function hasInlineObsidianTag(text: string): boolean {
|
|
604
|
-
let inFence = false;
|
|
605
|
-
const lines = text.split("\n");
|
|
606
|
-
for (const line of lines) {
|
|
607
|
-
const trimmed = line.trimStart();
|
|
608
|
-
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
609
|
-
inFence = !inFence;
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
if (inFence) {
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
if (trimmed.startsWith("#")) {
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
if (/(^|[^A-Za-z0-9_])#([A-Za-z][A-Za-z0-9/_-]*)\b/.test(line)) {
|
|
619
|
-
return true;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
return false;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function isOpenClawMemoryFile(filePath: string): boolean {
|
|
626
|
-
return path.basename(filePath).toLowerCase() === "memory.md";
|
|
627
|
-
}
|
package/src/memory-provider.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { MemoryPromptSectionBuilder } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
-
import type { PluginConfig, RecallCache, SearchResult } from "./types.js";
|
|
3
|
-
import type { RpcGetter } from "./plugin-runtime.js";
|
|
4
|
-
|
|
5
|
-
const MEMORY_PROMPT_HEADER = [
|
|
6
|
-
"## Memory",
|
|
7
|
-
"LibraVDB persistent memory is configured. Recalled memories may appear",
|
|
8
|
-
"in context via the context-engine assembler when available and relevant.",
|
|
9
|
-
"",
|
|
10
|
-
] as const;
|
|
11
|
-
|
|
12
|
-
export function buildMemoryPromptSection(
|
|
13
|
-
_getRpc: RpcGetter,
|
|
14
|
-
_cfg: PluginConfig,
|
|
15
|
-
_recallCache: RecallCache<SearchResult>,
|
|
16
|
-
): MemoryPromptSectionBuilder {
|
|
17
|
-
return function memoryPromptSection({
|
|
18
|
-
availableTools: _availableTools,
|
|
19
|
-
citationsMode: _citationsMode,
|
|
20
|
-
}): string[] {
|
|
21
|
-
// OpenClaw builds the memory prompt section synchronously for embedded runs.
|
|
22
|
-
// Actual retrieval and ranking happen in the context engine during assemble().
|
|
23
|
-
return [...MEMORY_PROMPT_HEADER];
|
|
24
|
-
};
|
|
25
|
-
}
|