@wipcomputer/memory-crystal 0.7.33 → 0.7.34-alpha.2
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/dist/cc-hook.js +144 -1
- package/dist/cli.js +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/mcp-server.js +1 -1
- package/dist/openclaw.js +92 -2
- package/package.json +1 -1
package/dist/cc-hook.js
CHANGED
|
@@ -26,15 +26,19 @@ import {
|
|
|
26
26
|
openSync,
|
|
27
27
|
readSync,
|
|
28
28
|
closeSync,
|
|
29
|
-
copyFileSync
|
|
29
|
+
copyFileSync,
|
|
30
|
+
readdirSync
|
|
30
31
|
} from "fs";
|
|
31
32
|
import { join, basename } from "path";
|
|
33
|
+
import { createHash } from "crypto";
|
|
32
34
|
var CC_AGENT_ID = getAgentId("claude-code");
|
|
33
35
|
var RELAY_URL = process.env.CRYSTAL_RELAY_URL || "";
|
|
34
36
|
var RELAY_TOKEN = process.env.CRYSTAL_RELAY_TOKEN || "";
|
|
35
37
|
var PRIVATE_MODE_PATH = resolveStatePath("memory-capture-state.json");
|
|
36
38
|
var WATERMARK_PATH = resolveStatePath("cc-capture-watermark.json");
|
|
37
39
|
var CC_ENABLED_PATH = resolveStatePath("cc-capture-enabled.json");
|
|
40
|
+
var MEMORY_SYNC_WATERMARK_PATH = resolveStatePath("cc-memory-sync-watermark.json");
|
|
41
|
+
var CLAUDE_PROJECTS_DIR = join(process.env.HOME || "", ".claude", "projects");
|
|
38
42
|
function getCaptureMode() {
|
|
39
43
|
if (RELAY_URL && RELAY_TOKEN) return "relay";
|
|
40
44
|
return "local";
|
|
@@ -80,6 +84,135 @@ function saveWatermark(wm) {
|
|
|
80
84
|
wm.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
81
85
|
writeFileSync(writePath, JSON.stringify(wm, null, 2));
|
|
82
86
|
}
|
|
87
|
+
function loadMemorySyncWatermark() {
|
|
88
|
+
try {
|
|
89
|
+
if (existsSync(MEMORY_SYNC_WATERMARK_PATH)) {
|
|
90
|
+
return JSON.parse(readFileSync(MEMORY_SYNC_WATERMARK_PATH, "utf-8"));
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
return { files: {}, lastRun: null };
|
|
95
|
+
}
|
|
96
|
+
function saveMemorySyncWatermark(wm) {
|
|
97
|
+
const writePath = stateWritePath("cc-memory-sync-watermark.json");
|
|
98
|
+
wm.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
99
|
+
writeFileSync(writePath, JSON.stringify(wm, null, 2));
|
|
100
|
+
}
|
|
101
|
+
function hashFileContent(content) {
|
|
102
|
+
return createHash("sha256").update(content).digest("hex");
|
|
103
|
+
}
|
|
104
|
+
function parseFrontmatter(content) {
|
|
105
|
+
const frontmatter = {};
|
|
106
|
+
if (!content.startsWith("---")) {
|
|
107
|
+
return { frontmatter, body: content };
|
|
108
|
+
}
|
|
109
|
+
const endIdx = content.indexOf("---", 3);
|
|
110
|
+
if (endIdx === -1) {
|
|
111
|
+
return { frontmatter, body: content };
|
|
112
|
+
}
|
|
113
|
+
const yamlBlock = content.slice(3, endIdx).trim();
|
|
114
|
+
const body = content.slice(endIdx + 3).trim();
|
|
115
|
+
for (const line of yamlBlock.split("\n")) {
|
|
116
|
+
const colonIdx = line.indexOf(":");
|
|
117
|
+
if (colonIdx === -1) continue;
|
|
118
|
+
const key = line.slice(0, colonIdx).trim();
|
|
119
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
120
|
+
if (key === "name") frontmatter.name = value;
|
|
121
|
+
else if (key === "description") frontmatter.description = value;
|
|
122
|
+
else if (key === "type") frontmatter.type = value;
|
|
123
|
+
}
|
|
124
|
+
return { frontmatter, body };
|
|
125
|
+
}
|
|
126
|
+
var VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
127
|
+
"fact",
|
|
128
|
+
"preference",
|
|
129
|
+
"event",
|
|
130
|
+
"opinion",
|
|
131
|
+
"skill",
|
|
132
|
+
"user",
|
|
133
|
+
"feedback",
|
|
134
|
+
"project",
|
|
135
|
+
"reference"
|
|
136
|
+
]);
|
|
137
|
+
function discoverMemoryFiles() {
|
|
138
|
+
const files = [];
|
|
139
|
+
try {
|
|
140
|
+
if (!existsSync(CLAUDE_PROJECTS_DIR)) return files;
|
|
141
|
+
for (const project of readdirSync(CLAUDE_PROJECTS_DIR)) {
|
|
142
|
+
const memDir = join(CLAUDE_PROJECTS_DIR, project, "memory");
|
|
143
|
+
if (!existsSync(memDir)) continue;
|
|
144
|
+
try {
|
|
145
|
+
for (const file of readdirSync(memDir)) {
|
|
146
|
+
if (!file.endsWith(".md")) continue;
|
|
147
|
+
files.push(join(memDir, file));
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
return files;
|
|
155
|
+
}
|
|
156
|
+
async function syncMemoryFiles() {
|
|
157
|
+
if (isPrivateMode()) return 0;
|
|
158
|
+
const files = discoverMemoryFiles();
|
|
159
|
+
if (files.length === 0) return 0;
|
|
160
|
+
const wm = loadMemorySyncWatermark();
|
|
161
|
+
const changed = [];
|
|
162
|
+
for (const filePath of files) {
|
|
163
|
+
try {
|
|
164
|
+
const content = readFileSync(filePath, "utf-8");
|
|
165
|
+
if (content.trim().length === 0) continue;
|
|
166
|
+
const hash = hashFileContent(content);
|
|
167
|
+
if (wm.files[filePath] && wm.files[filePath].hash === hash) continue;
|
|
168
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
169
|
+
changed.push({ path: filePath, content, hash, frontmatter, body });
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (changed.length === 0) {
|
|
174
|
+
saveMemorySyncWatermark(wm);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
const mode = getCaptureMode();
|
|
178
|
+
if (mode === "relay") {
|
|
179
|
+
process.stderr.write(`[cc-memory-sync] skipping ${changed.length} files (relay mode not yet supported)
|
|
180
|
+
`);
|
|
181
|
+
for (const item of changed) {
|
|
182
|
+
wm.files[item.path] = { hash: item.hash, syncedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
183
|
+
}
|
|
184
|
+
saveMemorySyncWatermark(wm);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
const config = resolveConfig();
|
|
188
|
+
const crystal = createCrystal(config);
|
|
189
|
+
await crystal.init();
|
|
190
|
+
let ingested = 0;
|
|
191
|
+
for (const item of changed) {
|
|
192
|
+
try {
|
|
193
|
+
const rawType = item.frontmatter.type || "reference";
|
|
194
|
+
const category = VALID_CATEGORIES.has(rawType) ? rawType : "reference";
|
|
195
|
+
let text = "";
|
|
196
|
+
if (item.frontmatter.name) {
|
|
197
|
+
text += `[${item.frontmatter.name}] `;
|
|
198
|
+
}
|
|
199
|
+
if (item.body.length > 0) {
|
|
200
|
+
text += item.body;
|
|
201
|
+
} else {
|
|
202
|
+
text += item.content;
|
|
203
|
+
}
|
|
204
|
+
if (text.trim().length < 10) continue;
|
|
205
|
+
await crystal.remember(text.trim(), category);
|
|
206
|
+
ingested++;
|
|
207
|
+
wm.files[item.path] = { hash: item.hash, syncedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
208
|
+
} catch (err) {
|
|
209
|
+
process.stderr.write(`[cc-memory-sync] error ingesting ${basename(item.path)}: ${err.message}
|
|
210
|
+
`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
saveMemorySyncWatermark(wm);
|
|
214
|
+
return ingested;
|
|
215
|
+
}
|
|
83
216
|
function extractMessages(filePath, lastByteOffset) {
|
|
84
217
|
const fileSize = statSync(filePath).size;
|
|
85
218
|
if (lastByteOffset >= fileSize) {
|
|
@@ -356,6 +489,16 @@ async function main() {
|
|
|
356
489
|
writeSummaryFile(paths.sessions, summary, CC_AGENT_ID, sessionId);
|
|
357
490
|
} catch {
|
|
358
491
|
}
|
|
492
|
+
try {
|
|
493
|
+
const syncCount = await syncMemoryFiles();
|
|
494
|
+
if (syncCount > 0) {
|
|
495
|
+
process.stderr.write(`[cc-memory-sync] ingested ${syncCount} changed memory file(s)
|
|
496
|
+
`);
|
|
497
|
+
}
|
|
498
|
+
} catch (err) {
|
|
499
|
+
process.stderr.write(`[cc-memory-sync] error: ${err.message}
|
|
500
|
+
`);
|
|
501
|
+
}
|
|
359
502
|
} catch (err) {
|
|
360
503
|
process.stderr.write(`[cc-memory-capture] error: ${err.message}
|
|
361
504
|
`);
|
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ crystal \u2014 Sovereign memory system
|
|
|
24
24
|
|
|
25
25
|
Commands:
|
|
26
26
|
crystal search <query> [-n limit] [--agent <id>] [--since <time>] [--until <date>] [--intent <context>] [--candidates <n>] [--explain] [--provider <openai|ollama|google>]
|
|
27
|
-
crystal remember <text> [--category fact|preference|event|opinion|skill]
|
|
27
|
+
crystal remember <text> [--category fact|preference|event|opinion|skill|user|feedback|project|reference]
|
|
28
28
|
crystal forget <id>
|
|
29
29
|
crystal status [--provider <openai|ollama|google>]
|
|
30
30
|
|
package/dist/core.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ interface Memory {
|
|
|
48
48
|
id?: number;
|
|
49
49
|
text: string;
|
|
50
50
|
embedding?: number[];
|
|
51
|
-
category: 'fact' | 'preference' | 'event' | 'opinion' | 'skill';
|
|
51
|
+
category: 'fact' | 'preference' | 'event' | 'opinion' | 'skill' | 'user' | 'feedback' | 'project' | 'reference';
|
|
52
52
|
confidence: number;
|
|
53
53
|
source_ids: string;
|
|
54
54
|
status: 'active' | 'deprecated' | 'deleted';
|
package/dist/mcp-server.js
CHANGED
|
@@ -81,7 +81,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
81
81
|
text: { type: "string", description: "The fact or observation to remember" },
|
|
82
82
|
category: {
|
|
83
83
|
type: "string",
|
|
84
|
-
enum: ["fact", "preference", "event", "opinion", "skill"],
|
|
84
|
+
enum: ["fact", "preference", "event", "opinion", "skill", "user", "feedback", "project", "reference"],
|
|
85
85
|
description: "Category of memory (default: fact)"
|
|
86
86
|
}
|
|
87
87
|
},
|
package/dist/openclaw.js
CHANGED
|
@@ -166,12 +166,13 @@ ${header}`));
|
|
|
166
166
|
import {
|
|
167
167
|
existsSync as existsSync2,
|
|
168
168
|
readFileSync as readFileSync2,
|
|
169
|
+
writeFileSync as writeFileSync2,
|
|
169
170
|
readdirSync,
|
|
170
171
|
copyFileSync,
|
|
171
172
|
statSync,
|
|
172
173
|
mkdirSync as mkdirSync2
|
|
173
174
|
} from "fs";
|
|
174
|
-
import { join as join2 } from "path";
|
|
175
|
+
import { join as join2, basename as basename2 } from "path";
|
|
175
176
|
var PRIVATE_MODE_PATH = resolveStatePath("memory-capture-state.json");
|
|
176
177
|
function isPrivateMode() {
|
|
177
178
|
try {
|
|
@@ -246,6 +247,90 @@ function syncDirRecursive(srcDir, destDir, ext) {
|
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
}
|
|
250
|
+
var WORKSPACE_WATERMARK_FILE = "workspace-memory-watermarks.json";
|
|
251
|
+
function loadWatermarks() {
|
|
252
|
+
try {
|
|
253
|
+
const path = resolveStatePath(WORKSPACE_WATERMARK_FILE);
|
|
254
|
+
if (existsSync2(path)) {
|
|
255
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
function saveWatermarks(watermarks) {
|
|
262
|
+
const path = stateWritePath(WORKSPACE_WATERMARK_FILE);
|
|
263
|
+
writeFileSync2(path, JSON.stringify(watermarks, null, 2), "utf-8");
|
|
264
|
+
}
|
|
265
|
+
var VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
266
|
+
"fact",
|
|
267
|
+
"preference",
|
|
268
|
+
"event",
|
|
269
|
+
"opinion",
|
|
270
|
+
"skill",
|
|
271
|
+
"user",
|
|
272
|
+
"feedback",
|
|
273
|
+
"project",
|
|
274
|
+
"reference"
|
|
275
|
+
]);
|
|
276
|
+
function parseFrontmatterType(content) {
|
|
277
|
+
const trimmed = content.trimStart();
|
|
278
|
+
if (!trimmed.startsWith("---")) return null;
|
|
279
|
+
const endIdx = trimmed.indexOf("---", 3);
|
|
280
|
+
if (endIdx === -1) return null;
|
|
281
|
+
const frontmatter = trimmed.slice(3, endIdx);
|
|
282
|
+
for (const line of frontmatter.split("\n")) {
|
|
283
|
+
const match = line.match(/^\s*type\s*:\s*(.+?)\s*$/);
|
|
284
|
+
if (match) {
|
|
285
|
+
const value = match[1].replace(/^["']|["']$/g, "");
|
|
286
|
+
if (VALID_CATEGORIES.has(value)) {
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function collectWorkspaceMemoryFiles() {
|
|
294
|
+
const HOME2 = process.env.HOME || "";
|
|
295
|
+
const workspaceDir = join2(HOME2, ".openclaw", "workspace");
|
|
296
|
+
const files = [];
|
|
297
|
+
const memoryMd = join2(workspaceDir, "MEMORY.md");
|
|
298
|
+
if (existsSync2(memoryMd)) files.push(memoryMd);
|
|
299
|
+
const memoryDir = join2(workspaceDir, "memory");
|
|
300
|
+
if (existsSync2(memoryDir)) {
|
|
301
|
+
for (const entry of readdirSync(memoryDir)) {
|
|
302
|
+
if (entry.endsWith(".md")) {
|
|
303
|
+
files.push(join2(memoryDir, entry));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return files;
|
|
308
|
+
}
|
|
309
|
+
async function syncWorkspaceMemory(crystal, agentId, logger) {
|
|
310
|
+
const watermarks = loadWatermarks();
|
|
311
|
+
const files = collectWorkspaceMemoryFiles();
|
|
312
|
+
let ingested = 0;
|
|
313
|
+
for (const filePath of files) {
|
|
314
|
+
try {
|
|
315
|
+
const stat = statSync(filePath);
|
|
316
|
+
const lastMtime = watermarks[filePath] || 0;
|
|
317
|
+
if (stat.mtimeMs <= lastMtime) continue;
|
|
318
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
319
|
+
if (!content || content.trim().length < 50) continue;
|
|
320
|
+
const category = parseFrontmatterType(content) || "fact";
|
|
321
|
+
await crystal.remember(content, category);
|
|
322
|
+
ingested++;
|
|
323
|
+
watermarks[filePath] = stat.mtimeMs;
|
|
324
|
+
} catch (err) {
|
|
325
|
+
logger.warn(`memory-crystal: workspace sync skipped ${basename2(filePath)}: ${err.message}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (ingested > 0) {
|
|
329
|
+
saveWatermarks(watermarks);
|
|
330
|
+
logger.info(`memory-crystal: synced ${ingested} workspace memory file(s) to Crystal`);
|
|
331
|
+
}
|
|
332
|
+
return ingested;
|
|
333
|
+
}
|
|
249
334
|
var openclaw_default = {
|
|
250
335
|
register(api) {
|
|
251
336
|
const crystal = new Crystal(resolveConfig());
|
|
@@ -319,6 +404,11 @@ var openclaw_default = {
|
|
|
319
404
|
} catch (err) {
|
|
320
405
|
api.logger.error(`memory-crystal: ingest error: ${err.message}`);
|
|
321
406
|
}
|
|
407
|
+
try {
|
|
408
|
+
await syncWorkspaceMemory(crystal, agentId, api.logger);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
api.logger.warn(`memory-crystal: workspace memory sync failed (non-fatal): ${err.message}`);
|
|
411
|
+
}
|
|
322
412
|
syncRawDataToLdm(api.logger);
|
|
323
413
|
});
|
|
324
414
|
function toolResult(text, isError = false) {
|
|
@@ -373,7 +463,7 @@ ${r.text}`;
|
|
|
373
463
|
type: "object",
|
|
374
464
|
properties: {
|
|
375
465
|
text: { type: "string", description: "The fact to remember" },
|
|
376
|
-
category: { type: "string", enum: ["fact", "preference", "event", "opinion", "skill"] }
|
|
466
|
+
category: { type: "string", enum: ["fact", "preference", "event", "opinion", "skill", "user", "feedback", "project", "reference"] }
|
|
377
467
|
},
|
|
378
468
|
required: ["text"]
|
|
379
469
|
},
|
package/package.json
CHANGED