@shadowforge0/aquifer-memory 1.5.12 → 1.6.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.
- package/.env.example +23 -0
- package/README.md +78 -73
- package/README_CN.md +659 -0
- package/README_TW.md +680 -0
- package/aquifer.config.example.json +34 -0
- package/consumers/claude-code.js +11 -11
- package/consumers/cli.js +353 -52
- package/consumers/codex-handoff.js +152 -0
- package/consumers/codex.js +1549 -0
- package/consumers/default/daily-entries.js +23 -4
- package/consumers/default/index.js +2 -2
- package/consumers/default/prompts/summary.js +6 -6
- package/consumers/mcp.js +96 -5
- package/consumers/openclaw-ext/index.js +0 -1
- package/consumers/openclaw-plugin.js +1 -1
- package/consumers/shared/config.js +8 -0
- package/consumers/shared/factory.js +1 -0
- package/consumers/shared/ingest.js +1 -1
- package/consumers/shared/normalize.js +14 -3
- package/consumers/shared/recall-format.js +27 -0
- package/consumers/shared/summary-parser.js +151 -0
- package/core/aquifer.js +372 -18
- package/core/finalization-review.js +319 -0
- package/core/mcp-manifest.js +52 -2
- package/core/memory-bootstrap.js +188 -0
- package/core/memory-consolidation.js +1236 -0
- package/core/memory-promotion.js +544 -0
- package/core/memory-recall.js +247 -0
- package/core/memory-records.js +581 -0
- package/core/memory-safety-gate.js +224 -0
- package/core/session-finalization.js +350 -0
- package/core/storage.js +385 -2
- package/docs/getting-started.md +99 -0
- package/docs/postprocess-contract.md +2 -2
- package/docs/setup.md +51 -2
- package/package.json +25 -11
- package/pipeline/normalize/adapters/codex.js +106 -0
- package/pipeline/normalize/detect.js +3 -2
- package/schema/001-base.sql +3 -0
- package/schema/007-v1-foundation.sql +273 -0
- package/schema/008-session-finalizations.sql +50 -0
- package/schema/009-v1-assertion-plane.sql +193 -0
- package/schema/010-v1-finalization-review.sql +160 -0
- package/schema/011-v1-compaction-claim.sql +46 -0
- package/schema/012-v1-compaction-lease.sql +39 -0
- package/schema/013-v1-compaction-lineage.sql +193 -0
- package/scripts/codex-recovery.js +532 -0
- package/consumers/miranda/context-inject.js +0 -120
- package/consumers/miranda/daily-entries.js +0 -224
- package/consumers/miranda/index.js +0 -364
- package/consumers/miranda/instance.js +0 -55
- package/consumers/miranda/llm.js +0 -99
- package/consumers/miranda/profile.json +0 -145
- package/consumers/miranda/prompts/summary.js +0 -303
- package/consumers/miranda/recall-format.js +0 -76
- package/consumers/miranda/render-daily-md.js +0 -186
- package/consumers/miranda/workspace-files.js +0 -91
- package/scripts/drop-entity-state-history.sql +0 -17
- package/scripts/drop-insights.sql +0 -12
- package/scripts/install-openclaw.sh +0 -59
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Miranda daily log renderer — reference implementation for the artifact
|
|
4
|
-
// capability (spec §12).
|
|
5
|
-
//
|
|
6
|
-
// Pulls the canonical state for a date from Aquifer (timeline events + latest
|
|
7
|
-
// state + active narrative + latest handoff) and renders it into a single
|
|
8
|
-
// markdown file. Pure logic — does not write to disk; returns the rendered
|
|
9
|
-
// string plus an artifact record declaration the caller can persist via
|
|
10
|
-
// aq.artifacts.record().
|
|
11
|
-
//
|
|
12
|
-
// Shape deliberately mirrors the historical Miranda daily-log format so the
|
|
13
|
-
// downstream consumers (CC, Discord pushes, weekly rollup) see no regression
|
|
14
|
-
// during the cutover from render-daily-log.js to this reference impl.
|
|
15
|
-
|
|
16
|
-
const crypto = require('crypto');
|
|
17
|
-
|
|
18
|
-
function startOfDayIso(dateStr) {
|
|
19
|
-
return `${dateStr}T00:00:00Z`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function endOfDayIso(dateStr) {
|
|
23
|
-
return `${dateStr}T23:59:59.999Z`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function ensureDate(input) {
|
|
27
|
-
if (!input) throw new Error('date (YYYY-MM-DD) is required');
|
|
28
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
29
|
-
throw new Error(`date must match YYYY-MM-DD, got: ${input}`);
|
|
30
|
-
}
|
|
31
|
-
return input;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function renderSection(title, lines) {
|
|
35
|
-
if (!lines || lines.length === 0) return null;
|
|
36
|
-
return `## ${title}\n\n${lines.join('\n')}\n`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function formatTimelineLine(evt) {
|
|
40
|
-
const ts = new Date(evt.occurredAt).toISOString().slice(11, 16);
|
|
41
|
-
const src = evt.sessionRef ? ` (${evt.sessionRef})` : '';
|
|
42
|
-
return `- \`${ts}\`${src} ${evt.text}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function formatHandoff(payload) {
|
|
46
|
-
if (!payload) return null;
|
|
47
|
-
const lines = [];
|
|
48
|
-
if (payload.last_step) lines.push(`**Last step.** ${payload.last_step}`);
|
|
49
|
-
if (payload.status) lines.push(`**Status.** ${payload.status}`);
|
|
50
|
-
if (payload.next) lines.push(`**Next.** ${payload.next}`);
|
|
51
|
-
if (Array.isArray(payload.blockers) && payload.blockers.length > 0) {
|
|
52
|
-
lines.push(`**Blockers.**`);
|
|
53
|
-
for (const b of payload.blockers) lines.push(`- ${b}`);
|
|
54
|
-
}
|
|
55
|
-
if (Array.isArray(payload.open_loops) && payload.open_loops.length > 0) {
|
|
56
|
-
lines.push(`**Open loops.**`);
|
|
57
|
-
for (const l of payload.open_loops) lines.push(`- ${l}`);
|
|
58
|
-
}
|
|
59
|
-
return lines.length > 0 ? lines.join('\n') + '\n' : null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function formatState(state) {
|
|
63
|
-
if (!state) return null;
|
|
64
|
-
const lines = [];
|
|
65
|
-
if (state.goal) lines.push(`**Goal.** ${state.goal}`);
|
|
66
|
-
if (Array.isArray(state.active_work) && state.active_work.length > 0) {
|
|
67
|
-
lines.push(`**Active work.**`);
|
|
68
|
-
for (const w of state.active_work) lines.push(`- ${w}`);
|
|
69
|
-
}
|
|
70
|
-
if (state.affect && typeof state.affect === 'object') {
|
|
71
|
-
const bits = [];
|
|
72
|
-
if (state.affect.mood) bits.push(`mood: ${state.affect.mood}`);
|
|
73
|
-
if (state.affect.energy) bits.push(`energy: ${state.affect.energy}`);
|
|
74
|
-
if (state.affect.confidence) bits.push(`confidence: ${state.affect.confidence}`);
|
|
75
|
-
if (bits.length > 0) lines.push(`**Affect.** ${bits.join(', ')}`);
|
|
76
|
-
}
|
|
77
|
-
return lines.length > 0 ? lines.join('\n') + '\n' : null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// -------------------------------------------------------------------------
|
|
81
|
-
// Public entry
|
|
82
|
-
// -------------------------------------------------------------------------
|
|
83
|
-
//
|
|
84
|
-
// renderDailyMd({ aquifer, date, agentId, tenantId?, categories? }) returns:
|
|
85
|
-
// {
|
|
86
|
-
// markdown: string,
|
|
87
|
-
// artifact: { producerId, type, format, destination, payload,
|
|
88
|
-
// idempotencyKey } // ready for aq.artifacts.record()
|
|
89
|
-
// }
|
|
90
|
-
//
|
|
91
|
-
// The caller decides whether to persist the artifact record and where the
|
|
92
|
-
// rendered file lands. Aquifer itself doesn't touch disk.
|
|
93
|
-
|
|
94
|
-
async function renderDailyMd({
|
|
95
|
-
aquifer, date, agentId, tenantId, categories,
|
|
96
|
-
destinationTemplate = 'workspace://memory/{date}.md',
|
|
97
|
-
producerId = 'miranda.workspace.daily-log',
|
|
98
|
-
}) {
|
|
99
|
-
if (!aquifer) throw new Error('aquifer instance is required');
|
|
100
|
-
if (!agentId) throw new Error('agentId is required');
|
|
101
|
-
const day = ensureDate(date);
|
|
102
|
-
|
|
103
|
-
const since = startOfDayIso(day);
|
|
104
|
-
const until = endOfDayIso(day);
|
|
105
|
-
|
|
106
|
-
const timelineResult = await aquifer.timeline.list({
|
|
107
|
-
tenantId, agentId, categories, since, until, limit: 500,
|
|
108
|
-
});
|
|
109
|
-
if (!timelineResult.ok) throw new Error(`timeline.list failed: ${timelineResult.error.message}`);
|
|
110
|
-
|
|
111
|
-
const stateResult = await aquifer.state.getLatest({ tenantId, agentId });
|
|
112
|
-
if (!stateResult.ok) throw new Error(`state.getLatest failed: ${stateResult.error.message}`);
|
|
113
|
-
|
|
114
|
-
const handoffResult = await aquifer.handoff.getLatest({ tenantId, agentId });
|
|
115
|
-
if (!handoffResult.ok) throw new Error(`handoff.getLatest failed: ${handoffResult.error.message}`);
|
|
116
|
-
|
|
117
|
-
const narrativeResult = await aquifer.narratives.getLatest({ tenantId, agentId });
|
|
118
|
-
if (!narrativeResult.ok) throw new Error(`narratives.getLatest failed: ${narrativeResult.error.message}`);
|
|
119
|
-
|
|
120
|
-
const events = (timelineResult.data.rows || []).slice().sort((a, b) =>
|
|
121
|
-
new Date(a.occurredAt).getTime() - new Date(b.occurredAt).getTime());
|
|
122
|
-
|
|
123
|
-
// Group timeline by category.
|
|
124
|
-
const grouped = new Map();
|
|
125
|
-
for (const evt of events) {
|
|
126
|
-
if (!grouped.has(evt.category)) grouped.set(evt.category, []);
|
|
127
|
-
grouped.get(evt.category).push(evt);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const sections = [];
|
|
131
|
-
sections.push(`# ${day}\n`);
|
|
132
|
-
const stateBlock = formatState(stateResult.data.state);
|
|
133
|
-
if (stateBlock) sections.push(`## State\n\n${stateBlock}`);
|
|
134
|
-
|
|
135
|
-
if (narrativeResult.data.narrative) {
|
|
136
|
-
sections.push(`## Narrative\n\n${narrativeResult.data.narrative.text}\n`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const categoryOrder = ['focus', 'todo', 'mood', 'handoff', 'narrative',
|
|
140
|
-
'organized', 'note', 'stats', 'health', 'garmin', 'weekly', 'monthly', 'cli'];
|
|
141
|
-
const emitted = new Set();
|
|
142
|
-
for (const cat of categoryOrder) {
|
|
143
|
-
if (!grouped.has(cat)) continue;
|
|
144
|
-
const lines = grouped.get(cat).map(formatTimelineLine);
|
|
145
|
-
const sec = renderSection(cat.charAt(0).toUpperCase() + cat.slice(1), lines);
|
|
146
|
-
if (sec) sections.push(sec);
|
|
147
|
-
emitted.add(cat);
|
|
148
|
-
}
|
|
149
|
-
for (const [cat, evts] of grouped) {
|
|
150
|
-
if (emitted.has(cat)) continue;
|
|
151
|
-
const lines = evts.map(formatTimelineLine);
|
|
152
|
-
const sec = renderSection(cat, lines);
|
|
153
|
-
if (sec) sections.push(sec);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const handoffBlock = formatHandoff(handoffResult.data.handoff);
|
|
157
|
-
if (handoffBlock) sections.push(`## Handoff\n\n${handoffBlock}`);
|
|
158
|
-
|
|
159
|
-
const markdown = sections.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
160
|
-
|
|
161
|
-
const destination = destinationTemplate.replace('{date}', day);
|
|
162
|
-
const idempotencyKey = crypto.createHash('sha256')
|
|
163
|
-
.update(`miranda:daily:${tenantId || 'default'}:${agentId}:${day}`)
|
|
164
|
-
.digest('hex');
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
markdown,
|
|
168
|
-
artifact: {
|
|
169
|
-
producerId,
|
|
170
|
-
type: 'daily-log',
|
|
171
|
-
format: 'markdown',
|
|
172
|
-
destination,
|
|
173
|
-
triggerPhase: 'artifact_dispatch',
|
|
174
|
-
payload: {
|
|
175
|
-
date: day,
|
|
176
|
-
event_count: events.length,
|
|
177
|
-
has_state: !!stateResult.data.state,
|
|
178
|
-
has_handoff: !!handoffResult.data.handoff,
|
|
179
|
-
has_narrative: !!narrativeResult.data.narrative,
|
|
180
|
-
},
|
|
181
|
-
idempotencyKey,
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
module.exports = { renderDailyMd };
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
// Host-owned file paths: emotional state + recap JSON + learned skills.
|
|
7
|
-
// These are Miranda persona artifacts; other consumers won't want them.
|
|
8
|
-
|
|
9
|
-
function extractFilePaths(conversationText) {
|
|
10
|
-
const re = /(?:^|\s|["'`])((?:\/[\w.@-]+)+(?:\/[\w.@-]+)*\.[\w]+)/g;
|
|
11
|
-
const paths = new Set();
|
|
12
|
-
let m;
|
|
13
|
-
while ((m = re.exec(conversationText || '')) !== null) {
|
|
14
|
-
const p = m[1];
|
|
15
|
-
if (p.startsWith('/bin/') || p.startsWith('/usr/') || p.startsWith('/etc/')) continue;
|
|
16
|
-
if (p.includes('node_modules/')) continue;
|
|
17
|
-
paths.add(p);
|
|
18
|
-
}
|
|
19
|
-
return [...paths].slice(0, 30);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function writeWorkspaceFiles(sections, recap, workspaceDir, context, logger = console) {
|
|
23
|
-
let emotionalStateWritten = false;
|
|
24
|
-
const recapFilesWritten = [];
|
|
25
|
-
let learnedSkillsWritten = 0;
|
|
26
|
-
|
|
27
|
-
if (sections?.emotional_state) {
|
|
28
|
-
await fs.promises.mkdir(path.join(workspaceDir, 'memory'), { recursive: true });
|
|
29
|
-
await fs.promises.writeFile(
|
|
30
|
-
path.join(workspaceDir, 'memory', 'emotional-state.md'),
|
|
31
|
-
sections.emotional_state, 'utf8',
|
|
32
|
-
);
|
|
33
|
-
emotionalStateWritten = true;
|
|
34
|
-
if (logger.info) logger.info('[miranda] wrote emotional state');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (recap?.title) {
|
|
38
|
-
const sessDir = path.join(workspaceDir, 'memory', 'sessions');
|
|
39
|
-
await fs.promises.mkdir(sessDir, { recursive: true });
|
|
40
|
-
if (context?.conversationText) recap.files_mentioned = extractFilePaths(context.conversationText);
|
|
41
|
-
|
|
42
|
-
await fs.promises.writeFile(
|
|
43
|
-
path.join(sessDir, 'afterburn-latest.json'),
|
|
44
|
-
JSON.stringify(recap, null, 2), 'utf8',
|
|
45
|
-
);
|
|
46
|
-
recapFilesWritten.push('afterburn-latest.json');
|
|
47
|
-
|
|
48
|
-
const safeId = String(context?.sessionId || '').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
49
|
-
if (safeId) {
|
|
50
|
-
const fname = `afterburn-${safeId}.json`;
|
|
51
|
-
await fs.promises.writeFile(path.join(sessDir, fname), JSON.stringify(recap, null, 2), 'utf8');
|
|
52
|
-
recapFilesWritten.push(fname);
|
|
53
|
-
}
|
|
54
|
-
if (logger.info) logger.info(`[miranda] wrote recap (title=${recap.title.slice(0, 40)})`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (Array.isArray(recap?.reusable_patterns) && recap.reusable_patterns.length > 0) {
|
|
58
|
-
try {
|
|
59
|
-
const skillsPath = path.join(workspaceDir, 'memory', 'topics', 'learned-skills.md');
|
|
60
|
-
let existing = '';
|
|
61
|
-
try { existing = await fs.promises.readFile(skillsPath, 'utf8'); } catch { /* first write */ }
|
|
62
|
-
if (!existing) {
|
|
63
|
-
existing = '# Learned Skills\n\n> 自動從 session 中抽取的可重用操作模式。\n> invariant = 永久規則。derived = 情境洞察(可能過期)。\n\n';
|
|
64
|
-
}
|
|
65
|
-
const now = new Intl.DateTimeFormat('sv-SE', {
|
|
66
|
-
timeZone: 'Asia/Taipei',
|
|
67
|
-
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
68
|
-
}).format(new Date());
|
|
69
|
-
const patterns = recap.reusable_patterns.filter(p => p.pattern && p.trigger);
|
|
70
|
-
const lines = [];
|
|
71
|
-
for (const p of patterns) {
|
|
72
|
-
const icon = p.durability === 'invariant' ? '🔒' : '📌';
|
|
73
|
-
const suffix = p.durability !== 'invariant' ? '(expires ~14d)' : '';
|
|
74
|
-
lines.push(`- ${icon} **${p.pattern}** — 觸發:${p.trigger};做法:${p.action || '(見 session)'}${suffix}`);
|
|
75
|
-
}
|
|
76
|
-
if (lines.length > 0) {
|
|
77
|
-
await fs.promises.mkdir(path.dirname(skillsPath), { recursive: true });
|
|
78
|
-
const section = `\n### ${now} (${String(context?.sessionId || '').slice(0, 8)})\n${lines.join('\n')}\n`;
|
|
79
|
-
await fs.promises.writeFile(skillsPath, existing + section, 'utf8');
|
|
80
|
-
learnedSkillsWritten = patterns.length;
|
|
81
|
-
if (logger.info) logger.info(`[miranda] wrote ${patterns.length} skill(s)`);
|
|
82
|
-
}
|
|
83
|
-
} catch (err) {
|
|
84
|
-
if (logger.info) logger.info(`[miranda] skill write skip: ${err.message}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { emotionalStateWritten, recapFilesWritten, learnedSkillsWritten };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
module.exports = { writeWorkspaceFiles, extractFilePaths };
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
-- DROP-clean script for entity_state_history (Q3 bitter-lesson escape hatch).
|
|
2
|
-
--
|
|
3
|
-
-- Run this if you decide native long-context / agentic memory has obviated the
|
|
4
|
-
-- temporal state-change layer. Removes the table and all dependent indexes;
|
|
5
|
-
-- nothing else in Aquifer references it directly (FK is one-way: this table
|
|
6
|
-
-- references entities/sessions, not the reverse).
|
|
7
|
-
--
|
|
8
|
-
-- Usage:
|
|
9
|
-
-- psql $DATABASE_URL -v schema=miranda -f scripts/drop-entity-state-history.sql
|
|
10
|
-
|
|
11
|
-
DROP TABLE IF EXISTS :"schema".entity_state_history CASCADE;
|
|
12
|
-
|
|
13
|
-
-- Verify nothing remains.
|
|
14
|
-
SELECT to_regclass(:'schema' || '.entity_state_history') AS table_after_drop;
|
|
15
|
-
SELECT to_regclass(:'schema' || '.idx_entity_state_history_current') AS idx_current_after_drop;
|
|
16
|
-
SELECT to_regclass(:'schema' || '.idx_entity_state_history_idempotency') AS idx_idempotency_after_drop;
|
|
17
|
-
-- All three should report NULL.
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
-- DROP-clean script for insights (Q4 bitter-lesson escape hatch).
|
|
2
|
-
--
|
|
3
|
-
-- Removes the table and all dependent indexes. Nothing else in Aquifer
|
|
4
|
-
-- references it directly, so DROP CASCADE is safe and complete.
|
|
5
|
-
|
|
6
|
-
DROP TABLE IF EXISTS :"schema".insights CASCADE;
|
|
7
|
-
|
|
8
|
-
-- Verify nothing remains.
|
|
9
|
-
SELECT to_regclass(:'schema' || '.insights') AS table_after_drop;
|
|
10
|
-
SELECT to_regclass(:'schema' || '.idx_insights_active') AS idx_active_after_drop;
|
|
11
|
-
SELECT to_regclass(:'schema' || '.idx_insights_embedding') AS idx_embedding_after_drop;
|
|
12
|
-
-- All three should report NULL.
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Aquifer — Install drop-in OpenClaw extension
|
|
3
|
-
#
|
|
4
|
-
# Usage:
|
|
5
|
-
# bash scripts/install-openclaw.sh [OPENCLAW_HOME]
|
|
6
|
-
#
|
|
7
|
-
# Default OPENCLAW_HOME: $HOME/.openclaw
|
|
8
|
-
#
|
|
9
|
-
# What it does:
|
|
10
|
-
# 1. Creates / overwrites $OPENCLAW_HOME/extensions/aquifer-memory/
|
|
11
|
-
# as a symlink to <this_package>/consumers/openclaw-ext/
|
|
12
|
-
# 2. Prints follow-up instructions: set the .env keys, restart the gateway.
|
|
13
|
-
#
|
|
14
|
-
# Idempotent; safe to re-run.
|
|
15
|
-
|
|
16
|
-
set -euo pipefail
|
|
17
|
-
|
|
18
|
-
OPENCLAW_HOME="${1:-${OPENCLAW_HOME:-$HOME/.openclaw}}"
|
|
19
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
-
PKG_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
21
|
-
EXT_SRC="$PKG_ROOT/consumers/openclaw-ext"
|
|
22
|
-
EXT_DEST="$OPENCLAW_HOME/extensions/aquifer-memory"
|
|
23
|
-
|
|
24
|
-
if [[ ! -d "$EXT_SRC" ]]; then
|
|
25
|
-
echo "error: $EXT_SRC not found (expected inside the Aquifer package)" >&2
|
|
26
|
-
exit 1
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
if [[ ! -d "$OPENCLAW_HOME" ]]; then
|
|
30
|
-
echo "error: OPENCLAW_HOME=$OPENCLAW_HOME not found" >&2
|
|
31
|
-
exit 1
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
mkdir -p "$OPENCLAW_HOME/extensions"
|
|
35
|
-
|
|
36
|
-
if [[ -L "$EXT_DEST" || -e "$EXT_DEST" ]]; then
|
|
37
|
-
echo "note: $EXT_DEST already exists — replacing"
|
|
38
|
-
rm -rf "$EXT_DEST"
|
|
39
|
-
fi
|
|
40
|
-
|
|
41
|
-
ln -s "$EXT_SRC" "$EXT_DEST"
|
|
42
|
-
echo "ok: linked $EXT_DEST → $EXT_SRC"
|
|
43
|
-
|
|
44
|
-
cat <<'EOF'
|
|
45
|
-
|
|
46
|
-
Next steps:
|
|
47
|
-
1. Edit $OPENCLAW_HOME/.env and set:
|
|
48
|
-
DATABASE_URL=postgresql://user:pass@host:5432/db
|
|
49
|
-
EMBED_PROVIDER=ollama # or openai
|
|
50
|
-
AQUIFER_LLM_PROVIDER=minimax # or openai / openrouter / opencode
|
|
51
|
-
MINIMAX_API_KEY=... # (or the key for your chosen provider)
|
|
52
|
-
# Optional:
|
|
53
|
-
AQUIFER_SCHEMA=my_namespace
|
|
54
|
-
AQUIFER_PERSONA=/path/to/host-local/persona-module
|
|
55
|
-
2. Restart OpenClaw:
|
|
56
|
-
systemctl --user restart openclaw-gateway
|
|
57
|
-
3. Verify:
|
|
58
|
-
journalctl --user -u openclaw-gateway -f | grep aquifer-memory
|
|
59
|
-
EOF
|