@cognee/cognee-openclaw 2026.2.4 → 2026.3.1
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 +208 -33
- package/dist/index.d.ts +8 -9
- package/dist/index.js +17 -611
- package/dist/index.js.map +1 -1
- package/dist/src/client.d.ts +74 -0
- package/dist/src/client.js +302 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/config.d.ts +21 -0
- package/dist/src/config.js +89 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/files.d.ts +6 -0
- package/dist/src/files.js +60 -0
- package/dist/src/files.js.map +1 -0
- package/dist/src/persistence.d.ts +17 -0
- package/dist/src/persistence.js +107 -0
- package/dist/src/persistence.js.map +1 -0
- package/dist/src/plugin.d.ts +9 -0
- package/dist/src/plugin.js +510 -0
- package/dist/src/plugin.js.map +1 -0
- package/dist/src/scope.d.ts +20 -0
- package/dist/src/scope.js +122 -0
- package/dist/src/scope.js.map +1 -0
- package/dist/src/sync.d.ts +18 -0
- package/dist/src/sync.js +219 -0
- package/dist/src/sync.js.map +1 -0
- package/dist/src/types.d.ts +80 -0
- package/dist/src/types.js +5 -0
- package/dist/src/types.js.map +1 -0
- package/openclaw.plugin.json +151 -9
- package/package.json +22 -3
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DatasetState, MemoryScope, ScopedSyncIndexes, SyncIndex } from "./types.js";
|
|
2
|
+
export declare const STATE_DIR: string;
|
|
3
|
+
export declare const STATE_PATH: string;
|
|
4
|
+
export declare const SYNC_INDEX_PATH: string;
|
|
5
|
+
export declare const SCOPED_SYNC_INDEX_PATH: string;
|
|
6
|
+
export declare function loadDatasetState(): Promise<DatasetState>;
|
|
7
|
+
export declare function saveDatasetState(state: DatasetState): Promise<void>;
|
|
8
|
+
export declare function loadSyncIndex(): Promise<SyncIndex>;
|
|
9
|
+
export declare function saveSyncIndex(state: SyncIndex): Promise<void>;
|
|
10
|
+
export declare function loadScopedSyncIndexes(): Promise<ScopedSyncIndexes>;
|
|
11
|
+
export declare function saveScopedSyncIndexes(indexes: ScopedSyncIndexes): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Fix #7: Migrate legacy single-scope sync index into multi-scope indexes.
|
|
14
|
+
* Moves all entries from the old sync-index.json into the specified default scope.
|
|
15
|
+
* After migration, the legacy file is left in place (harmless) but no longer read.
|
|
16
|
+
*/
|
|
17
|
+
export declare function migrateLegacyIndex(defaultScope: MemoryScope): Promise<ScopedSyncIndexes | null>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// State file paths
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export const STATE_DIR = join(homedir(), ".openclaw", "memory", "cognee");
|
|
8
|
+
export const STATE_PATH = join(STATE_DIR, "datasets.json");
|
|
9
|
+
export const SYNC_INDEX_PATH = join(STATE_DIR, "sync-index.json");
|
|
10
|
+
export const SCOPED_SYNC_INDEX_PATH = join(STATE_DIR, "scoped-sync-indexes.json");
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Dataset state (maps dataset name -> dataset ID)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export async function loadDatasetState() {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await fs.readFile(STATE_PATH, "utf-8");
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
if (!parsed || typeof parsed !== "object")
|
|
19
|
+
return {};
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === "ENOENT")
|
|
24
|
+
return {};
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function saveDatasetState(state) {
|
|
29
|
+
await fs.mkdir(dirname(STATE_PATH), { recursive: true });
|
|
30
|
+
await fs.writeFile(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Sync index (legacy single-scope)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
export async function loadSyncIndex() {
|
|
36
|
+
try {
|
|
37
|
+
const raw = await fs.readFile(SYNC_INDEX_PATH, "utf-8");
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
if (!parsed || typeof parsed !== "object")
|
|
40
|
+
return { entries: {} };
|
|
41
|
+
const record = parsed;
|
|
42
|
+
record.entries ??= {};
|
|
43
|
+
return record;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code === "ENOENT")
|
|
47
|
+
return { entries: {} };
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function saveSyncIndex(state) {
|
|
52
|
+
await fs.mkdir(dirname(SYNC_INDEX_PATH), { recursive: true });
|
|
53
|
+
await fs.writeFile(SYNC_INDEX_PATH, JSON.stringify(state, null, 2), "utf-8");
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Scoped sync indexes (multi-scope)
|
|
57
|
+
//
|
|
58
|
+
// Fix #6: On load, we validate that keys are valid MemoryScope values
|
|
59
|
+
// and discard any garbage entries (e.g. typos like "compnay").
|
|
60
|
+
//
|
|
61
|
+
// Fix #7: Migration support — when switching from single to multi-scope,
|
|
62
|
+
// we migrate the legacy sync index into the appropriate scope.
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
const VALID_SCOPES = new Set(["company", "user", "agent"]);
|
|
65
|
+
export async function loadScopedSyncIndexes() {
|
|
66
|
+
try {
|
|
67
|
+
const raw = await fs.readFile(SCOPED_SYNC_INDEX_PATH, "utf-8");
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (!parsed || typeof parsed !== "object")
|
|
70
|
+
return {};
|
|
71
|
+
// Validate keys are valid scopes
|
|
72
|
+
const result = {};
|
|
73
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
74
|
+
if (VALID_SCOPES.has(key) && value && typeof value === "object") {
|
|
75
|
+
const idx = value;
|
|
76
|
+
idx.entries ??= {};
|
|
77
|
+
result[key] = idx;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error.code === "ENOENT")
|
|
84
|
+
return {};
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export async function saveScopedSyncIndexes(indexes) {
|
|
89
|
+
await fs.mkdir(dirname(SCOPED_SYNC_INDEX_PATH), { recursive: true });
|
|
90
|
+
await fs.writeFile(SCOPED_SYNC_INDEX_PATH, JSON.stringify(indexes, null, 2), "utf-8");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Fix #7: Migrate legacy single-scope sync index into multi-scope indexes.
|
|
94
|
+
* Moves all entries from the old sync-index.json into the specified default scope.
|
|
95
|
+
* After migration, the legacy file is left in place (harmless) but no longer read.
|
|
96
|
+
*/
|
|
97
|
+
export async function migrateLegacyIndex(defaultScope) {
|
|
98
|
+
const legacy = await loadSyncIndex();
|
|
99
|
+
if (Object.keys(legacy.entries).length === 0)
|
|
100
|
+
return null;
|
|
101
|
+
const scoped = {
|
|
102
|
+
[defaultScope]: { ...legacy },
|
|
103
|
+
};
|
|
104
|
+
await saveScopedSyncIndexes(scoped);
|
|
105
|
+
return scoped;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=persistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../src/persistence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { MEMORY_SCOPES } from "./types.js";
|
|
3
|
+
import { CogneeHttpClient } from "./client.js";
|
|
4
|
+
import { resolveConfig } from "./config.js";
|
|
5
|
+
import { collectMemoryFiles } from "./files.js";
|
|
6
|
+
import { loadDatasetState, loadScopedSyncIndexes, loadSyncIndex, migrateLegacyIndex, SYNC_INDEX_PATH, } from "./persistence.js";
|
|
7
|
+
import { datasetNameForScope, isMultiScopeEnabled, routeFileToScope } from "./scope.js";
|
|
8
|
+
import { syncFiles, syncFilesScoped } from "./sync.js";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Plugin registration
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const memoryCogneePlugin = {
|
|
13
|
+
id: "cognee-openclaw",
|
|
14
|
+
name: "Memory (Cognee)",
|
|
15
|
+
description: "Cognee-backed memory with multi-scope support (company/user/agent), session tracking, and auto-recall",
|
|
16
|
+
kind: "memory",
|
|
17
|
+
register(api) {
|
|
18
|
+
const cfg = resolveConfig(api.pluginConfig);
|
|
19
|
+
const client = new CogneeHttpClient(cfg.baseUrl, cfg.apiKey, cfg.username, cfg.password, cfg.requestTimeoutMs, cfg.ingestionTimeoutMs);
|
|
20
|
+
const multiScope = isMultiScopeEnabled(cfg);
|
|
21
|
+
// Legacy single-scope state
|
|
22
|
+
let datasetId;
|
|
23
|
+
let syncIndex = { entries: {} };
|
|
24
|
+
// Multi-scope state
|
|
25
|
+
let scopedIndexes = {};
|
|
26
|
+
// Session state
|
|
27
|
+
let sessionId;
|
|
28
|
+
let resolvedWorkspaceDir;
|
|
29
|
+
// Load persisted state on startup
|
|
30
|
+
const stateReady = Promise.all([
|
|
31
|
+
loadDatasetState()
|
|
32
|
+
.then((state) => {
|
|
33
|
+
if (!multiScope) {
|
|
34
|
+
datasetId = state[cfg.datasetName];
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
.catch((error) => {
|
|
38
|
+
api.logger.warn?.(`cognee-openclaw: failed to load dataset state: ${String(error)}`);
|
|
39
|
+
}),
|
|
40
|
+
multiScope
|
|
41
|
+
? loadScopedSyncIndexes()
|
|
42
|
+
.then(async (indexes) => {
|
|
43
|
+
// Fix #7: Migrate legacy index if scoped indexes are empty
|
|
44
|
+
if (Object.keys(indexes).length === 0) {
|
|
45
|
+
const migrated = await migrateLegacyIndex(cfg.defaultWriteScope);
|
|
46
|
+
if (migrated) {
|
|
47
|
+
scopedIndexes = migrated;
|
|
48
|
+
api.logger.info?.(`cognee-openclaw: migrated legacy sync index to scope "${cfg.defaultWriteScope}"`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
scopedIndexes = indexes;
|
|
53
|
+
})
|
|
54
|
+
.catch((error) => {
|
|
55
|
+
api.logger.warn?.(`cognee-openclaw: failed to load scoped sync indexes: ${String(error)}`);
|
|
56
|
+
})
|
|
57
|
+
: loadSyncIndex()
|
|
58
|
+
.then((state) => {
|
|
59
|
+
syncIndex = state;
|
|
60
|
+
if (!datasetId && state.datasetId && state.datasetName === cfg.datasetName) {
|
|
61
|
+
datasetId = state.datasetId;
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.catch((error) => {
|
|
65
|
+
api.logger.warn?.(`cognee-openclaw: failed to load sync index: ${String(error)}`);
|
|
66
|
+
}),
|
|
67
|
+
]);
|
|
68
|
+
// Fix #8: Log when scopes have no dataset ID during recall
|
|
69
|
+
async function getRecallDatasetIds() {
|
|
70
|
+
const state = await loadDatasetState();
|
|
71
|
+
const ids = [];
|
|
72
|
+
const missingScopes = [];
|
|
73
|
+
if (multiScope) {
|
|
74
|
+
for (const scope of cfg.recallScopes) {
|
|
75
|
+
const dsName = datasetNameForScope(scope, cfg);
|
|
76
|
+
const dsId = state[dsName] ?? scopedIndexes[scope]?.datasetId;
|
|
77
|
+
if (dsId) {
|
|
78
|
+
ids.push(dsId);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
missingScopes.push(scope);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
if (datasetId)
|
|
87
|
+
ids.push(datasetId);
|
|
88
|
+
}
|
|
89
|
+
return { ids, missingScopes };
|
|
90
|
+
}
|
|
91
|
+
// Helper: run sync
|
|
92
|
+
async function runSync(workspaceDir, logger) {
|
|
93
|
+
await stateReady;
|
|
94
|
+
const files = await collectMemoryFiles(workspaceDir);
|
|
95
|
+
if (files.length === 0) {
|
|
96
|
+
logger.info?.("cognee-openclaw: no memory files found");
|
|
97
|
+
return { added: 0, updated: 0, skipped: 0, errors: 0, deleted: 0 };
|
|
98
|
+
}
|
|
99
|
+
logger.info?.(`cognee-openclaw: found ${files.length} memory file(s), syncing...`);
|
|
100
|
+
if (multiScope) {
|
|
101
|
+
return syncFilesScoped(client, files, files, scopedIndexes, cfg, logger);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const result = await syncFiles(client, files, files, syncIndex, cfg, logger);
|
|
105
|
+
if (result.datasetId)
|
|
106
|
+
datasetId = result.datasetId;
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ------------------------------------------------------------------
|
|
111
|
+
// CLI commands
|
|
112
|
+
// ------------------------------------------------------------------
|
|
113
|
+
api.registerCli((ctx) => {
|
|
114
|
+
const cognee = ctx.program.command("cognee").description("Cognee memory management");
|
|
115
|
+
const cliWorkspaceDir = ctx.workspaceDir || process.cwd();
|
|
116
|
+
cognee
|
|
117
|
+
.command("index")
|
|
118
|
+
.description("Sync memory files to Cognee (add new, update changed, skip unchanged)")
|
|
119
|
+
.action(async () => {
|
|
120
|
+
const result = await runSync(cliWorkspaceDir, ctx.logger);
|
|
121
|
+
const summary = `Sync complete: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted, ${result.skipped} unchanged, ${result.errors} errors`;
|
|
122
|
+
ctx.logger.info?.(summary);
|
|
123
|
+
console.log(summary);
|
|
124
|
+
process.exit(0);
|
|
125
|
+
});
|
|
126
|
+
cognee
|
|
127
|
+
.command("status")
|
|
128
|
+
.description("Show Cognee sync state")
|
|
129
|
+
.action(async () => {
|
|
130
|
+
await stateReady;
|
|
131
|
+
const files = await collectMemoryFiles(cliWorkspaceDir);
|
|
132
|
+
if (multiScope) {
|
|
133
|
+
const state = await loadDatasetState();
|
|
134
|
+
for (const scope of MEMORY_SCOPES) {
|
|
135
|
+
const dsName = datasetNameForScope(scope, cfg);
|
|
136
|
+
const scopeIndex = scopedIndexes[scope] ?? { entries: {} };
|
|
137
|
+
const entryCount = Object.keys(scopeIndex.entries).length;
|
|
138
|
+
const scopeFiles = files.filter(f => routeFileToScope(f.path, cfg.scopeRouting, cfg.defaultWriteScope) === scope);
|
|
139
|
+
let dirty = 0, newCount = 0;
|
|
140
|
+
for (const file of scopeFiles) {
|
|
141
|
+
const existing = scopeIndex.entries[file.path];
|
|
142
|
+
if (!existing)
|
|
143
|
+
newCount++;
|
|
144
|
+
else if (existing.hash !== file.hash)
|
|
145
|
+
dirty++;
|
|
146
|
+
}
|
|
147
|
+
console.log(`\n[${scope.toUpperCase()}] Dataset: ${dsName}`);
|
|
148
|
+
console.log(` Dataset ID: ${state[dsName] ?? scopeIndex.datasetId ?? "(not set)"}`);
|
|
149
|
+
console.log(` Indexed files: ${entryCount}`);
|
|
150
|
+
console.log(` Workspace files: ${scopeFiles.length}`);
|
|
151
|
+
console.log(` New (unindexed): ${newCount}`);
|
|
152
|
+
console.log(` Changed (dirty): ${dirty}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const entryCount = Object.keys(syncIndex.entries).length;
|
|
157
|
+
const entriesWithDataId = Object.values(syncIndex.entries).filter((e) => e.dataId).length;
|
|
158
|
+
let dirty = 0, newCount = 0;
|
|
159
|
+
for (const file of files) {
|
|
160
|
+
const existing = syncIndex.entries[file.path];
|
|
161
|
+
if (!existing)
|
|
162
|
+
newCount++;
|
|
163
|
+
else if (existing.hash !== file.hash)
|
|
164
|
+
dirty++;
|
|
165
|
+
}
|
|
166
|
+
console.log([
|
|
167
|
+
`Dataset: ${syncIndex.datasetName ?? cfg.datasetName}`,
|
|
168
|
+
`Dataset ID: ${datasetId ?? syncIndex.datasetId ?? "(not set)"}`,
|
|
169
|
+
`Indexed files: ${entryCount} (${entriesWithDataId} with data ID)`,
|
|
170
|
+
`Workspace files: ${files.length}`,
|
|
171
|
+
`New (unindexed): ${newCount}`,
|
|
172
|
+
`Changed (dirty): ${dirty}`,
|
|
173
|
+
`Sync index: ${SYNC_INDEX_PATH}`,
|
|
174
|
+
].join("\n"));
|
|
175
|
+
}
|
|
176
|
+
process.exit(0);
|
|
177
|
+
});
|
|
178
|
+
cognee
|
|
179
|
+
.command("health")
|
|
180
|
+
.description("Check Cognee API connectivity")
|
|
181
|
+
.action(async () => {
|
|
182
|
+
try {
|
|
183
|
+
const result = await client.health();
|
|
184
|
+
console.log(`Cognee API: OK (${cfg.baseUrl})`);
|
|
185
|
+
if (result.status)
|
|
186
|
+
console.log(`Status: ${result.status}`);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.log(`Cognee API: UNREACHABLE (${cfg.baseUrl})`);
|
|
190
|
+
console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
process.exit(0);
|
|
194
|
+
});
|
|
195
|
+
cognee
|
|
196
|
+
.command("setup")
|
|
197
|
+
.description("Configure OpenClaw to use Cognee for memory (default: replaces built-in, --hybrid: alongside built-in)")
|
|
198
|
+
.option("--hybrid", "Keep built-in memory providers enabled alongside Cognee")
|
|
199
|
+
.action(async (opts) => {
|
|
200
|
+
const { loadConfig, writeConfigFile } = api.runtime.config;
|
|
201
|
+
const config = loadConfig();
|
|
202
|
+
// Set Cognee as the memory slot
|
|
203
|
+
config.plugins ??= {};
|
|
204
|
+
config.plugins.slots ??= {};
|
|
205
|
+
config.plugins.slots.memory = "cognee-openclaw";
|
|
206
|
+
config.plugins.entries ??= {};
|
|
207
|
+
const entries = config.plugins.entries;
|
|
208
|
+
if (opts.hybrid) {
|
|
209
|
+
// Hybrid mode: keep built-in memory enabled
|
|
210
|
+
entries["memory-core"] ??= { enabled: true };
|
|
211
|
+
entries["memory-core"].enabled = true;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// Exclusive mode: disable built-in memory providers
|
|
215
|
+
entries["memory-core"] = { enabled: false };
|
|
216
|
+
entries["memory-lancedb"] = { enabled: false };
|
|
217
|
+
}
|
|
218
|
+
// Ensure cognee-openclaw is enabled
|
|
219
|
+
entries["cognee-openclaw"] ??= { enabled: true };
|
|
220
|
+
entries["cognee-openclaw"].enabled = true;
|
|
221
|
+
await writeConfigFile(config);
|
|
222
|
+
if (opts.hybrid) {
|
|
223
|
+
console.log("Cognee memory setup complete (hybrid mode):");
|
|
224
|
+
console.log(" - Memory slot set to cognee-openclaw");
|
|
225
|
+
console.log(" - memory-core enabled (built-in memory active)");
|
|
226
|
+
console.log("\nBoth Cognee recall and built-in memory search are active.");
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.log("Cognee memory setup complete:");
|
|
230
|
+
console.log(" - Memory slot set to cognee-openclaw");
|
|
231
|
+
console.log(" - memory-core disabled");
|
|
232
|
+
console.log(" - memory-lancedb disabled");
|
|
233
|
+
}
|
|
234
|
+
console.log("\nRun 'openclaw cognee health' to verify Cognee connectivity.");
|
|
235
|
+
process.exit(0);
|
|
236
|
+
});
|
|
237
|
+
cognee
|
|
238
|
+
.command("scopes")
|
|
239
|
+
.description("Show memory scope routing for current workspace files")
|
|
240
|
+
.action(async () => {
|
|
241
|
+
const files = await collectMemoryFiles(cliWorkspaceDir);
|
|
242
|
+
if (files.length === 0) {
|
|
243
|
+
console.log("No memory files found.");
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
if (!multiScope) {
|
|
247
|
+
console.log(`Multi-scope mode is OFF. All files go to dataset "${cfg.datasetName}".`);
|
|
248
|
+
console.log(`Set companyDataset, userDatasetPrefix, or agentDatasetPrefix to enable.`);
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
const grouped = { company: [], user: [], agent: [] };
|
|
252
|
+
for (const file of files) {
|
|
253
|
+
const scope = routeFileToScope(file.path, cfg.scopeRouting, cfg.defaultWriteScope);
|
|
254
|
+
grouped[scope].push(file.path);
|
|
255
|
+
}
|
|
256
|
+
for (const scope of MEMORY_SCOPES) {
|
|
257
|
+
const dsName = datasetNameForScope(scope, cfg);
|
|
258
|
+
console.log(`\n[${scope.toUpperCase()}] -> dataset "${dsName}"`);
|
|
259
|
+
if (grouped[scope].length === 0)
|
|
260
|
+
console.log(" (no files)");
|
|
261
|
+
else
|
|
262
|
+
for (const p of grouped[scope])
|
|
263
|
+
console.log(` ${p}`);
|
|
264
|
+
}
|
|
265
|
+
process.exit(0);
|
|
266
|
+
});
|
|
267
|
+
}, { commands: ["cognee"] });
|
|
268
|
+
// ------------------------------------------------------------------
|
|
269
|
+
// Auto-sync on startup (with health check)
|
|
270
|
+
// ------------------------------------------------------------------
|
|
271
|
+
if (cfg.autoIndex) {
|
|
272
|
+
api.registerService({
|
|
273
|
+
id: "cognee-auto-sync",
|
|
274
|
+
async start(ctx) {
|
|
275
|
+
resolvedWorkspaceDir = ctx.workspaceDir || process.cwd();
|
|
276
|
+
try {
|
|
277
|
+
await client.health();
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
ctx.logger.warn?.(`cognee-openclaw: Cognee API unreachable at ${cfg.baseUrl} — auto-sync disabled for this session. Error: ${String(error)}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (cfg.enableSessions) {
|
|
284
|
+
sessionId = `openclaw-${randomUUID()}`;
|
|
285
|
+
ctx.logger.info?.(`cognee-openclaw: session ${sessionId}`);
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const result = await runSync(resolvedWorkspaceDir, ctx.logger);
|
|
289
|
+
ctx.logger.info?.(`cognee-openclaw: auto-sync complete: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted, ${result.skipped} unchanged`);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
ctx.logger.warn?.(`cognee-openclaw: auto-sync failed: ${String(error)}`);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// ------------------------------------------------------------------
|
|
298
|
+
// Auto-recall: inject memories before each agent run
|
|
299
|
+
// ------------------------------------------------------------------
|
|
300
|
+
if (cfg.autoRecall) {
|
|
301
|
+
api.on("before_agent_start", async (event, ctx) => {
|
|
302
|
+
await stateReady;
|
|
303
|
+
if (!event.prompt || event.prompt.length < 5) {
|
|
304
|
+
api.logger.debug?.("cognee-openclaw: skipping recall (prompt too short)");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const { ids: recallDatasetIds, missingScopes } = await getRecallDatasetIds();
|
|
308
|
+
// Fix #8: Log missing scopes so users know what's not being searched
|
|
309
|
+
if (missingScopes.length > 0) {
|
|
310
|
+
api.logger.info?.(`cognee-openclaw: scope(s) not yet indexed (no data): ${missingScopes.join(", ")}`);
|
|
311
|
+
}
|
|
312
|
+
if (recallDatasetIds.length === 0) {
|
|
313
|
+
api.logger.debug?.("cognee-openclaw: skipping recall (no datasetIds)");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
if (multiScope) {
|
|
318
|
+
// Fix #10: Use Promise.allSettled for resilience
|
|
319
|
+
const state = await loadDatasetState();
|
|
320
|
+
const searchPromises = cfg.recallScopes.map(async (scope) => {
|
|
321
|
+
const dsName = datasetNameForScope(scope, cfg);
|
|
322
|
+
const dsId = state[dsName] ?? scopedIndexes[scope]?.datasetId;
|
|
323
|
+
if (!dsId)
|
|
324
|
+
return null;
|
|
325
|
+
const results = await client.search({
|
|
326
|
+
queryText: event.prompt,
|
|
327
|
+
searchType: cfg.searchType,
|
|
328
|
+
datasetIds: [dsId],
|
|
329
|
+
searchPrompt: cfg.searchPrompt,
|
|
330
|
+
maxTokens: cfg.maxTokens,
|
|
331
|
+
sessionId,
|
|
332
|
+
});
|
|
333
|
+
const filtered = results
|
|
334
|
+
.filter((r) => r.score >= cfg.minScore)
|
|
335
|
+
.slice(0, cfg.maxResults);
|
|
336
|
+
return filtered.length > 0 ? { scope, results: filtered } : null;
|
|
337
|
+
});
|
|
338
|
+
// Fix #10: allSettled — inject whatever succeeds, log failures
|
|
339
|
+
const settled = await Promise.allSettled(searchPromises);
|
|
340
|
+
const scopeResults = {};
|
|
341
|
+
for (let i = 0; i < settled.length; i++) {
|
|
342
|
+
const outcome = settled[i];
|
|
343
|
+
const scope = cfg.recallScopes[i];
|
|
344
|
+
if (outcome.status === "fulfilled" && outcome.value) {
|
|
345
|
+
scopeResults[outcome.value.scope] = outcome.value.results;
|
|
346
|
+
}
|
|
347
|
+
else if (outcome.status === "rejected") {
|
|
348
|
+
api.logger.warn?.(`cognee-openclaw: recall failed for scope ${scope}: ${String(outcome.reason)}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (Object.keys(scopeResults).length === 0) {
|
|
352
|
+
api.logger.debug?.("cognee-openclaw: search returned no results above minScore");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const sections = [];
|
|
356
|
+
for (const scope of cfg.recallScopes) {
|
|
357
|
+
const results = scopeResults[scope];
|
|
358
|
+
if (!results || results.length === 0)
|
|
359
|
+
continue;
|
|
360
|
+
const payload = JSON.stringify(results.map((r) => ({ id: r.id, score: r.score, text: r.text, metadata: r.metadata })), null, 2);
|
|
361
|
+
sections.push(`<${scope}_memory>\n${payload}\n</${scope}_memory>`);
|
|
362
|
+
}
|
|
363
|
+
const totalResults = Object.values(scopeResults).reduce((sum, arr) => sum + arr.length, 0);
|
|
364
|
+
api.logger.info?.(`cognee-openclaw: injecting ${totalResults} memories across ${Object.keys(scopeResults).length} scope(s)`);
|
|
365
|
+
return { prependContext: `<cognee_memories>\n${sections.join("\n")}\n</cognee_memories>` };
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
// Legacy single-scope
|
|
369
|
+
const results = await client.search({
|
|
370
|
+
queryText: event.prompt,
|
|
371
|
+
searchType: cfg.searchType,
|
|
372
|
+
datasetIds: recallDatasetIds,
|
|
373
|
+
searchPrompt: cfg.searchPrompt,
|
|
374
|
+
maxTokens: cfg.maxTokens,
|
|
375
|
+
sessionId,
|
|
376
|
+
});
|
|
377
|
+
const filtered = results
|
|
378
|
+
.filter((r) => r.score >= cfg.minScore)
|
|
379
|
+
.slice(0, cfg.maxResults);
|
|
380
|
+
if (filtered.length === 0) {
|
|
381
|
+
api.logger.debug?.("cognee-openclaw: search returned no results above minScore");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const payload = JSON.stringify(filtered.map((r) => ({ id: r.id, score: r.score, text: r.text, metadata: r.metadata })), null, 2);
|
|
385
|
+
api.logger.info?.(`cognee-openclaw: injecting ${filtered.length} memories`);
|
|
386
|
+
return { prependContext: `<cognee_memories>\nRelevant memories:\n${payload}\n</cognee_memories>` };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
api.logger.warn?.(`cognee-openclaw: recall failed: ${String(error)}`);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
// ------------------------------------------------------------------
|
|
395
|
+
// Post-agent sync + session persistence
|
|
396
|
+
// ------------------------------------------------------------------
|
|
397
|
+
if (cfg.autoIndex) {
|
|
398
|
+
api.on("agent_end", async (event, ctx) => {
|
|
399
|
+
if (!event.success)
|
|
400
|
+
return;
|
|
401
|
+
await stateReady;
|
|
402
|
+
const workspaceDir = resolvedWorkspaceDir || process.cwd();
|
|
403
|
+
// Fix #4: Actually persist the session into the knowledge graph
|
|
404
|
+
if (cfg.enableSessions && cfg.persistSessionsAfterEnd && sessionId) {
|
|
405
|
+
try {
|
|
406
|
+
// Determine target dataset for session persistence
|
|
407
|
+
const targetDatasetIds = [];
|
|
408
|
+
if (multiScope) {
|
|
409
|
+
const state = await loadDatasetState();
|
|
410
|
+
const agentDsName = datasetNameForScope("agent", cfg);
|
|
411
|
+
const agentDsId = state[agentDsName] ?? scopedIndexes.agent?.datasetId;
|
|
412
|
+
if (agentDsId)
|
|
413
|
+
targetDatasetIds.push(agentDsId);
|
|
414
|
+
}
|
|
415
|
+
else if (datasetId) {
|
|
416
|
+
targetDatasetIds.push(datasetId);
|
|
417
|
+
}
|
|
418
|
+
if (targetDatasetIds.length > 0) {
|
|
419
|
+
// Call Cognee's session persistence endpoint via the generic fetchJson
|
|
420
|
+
await client.fetchJson("/api/v1/sessions/persist", {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: { "Content-Type": "application/json" },
|
|
423
|
+
body: JSON.stringify({
|
|
424
|
+
session_ids: [sessionId],
|
|
425
|
+
dataset_ids: targetDatasetIds,
|
|
426
|
+
}),
|
|
427
|
+
}).catch(() => {
|
|
428
|
+
// Session persistence endpoint may not exist on all Cognee versions.
|
|
429
|
+
// Fail silently — this is an enhancement, not critical path.
|
|
430
|
+
api.logger.debug?.("cognee-openclaw: session persistence endpoint not available");
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
api.logger.warn?.(`cognee-openclaw: session persistence failed: ${String(error)}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Sync file changes
|
|
439
|
+
try {
|
|
440
|
+
if (multiScope) {
|
|
441
|
+
try {
|
|
442
|
+
const freshIndexes = await loadScopedSyncIndexes();
|
|
443
|
+
scopedIndexes = freshIndexes;
|
|
444
|
+
}
|
|
445
|
+
catch { /* fall through */ }
|
|
446
|
+
const files = await collectMemoryFiles(workspaceDir);
|
|
447
|
+
let hasChanges = false;
|
|
448
|
+
for (const file of files) {
|
|
449
|
+
const scope = routeFileToScope(file.path, cfg.scopeRouting, cfg.defaultWriteScope);
|
|
450
|
+
const scopeIndex = scopedIndexes[scope];
|
|
451
|
+
if (!scopeIndex) {
|
|
452
|
+
hasChanges = true;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
const existing = scopeIndex.entries[file.path];
|
|
456
|
+
if (!existing || existing.hash !== file.hash) {
|
|
457
|
+
hasChanges = true;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (!hasChanges) {
|
|
462
|
+
const currentPaths = new Set(files.map(f => f.path));
|
|
463
|
+
for (const scopeIndex of Object.values(scopedIndexes)) {
|
|
464
|
+
if (scopeIndex && Object.keys(scopeIndex.entries).some(p => !currentPaths.has(p))) {
|
|
465
|
+
hasChanges = true;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (!hasChanges)
|
|
471
|
+
return;
|
|
472
|
+
api.logger.info?.("cognee-openclaw: detected changes, syncing across scopes...");
|
|
473
|
+
const result = await syncFilesScoped(client, files, files, scopedIndexes, cfg, api.logger);
|
|
474
|
+
api.logger.info?.(`cognee-openclaw: post-agent sync: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted`);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
try {
|
|
478
|
+
const freshIndex = await loadSyncIndex();
|
|
479
|
+
syncIndex.entries = freshIndex.entries;
|
|
480
|
+
if (freshIndex.datasetId)
|
|
481
|
+
syncIndex.datasetId = freshIndex.datasetId;
|
|
482
|
+
if (freshIndex.datasetName)
|
|
483
|
+
syncIndex.datasetName = freshIndex.datasetName;
|
|
484
|
+
}
|
|
485
|
+
catch { /* fall through */ }
|
|
486
|
+
const files = await collectMemoryFiles(workspaceDir);
|
|
487
|
+
const changedFiles = files.filter((f) => {
|
|
488
|
+
const existing = syncIndex.entries[f.path];
|
|
489
|
+
return !existing || existing.hash !== f.hash;
|
|
490
|
+
});
|
|
491
|
+
const currentPaths = new Set(files.map(f => f.path));
|
|
492
|
+
const hasDeletedFiles = Object.keys(syncIndex.entries).some(p => !currentPaths.has(p));
|
|
493
|
+
if (changedFiles.length === 0 && !hasDeletedFiles)
|
|
494
|
+
return;
|
|
495
|
+
api.logger.info?.(`cognee-openclaw: detected ${changedFiles.length} changed file(s)${hasDeletedFiles ? " + deletions" : ""}, syncing...`);
|
|
496
|
+
const result = await syncFiles(client, changedFiles, files, syncIndex, cfg, api.logger);
|
|
497
|
+
if (result.datasetId)
|
|
498
|
+
datasetId = result.datasetId;
|
|
499
|
+
api.logger.info?.(`cognee-openclaw: post-agent sync: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
api.logger.warn?.(`cognee-openclaw: post-agent sync failed: ${String(error)}`);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
export default memoryCogneePlugin;
|
|
510
|
+
//# sourceMappingURL=plugin.js.map
|