@shadowforge0/aquifer-memory 1.0.3 → 1.2.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.
Files changed (45) hide show
  1. package/README.md +29 -20
  2. package/consumers/claude-code.js +117 -0
  3. package/consumers/cli.js +17 -0
  4. package/consumers/default/daily-entries.js +196 -0
  5. package/consumers/default/index.js +282 -0
  6. package/consumers/default/prompts/summary.js +153 -0
  7. package/consumers/mcp.js +3 -23
  8. package/consumers/miranda/context-inject.js +119 -0
  9. package/consumers/miranda/daily-entries.js +224 -0
  10. package/consumers/miranda/index.js +353 -0
  11. package/consumers/miranda/instance.js +55 -0
  12. package/consumers/miranda/llm.js +99 -0
  13. package/consumers/miranda/prompts/summary.js +303 -0
  14. package/consumers/miranda/recall-format.js +74 -0
  15. package/consumers/miranda/workspace-files.js +91 -0
  16. package/consumers/openclaw-ext/index.js +38 -0
  17. package/consumers/openclaw-ext/openclaw.plugin.json +9 -0
  18. package/consumers/openclaw-ext/package.json +10 -0
  19. package/consumers/openclaw-plugin.js +66 -74
  20. package/consumers/opencode.js +21 -24
  21. package/consumers/shared/autodetect.js +64 -0
  22. package/consumers/shared/entity-parser.js +119 -0
  23. package/consumers/shared/ingest.js +148 -0
  24. package/consumers/shared/llm-autodetect.js +137 -0
  25. package/consumers/shared/normalize.js +129 -0
  26. package/consumers/shared/recall-format.js +110 -0
  27. package/core/aquifer.js +180 -71
  28. package/core/entity.js +1 -3
  29. package/core/storage.js +86 -28
  30. package/docs/postprocess-contract.md +132 -0
  31. package/index.js +9 -1
  32. package/package.json +23 -2
  33. package/pipeline/_http.js +1 -1
  34. package/pipeline/consolidation/apply.js +176 -0
  35. package/pipeline/consolidation/index.js +21 -0
  36. package/pipeline/extract-entities.js +2 -2
  37. package/pipeline/rerank.js +1 -1
  38. package/pipeline/summarize.js +4 -1
  39. package/schema/001-base.sql +61 -24
  40. package/schema/002-entities.sql +17 -3
  41. package/schema/004-facts.sql +67 -0
  42. package/scripts/diagnose-fts-zh.js +168 -134
  43. package/scripts/diagnose-vector.js +188 -0
  44. package/scripts/install-openclaw.sh +59 -0
  45. package/scripts/smoke.mjs +2 -2
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ // Aquifer v1.2.0: LLM provider autodetect from env for install-and-go.
4
+ //
5
+ // Precedence:
6
+ // 1. config.llm.fn (explicit function — host supplies)
7
+ // 2. AQUIFER_LLM_PROVIDER env + provider-specific api key + optional model
8
+ //
9
+ // We do NOT silently pick a provider from multiple keys (ambiguous). Hosts
10
+ // must opt in by setting AQUIFER_LLM_PROVIDER explicitly when they want env
11
+ // autowiring.
12
+ //
13
+ // Two response shapes in flight:
14
+ // - Anthropic-shape: { content: [{ type:'text', text:'...' }] }
15
+ // Used by: minimax, opencode
16
+ // - OpenAI-shape: { choices:[{ message:{ content:'...' } }] }
17
+ // Used by: openai, openrouter
18
+
19
+ const { createLlmFn } = require('./llm');
20
+
21
+ const ANTHROPIC_PROVIDERS = {
22
+ minimax: {
23
+ envKey: 'MINIMAX_API_KEY',
24
+ baseUrl: 'https://api.minimax.io/anthropic/v1/messages',
25
+ defaultModel: 'MiniMax-M2.7',
26
+ extraHeaders: { 'anthropic-version': '2023-06-01' },
27
+ },
28
+ opencode: {
29
+ envKey: 'OPENCODE_API_KEY',
30
+ baseUrl: 'https://opencode.ai/zen/go/v1/messages',
31
+ defaultModel: 'minimax-m2.5',
32
+ extraHeaders: { 'anthropic-version': '2023-06-01' },
33
+ },
34
+ };
35
+
36
+ const OPENAI_PROVIDERS = {
37
+ openai: {
38
+ envKey: 'OPENAI_API_KEY',
39
+ baseUrl: 'https://api.openai.com/v1',
40
+ defaultModel: 'gpt-4o-mini',
41
+ },
42
+ openrouter: {
43
+ envKey: 'OPENROUTER_API_KEY',
44
+ baseUrl: 'https://openrouter.ai/api/v1',
45
+ defaultModel: 'openai/gpt-4o-mini',
46
+ },
47
+ };
48
+
49
+ function createAnthropicShapeFn({ baseUrl, apiKey, model, extraHeaders, timeoutMs, maxTokens }) {
50
+ const timeout = timeoutMs || 120000;
51
+ const mt = maxTokens || 4096;
52
+ return async function llmFn(prompt) {
53
+ const controller = new AbortController();
54
+ const timer = setTimeout(() => controller.abort(), timeout);
55
+ try {
56
+ const res = await fetch(baseUrl, {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ 'Authorization': `Bearer ${apiKey}`,
61
+ ...(extraHeaders || {}),
62
+ },
63
+ body: JSON.stringify({
64
+ model,
65
+ messages: [{ role: 'user', content: prompt }],
66
+ max_tokens: mt,
67
+ }),
68
+ signal: controller.signal,
69
+ });
70
+ if (!res.ok) {
71
+ const body = await res.text().catch(() => '');
72
+ const err = new Error(`LLM ${res.status}: ${body.slice(0, 200).replace(/[\n\r]/g, ' ')}`);
73
+ err.statusCode = res.status;
74
+ throw err;
75
+ }
76
+ const data = await res.json();
77
+ let raw = '';
78
+ if (data.content && Array.isArray(data.content)) {
79
+ raw = data.content.map((c) => c.text || '').join('');
80
+ } else if (data.choices && Array.isArray(data.choices)) {
81
+ raw = data.choices[0]?.message?.content || '';
82
+ }
83
+ return raw.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
84
+ } finally {
85
+ clearTimeout(timer);
86
+ }
87
+ };
88
+ }
89
+
90
+ function resolveLlmFn(llmConfig, env) {
91
+ if (llmConfig && typeof llmConfig.fn === 'function') {
92
+ return llmConfig.fn;
93
+ }
94
+ const provider = env.AQUIFER_LLM_PROVIDER;
95
+ if (!provider) return null;
96
+
97
+ const model = env.AQUIFER_LLM_MODEL || null;
98
+ const timeoutMs = env.AQUIFER_LLM_TIMEOUT ? Number(env.AQUIFER_LLM_TIMEOUT) : undefined;
99
+
100
+ if (ANTHROPIC_PROVIDERS[provider]) {
101
+ const p = ANTHROPIC_PROVIDERS[provider];
102
+ const apiKey = env[p.envKey];
103
+ if (!apiKey) {
104
+ throw new Error(`AQUIFER_LLM_PROVIDER=${provider} requires ${p.envKey}`);
105
+ }
106
+ return createAnthropicShapeFn({
107
+ baseUrl: p.baseUrl,
108
+ apiKey,
109
+ model: model || p.defaultModel,
110
+ extraHeaders: p.extraHeaders,
111
+ timeoutMs,
112
+ });
113
+ }
114
+
115
+ if (OPENAI_PROVIDERS[provider]) {
116
+ const p = OPENAI_PROVIDERS[provider];
117
+ const apiKey = env[p.envKey];
118
+ if (!apiKey) {
119
+ throw new Error(`AQUIFER_LLM_PROVIDER=${provider} requires ${p.envKey}`);
120
+ }
121
+ return createLlmFn({
122
+ baseUrl: p.baseUrl,
123
+ model: model || p.defaultModel,
124
+ apiKey,
125
+ timeoutMs,
126
+ });
127
+ }
128
+
129
+ throw new Error(
130
+ `AQUIFER_LLM_PROVIDER=${provider} not supported. Valid: ${[
131
+ ...Object.keys(ANTHROPIC_PROVIDERS),
132
+ ...Object.keys(OPENAI_PROVIDERS),
133
+ ].join(', ')}`
134
+ );
135
+ }
136
+
137
+ module.exports = { resolveLlmFn };
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Shared normalize — turns raw host entries into commit-ready messages plus
5
+ // session-level metadata. Wraps pipeline/normalize so consumers don't each
6
+ // reinvent their own role/content extraction.
7
+ //
8
+ // Supported adapters: 'gateway' | 'cc' (alias of 'claude-code'). The OpenCode
9
+ // consumer reads from SQLite and constructs the output shape directly; it is
10
+ // not expected to route through here.
11
+ //
12
+ // Output shape is the one commit() + enrich() expect:
13
+ // { messages:[{role,content,timestamp}], userCount, assistantCount,
14
+ // model, tokensIn, tokensOut, startedAt, lastMessageAt,
15
+ // skipStats, boundaries, toolsUsed }
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const { normalizeSession } = require('../../pipeline/normalize');
19
+
20
+ const ADAPTER_ALIASES = {
21
+ 'cc': 'claude-code',
22
+ 'claude-code': 'claude-code',
23
+ 'gateway': 'gateway',
24
+ };
25
+
26
+ function resolveAdapter(adapter) {
27
+ if (!adapter) return null; // auto-detect
28
+ const name = ADAPTER_ALIASES[adapter];
29
+ if (!name) {
30
+ throw new Error(`Unknown adapter: "${adapter}". Supported: gateway, cc (alias claude-code).`);
31
+ }
32
+ return name;
33
+ }
34
+
35
+ function extractRawMeta(rawEntries) {
36
+ let model = null;
37
+ let tokensIn = 0;
38
+ let tokensOut = 0;
39
+
40
+ for (const entry of rawEntries || []) {
41
+ if (!entry || typeof entry !== 'object') continue;
42
+ const msg = entry.message || entry;
43
+ if (msg && typeof msg === 'object') {
44
+ if (msg.model && !model) model = msg.model;
45
+ if (msg.usage) {
46
+ tokensIn += msg.usage.input_tokens || msg.usage.input || 0;
47
+ tokensOut += msg.usage.output_tokens || msg.usage.output || 0;
48
+ }
49
+ }
50
+ }
51
+
52
+ return { model, tokensIn, tokensOut };
53
+ }
54
+
55
+ /**
56
+ * Normalize raw host entries to Aquifer-commit shape + session metadata.
57
+ *
58
+ * @param {any[]} rawEntries
59
+ * @param {object} [opts]
60
+ * @param {'gateway'|'cc'|'claude-code'} [opts.adapter] — host adapter; auto-detected if omitted
61
+ * @returns {{
62
+ * messages: {role:string,content:string,timestamp:string|null}[],
63
+ * userCount: number, assistantCount: number,
64
+ * model: string|null, tokensIn: number, tokensOut: number,
65
+ * startedAt: string|null, lastMessageAt: string|null,
66
+ * skipStats: object, boundaries: object[], toolsUsed: string[]
67
+ * }}
68
+ */
69
+ function normalizeMessages(rawEntries, opts = {}) {
70
+ const safeEntries = Array.isArray(rawEntries) ? rawEntries : [];
71
+
72
+ if (safeEntries.length === 0) {
73
+ return {
74
+ messages: [],
75
+ userCount: 0,
76
+ assistantCount: 0,
77
+ model: null,
78
+ tokensIn: 0,
79
+ tokensOut: 0,
80
+ startedAt: null,
81
+ lastMessageAt: null,
82
+ skipStats: { total: 0, nonMessage: 0, noRole: 0, meta: 0, caveat: 0,
83
+ empty: 0, toolOnly: 0, narration: 0, toolResult: 0, routine: 0, command: 0 },
84
+ boundaries: [],
85
+ toolsUsed: [],
86
+ };
87
+ }
88
+
89
+ const client = resolveAdapter(opts.adapter);
90
+ const { normalized, skipStats, boundaries, toolsUsed } = normalizeSession(
91
+ safeEntries,
92
+ client ? { client } : {},
93
+ );
94
+
95
+ const messages = normalized.map(m => ({
96
+ role: m.role,
97
+ content: m.text || '',
98
+ timestamp: m.timestamp || null,
99
+ }));
100
+
101
+ let userCount = 0, assistantCount = 0;
102
+ let startedAt = null, lastMessageAt = null;
103
+ for (const m of messages) {
104
+ if (m.role === 'user') userCount++;
105
+ else if (m.role === 'assistant') assistantCount++;
106
+ if (m.timestamp) {
107
+ if (!startedAt) startedAt = m.timestamp;
108
+ lastMessageAt = m.timestamp;
109
+ }
110
+ }
111
+
112
+ const { model, tokensIn, tokensOut } = extractRawMeta(safeEntries);
113
+
114
+ return {
115
+ messages,
116
+ userCount,
117
+ assistantCount,
118
+ model,
119
+ tokensIn,
120
+ tokensOut,
121
+ startedAt,
122
+ lastMessageAt,
123
+ skipStats,
124
+ boundaries,
125
+ toolsUsed,
126
+ };
127
+ }
128
+
129
+ module.exports = { normalizeMessages, extractRawMeta };
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Shared recall formatter — turns aquifer.recall() rows into human-readable
5
+ // text. The default is English and markdown-ish; consumers with a persona
6
+ // (Miranda: zh-TW narrative) can override individual renderers.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ function truncate(s, n) {
10
+ if (!s) return '';
11
+ const str = String(s);
12
+ return str.length > n ? `${str.slice(0, n)}...` : str;
13
+ }
14
+
15
+ function formatDateIso(value) {
16
+ if (!value) return 'unknown';
17
+ const d = new Date(value);
18
+ return Number.isNaN(d.getTime()) ? 'unknown' : d.toISOString().slice(0, 10);
19
+ }
20
+
21
+ // Default English renderers --------------------------------------------------
22
+
23
+ const defaultRenderers = {
24
+ header({ results, query }) {
25
+ if (!query) return null;
26
+ return `Found ${results.length} result(s) for "${query}":`;
27
+ },
28
+ empty({ query }) {
29
+ return query ? `No results found for "${query}".` : 'No matching sessions found.';
30
+ },
31
+ title(result, index) {
32
+ const ss = result.structuredSummary || {};
33
+ const title = ss.title || truncate(result.summaryText, 60) || '(untitled)';
34
+ const date = formatDateIso(result.startedAt);
35
+ const agent = result.agentId || 'default';
36
+ return `### ${index + 1}. ${title} (${date}, ${agent})`;
37
+ },
38
+ body(result) {
39
+ const ss = result.structuredSummary || {};
40
+ const text = ss.overview || result.summaryText || '';
41
+ return text ? truncate(text, 300) : null;
42
+ },
43
+ matched(result) {
44
+ return result.matchedTurnText ? `Matched turn: ${truncate(result.matchedTurnText, 200)}` : null;
45
+ },
46
+ score(result, { showScore }) {
47
+ if (!showScore) return null;
48
+ return `Score: ${typeof result.score === 'number' ? result.score.toFixed(3) : '?'}`;
49
+ },
50
+ separator() {
51
+ return '';
52
+ },
53
+ };
54
+
55
+ /**
56
+ * Create a formatter with optional per-renderer overrides.
57
+ *
58
+ * @param {object} [overrides] — renderers to override: header/empty/title/body/matched/score/separator
59
+ * @returns {(results: any[], opts?: object) => string}
60
+ */
61
+ function createRecallFormatter(overrides = {}) {
62
+ const r = { ...defaultRenderers, ...overrides };
63
+
64
+ return function format(results, opts = {}) {
65
+ const safeResults = Array.isArray(results) ? results : [];
66
+ const ctx = { query: opts.query || null, results: safeResults };
67
+
68
+ if (safeResults.length === 0) {
69
+ return r.empty(ctx);
70
+ }
71
+
72
+ const lines = [];
73
+ const header = r.header(ctx);
74
+ if (header) { lines.push(header); lines.push(''); }
75
+
76
+ for (let i = 0; i < safeResults.length; i++) {
77
+ const res = safeResults[i];
78
+ const title = r.title(res, i, ctx);
79
+ if (title) lines.push(title);
80
+ const body = r.body(res, i, ctx);
81
+ if (body) lines.push(body);
82
+ const matched = r.matched(res, i, ctx);
83
+ if (matched) lines.push(matched);
84
+ const score = r.score(res, { showScore: !!opts.showScore, ...ctx });
85
+ if (score) lines.push(score);
86
+ const sep = r.separator(i, ctx);
87
+ if (sep !== null && sep !== undefined) lines.push(sep);
88
+ }
89
+
90
+ // Trim trailing empty separator
91
+ while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
92
+
93
+ return lines.join('\n');
94
+ };
95
+ }
96
+
97
+ // Pre-built default English formatter
98
+ const defaultFormatter = createRecallFormatter();
99
+
100
+ function formatRecallResults(results, opts = {}) {
101
+ return defaultFormatter(results, opts);
102
+ }
103
+
104
+ module.exports = {
105
+ createRecallFormatter,
106
+ formatRecallResults,
107
+ truncate,
108
+ formatDateIso,
109
+ defaultRenderers,
110
+ };