@shadowforge0/aquifer-memory 1.8.1 → 1.9.0

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.
@@ -6,8 +6,10 @@
6
6
  // $OPENCLAW_HOME/extensions/aquifer-memory/ ← symlink to this directory
7
7
  //
8
8
  // Behavior:
9
- // - Loads $OPENCLAW_HOME/.env so DATABASE_URL / EMBED_PROVIDER /
10
- // AQUIFER_LLM_PROVIDER etc. are visible to the plugin.
9
+ // - Reads $OPENCLAW_HOME/.env and then mcp.servers.aquifer.env from
10
+ // $OPENCLAW_HOME/openclaw.json so ingest uses the same DB/schema/env as
11
+ // the Aquifer MCP recall tools, without leaving those values in the
12
+ // shared OpenClaw process environment.
11
13
  // - Delegates to consumers/openclaw-plugin.js. If AQUIFER_PERSONA is set
12
14
  // (pluginConfig.persona or env), the plugin loads the persona module
13
15
  // and hands off mountOnOpenClaw(api); otherwise the default generic
@@ -21,17 +23,73 @@ const os = require('os');
21
23
 
22
24
  const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw');
23
25
 
24
- function loadEnvFile(envPath) {
26
+ function mergeEnvObject(target, values, opts = {}) {
27
+ if (!values || typeof values !== 'object') return;
28
+ for (const [key, value] of Object.entries(values)) {
29
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) continue;
30
+ if (value === undefined || value === null) continue;
31
+ if (!opts.override && (process.env[key] || target[key])) continue;
32
+ target[key] = String(value);
33
+ }
34
+ }
35
+
36
+ function readEnvFile(envPath) {
37
+ const values = {};
25
38
  try {
26
39
  const text = fs.readFileSync(envPath, 'utf8');
27
40
  for (const line of text.split('\n')) {
28
41
  const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
29
- if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
42
+ if (m) values[m[1]] = m[2].trim();
30
43
  }
31
44
  } catch { /* .env missing — ok */ }
45
+ return values;
46
+ }
47
+
48
+ function readOpenClawAquiferMcpEnv(configPath) {
49
+ try {
50
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
51
+ return cfg.mcp?.servers?.aquifer?.env || {};
52
+ } catch { /* openclaw.json missing or malformed — plugin config/env may still work */ }
53
+ return {};
54
+ }
55
+
56
+ function buildAquiferEnv() {
57
+ const env = {};
58
+ mergeEnvObject(env, readEnvFile(path.join(OPENCLAW_HOME, '.env')));
59
+ mergeEnvObject(env, readOpenClawAquiferMcpEnv(path.join(OPENCLAW_HOME, 'openclaw.json')), { override: true });
60
+ return env;
32
61
  }
33
62
 
34
- loadEnvFile(path.join(OPENCLAW_HOME, '.env'));
63
+ function withEnvOverlay(env, fn) {
64
+ const previous = new Map();
65
+ for (const [key, value] of Object.entries(env || {})) {
66
+ previous.set(key, Object.prototype.hasOwnProperty.call(process.env, key) ? process.env[key] : undefined);
67
+ process.env[key] = value;
68
+ }
69
+ try {
70
+ return fn();
71
+ } finally {
72
+ for (const [key, value] of previous.entries()) {
73
+ if (value === undefined) delete process.env[key];
74
+ else process.env[key] = value;
75
+ }
76
+ }
77
+ }
35
78
 
36
79
  // Re-export the plugin as-is. OpenClaw expects { id, name, register }.
37
- module.exports = require('../openclaw-plugin');
80
+ const plugin = require('../openclaw-plugin');
81
+
82
+ module.exports = {
83
+ ...plugin,
84
+ register(api) {
85
+ const aquiferEnv = buildAquiferEnv();
86
+ const pluginConfig = { ...(api.pluginConfig || {}) };
87
+ if (!pluginConfig.persona && aquiferEnv.AQUIFER_PERSONA) {
88
+ pluginConfig.persona = aquiferEnv.AQUIFER_PERSONA;
89
+ }
90
+ return withEnvOverlay(aquiferEnv, () => plugin.register({
91
+ ...api,
92
+ pluginConfig,
93
+ }));
94
+ },
95
+ };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "aquifer-memory",
3
3
  "name": "Aquifer Memory",
4
- "version": "1.2.0",
4
+ "version": "1.9.0",
5
5
  "description": "Session ingest + recall + feedback. Reads DATABASE_URL / EMBED_PROVIDER / AQUIFER_LLM_PROVIDER from host env; delegates to AQUIFER_PERSONA module if set.",
6
6
  "main": "index.js",
7
7
  "hooks": ["before_reset"],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aquifer-openclaw-ext",
3
- "version": "1.2.0",
3
+ "version": "1.9.0",
4
4
  "private": true,
5
5
  "main": "index.js",
6
6
  "description": "Drop-in OpenClaw extension for Aquifer Memory. Symlink into $OPENCLAW_HOME/extensions/aquifer-memory/ — no host-side boilerplate required.",
@@ -0,0 +1,326 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { version: packageVersion } = require('../package.json');
7
+
8
+ const OPENCLAW_PLUGIN_ID = 'aquifer-memory';
9
+
10
+ function nowStamp() {
11
+ return new Date().toISOString().replace(/[:.]/g, '-');
12
+ }
13
+
14
+ function uniqueBackupPath(filePath) {
15
+ const base = `${filePath}.bak-${nowStamp()}`;
16
+ if (!fs.existsSync(base)) return base;
17
+ for (let i = 1; i < 1000; i += 1) {
18
+ const candidate = `${base}-${i}`;
19
+ if (!fs.existsSync(candidate)) return candidate;
20
+ }
21
+ throw new Error(`Could not allocate unique backup path for ${filePath}`);
22
+ }
23
+
24
+ function valueFlag(flags = {}, name, fallback = null) {
25
+ const value = flags[name];
26
+ if (value === undefined || value === null || value === true || value === '') return fallback;
27
+ return String(value);
28
+ }
29
+
30
+ function resolveOpenClawHome(flags = {}, env = process.env) {
31
+ return path.resolve(valueFlag(flags, 'openclaw-home', env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw')));
32
+ }
33
+
34
+ function readJson(filePath) {
35
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
36
+ }
37
+
38
+ function writeJsonWithBackup(filePath, value, opts = {}) {
39
+ if (opts.dryRun) return null;
40
+ const backupPath = uniqueBackupPath(filePath);
41
+ if (fs.existsSync(filePath)) fs.copyFileSync(filePath, backupPath);
42
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
43
+ return backupPath;
44
+ }
45
+
46
+ function samePath(a, b) {
47
+ return path.resolve(a) === path.resolve(b);
48
+ }
49
+
50
+ function ensureOpenClawHome(openclawHome) {
51
+ if (!fs.existsSync(openclawHome) || !fs.statSync(openclawHome).isDirectory()) {
52
+ throw new Error(`OPENCLAW_HOME not found: ${openclawHome}`);
53
+ }
54
+ }
55
+
56
+ function ensureExtensionLink({ openclawHome, packageRoot, dryRun = false, force = false }) {
57
+ const source = path.join(packageRoot, 'consumers', 'openclaw-ext');
58
+ const dest = path.join(openclawHome, 'extensions', 'aquifer-memory');
59
+ if (!fs.existsSync(source)) throw new Error(`OpenClaw extension source not found: ${source}`);
60
+
61
+ const result = {
62
+ source,
63
+ dest,
64
+ action: 'link',
65
+ backupPath: null,
66
+ };
67
+
68
+ if (fs.existsSync(dest)) {
69
+ const stat = fs.lstatSync(dest);
70
+ if (stat.isSymbolicLink()) {
71
+ const target = fs.readlinkSync(dest);
72
+ const resolvedTarget = path.resolve(path.dirname(dest), target);
73
+ if (samePath(resolvedTarget, source)) {
74
+ return { ...result, action: 'already-linked' };
75
+ }
76
+ }
77
+
78
+ if (!force) {
79
+ throw new Error(`${dest} already exists; rerun with --force to move it aside and relink`);
80
+ }
81
+
82
+ result.backupPath = `${dest}.bak-${nowStamp()}`;
83
+ result.action = 'replace-link';
84
+ if (!dryRun) fs.renameSync(dest, result.backupPath);
85
+ }
86
+
87
+ if (!dryRun) {
88
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
89
+ fs.symlinkSync(source, dest, 'dir');
90
+ }
91
+ return result;
92
+ }
93
+
94
+ function normalizeMcpServer(existing = {}, packageRoot) {
95
+ return {
96
+ ...existing,
97
+ command: 'node',
98
+ args: [path.join(packageRoot, 'consumers', 'mcp.js')],
99
+ env: {
100
+ ...(existing.env && typeof existing.env === 'object' ? existing.env : {}),
101
+ },
102
+ };
103
+ }
104
+
105
+ function ensureMcpConfig({ openclawHome, packageRoot, dryRun = false }) {
106
+ const configPath = path.join(openclawHome, 'openclaw.json');
107
+ const mcpPath = path.join(packageRoot, 'consumers', 'mcp.js');
108
+ const result = {
109
+ configPath,
110
+ mcpPath,
111
+ action: 'skipped',
112
+ backupPath: null,
113
+ previousArgs: null,
114
+ nextArgs: [mcpPath],
115
+ };
116
+
117
+ if (!fs.existsSync(configPath)) {
118
+ return { ...result, reason: 'openclaw.json not found' };
119
+ }
120
+
121
+ const config = readJson(configPath);
122
+ const existing = config.mcp?.servers?.aquifer || {};
123
+ result.previousArgs = Array.isArray(existing.args) ? existing.args : null;
124
+
125
+ const nextServer = normalizeMcpServer(existing, packageRoot);
126
+ const previousText = JSON.stringify(existing);
127
+ const nextText = JSON.stringify(nextServer);
128
+ if (previousText === nextText) {
129
+ return { ...result, action: 'already-configured' };
130
+ }
131
+
132
+ config.mcp = config.mcp && typeof config.mcp === 'object' ? config.mcp : {};
133
+ config.mcp.servers = config.mcp.servers && typeof config.mcp.servers === 'object'
134
+ ? config.mcp.servers
135
+ : {};
136
+ config.mcp.servers.aquifer = nextServer;
137
+ result.action = 'write-config';
138
+ result.backupPath = writeJsonWithBackup(configPath, config, { dryRun });
139
+ return result;
140
+ }
141
+
142
+ function normalizePluginEntry(existing = {}) {
143
+ const entry = existing && typeof existing === 'object' ? existing : {};
144
+ return {
145
+ ...entry,
146
+ enabled: true,
147
+ config: entry.config && typeof entry.config === 'object' ? entry.config : {},
148
+ };
149
+ }
150
+
151
+ function ensurePluginConfig({ openclawHome, extensionPath, dryRun = false }) {
152
+ const configPath = path.join(openclawHome, 'openclaw.json');
153
+ const result = {
154
+ configPath,
155
+ pluginId: OPENCLAW_PLUGIN_ID,
156
+ extensionPath,
157
+ action: 'skipped',
158
+ backupPath: null,
159
+ previousLoadPaths: null,
160
+ nextLoadPaths: null,
161
+ };
162
+
163
+ if (!fs.existsSync(configPath)) {
164
+ return { ...result, reason: 'openclaw.json not found' };
165
+ }
166
+
167
+ const config = readJson(configPath);
168
+ const plugins = config.plugins && typeof config.plugins === 'object' ? config.plugins : {};
169
+ const load = plugins.load && typeof plugins.load === 'object' ? plugins.load : {};
170
+ const previousLoadPaths = Array.isArray(load.paths) ? load.paths : [];
171
+ const nextLoadPaths = previousLoadPaths.includes(extensionPath)
172
+ ? previousLoadPaths
173
+ : [...previousLoadPaths, extensionPath];
174
+ const entries = plugins.entries && typeof plugins.entries === 'object' ? plugins.entries : {};
175
+ const previousEntry = entries[OPENCLAW_PLUGIN_ID];
176
+ const nextEntry = normalizePluginEntry(previousEntry);
177
+ const previousAllow = Array.isArray(plugins.allow) ? plugins.allow : null;
178
+ const nextAllow = previousAllow && !previousAllow.includes(OPENCLAW_PLUGIN_ID)
179
+ ? [...previousAllow, OPENCLAW_PLUGIN_ID]
180
+ : previousAllow;
181
+
182
+ result.previousLoadPaths = previousLoadPaths;
183
+ result.nextLoadPaths = nextLoadPaths;
184
+
185
+ const unchanged = JSON.stringify(previousLoadPaths) === JSON.stringify(nextLoadPaths)
186
+ && JSON.stringify(previousEntry) === JSON.stringify(nextEntry)
187
+ && JSON.stringify(previousAllow) === JSON.stringify(nextAllow);
188
+ if (unchanged) {
189
+ return { ...result, action: 'already-configured' };
190
+ }
191
+
192
+ config.plugins = {
193
+ ...plugins,
194
+ entries: {
195
+ ...entries,
196
+ [OPENCLAW_PLUGIN_ID]: nextEntry,
197
+ },
198
+ load: {
199
+ ...load,
200
+ paths: nextLoadPaths,
201
+ },
202
+ };
203
+ if (nextAllow) config.plugins.allow = nextAllow;
204
+
205
+ result.action = 'write-config';
206
+ result.backupPath = writeJsonWithBackup(configPath, config, { dryRun });
207
+ return result;
208
+ }
209
+
210
+ function inspectInstalledPackage(openclawHome) {
211
+ const packagePath = path.join(openclawHome, 'node_modules', '@shadowforge0', 'aquifer-memory', 'package.json');
212
+ if (!fs.existsSync(packagePath)) {
213
+ return { packagePath, installed: false, version: null };
214
+ }
215
+ const pkg = readJson(packagePath);
216
+ return { packagePath, installed: true, version: pkg.version || null };
217
+ }
218
+
219
+ function buildInstallReport(args = {}, opts = {}) {
220
+ const flags = args.flags || {};
221
+ const env = opts.env || process.env;
222
+ const openclawHome = resolveOpenClawHome(flags, env);
223
+ const packageRoot = opts.packageRoot || path.resolve(__dirname, '..');
224
+ const dryRun = flags['dry-run'] === true;
225
+ const force = flags.force === true;
226
+ const linkCurrentPackage = flags['link-current-package'] === true;
227
+ const skipExtension = flags['skip-extension'] === true;
228
+ const skipMcp = flags['skip-mcp'] === true;
229
+
230
+ ensureOpenClawHome(openclawHome);
231
+
232
+ const installedPackage = inspectInstalledPackage(openclawHome);
233
+ const expectedPackageRoot = path.join(openclawHome, 'node_modules', '@shadowforge0', 'aquifer-memory');
234
+ const targetPackageRoot = linkCurrentPackage || !installedPackage.installed
235
+ ? packageRoot
236
+ : expectedPackageRoot;
237
+ const report = {
238
+ ok: true,
239
+ dryRun,
240
+ openclawHome,
241
+ packageRoot,
242
+ targetPackageRoot,
243
+ linkCurrentPackage,
244
+ packageVersion,
245
+ expectedPackageRoot,
246
+ installedPackage,
247
+ extension: skipExtension ? { action: 'skipped', reason: '--skip-extension' } : null,
248
+ plugin: skipExtension ? { action: 'skipped', reason: '--skip-extension' } : null,
249
+ mcp: skipMcp ? { action: 'skipped', reason: '--skip-mcp' } : null,
250
+ warnings: [],
251
+ };
252
+
253
+ if (installedPackage.installed && installedPackage.version && installedPackage.version !== packageVersion) {
254
+ report.warnings.push(
255
+ `OpenClaw node_modules has @shadowforge0/aquifer-memory ${installedPackage.version}; installer package is ${packageVersion}.`
256
+ );
257
+ }
258
+ if (!samePath(targetPackageRoot, expectedPackageRoot)) {
259
+ report.warnings.push(
260
+ `MCP will point outside OpenClaw node_modules: ${targetPackageRoot}. For a durable package install, run npm install --prefix "${openclawHome}" @shadowforge0/aquifer-memory@${packageVersion}.`
261
+ );
262
+ }
263
+ if (!samePath(packageRoot, expectedPackageRoot) && samePath(targetPackageRoot, expectedPackageRoot)) {
264
+ report.warnings.push(
265
+ `Installer is running from ${packageRoot}, but durable OpenClaw wiring targets ${expectedPackageRoot}. Use --link-current-package only for source-checkout development.`
266
+ );
267
+ }
268
+
269
+ if (!skipExtension) {
270
+ report.extension = ensureExtensionLink({ openclawHome, packageRoot: targetPackageRoot, dryRun, force });
271
+ report.plugin = ensurePluginConfig({
272
+ openclawHome,
273
+ extensionPath: report.extension.dest,
274
+ dryRun,
275
+ });
276
+ }
277
+
278
+ if (!skipMcp) {
279
+ report.mcp = ensureMcpConfig({ openclawHome, packageRoot: targetPackageRoot, dryRun });
280
+ }
281
+
282
+ return report;
283
+ }
284
+
285
+ function formatInstallReport(report) {
286
+ const lines = [];
287
+ lines.push(`Aquifer package: ${report.packageVersion} at ${report.packageRoot}`);
288
+ if (report.installedPackage.installed) {
289
+ lines.push(`OpenClaw package: ${report.installedPackage.version} at ${report.installedPackage.packagePath}`);
290
+ } else {
291
+ lines.push(`OpenClaw package: not installed at ${report.installedPackage.packagePath}`);
292
+ }
293
+ lines.push(`Extension: ${report.extension.action}${report.extension.dest ? ` (${report.extension.dest})` : ''}`);
294
+ if (report.extension.backupPath) lines.push(`Extension backup: ${report.extension.backupPath}`);
295
+ lines.push(`Plugin config: ${report.plugin.action}${report.plugin.configPath ? ` (${report.plugin.configPath})` : ''}`);
296
+ if (report.plugin.backupPath) lines.push(`Plugin config backup: ${report.plugin.backupPath}`);
297
+ if (report.plugin.reason) lines.push(`Plugin config note: ${report.plugin.reason}`);
298
+ lines.push(`MCP config: ${report.mcp.action}${report.mcp.configPath ? ` (${report.mcp.configPath})` : ''}`);
299
+ if (report.mcp.backupPath) lines.push(`MCP config backup: ${report.mcp.backupPath}`);
300
+ if (report.mcp.reason) lines.push(`MCP config note: ${report.mcp.reason}`);
301
+ for (const warning of report.warnings) lines.push(`Warning: ${warning}`);
302
+ if (report.dryRun) lines.push('Dry run: no files were changed.');
303
+ return lines.join('\n');
304
+ }
305
+
306
+ async function cmdInstallOpenClaw(args = {}, opts = {}) {
307
+ const report = buildInstallReport(args, opts);
308
+ if (args.flags?.json) {
309
+ console.log(JSON.stringify(report, null, 2));
310
+ } else {
311
+ console.log(formatInstallReport(report));
312
+ }
313
+ return report;
314
+ }
315
+
316
+ module.exports = {
317
+ buildInstallReport,
318
+ cmdInstallOpenClaw,
319
+ ensureExtensionLink,
320
+ ensurePluginConfig,
321
+ ensureMcpConfig,
322
+ formatInstallReport,
323
+ inspectInstalledPackage,
324
+ normalizeMcpServer,
325
+ resolveOpenClawHome,
326
+ };
@@ -33,6 +33,43 @@ function coerceRawEntries(messages) {
33
33
  });
34
34
  }
35
35
 
36
+ function normalizeTimestamp(value) {
37
+ if (value === null || value === undefined || value === '') return null;
38
+
39
+ if (value instanceof Date) {
40
+ const time = value.getTime();
41
+ return Number.isFinite(time) ? value.toISOString() : null;
42
+ }
43
+
44
+ if (typeof value === 'number') {
45
+ if (!Number.isFinite(value)) return null;
46
+ const millis = Math.abs(value) < 1e12 ? value * 1000 : value;
47
+ const date = new Date(millis);
48
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
49
+ }
50
+
51
+ if (typeof value === 'string') {
52
+ const trimmed = value.trim();
53
+ if (!trimmed) return null;
54
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) {
55
+ return normalizeTimestamp(Number(trimmed));
56
+ }
57
+ const parsed = Date.parse(trimmed);
58
+ if (!Number.isFinite(parsed)) return null;
59
+ return new Date(parsed).toISOString();
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ function selectTimestamp(...values) {
66
+ for (const value of values) {
67
+ const ts = normalizeTimestamp(value);
68
+ if (ts) return ts;
69
+ }
70
+ return null;
71
+ }
72
+
36
73
  function normalizeEntries(rawEntries) {
37
74
  const normalized = [];
38
75
  let userCount = 0, assistantCount = 0;
@@ -55,7 +92,7 @@ function normalizeEntries(rawEntries) {
55
92
  .join('\n');
56
93
  }
57
94
 
58
- const ts = entry.timestamp || msg.timestamp || null;
95
+ const ts = selectTimestamp(entry.timestamp, msg.timestamp);
59
96
  if (ts && !startedAt) startedAt = ts;
60
97
  if (ts) lastMessageAt = ts;
61
98
 
@@ -117,6 +154,7 @@ module.exports = buildPlugin();
117
154
  // contract; OpenClaw reads { id, name, register } only.
118
155
  module.exports.normalizeEntries = normalizeEntries;
119
156
  module.exports.coerceRawEntries = coerceRawEntries;
157
+ module.exports.normalizeTimestamp = normalizeTimestamp;
120
158
 
121
159
  function register(api) {
122
160
  const pluginConfig = api.pluginConfig || {};
@@ -55,6 +55,7 @@ const DEFAULTS = {
55
55
  servingMode: 'legacy', // 'legacy' | 'curated'
56
56
  activeScopeKey: null,
57
57
  activeScopePath: null,
58
+ allowedScopeKeys: null,
58
59
  },
59
60
  codex: {
60
61
  checkpoint: {
@@ -120,6 +121,7 @@ const ENV_MAP = [
120
121
  ['AQUIFER_MEMORY_SERVING_MODE', 'memory.servingMode'],
121
122
  ['AQUIFER_MEMORY_ACTIVE_SCOPE_KEY', 'memory.activeScopeKey'],
122
123
  ['AQUIFER_MEMORY_ACTIVE_SCOPE_PATH', 'memory.activeScopePath'],
124
+ ['AQUIFER_MEMORY_ALLOWED_SCOPE_KEYS', 'memory.allowedScopeKeys'],
123
125
  ['AQUIFER_CODEX_CHECKPOINT_CHECK_INTERVAL_MS', 'codex.checkpoint.checkIntervalMs', Number],
124
126
  ['AQUIFER_CODEX_CHECKPOINT_CHECK_INTERVAL_MINUTES', 'codex.checkpoint.checkIntervalMinutes', Number],
125
127
  ['AQUIFER_CODEX_CHECKPOINT_EVERY_MESSAGES', 'codex.checkpoint.everyMessages', Number],