@wipcomputer/memory-crystal 0.7.34-alpha.1 → 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 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/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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/memory-crystal",
3
- "version": "0.7.34-alpha.1",
3
+ "version": "0.7.34-alpha.2",
4
4
  "description": "Sovereign memory system — local-first with ephemeral encrypted relay. Your memory, your machine, your rules.",
5
5
  "repository": {
6
6
  "type": "git",