@shadowforge0/aquifer-memory 0.7.0 → 0.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.
package/README.md CHANGED
@@ -32,7 +32,7 @@ Sessions, summaries, turn-level embeddings, entity graph — all live in one dat
32
32
  | **Ranking** | 3-way RRF: FTS + session embedding + turn embedding | Single vector similarity |
33
33
  | **Knowledge graph** | Built-in entity extraction & co-occurrence | Usually separate system |
34
34
  | **Multi-tenant** | `tenant_id` on every table, day-1 | Often an afterthought |
35
- | **Dependencies** | Just `pg` | Multiple SDKs |
35
+ | **Dependencies** | `pg` + MCP SDK | Multiple SDKs |
36
36
 
37
37
  ### Before and after
38
38
 
@@ -48,97 +48,177 @@ Sessions, summaries, turn-level embeddings, entity graph — all live in one dat
48
48
 
49
49
  ---
50
50
 
51
- ## Quick Start
51
+ ## Requirements
52
52
 
53
- ### Prerequisites
53
+ | Component | Required? | Purpose | Example |
54
+ |-----------|-----------|---------|---------|
55
+ | Node.js >= 18 | Yes | Runtime | — |
56
+ | PostgreSQL 15+ | Yes | Storage for sessions, summaries, entities | Local, Docker, or managed |
57
+ | pgvector extension | Yes | Vector similarity search | `CREATE EXTENSION vector;` (included in `pgvector/pgvector` Docker image) |
58
+ | Embedding endpoint | Yes (for recall) | Turn + session embedding | Ollama `bge-m3`, OpenAI `text-embedding-3-small`, any OpenAI-compatible API |
59
+ | LLM endpoint | Optional | Built-in summarization during `enrich` | Ollama, OpenRouter, OpenAI — or provide your own `summaryFn` |
60
+ | `@modelcontextprotocol/sdk` + `zod` | Yes (for MCP server) | MCP protocol runtime | Included in dependencies — installed automatically |
54
61
 
55
- - Node.js >= 18
56
- - PostgreSQL 15+ with [pgvector](https://github.com/pgvector/pgvector) extension
57
- - An embedding API (OpenAI, Ollama, or any OpenAI-compatible endpoint)
62
+ ---
63
+
64
+ ## Quick Start (MCP Server)
65
+
66
+ This gets you from zero to a working MCP memory server. For library API usage, see [API Reference](#api-reference) below.
58
67
 
59
- ### Install
68
+ ### 1. Start the stack
69
+
70
+ ```bash
71
+ docker compose up -d
72
+ # Starts PostgreSQL 16 + pgvector and Ollama with bge-m3 (auto-pulled).
73
+ # First run takes a few minutes while Ollama downloads the model.
74
+ ```
75
+
76
+ Already have PostgreSQL + pgvector and an embedding endpoint? Skip this step.
77
+
78
+ ### 2. Install
60
79
 
61
80
  ```bash
62
81
  npm install @shadowforge0/aquifer-memory
63
82
  ```
64
83
 
65
- ### Initialize
84
+ ### 3. Configure + verify
66
85
 
67
- ```javascript
68
- const { createAquifer } = require('@shadowforge0/aquifer-memory');
86
+ ```bash
87
+ export DATABASE_URL="postgresql://aquifer:aquifer@localhost:5432/aquifer"
88
+ export AQUIFER_EMBED_BASE_URL="http://localhost:11434/v1"
89
+ export AQUIFER_EMBED_MODEL="bge-m3"
69
90
 
70
- const aquifer = createAquifer({
71
- db: 'postgresql://user:pass@localhost:5432/mydb', // connection string or pg.Pool
72
- schema: 'memory', // PG schema name (default: 'aquifer')
73
- tenantId: 'default', // multi-tenant isolation
74
- embed: {
75
- fn: async (texts) => embeddings, // your embedding function
76
- dim: 1024, // optional dimension hint
77
- },
78
- llm: {
79
- fn: async (prompt) => text, // your LLM function (for built-in summarize)
80
- },
81
- entities: {
82
- enabled: true,
83
- scope: 'my-app', // entity namespace (default: 'default')
84
- },
85
- });
91
+ npx aquifer quickstart
92
+ ```
93
+
94
+ `quickstart` runs migrations, commits a test session, embeds it, recalls it, and cleans up. If it prints `✓ Aquifer is working`, you're done.
86
95
 
87
- // Run migrations (safe to call multiple times)
88
- await aquifer.migrate();
96
+ ### 4. Start the MCP server
97
+
98
+ ```bash
99
+ npx aquifer mcp
89
100
  ```
90
101
 
91
- ### Write path: commit + enrich
102
+ See [.env.example](.env.example) for all env vars, or [docs/setup.md](docs/setup.md) for the full setup guide.
92
103
 
93
- ```javascript
94
- // 1. Store the session
95
- await aquifer.commit('conv-001', [
96
- { role: 'user', content: 'Let me tell you about our new auth approach...' },
97
- { role: 'assistant', content: 'Got it. So the plan is...' },
98
- ], { agentId: 'main' });
99
-
100
- // 2. Enrich: summarize + embed turns + extract entities
101
- const result = await aquifer.enrich('conv-001', {
102
- agentId: 'main',
103
- // Optional: bring your own summarize pipeline
104
- summaryFn: async (msgs) => ({ summaryText, structuredSummary, entityRaw }),
105
- entityParseFn: (text) => [{ name, normalizedName, type, aliases }],
106
- // Optional: post-commit hook for downstream processing
107
- postProcess: async (ctx) => {
108
- // ctx contains session, summary, embedding, parsedEntities, etc.
109
- },
110
- });
104
+ ---
105
+
106
+ ## Environment Variables
107
+
108
+ | Variable | Required? | Purpose | Example |
109
+ |----------|-----------|---------|---------|
110
+ | `DATABASE_URL` | Yes | PostgreSQL connection string | `postgresql://user:pass@localhost:5432/mydb` |
111
+ | `AQUIFER_SCHEMA` | No | PG schema name (default: `aquifer`) | `memory` |
112
+ | `AQUIFER_TENANT_ID` | No | Multi-tenant key (default: `default`) | `my-app` |
113
+ | `AQUIFER_EMBED_BASE_URL` | Yes (for recall) | Embedding API base URL | `http://localhost:11434/v1` |
114
+ | `AQUIFER_EMBED_MODEL` | Yes (for recall) | Embedding model name | `bge-m3` |
115
+ | `AQUIFER_EMBED_API_KEY` | Provider-dependent | API key for hosted embedding providers | `sk-...` |
116
+ | `AQUIFER_EMBED_DIM` | No | Embedding dimension override (auto-detected) | `1024` |
117
+ | `AQUIFER_LLM_BASE_URL` | No | LLM API base URL (for built-in summarization) | `http://localhost:11434/v1` |
118
+ | `AQUIFER_LLM_MODEL` | No | LLM model name | `llama3.1` |
119
+ | `AQUIFER_LLM_API_KEY` | Provider-dependent | API key for hosted LLM providers | `sk-...` |
120
+ | `AQUIFER_ENTITIES_ENABLED` | No | Enable knowledge graph (default: `false`) | `true` |
121
+ | `AQUIFER_ENTITY_SCOPE` | No | Entity namespace (default: `default`) | `my-app` |
122
+ | `AQUIFER_RERANK_ENABLED` | No | Enable cross-encoder reranking | `true` |
123
+ | `AQUIFER_RERANK_PROVIDER` | No | Reranker provider: `tei`, `jina`, `openrouter` | `tei` |
124
+ | `AQUIFER_RERANK_BASE_URL` | No | Reranker endpoint | `http://localhost:8080` |
125
+ | `AQUIFER_AGENT_ID` | No | Default agent ID | `main` |
126
+
127
+ Full env-to-config mapping is in [consumers/shared/config.js](consumers/shared/config.js).
128
+
129
+ ---
130
+
131
+ ## Host Integration
132
+
133
+ MCP is the primary integration surface. Agent hosts connect to the Aquifer MCP server, which exposes four tools: `session_recall`, `session_feedback`, `memory_stats`, `memory_pending`.
134
+
135
+ | Integration | Route | Status | When to use |
136
+ |-------------|-------|--------|-------------|
137
+ | MCP server | `consumers/mcp.js` | Primary | Claude Code, OpenClaw, Codex, any MCP-capable host |
138
+ | Library API | `createAquifer()` | Primary | Backend apps, custom pipelines, direct Node.js usage |
139
+ | CLI | `consumers/cli.js` | Secondary | Operations, debugging, manual recall/backfill |
140
+ | OpenClaw plugin | `consumers/openclaw-plugin.js` | Compatibility only | Session capture via `before_reset` — not for tool delivery |
141
+
142
+ ### Claude Code
143
+
144
+ Add to your project's `.claude.json` or user-level MCP config:
145
+
146
+ ```json
147
+ {
148
+ "mcpServers": {
149
+ "aquifer": {
150
+ "type": "stdio",
151
+ "command": "node",
152
+ "args": ["/path/to/aquifer/consumers/mcp.js"],
153
+ "env": {
154
+ "DATABASE_URL": "postgresql://...",
155
+ "AQUIFER_EMBED_BASE_URL": "http://localhost:11434/v1",
156
+ "AQUIFER_EMBED_MODEL": "bge-m3"
157
+ }
158
+ }
159
+ }
160
+ }
111
161
  ```
112
162
 
113
- ### Read path: recall
163
+ Tools appear as `mcp__aquifer__session_recall`, `mcp__aquifer__session_feedback`, etc.
114
164
 
115
- ```javascript
116
- const results = await aquifer.recall('auth middleware decision', {
117
- agentId: 'main',
118
- limit: 5,
119
- entities: ['auth-middleware'], // optional: entity-aware search
120
- entityMode: 'all', // 'any' (boost) or 'all' (hard filter)
121
- });
122
- // Returns ranked sessions with scores, using 3-way RRF fusion
165
+ ### OpenClaw
166
+
167
+ Add to `openclaw.json` under `mcp.servers`:
168
+
169
+ ```json
170
+ {
171
+ "mcp": {
172
+ "servers": {
173
+ "aquifer": {
174
+ "command": "node",
175
+ "args": ["/path/to/aquifer/consumers/mcp.js"],
176
+ "env": {
177
+ "DATABASE_URL": "postgresql://...",
178
+ "AQUIFER_EMBED_BASE_URL": "http://localhost:11434/v1",
179
+ "AQUIFER_EMBED_MODEL": "bge-m3"
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
123
185
  ```
124
186
 
187
+ Tools materialize as `aquifer__session_recall`, `aquifer__session_feedback`, `aquifer__memory_stats`, `aquifer__memory_pending` (server name prefix added by the host).
188
+
189
+ The OpenClaw plugin (`consumers/openclaw-plugin.js`) is retained for session capture via `before_reset` but is **not** the recommended tool delivery path. Use MCP.
190
+
191
+ ### Other MCP-capable hosts
192
+
193
+ Any host that supports MCP stdio can connect the same way — point it at `node consumers/mcp.js` with the required env vars. The MCP server is the canonical external contract.
194
+
125
195
  ---
126
196
 
127
197
  ## Architecture
128
198
 
129
199
  ```
130
- ┌─────────────────────────────────────────────────────────────┐
131
- createAquifer (entry)
132
- Config · Migration · Ingest · Recall · Enrich
133
- └────────┬──────────┬──────────┬──────────┬───────────────────┘
200
+ ┌──────────────────────────────────────────────────────────────┐
201
+ Agent Hosts
202
+ Claude Code · OpenClaw · Codex · OpenCode · ...
203
+ └──────────────────────┬───────────────────────────────────────┘
204
+ │ MCP (stdio or HTTP)
205
+ ┌──────────────────────▼───────────────────────────────────────┐
206
+ │ Aquifer MCP Server (canonical API) │
207
+ │ session_recall · session_feedback · memory_stats · ... │
208
+ └──────────────────────┬───────────────────────────────────────┘
209
+
210
+ ┌──────────────────────▼───────────────────────────────────────┐
211
+ │ createAquifer (engine) │
212
+ │ Config · Migration · Ingest · Recall · Enrich │
213
+ └────────┬──────────┬──────────┬──────────┬────────────────────┘
134
214
  │ │ │ │
135
215
  ┌────▼───┐ ┌────▼────┐ ┌──▼───┐ ┌───▼──────────┐
136
216
  │storage │ │hybrid- │ │entity│ │ pipeline/ │
137
217
  │ .js │ │rank.js │ │ .js │ │summarize.js │
138
218
  └────────┘ └─────────┘ └──────┘ │embed.js │
139
219
  │ │ │extract-ent.js │
140
- ┌────▼───────────┐ ┌───▼──┐ └───────────────┘
141
- │ PostgreSQL │ │ LLM │
220
+ ┌────▼───────────┐ ┌───▼──┐ │rerank.js │
221
+ │ PostgreSQL │ │ LLM │ └───────────────┘
142
222
  │ + pgvector │ │ API │
143
223
  └────────────────┘ └──────┘
144
224
 
@@ -155,7 +235,7 @@ const results = await aquifer.recall('auth middleware decision', {
155
235
 
156
236
  | File | Purpose |
157
237
  |------|---------|
158
- | `index.js` | Entry point — exports `createAquifer`, `createEmbedder` |
238
+ | `index.js` | Entry point — exports `createAquifer`, `createEmbedder`, `createReranker`, `normalizeSession` |
159
239
  | `core/aquifer.js` | Main facade: `migrate()`, `ingest()`, `recall()`, `enrich()` |
160
240
  | `core/storage.js` | Session/summary/turn CRUD, FTS search, embedding search |
161
241
  | `core/entity.js` | Entity upsert, mention tracking, relation graph, normalization |
@@ -163,6 +243,8 @@ const results = await aquifer.recall('auth middleware decision', {
163
243
  | `pipeline/summarize.js` | LLM-powered session summarization with structured output |
164
244
  | `pipeline/embed.js` | Embedding client (any OpenAI-compatible API) |
165
245
  | `pipeline/extract-entities.js` | LLM-powered entity extraction (12 types) |
246
+ | `pipeline/rerank.js` | Cross-encoder reranking (TEI, Jina, OpenRouter) |
247
+ | `pipeline/normalize/` | Session normalization for Claude Code / gateway noise |
166
248
  | `schema/001-base.sql` | DDL: sessions, summaries, turn_embeddings, FTS indexes |
167
249
  | `schema/002-entities.sql` | DDL: entities, mentions, relations, entity_sessions |
168
250
  | `schema/003-trust-feedback.sql` | DDL: trust_score column, session_feedback audit trail |
@@ -361,7 +443,9 @@ Closes the PostgreSQL connection pool (only if Aquifer created it).
361
443
 
362
444
  ## Configuration
363
445
 
364
- Aquifer takes a `db` connection (string or `pg.Pool`), plus optional `embed` and `llm` functions:
446
+ Aquifer resolves config from three sources in priority order: config file → environment variables → programmatic overrides. See [consumers/shared/config.js](consumers/shared/config.js) for the full env-to-config mapping.
447
+
448
+ Config file is auto-discovered at `aquifer.config.json` in the working directory, or set `AQUIFER_CONFIG=/path/to/config.json`.
365
449
 
366
450
  ```javascript
367
451
  createAquifer({
@@ -395,22 +479,6 @@ createAquifer({
395
479
 
396
480
  Fallback chain: `config.entities.scope` → `'default'`.
397
481
 
398
- ### Consumers (CLI, MCP, OpenClaw plugin)
399
-
400
- For consumer-based setup using environment variables instead of code:
401
-
402
- ```bash
403
- export DATABASE_URL="postgresql://..."
404
- export AQUIFER_EMBED_BASE_URL="http://localhost:11434/v1"
405
- export AQUIFER_EMBED_MODEL="bge-m3"
406
- export AQUIFER_ENTITIES_ENABLED=true
407
-
408
- aquifer migrate
409
- aquifer recall "search query" --limit 5
410
- aquifer backfill --concurrency 3
411
- aquifer stats --json
412
- ```
413
-
414
482
  ---
415
483
 
416
484
  ## Database Schema
@@ -425,6 +493,8 @@ aquifer stats --json
425
493
 
426
494
  Key indexes: GIN on messages, GiST on `tsvector`, ivfflat on embeddings, B-tree on tenant/agent/timestamps.
427
495
 
496
+ Note: the schema uses basic ivfflat indexes suitable for development and moderate-scale use. For large deployments (100k+ embeddings), consider adding HNSW indexes — this is a future optimization area, not included out of the box.
497
+
428
498
  ### 002-entities.sql
429
499
 
430
500
  | Table | Purpose |
@@ -446,15 +516,29 @@ Also adds `trust_score` column to `session_summaries` (default 0.5, range 0–1)
446
516
 
447
517
  ---
448
518
 
519
+ ## Troubleshooting
520
+
521
+ **`error: type "vector" does not exist`** — pgvector extension is not installed. Run `CREATE EXTENSION IF NOT EXISTS vector;` as a superuser, or use the `pgvector/pgvector` Docker image which includes it.
522
+
523
+ **`aquifer mcp requires @modelcontextprotocol/sdk and zod`** — These are now regular dependencies and should be installed automatically. If you see this error, run `npm install` again to ensure all deps are present.
524
+
525
+ **Recall returns no results** — Make sure you've run `enrich` after `commit`. Raw sessions are not searchable until enriched (summarized + embedded). Check `aquifer stats` to see if summaries and turn embeddings exist.
526
+
527
+ **OpenClaw tools not visible** — Use `mcp.servers.aquifer` in `openclaw.json`, not the plugin. Tools appear as `aquifer__session_recall` etc. The plugin (`consumers/openclaw-plugin.js`) is for session capture only.
528
+
529
+ **Embedding provider connection refused** — Verify your `AQUIFER_EMBED_BASE_URL` is reachable. For local Ollama, make sure the server is running and the model is pulled (`ollama pull bge-m3`).
530
+
531
+ ---
532
+
449
533
  ## Dependencies
450
534
 
451
535
  | Package | Purpose |
452
536
  |---------|---------|
453
537
  | `pg` ≥ 8.13 | PostgreSQL client |
538
+ | `@modelcontextprotocol/sdk` ≥ 1.29 | MCP server protocol |
539
+ | `zod` ≥ 3.25 | Schema validation (MCP tools) |
454
540
 
455
- That's it. Aquifer has **one runtime dependency**.
456
-
457
- LLM and embedding calls use raw HTTP — no SDK required.
541
+ LLM and embedding calls use raw HTTP — no additional SDK required.
458
542
 
459
543
  ---
460
544
 
package/consumers/cli.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * Aquifer CLI
6
6
  *
7
7
  * Usage:
8
+ * aquifer quickstart Verify end-to-end setup
8
9
  * aquifer migrate Run database migrations
9
10
  * aquifer recall <query> [options] Search sessions
10
11
  * aquifer backfill [options] Enrich pending sessions
@@ -14,7 +15,6 @@
14
15
  */
15
16
 
16
17
  const { createAquiferFromConfig } = require('./shared/factory');
17
- const { loadConfig } = require('./shared/config');
18
18
 
19
19
  // ---------------------------------------------------------------------------
20
20
  // Argument parser (minimal, no deps)
@@ -120,30 +120,12 @@ async function cmdBackfill(aquifer, args) {
120
120
  const skipTurnEmbed = !!args.flags['skip-turn-embed'];
121
121
  const skipEntities = !!args.flags['skip-entities'];
122
122
 
123
- const config = aquifer._config || {};
124
- const schema = config.schema || 'aquifer';
125
- const tenantId = config.tenantId || 'default';
126
- const pool = aquifer._pool;
127
-
128
- if (!pool) {
129
- console.error('Backfill requires direct pool access.');
130
- process.exit(1);
131
- }
123
+ const pending = await aquifer.getPendingSessions({ limit });
132
124
 
133
- const qi = (id) => `"${id}"`;
134
- const { rows } = await pool.query(`
135
- SELECT session_id, agent_id, processing_status
136
- FROM ${qi(schema)}.sessions
137
- WHERE tenant_id = $1
138
- AND processing_status IN ('pending', 'failed')
139
- ORDER BY started_at DESC
140
- LIMIT $2
141
- `, [tenantId, limit]);
142
-
143
- console.log(`Found ${rows.length} sessions to backfill${dryRun ? ' (dry-run)' : ''}`);
125
+ console.log(`Found ${pending.length} sessions to backfill${dryRun ? ' (dry-run)' : ''}`);
144
126
 
145
127
  let enriched = 0, failed = 0;
146
- for (const row of rows) {
128
+ for (const row of pending) {
147
129
  if (dryRun) {
148
130
  console.log(` [dry-run] ${row.session_id} (${row.agent_id}) status=${row.processing_status}`);
149
131
  continue;
@@ -164,40 +146,12 @@ async function cmdBackfill(aquifer, args) {
164
146
  }
165
147
  }
166
148
 
167
- console.log(`\nDone. enriched=${enriched} failed=${failed} total=${rows.length}`);
149
+ console.log(`\nDone. enriched=${enriched} failed=${failed} total=${pending.length}`);
168
150
  if (failed > 0) process.exitCode = 2;
169
151
  }
170
152
 
171
153
  async function cmdStats(aquifer, args) {
172
- const config = aquifer._config || {};
173
- const schema = config.schema || 'aquifer';
174
- const tenantId = config.tenantId || 'default';
175
- const pool = aquifer._pool;
176
-
177
- if (!pool) {
178
- console.error('Stats requires direct pool access.');
179
- process.exit(1);
180
- }
181
-
182
- const qi = (id) => `"${id}"`;
183
- const [sessions, summaries, turns, entities] = await Promise.all([
184
- pool.query(`SELECT processing_status, COUNT(*)::int as count FROM ${qi(schema)}.sessions WHERE tenant_id = $1 GROUP BY processing_status`, [tenantId]),
185
- pool.query(`SELECT COUNT(*)::int as count FROM ${qi(schema)}.session_summaries WHERE tenant_id = $1`, [tenantId]),
186
- pool.query(`SELECT COUNT(*)::int as count FROM ${qi(schema)}.turn_embeddings WHERE tenant_id = $1`, [tenantId]),
187
- pool.query(`SELECT COUNT(*)::int as count FROM ${qi(schema)}.entities WHERE tenant_id = $1`, [tenantId]).catch(() => ({ rows: [{ count: 0 }] })),
188
- ]);
189
-
190
- const timeRange = await pool.query(`SELECT MIN(started_at) as earliest, MAX(started_at) as latest FROM ${qi(schema)}.sessions WHERE tenant_id = $1`, [tenantId]);
191
-
192
- const stats = {
193
- sessions: Object.fromEntries(sessions.rows.map(r => [r.processing_status, r.count])),
194
- sessionTotal: sessions.rows.reduce((s, r) => s + r.count, 0),
195
- summaries: summaries.rows[0]?.count || 0,
196
- turnEmbeddings: turns.rows[0]?.count || 0,
197
- entities: entities.rows[0]?.count || 0,
198
- earliest: timeRange.rows[0]?.earliest || null,
199
- latest: timeRange.rows[0]?.latest || null,
200
- };
154
+ const stats = await aquifer.getStats();
201
155
 
202
156
  if (args.flags.json) {
203
157
  console.log(JSON.stringify(stats, null, 2));
@@ -210,35 +164,73 @@ async function cmdStats(aquifer, args) {
210
164
  }
211
165
  }
212
166
 
213
- async function cmdExport(aquifer, args) {
214
- const config = aquifer._config || {};
215
- const schema = config.schema || 'aquifer';
216
- const tenantId = config.tenantId || 'default';
217
- const pool = aquifer._pool;
218
- const output = args.flags.output || null;
219
- const limit = parseInt(args.flags.limit || '1000', 10);
167
+ async function cmdQuickstart(aquifer) {
168
+ console.log('Aquifer quickstart verifying end-to-end setup.\n');
220
169
 
221
- if (!pool) {
222
- console.error('Export requires direct pool access.');
223
- process.exit(1);
224
- }
170
+ // 1. Migrate
171
+ console.log('1/5 Running migrations...');
172
+ await aquifer.migrate();
173
+ console.log(' OK\n');
174
+
175
+ // 2. Commit
176
+ const sessionId = `quickstart-${Date.now()}`;
177
+ console.log('2/5 Committing test session...');
178
+ await aquifer.commit(sessionId, [
179
+ { role: 'user', content: 'We decided to use PostgreSQL with pgvector for the AI memory store instead of a separate vector database.' },
180
+ { role: 'assistant', content: 'Good choice. PG gives us ACID transactions, full-text search, and vector similarity all in one place.' },
181
+ { role: 'user', content: 'The main advantage is turn-level embedding — we can find the exact moment a decision was made.' },
182
+ ], { agentId: 'quickstart', source: 'quickstart' });
183
+ console.log(' OK\n');
184
+
185
+ // 3. Enrich (skip summary — LLM may not be configured)
186
+ console.log('3/5 Enriching (turn embeddings)...');
187
+ const enrichResult = await aquifer.enrich(sessionId, {
188
+ agentId: 'quickstart',
189
+ skipSummary: true,
190
+ skipEntities: true,
191
+ });
192
+ console.log(` OK — ${enrichResult.turnsEmbedded} turns embedded\n`);
225
193
 
226
- const qi = (id) => `"${id}"`;
227
- const where = [`s.tenant_id = $1`];
228
- const params = [tenantId];
194
+ // 4. Recall
195
+ console.log('4/5 Recalling "PostgreSQL memory store"...');
196
+ const results = await aquifer.recall('PostgreSQL memory store', { limit: 3 });
197
+ if (results.length === 0) {
198
+ console.error(' FAIL — no results returned. Check your embedding config.');
199
+ process.exitCode = 1;
200
+ return;
201
+ }
202
+ console.log(` OK — ${results.length} result(s), top score: ${results[0].score?.toFixed(3)}`);
203
+ if (results[0].matchedTurnText) {
204
+ console.log(` Matched: "${results[0].matchedTurnText.slice(0, 100)}..."`);
205
+ }
206
+ console.log();
207
+
208
+ // 5. Cleanup
209
+ console.log('5/5 Cleaning up test data...');
210
+ const { Pool } = require('pg');
211
+ const { loadConfig } = require('./shared/config');
212
+ const config = loadConfig();
213
+ const pool = new Pool({ connectionString: config.db.url });
214
+ const schema = config.schema || 'aquifer';
215
+ await pool.query(`DELETE FROM ${schema}.turn_embeddings WHERE session_id IN (SELECT id FROM ${schema}.sessions WHERE session_id = $1)`, [sessionId]);
216
+ await pool.query(`DELETE FROM ${schema}.session_summaries WHERE session_id IN (SELECT id FROM ${schema}.sessions WHERE session_id = $1)`, [sessionId]);
217
+ await pool.query(`DELETE FROM ${schema}.sessions WHERE session_id = $1`, [sessionId]);
218
+ await pool.end();
219
+ console.log(' OK\n');
220
+
221
+ console.log('✓ Aquifer is working. You can now start the MCP server:');
222
+ console.log(' npx aquifer mcp');
223
+ }
229
224
 
230
- if (args.flags['agent-id']) { params.push(args.flags['agent-id']); where.push(`s.agent_id = $${params.length}`); }
231
- if (args.flags.source) { params.push(args.flags.source); where.push(`s.source = $${params.length}`); }
232
- params.push(limit);
225
+ async function cmdExport(aquifer, args) {
226
+ const output = args.flags.output || null;
227
+ const limit = parseInt(args.flags.limit || '1000', 10);
233
228
 
234
- const { rows } = await pool.query(`
235
- SELECT s.*, ss.summary_text, ss.structured_summary
236
- FROM ${qi(schema)}.sessions s
237
- LEFT JOIN ${qi(schema)}.session_summaries ss ON ss.session_row_id = s.id
238
- WHERE ${where.join(' AND ')}
239
- ORDER BY s.started_at DESC
240
- LIMIT $${params.length}
241
- `, params);
229
+ const rows = await aquifer.exportSessions({
230
+ agentId: args.flags['agent-id'],
231
+ source: args.flags.source,
232
+ limit,
233
+ });
242
234
 
243
235
  const stream = output ? require('fs').createWriteStream(output) : process.stdout;
244
236
  for (const row of rows) {
@@ -268,6 +260,7 @@ async function main() {
268
260
  console.log(`Usage: aquifer <command> [options]
269
261
 
270
262
  Commands:
263
+ quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
271
264
  migrate Run database migrations
272
265
  recall <query> Search sessions (requires embed config)
273
266
  feedback Record trust feedback on a session
@@ -317,6 +310,9 @@ Options:
317
310
 
318
311
  try {
319
312
  switch (command) {
313
+ case 'quickstart':
314
+ await cmdQuickstart(aquifer);
315
+ break;
320
316
  case 'migrate':
321
317
  await cmdMigrate(aquifer);
322
318
  break;
@@ -340,7 +336,7 @@ Options:
340
336
  process.exit(1);
341
337
  }
342
338
  } finally {
343
- if (aquifer._pool) await aquifer._pool.end();
339
+ await aquifer.close();
344
340
  }
345
341
  }
346
342