@steno-ai/mcp 0.1.4 → 0.1.5

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
@@ -1,24 +1,65 @@
1
1
  # @steno-ai/mcp
2
2
 
3
- MCP server that gives Claude persistent long-term memory. Works with Claude Desktop, Claude Code, Cursor, Windsurf, and any MCP-compatible client.
3
+ Persistent long-term memory for Claude. One command to set up. Works with Claude Desktop, Claude Code, Cursor, and any MCP client.
4
4
 
5
- ## Setup
5
+ ## Quick Start (2 minutes)
6
6
 
7
- ### 1. Get your keys
7
+ ### 1. Create a free Supabase project
8
8
 
9
- You need a [Supabase](https://supabase.com) project and an [OpenAI](https://platform.openai.com) API key. Optionally a [Perplexity](https://perplexity.ai) key for cheaper embeddings.
9
+ Go to [supabase.com](https://supabase.com), create a new project. Copy your:
10
+ - **Project URL** (looks like `https://abc123.supabase.co`)
11
+ - **Service Role Key** (in Settings > API > service_role key — NOT the anon key)
10
12
 
11
- ### 2. Run the Supabase migrations
13
+ ### 2. Get an OpenAI key
12
14
 
13
- Clone the repo and run the schema migrations against your Supabase project:
15
+ Go to [platform.openai.com/api-keys](https://platform.openai.com/api-keys), create a key.
16
+
17
+ ### 3. Run setup
14
18
 
15
19
  ```bash
16
- git clone https://github.com/SankrityaT/steno-ai.git
17
- cd steno-ai/packages/supabase-adapter/src/migrations
18
- # Run each .sql file (001-025) in order via Supabase SQL Editor or CLI
20
+ npx steno-mcp-init
19
21
  ```
20
22
 
21
- ### 3. Add to Claude Desktop
23
+ This will:
24
+ - Ask for your Supabase URL, Service Role Key, and OpenAI key
25
+ - Create all database tables automatically
26
+ - Write the Claude Desktop config for you
27
+
28
+ ### 4. Restart Claude Desktop
29
+
30
+ Quit (Cmd+Q) and reopen. Then:
31
+ - Go to **Settings > General** → set **"Tools already loaded"**
32
+ - Start chatting — Claude now has persistent memory
33
+
34
+ That's it. Your data stays in YOUR Supabase project. Nothing is shared.
35
+
36
+ ---
37
+
38
+ ## What you get
39
+
40
+ | Tool | What it does |
41
+ |------|-------------|
42
+ | `steno_remember` | Stores facts, preferences, decisions, people, events |
43
+ | `steno_recall` | Searches memory with 6-signal fusion (vector + keyword + graph + temporal + recency + salience) |
44
+ | `steno_flush` | Forces extraction of buffered session messages |
45
+ | `steno_feedback` | Rates whether a recalled memory was useful |
46
+ | `steno_stats` | Shows memory statistics |
47
+
48
+ ## How it works
49
+
50
+ **Storing memories:** Every message goes through LLM extraction → entity/edge creation → temporal grounding → contextual embedding → dedup → knowledge graph update.
51
+
52
+ **Recalling memories:** Every query runs through 6 parallel signals fused with configurable weights. Knowledge updates are tracked — newer facts supersede older ones.
53
+
54
+ **Features:**
55
+ - Knowledge graph with typed entities and relationships
56
+ - Temporal reasoning (eventDate + documentDate on every fact)
57
+ - Knowledge updates (newer facts automatically supersede older ones)
58
+ - Domain-scoped entity types (vehicle, startup, project — or define your own)
59
+ - Session buffering for cross-message context
60
+ - Source chunk preservation for full-context answers
61
+
62
+ ## Manual Setup (if you prefer)
22
63
 
23
64
  Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
24
65
 
@@ -32,56 +73,41 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
32
73
  "SUPABASE_URL": "https://YOUR-PROJECT.supabase.co",
33
74
  "SUPABASE_SERVICE_ROLE_KEY": "eyJ...",
34
75
  "OPENAI_API_KEY": "sk-...",
35
- "PERPLEXITY_API_KEY": "pplx-... (optional)"
76
+ "PERPLEXITY_API_KEY": "pplx-... (optional, for cheaper embeddings)"
36
77
  }
37
78
  }
38
79
  }
39
80
  }
40
81
  ```
41
82
 
42
- ### 4. Restart Claude Desktop
43
-
44
- The MCP server will connect automatically. Claude gets 5 memory tools:
45
-
46
- | Tool | Description |
47
- |------|-------------|
48
- | `steno_remember` | Store facts, preferences, decisions, people, events |
49
- | `steno_recall` | Search memory with 6-signal fusion retrieval |
50
- | `steno_flush` | Force extraction of buffered session messages |
51
- | `steno_feedback` | Rate whether a recalled memory was useful |
52
- | `steno_stats` | View memory statistics |
53
-
54
- ## How it works
55
-
56
- Every `steno_remember` call runs through the full extraction pipeline:
57
- - **LLM fact extraction** with temporal grounding (eventDate + documentDate)
58
- - **Knowledge graph** building (entities, typed edges, domain-scoped schemas)
59
- - **Dedup + knowledge updates** (newer facts supersede older ones)
60
- - **Contextual embeddings** (facts embedded with conversation context)
61
- - **Session buffering** (messages batched for cross-message context)
62
-
63
- Every `steno_recall` query uses **6-signal fusion**:
64
- - Vector similarity (0.30) — semantic search
65
- - Temporal proximity (0.20) — date-aware retrieval
66
- - Graph traversal (0.15) — entity relationships
67
- - Keyword/FTS (0.15) — exact term matching
68
- - Recency decay (0.10) — prefer recent memories
69
- - Salience (0.10) — importance × access frequency
70
-
71
- ## Claude Code
72
-
73
- Works the same way — add to your Claude Code MCP config or install as a plugin.
83
+ Then run the migrations manually — see [migrations folder](https://github.com/SankrityaT/steno-ai/tree/main/packages/supabase-adapter/src/migrations).
74
84
 
75
85
  ## Environment Variables
76
86
 
77
87
  | Variable | Required | Description |
78
88
  |----------|----------|-------------|
79
89
  | `SUPABASE_URL` | Yes | Your Supabase project URL |
80
- | `SUPABASE_SERVICE_ROLE_KEY` | Yes | Supabase service role key |
81
- | `OPENAI_API_KEY` | Yes | OpenAI API key (for LLM extraction) |
82
- | `PERPLEXITY_API_KEY` | No | Perplexity key for cheaper embeddings |
90
+ | `SUPABASE_SERVICE_ROLE_KEY` | Yes | Supabase service role key (not anon key) |
91
+ | `OPENAI_API_KEY` | Yes | For LLM extraction and embeddings |
92
+ | `PERPLEXITY_API_KEY` | No | Cheaper embeddings ($0.03/1M tokens vs $0.13) |
83
93
  | `STENO_SCOPE_ID` | No | Scope identifier (default: "default") |
84
94
 
95
+ ## For Developers
96
+
97
+ Use the engine directly in your app:
98
+
99
+ ```bash
100
+ npm install @steno-ai/engine @steno-ai/supabase-adapter @steno-ai/openai-adapter
101
+ ```
102
+
103
+ ```typescript
104
+ import { runExtractionPipeline, search } from '@steno-ai/engine';
105
+ import { SupabaseStorageAdapter } from '@steno-ai/supabase-adapter';
106
+ import { OpenAILLMAdapter } from '@steno-ai/openai-adapter';
107
+ ```
108
+
109
+ See [@steno-ai/engine](https://www.npmjs.com/package/@steno-ai/engine) for full API docs.
110
+
85
111
  ## Part of [Steno](https://github.com/SankrityaT/steno-ai)
86
112
 
87
113
  The memory layer for AI agents. 13 packages — engine, adapters, SDK, MCP server, and more.
package/dist/init.js ADDED
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+ #\!/usr/bin/env node
3
+
4
+ // packages/mcp-server/src/init.ts
5
+ import { createClient } from "@supabase/supabase-js";
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import * as os from "os";
9
+ import * as readline from "readline";
10
+ var rl = readline.createInterface({ input: process.stdin, output: process.stdout });
11
+ var ask = (q) => new Promise((r) => rl.question(q, r));
12
+ var MIGRATIONS = [
13
+ `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`,
14
+ `CREATE EXTENSION IF NOT EXISTS "vector";`,
15
+ `CREATE EXTENSION IF NOT EXISTS "pg_trgm";`,
16
+ // Tenants
17
+ `CREATE TABLE IF NOT EXISTS tenants (
18
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
19
+ name TEXT NOT NULL,
20
+ slug TEXT NOT NULL UNIQUE,
21
+ config JSONB NOT NULL DEFAULT '{}',
22
+ plan TEXT NOT NULL DEFAULT 'free',
23
+ token_limit_monthly INTEGER NOT NULL DEFAULT 1000000,
24
+ query_limit_monthly INTEGER NOT NULL DEFAULT 10000,
25
+ stripe_customer_id TEXT,
26
+ stripe_subscription_id TEXT,
27
+ active BOOLEAN NOT NULL DEFAULT true,
28
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
29
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
30
+ );`,
31
+ // API Keys
32
+ `CREATE TABLE IF NOT EXISTS api_keys (
33
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
34
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
35
+ key_hash TEXT NOT NULL,
36
+ key_prefix TEXT NOT NULL,
37
+ name TEXT NOT NULL DEFAULT 'Default',
38
+ scopes TEXT[] NOT NULL DEFAULT ARRAY['read','write'],
39
+ expires_at TIMESTAMPTZ,
40
+ last_used_at TIMESTAMPTZ,
41
+ active BOOLEAN NOT NULL DEFAULT true,
42
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
43
+ );`,
44
+ // Sessions
45
+ `CREATE TABLE IF NOT EXISTS sessions (
46
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
47
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
48
+ scope TEXT NOT NULL,
49
+ scope_id TEXT NOT NULL,
50
+ started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
51
+ ended_at TIMESTAMPTZ,
52
+ summary TEXT,
53
+ topics TEXT[] NOT NULL DEFAULT '{}',
54
+ message_count INTEGER NOT NULL DEFAULT 0,
55
+ fact_count INTEGER NOT NULL DEFAULT 0,
56
+ metadata JSONB NOT NULL DEFAULT '{}',
57
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
58
+ );`,
59
+ // Extractions
60
+ `CREATE TABLE IF NOT EXISTS extractions (
61
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
62
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
63
+ status TEXT NOT NULL DEFAULT 'queued',
64
+ input_type TEXT NOT NULL,
65
+ input_data TEXT,
66
+ input_hash TEXT NOT NULL,
67
+ input_size INTEGER,
68
+ scope TEXT NOT NULL,
69
+ scope_id TEXT NOT NULL,
70
+ session_id UUID,
71
+ tier_used TEXT,
72
+ llm_model TEXT,
73
+ facts_created INTEGER NOT NULL DEFAULT 0,
74
+ facts_updated INTEGER NOT NULL DEFAULT 0,
75
+ facts_invalidated INTEGER NOT NULL DEFAULT 0,
76
+ entities_created INTEGER NOT NULL DEFAULT 0,
77
+ edges_created INTEGER NOT NULL DEFAULT 0,
78
+ cost_tokens_input INTEGER NOT NULL DEFAULT 0,
79
+ cost_tokens_output INTEGER NOT NULL DEFAULT 0,
80
+ cost_usd NUMERIC NOT NULL DEFAULT 0,
81
+ duration_ms INTEGER,
82
+ error TEXT,
83
+ retry_count INTEGER NOT NULL DEFAULT 0,
84
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
85
+ completed_at TIMESTAMPTZ
86
+ );`,
87
+ // Facts
88
+ `CREATE TABLE IF NOT EXISTS facts (
89
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
90
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
91
+ scope TEXT NOT NULL CHECK (scope IN ('user','agent','session','hive')),
92
+ scope_id TEXT NOT NULL,
93
+ session_id UUID REFERENCES sessions(id) ON DELETE SET NULL,
94
+ content TEXT NOT NULL,
95
+ embedding VECTOR(2000),
96
+ embedding_model TEXT,
97
+ embedding_dim INTEGER,
98
+ search_vector TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
99
+ version INTEGER NOT NULL DEFAULT 1,
100
+ lineage_id UUID NOT NULL DEFAULT uuid_generate_v4(),
101
+ valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
102
+ valid_until TIMESTAMPTZ,
103
+ operation TEXT NOT NULL DEFAULT 'create' CHECK (operation IN ('create','update','invalidate')),
104
+ parent_id UUID REFERENCES facts(id) ON DELETE SET NULL,
105
+ importance NUMERIC(5,4) NOT NULL DEFAULT 0.5,
106
+ frequency INTEGER NOT NULL DEFAULT 1,
107
+ last_accessed TIMESTAMPTZ NOT NULL DEFAULT NOW(),
108
+ decay_score NUMERIC(8,6) NOT NULL DEFAULT 0.5,
109
+ contradiction_status TEXT NOT NULL DEFAULT 'none',
110
+ contradicts_id UUID REFERENCES facts(id) ON DELETE SET NULL,
111
+ source_type TEXT NOT NULL CHECK (source_type IN ('conversation','document','url','raw_text','api','agent_self')),
112
+ source_ref JSONB,
113
+ confidence NUMERIC(5,4) NOT NULL DEFAULT 0.8,
114
+ original_content TEXT,
115
+ extraction_id UUID,
116
+ extraction_tier TEXT,
117
+ modality TEXT NOT NULL DEFAULT 'text',
118
+ tags TEXT[] NOT NULL DEFAULT '{}',
119
+ metadata JSONB NOT NULL DEFAULT '{}',
120
+ event_date TIMESTAMPTZ,
121
+ document_date TIMESTAMPTZ,
122
+ source_chunk TEXT,
123
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
124
+ );`,
125
+ // Fact indexes
126
+ `CREATE INDEX IF NOT EXISTS idx_facts_tenant_scope ON facts(tenant_id, scope, scope_id);
127
+ CREATE INDEX IF NOT EXISTS idx_facts_lineage ON facts(tenant_id, lineage_id);
128
+ CREATE INDEX IF NOT EXISTS idx_facts_search_vector ON facts USING GIN(search_vector);
129
+ CREATE INDEX IF NOT EXISTS idx_facts_event_date ON facts(event_date) WHERE event_date IS NOT NULL;`,
130
+ // HNSW vector index
131
+ `CREATE INDEX IF NOT EXISTS idx_facts_embedding_hnsw ON facts USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);`,
132
+ // Entities
133
+ `CREATE TABLE IF NOT EXISTS entities (
134
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
135
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
136
+ name TEXT NOT NULL,
137
+ entity_type TEXT NOT NULL,
138
+ canonical_name TEXT NOT NULL,
139
+ properties JSONB NOT NULL DEFAULT '{}',
140
+ embedding VECTOR(2000),
141
+ embedding_model TEXT,
142
+ embedding_dim INTEGER,
143
+ merge_target_id UUID,
144
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
145
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
146
+ );
147
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_entities_canonical ON entities(tenant_id, canonical_name, entity_type);`,
148
+ // Fact-Entity junction
149
+ `CREATE TABLE IF NOT EXISTS fact_entities (
150
+ fact_id UUID NOT NULL REFERENCES facts(id) ON DELETE CASCADE,
151
+ entity_id UUID NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
152
+ role TEXT NOT NULL DEFAULT 'mentioned',
153
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
154
+ PRIMARY KEY (fact_id, entity_id)
155
+ );`,
156
+ // Edges
157
+ `CREATE TABLE IF NOT EXISTS edges (
158
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
159
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
160
+ source_id UUID NOT NULL REFERENCES entities(id),
161
+ target_id UUID NOT NULL REFERENCES entities(id),
162
+ relation TEXT NOT NULL,
163
+ edge_type TEXT NOT NULL,
164
+ weight NUMERIC(5,4) NOT NULL DEFAULT 1.0,
165
+ valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
166
+ valid_until TIMESTAMPTZ,
167
+ fact_id UUID,
168
+ confidence NUMERIC(5,4) NOT NULL DEFAULT 0.8,
169
+ metadata JSONB NOT NULL DEFAULT '{}',
170
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
171
+ );
172
+ CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(tenant_id, source_id);
173
+ CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(tenant_id, target_id);`,
174
+ // Triggers
175
+ `CREATE TABLE IF NOT EXISTS triggers (
176
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
177
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
178
+ scope TEXT NOT NULL,
179
+ scope_id TEXT NOT NULL,
180
+ condition JSONB NOT NULL DEFAULT '{}',
181
+ fact_ids UUID[] NOT NULL DEFAULT '{}',
182
+ entity_ids UUID[] NOT NULL DEFAULT '{}',
183
+ query_template TEXT,
184
+ priority INTEGER NOT NULL DEFAULT 0,
185
+ active BOOLEAN NOT NULL DEFAULT true,
186
+ times_fired INTEGER NOT NULL DEFAULT 0,
187
+ last_fired_at TIMESTAMPTZ,
188
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
189
+ );`,
190
+ // Memory accesses
191
+ `CREATE TABLE IF NOT EXISTS memory_accesses (
192
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
193
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
194
+ fact_id UUID NOT NULL REFERENCES facts(id),
195
+ query TEXT NOT NULL,
196
+ retrieval_method TEXT NOT NULL,
197
+ similarity_score NUMERIC,
198
+ rank_position INTEGER,
199
+ was_useful BOOLEAN,
200
+ was_corrected BOOLEAN NOT NULL DEFAULT false,
201
+ feedback_type TEXT,
202
+ feedback_detail TEXT,
203
+ trigger_id UUID,
204
+ accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
205
+ );`,
206
+ // Usage records
207
+ `CREATE TABLE IF NOT EXISTS usage_records (
208
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
209
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
210
+ period_start TIMESTAMPTZ NOT NULL,
211
+ period_end TIMESTAMPTZ NOT NULL,
212
+ tokens_used INTEGER NOT NULL DEFAULT 0,
213
+ queries_used INTEGER NOT NULL DEFAULT 0,
214
+ extractions_count INTEGER NOT NULL DEFAULT 0,
215
+ cost_usd NUMERIC NOT NULL DEFAULT 0,
216
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
217
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
218
+ );`,
219
+ // Webhooks
220
+ `CREATE TABLE IF NOT EXISTS webhooks (
221
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
222
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
223
+ url TEXT NOT NULL,
224
+ events TEXT[] NOT NULL DEFAULT '{}',
225
+ secret_hash TEXT NOT NULL,
226
+ signing_key TEXT,
227
+ active BOOLEAN NOT NULL DEFAULT true,
228
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
229
+ );`,
230
+ // Compound search RPC
231
+ `CREATE OR REPLACE FUNCTION steno_search(
232
+ query_embedding TEXT, search_query TEXT, match_tenant_id UUID,
233
+ match_scope TEXT, match_scope_id TEXT, match_count INT DEFAULT 20, min_similarity FLOAT DEFAULT 0.0
234
+ ) RETURNS TABLE (
235
+ source TEXT, id UUID, tenant_id UUID, scope TEXT, scope_id TEXT, session_id UUID,
236
+ content TEXT, embedding_model TEXT, embedding_dim INT, version INT, lineage_id UUID,
237
+ valid_from TIMESTAMPTZ, valid_until TIMESTAMPTZ, operation TEXT, parent_id UUID,
238
+ importance NUMERIC, frequency INT, last_accessed TIMESTAMPTZ, decay_score NUMERIC,
239
+ contradiction_status TEXT, contradicts_id UUID, source_type TEXT, source_ref JSONB,
240
+ confidence NUMERIC, original_content TEXT, extraction_id UUID, extraction_tier TEXT,
241
+ modality TEXT, tags TEXT[], metadata JSONB, created_at TIMESTAMPTZ,
242
+ event_date TIMESTAMPTZ, document_date TIMESTAMPTZ, source_chunk TEXT, relevance_score FLOAT
243
+ ) LANGUAGE plpgsql AS $$
244
+ BEGIN RETURN QUERY
245
+ (SELECT 'vector'::TEXT, f.id, f.tenant_id, f.scope, f.scope_id, f.session_id,
246
+ f.content, f.embedding_model, f.embedding_dim, f.version, f.lineage_id,
247
+ f.valid_from, f.valid_until, f.operation, f.parent_id, f.importance, f.frequency,
248
+ f.last_accessed, f.decay_score, f.contradiction_status, f.contradicts_id,
249
+ f.source_type, f.source_ref, f.confidence, f.original_content, f.extraction_id,
250
+ f.extraction_tier, f.modality, f.tags, f.metadata, f.created_at,
251
+ f.event_date, f.document_date, f.source_chunk,
252
+ (1 - (f.embedding <=> query_embedding::vector))::float
253
+ FROM facts f WHERE f.tenant_id = match_tenant_id AND f.scope = match_scope
254
+ AND f.scope_id = match_scope_id AND f.valid_until IS NULL
255
+ AND NOT ('raw_chunk' = ANY(f.tags))
256
+ AND (1 - (f.embedding <=> query_embedding::vector)) >= min_similarity
257
+ ORDER BY f.embedding <=> query_embedding::vector LIMIT match_count)
258
+ UNION ALL
259
+ (SELECT 'keyword'::TEXT, f.id, f.tenant_id, f.scope, f.scope_id, f.session_id,
260
+ f.content, f.embedding_model, f.embedding_dim, f.version, f.lineage_id,
261
+ f.valid_from, f.valid_until, f.operation, f.parent_id, f.importance, f.frequency,
262
+ f.last_accessed, f.decay_score, f.contradiction_status, f.contradicts_id,
263
+ f.source_type, f.source_ref, f.confidence, f.original_content, f.extraction_id,
264
+ f.extraction_tier, f.modality, f.tags, f.metadata, f.created_at,
265
+ f.event_date, f.document_date, f.source_chunk,
266
+ ts_rank(f.search_vector, plainto_tsquery('english', search_query))::float
267
+ FROM facts f WHERE f.tenant_id = match_tenant_id AND f.scope = match_scope
268
+ AND f.scope_id = match_scope_id AND f.valid_until IS NULL
269
+ AND NOT ('raw_chunk' = ANY(f.tags))
270
+ AND f.search_vector @@ plainto_tsquery('english', search_query)
271
+ ORDER BY ts_rank(f.search_vector, plainto_tsquery('english', search_query)) DESC LIMIT match_count);
272
+ END; $$;`,
273
+ // Default tenant
274
+ `INSERT INTO tenants (id, name, slug, plan) VALUES ('00000000-0000-0000-0000-000000000001', 'Default', 'default', 'enterprise') ON CONFLICT DO NOTHING;`
275
+ ];
276
+ async function main() {
277
+ console.log("\n \u{1F9E0} Steno Memory \u2014 Setup Wizard\n");
278
+ const supabaseUrl = await ask(" Supabase URL: ");
279
+ const supabaseKey = await ask(" Supabase Service Role Key: ");
280
+ const openaiKey = await ask(" OpenAI API Key: ");
281
+ const perplexityKey = await ask(" Perplexity API Key (optional, press Enter to skip): ");
282
+ if (!supabaseUrl || !supabaseKey || !openaiKey) {
283
+ console.error("\n \u274C Supabase URL, Service Role Key, and OpenAI Key are required.\n");
284
+ process.exit(1);
285
+ }
286
+ console.log("\n Running database migrations...");
287
+ const supabase = createClient(supabaseUrl, supabaseKey);
288
+ let success = 0;
289
+ let skipped = 0;
290
+ for (let i = 0; i < MIGRATIONS.length; i++) {
291
+ try {
292
+ const { error } = await supabase.rpc("exec_sql", { query: MIGRATIONS[i] }).catch(() => ({ error: { message: "rpc not available" } }));
293
+ if (error) {
294
+ const res = await fetch(`${supabaseUrl}/rest/v1/rpc/exec_sql`, {
295
+ method: "POST",
296
+ headers: { "apikey": supabaseKey, "Authorization": `Bearer ${supabaseKey}`, "Content-Type": "application/json" },
297
+ body: JSON.stringify({ query: MIGRATIONS[i] })
298
+ });
299
+ if (res.ok) {
300
+ success++;
301
+ } else {
302
+ skipped++;
303
+ }
304
+ } else {
305
+ success++;
306
+ }
307
+ } catch {
308
+ skipped++;
309
+ }
310
+ }
311
+ console.log(` \u2713 ${success} migrations applied, ${skipped} skipped (may already exist)`);
312
+ const configDir = path.join(os.homedir(), "Library", "Application Support", "Claude");
313
+ const configPath = path.join(configDir, "claude_desktop_config.json");
314
+ let config = {};
315
+ try {
316
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
317
+ } catch {
318
+ }
319
+ if (!config.mcpServers) config.mcpServers = {};
320
+ config.mcpServers["steno-memory"] = {
321
+ command: "npx",
322
+ args: ["-y", "@steno-ai/mcp"],
323
+ env: {
324
+ SUPABASE_URL: supabaseUrl,
325
+ SUPABASE_SERVICE_ROLE_KEY: supabaseKey,
326
+ OPENAI_API_KEY: openaiKey,
327
+ ...perplexityKey ? { PERPLEXITY_API_KEY: perplexityKey } : {}
328
+ }
329
+ };
330
+ fs.mkdirSync(configDir, { recursive: true });
331
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
332
+ console.log(` \u2713 Claude Desktop config written to ${configPath}`);
333
+ console.log(`
334
+ \u2705 Setup complete!
335
+
336
+ Next steps:
337
+ 1. Restart Claude Desktop (Cmd+Q, reopen)
338
+ 2. Go to Settings > General > set "Tools already loaded"
339
+ 3. Start chatting \u2014 Claude will remember everything
340
+
341
+ Your data stays in YOUR Supabase. Nothing is shared.
342
+ `);
343
+ rl.close();
344
+ }
345
+ main().catch((err) => {
346
+ console.error("Setup failed:", err.message);
347
+ process.exit(1);
348
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steno-ai/mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "MCP server for Claude Code, Claude Desktop, and other MCP clients",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,7 +9,10 @@
9
9
  "directory": "packages/mcp-server"
10
10
  },
11
11
  "type": "module",
12
- "bin": { "steno-mcp": "./dist/cli.js" },
12
+ "bin": {
13
+ "steno-mcp": "./dist/cli.js",
14
+ "steno-mcp-init": "./dist/init.js"
15
+ },
13
16
  "exports": {
14
17
  ".": {
15
18
  "import": "./dist/index.js",
package/src/init.ts ADDED
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * steno-mcp init — interactive setup wizard
4
+ *
5
+ * 1. Asks for Supabase + OpenAI keys
6
+ * 2. Runs all migrations automatically
7
+ * 3. Writes Claude Desktop config
8
+ * 4. Tests the connection
9
+ */
10
+ import { createClient } from '@supabase/supabase-js';
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import * as os from 'os';
14
+ import * as readline from 'readline';
15
+
16
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
17
+ const ask = (q: string): Promise<string> => new Promise(r => rl.question(q, r));
18
+
19
+ // All migrations in order
20
+ const MIGRATIONS = [
21
+ `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`,
22
+ `CREATE EXTENSION IF NOT EXISTS "vector";`,
23
+ `CREATE EXTENSION IF NOT EXISTS "pg_trgm";`,
24
+ // Tenants
25
+ `CREATE TABLE IF NOT EXISTS tenants (
26
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
27
+ name TEXT NOT NULL,
28
+ slug TEXT NOT NULL UNIQUE,
29
+ config JSONB NOT NULL DEFAULT '{}',
30
+ plan TEXT NOT NULL DEFAULT 'free',
31
+ token_limit_monthly INTEGER NOT NULL DEFAULT 1000000,
32
+ query_limit_monthly INTEGER NOT NULL DEFAULT 10000,
33
+ stripe_customer_id TEXT,
34
+ stripe_subscription_id TEXT,
35
+ active BOOLEAN NOT NULL DEFAULT true,
36
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
37
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
38
+ );`,
39
+ // API Keys
40
+ `CREATE TABLE IF NOT EXISTS api_keys (
41
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
42
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
43
+ key_hash TEXT NOT NULL,
44
+ key_prefix TEXT NOT NULL,
45
+ name TEXT NOT NULL DEFAULT 'Default',
46
+ scopes TEXT[] NOT NULL DEFAULT ARRAY['read','write'],
47
+ expires_at TIMESTAMPTZ,
48
+ last_used_at TIMESTAMPTZ,
49
+ active BOOLEAN NOT NULL DEFAULT true,
50
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
51
+ );`,
52
+ // Sessions
53
+ `CREATE TABLE IF NOT EXISTS sessions (
54
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
55
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
56
+ scope TEXT NOT NULL,
57
+ scope_id TEXT NOT NULL,
58
+ started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
59
+ ended_at TIMESTAMPTZ,
60
+ summary TEXT,
61
+ topics TEXT[] NOT NULL DEFAULT '{}',
62
+ message_count INTEGER NOT NULL DEFAULT 0,
63
+ fact_count INTEGER NOT NULL DEFAULT 0,
64
+ metadata JSONB NOT NULL DEFAULT '{}',
65
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
66
+ );`,
67
+ // Extractions
68
+ `CREATE TABLE IF NOT EXISTS extractions (
69
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
70
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
71
+ status TEXT NOT NULL DEFAULT 'queued',
72
+ input_type TEXT NOT NULL,
73
+ input_data TEXT,
74
+ input_hash TEXT NOT NULL,
75
+ input_size INTEGER,
76
+ scope TEXT NOT NULL,
77
+ scope_id TEXT NOT NULL,
78
+ session_id UUID,
79
+ tier_used TEXT,
80
+ llm_model TEXT,
81
+ facts_created INTEGER NOT NULL DEFAULT 0,
82
+ facts_updated INTEGER NOT NULL DEFAULT 0,
83
+ facts_invalidated INTEGER NOT NULL DEFAULT 0,
84
+ entities_created INTEGER NOT NULL DEFAULT 0,
85
+ edges_created INTEGER NOT NULL DEFAULT 0,
86
+ cost_tokens_input INTEGER NOT NULL DEFAULT 0,
87
+ cost_tokens_output INTEGER NOT NULL DEFAULT 0,
88
+ cost_usd NUMERIC NOT NULL DEFAULT 0,
89
+ duration_ms INTEGER,
90
+ error TEXT,
91
+ retry_count INTEGER NOT NULL DEFAULT 0,
92
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
93
+ completed_at TIMESTAMPTZ
94
+ );`,
95
+ // Facts
96
+ `CREATE TABLE IF NOT EXISTS facts (
97
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
98
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
99
+ scope TEXT NOT NULL CHECK (scope IN ('user','agent','session','hive')),
100
+ scope_id TEXT NOT NULL,
101
+ session_id UUID REFERENCES sessions(id) ON DELETE SET NULL,
102
+ content TEXT NOT NULL,
103
+ embedding VECTOR(2000),
104
+ embedding_model TEXT,
105
+ embedding_dim INTEGER,
106
+ search_vector TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
107
+ version INTEGER NOT NULL DEFAULT 1,
108
+ lineage_id UUID NOT NULL DEFAULT uuid_generate_v4(),
109
+ valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
110
+ valid_until TIMESTAMPTZ,
111
+ operation TEXT NOT NULL DEFAULT 'create' CHECK (operation IN ('create','update','invalidate')),
112
+ parent_id UUID REFERENCES facts(id) ON DELETE SET NULL,
113
+ importance NUMERIC(5,4) NOT NULL DEFAULT 0.5,
114
+ frequency INTEGER NOT NULL DEFAULT 1,
115
+ last_accessed TIMESTAMPTZ NOT NULL DEFAULT NOW(),
116
+ decay_score NUMERIC(8,6) NOT NULL DEFAULT 0.5,
117
+ contradiction_status TEXT NOT NULL DEFAULT 'none',
118
+ contradicts_id UUID REFERENCES facts(id) ON DELETE SET NULL,
119
+ source_type TEXT NOT NULL CHECK (source_type IN ('conversation','document','url','raw_text','api','agent_self')),
120
+ source_ref JSONB,
121
+ confidence NUMERIC(5,4) NOT NULL DEFAULT 0.8,
122
+ original_content TEXT,
123
+ extraction_id UUID,
124
+ extraction_tier TEXT,
125
+ modality TEXT NOT NULL DEFAULT 'text',
126
+ tags TEXT[] NOT NULL DEFAULT '{}',
127
+ metadata JSONB NOT NULL DEFAULT '{}',
128
+ event_date TIMESTAMPTZ,
129
+ document_date TIMESTAMPTZ,
130
+ source_chunk TEXT,
131
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
132
+ );`,
133
+ // Fact indexes
134
+ `CREATE INDEX IF NOT EXISTS idx_facts_tenant_scope ON facts(tenant_id, scope, scope_id);
135
+ CREATE INDEX IF NOT EXISTS idx_facts_lineage ON facts(tenant_id, lineage_id);
136
+ CREATE INDEX IF NOT EXISTS idx_facts_search_vector ON facts USING GIN(search_vector);
137
+ CREATE INDEX IF NOT EXISTS idx_facts_event_date ON facts(event_date) WHERE event_date IS NOT NULL;`,
138
+ // HNSW vector index
139
+ `CREATE INDEX IF NOT EXISTS idx_facts_embedding_hnsw ON facts USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);`,
140
+ // Entities
141
+ `CREATE TABLE IF NOT EXISTS entities (
142
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
143
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
144
+ name TEXT NOT NULL,
145
+ entity_type TEXT NOT NULL,
146
+ canonical_name TEXT NOT NULL,
147
+ properties JSONB NOT NULL DEFAULT '{}',
148
+ embedding VECTOR(2000),
149
+ embedding_model TEXT,
150
+ embedding_dim INTEGER,
151
+ merge_target_id UUID,
152
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
153
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
154
+ );
155
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_entities_canonical ON entities(tenant_id, canonical_name, entity_type);`,
156
+ // Fact-Entity junction
157
+ `CREATE TABLE IF NOT EXISTS fact_entities (
158
+ fact_id UUID NOT NULL REFERENCES facts(id) ON DELETE CASCADE,
159
+ entity_id UUID NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
160
+ role TEXT NOT NULL DEFAULT 'mentioned',
161
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
162
+ PRIMARY KEY (fact_id, entity_id)
163
+ );`,
164
+ // Edges
165
+ `CREATE TABLE IF NOT EXISTS edges (
166
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
167
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
168
+ source_id UUID NOT NULL REFERENCES entities(id),
169
+ target_id UUID NOT NULL REFERENCES entities(id),
170
+ relation TEXT NOT NULL,
171
+ edge_type TEXT NOT NULL,
172
+ weight NUMERIC(5,4) NOT NULL DEFAULT 1.0,
173
+ valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
174
+ valid_until TIMESTAMPTZ,
175
+ fact_id UUID,
176
+ confidence NUMERIC(5,4) NOT NULL DEFAULT 0.8,
177
+ metadata JSONB NOT NULL DEFAULT '{}',
178
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
179
+ );
180
+ CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(tenant_id, source_id);
181
+ CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(tenant_id, target_id);`,
182
+ // Triggers
183
+ `CREATE TABLE IF NOT EXISTS triggers (
184
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
185
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
186
+ scope TEXT NOT NULL,
187
+ scope_id TEXT NOT NULL,
188
+ condition JSONB NOT NULL DEFAULT '{}',
189
+ fact_ids UUID[] NOT NULL DEFAULT '{}',
190
+ entity_ids UUID[] NOT NULL DEFAULT '{}',
191
+ query_template TEXT,
192
+ priority INTEGER NOT NULL DEFAULT 0,
193
+ active BOOLEAN NOT NULL DEFAULT true,
194
+ times_fired INTEGER NOT NULL DEFAULT 0,
195
+ last_fired_at TIMESTAMPTZ,
196
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
197
+ );`,
198
+ // Memory accesses
199
+ `CREATE TABLE IF NOT EXISTS memory_accesses (
200
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
201
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
202
+ fact_id UUID NOT NULL REFERENCES facts(id),
203
+ query TEXT NOT NULL,
204
+ retrieval_method TEXT NOT NULL,
205
+ similarity_score NUMERIC,
206
+ rank_position INTEGER,
207
+ was_useful BOOLEAN,
208
+ was_corrected BOOLEAN NOT NULL DEFAULT false,
209
+ feedback_type TEXT,
210
+ feedback_detail TEXT,
211
+ trigger_id UUID,
212
+ accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
213
+ );`,
214
+ // Usage records
215
+ `CREATE TABLE IF NOT EXISTS usage_records (
216
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
217
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
218
+ period_start TIMESTAMPTZ NOT NULL,
219
+ period_end TIMESTAMPTZ NOT NULL,
220
+ tokens_used INTEGER NOT NULL DEFAULT 0,
221
+ queries_used INTEGER NOT NULL DEFAULT 0,
222
+ extractions_count INTEGER NOT NULL DEFAULT 0,
223
+ cost_usd NUMERIC NOT NULL DEFAULT 0,
224
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
225
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
226
+ );`,
227
+ // Webhooks
228
+ `CREATE TABLE IF NOT EXISTS webhooks (
229
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
230
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
231
+ url TEXT NOT NULL,
232
+ events TEXT[] NOT NULL DEFAULT '{}',
233
+ secret_hash TEXT NOT NULL,
234
+ signing_key TEXT,
235
+ active BOOLEAN NOT NULL DEFAULT true,
236
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
237
+ );`,
238
+ // Compound search RPC
239
+ `CREATE OR REPLACE FUNCTION steno_search(
240
+ query_embedding TEXT, search_query TEXT, match_tenant_id UUID,
241
+ match_scope TEXT, match_scope_id TEXT, match_count INT DEFAULT 20, min_similarity FLOAT DEFAULT 0.0
242
+ ) RETURNS TABLE (
243
+ source TEXT, id UUID, tenant_id UUID, scope TEXT, scope_id TEXT, session_id UUID,
244
+ content TEXT, embedding_model TEXT, embedding_dim INT, version INT, lineage_id UUID,
245
+ valid_from TIMESTAMPTZ, valid_until TIMESTAMPTZ, operation TEXT, parent_id UUID,
246
+ importance NUMERIC, frequency INT, last_accessed TIMESTAMPTZ, decay_score NUMERIC,
247
+ contradiction_status TEXT, contradicts_id UUID, source_type TEXT, source_ref JSONB,
248
+ confidence NUMERIC, original_content TEXT, extraction_id UUID, extraction_tier TEXT,
249
+ modality TEXT, tags TEXT[], metadata JSONB, created_at TIMESTAMPTZ,
250
+ event_date TIMESTAMPTZ, document_date TIMESTAMPTZ, source_chunk TEXT, relevance_score FLOAT
251
+ ) LANGUAGE plpgsql AS $$
252
+ BEGIN RETURN QUERY
253
+ (SELECT 'vector'::TEXT, f.id, f.tenant_id, f.scope, f.scope_id, f.session_id,
254
+ f.content, f.embedding_model, f.embedding_dim, f.version, f.lineage_id,
255
+ f.valid_from, f.valid_until, f.operation, f.parent_id, f.importance, f.frequency,
256
+ f.last_accessed, f.decay_score, f.contradiction_status, f.contradicts_id,
257
+ f.source_type, f.source_ref, f.confidence, f.original_content, f.extraction_id,
258
+ f.extraction_tier, f.modality, f.tags, f.metadata, f.created_at,
259
+ f.event_date, f.document_date, f.source_chunk,
260
+ (1 - (f.embedding <=> query_embedding::vector))::float
261
+ FROM facts f WHERE f.tenant_id = match_tenant_id AND f.scope = match_scope
262
+ AND f.scope_id = match_scope_id AND f.valid_until IS NULL
263
+ AND NOT ('raw_chunk' = ANY(f.tags))
264
+ AND (1 - (f.embedding <=> query_embedding::vector)) >= min_similarity
265
+ ORDER BY f.embedding <=> query_embedding::vector LIMIT match_count)
266
+ UNION ALL
267
+ (SELECT 'keyword'::TEXT, f.id, f.tenant_id, f.scope, f.scope_id, f.session_id,
268
+ f.content, f.embedding_model, f.embedding_dim, f.version, f.lineage_id,
269
+ f.valid_from, f.valid_until, f.operation, f.parent_id, f.importance, f.frequency,
270
+ f.last_accessed, f.decay_score, f.contradiction_status, f.contradicts_id,
271
+ f.source_type, f.source_ref, f.confidence, f.original_content, f.extraction_id,
272
+ f.extraction_tier, f.modality, f.tags, f.metadata, f.created_at,
273
+ f.event_date, f.document_date, f.source_chunk,
274
+ ts_rank(f.search_vector, plainto_tsquery('english', search_query))::float
275
+ FROM facts f WHERE f.tenant_id = match_tenant_id AND f.scope = match_scope
276
+ AND f.scope_id = match_scope_id AND f.valid_until IS NULL
277
+ AND NOT ('raw_chunk' = ANY(f.tags))
278
+ AND f.search_vector @@ plainto_tsquery('english', search_query)
279
+ ORDER BY ts_rank(f.search_vector, plainto_tsquery('english', search_query)) DESC LIMIT match_count);
280
+ END; $$;`,
281
+ // Default tenant
282
+ `INSERT INTO tenants (id, name, slug, plan) VALUES ('00000000-0000-0000-0000-000000000001', 'Default', 'default', 'enterprise') ON CONFLICT DO NOTHING;`,
283
+ ];
284
+
285
+ async function main() {
286
+ console.log('\n 🧠 Steno Memory — Setup Wizard\n');
287
+
288
+ // 1. Get keys
289
+ const supabaseUrl = await ask(' Supabase URL: ');
290
+ const supabaseKey = await ask(' Supabase Service Role Key: ');
291
+ const openaiKey = await ask(' OpenAI API Key: ');
292
+ const perplexityKey = await ask(' Perplexity API Key (optional, press Enter to skip): ');
293
+
294
+ if (!supabaseUrl || !supabaseKey || !openaiKey) {
295
+ console.error('\n ❌ Supabase URL, Service Role Key, and OpenAI Key are required.\n');
296
+ process.exit(1);
297
+ }
298
+
299
+ // 2. Run migrations
300
+ console.log('\n Running database migrations...');
301
+ const supabase = createClient(supabaseUrl, supabaseKey);
302
+
303
+ let success = 0;
304
+ let skipped = 0;
305
+ for (let i = 0; i < MIGRATIONS.length; i++) {
306
+ try {
307
+ const { error } = await supabase.rpc('exec_sql', { query: MIGRATIONS[i] }).catch(() => ({ error: { message: 'rpc not available' } }));
308
+ if (error) {
309
+ // Try direct REST approach
310
+ const res = await fetch(`${supabaseUrl}/rest/v1/rpc/exec_sql`, {
311
+ method: 'POST',
312
+ headers: { 'apikey': supabaseKey, 'Authorization': `Bearer ${supabaseKey}`, 'Content-Type': 'application/json' },
313
+ body: JSON.stringify({ query: MIGRATIONS[i] }),
314
+ });
315
+ if (res.ok) { success++; } else { skipped++; }
316
+ } else {
317
+ success++;
318
+ }
319
+ } catch {
320
+ skipped++;
321
+ }
322
+ }
323
+ console.log(` ✓ ${success} migrations applied, ${skipped} skipped (may already exist)`);
324
+
325
+ // 3. Write Claude Desktop config
326
+ const configDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude');
327
+ const configPath = path.join(configDir, 'claude_desktop_config.json');
328
+
329
+ let config: any = {};
330
+ try {
331
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
332
+ } catch { /* new config */ }
333
+
334
+ if (!config.mcpServers) config.mcpServers = {};
335
+ config.mcpServers['steno-memory'] = {
336
+ command: 'npx',
337
+ args: ['-y', '@steno-ai/mcp'],
338
+ env: {
339
+ SUPABASE_URL: supabaseUrl,
340
+ SUPABASE_SERVICE_ROLE_KEY: supabaseKey,
341
+ OPENAI_API_KEY: openaiKey,
342
+ ...(perplexityKey ? { PERPLEXITY_API_KEY: perplexityKey } : {}),
343
+ },
344
+ };
345
+
346
+ fs.mkdirSync(configDir, { recursive: true });
347
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
348
+ console.log(` ✓ Claude Desktop config written to ${configPath}`);
349
+
350
+ // 4. Done
351
+ console.log(`
352
+ ✅ Setup complete!
353
+
354
+ Next steps:
355
+ 1. Restart Claude Desktop (Cmd+Q, reopen)
356
+ 2. Go to Settings > General > set "Tools already loaded"
357
+ 3. Start chatting — Claude will remember everything
358
+
359
+ Your data stays in YOUR Supabase. Nothing is shared.
360
+ `);
361
+
362
+ rl.close();
363
+ }
364
+
365
+ main().catch((err) => {
366
+ console.error('Setup failed:', err.message);
367
+ process.exit(1);
368
+ });