gitmem-mcp 1.5.1 → 1.6.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.
- package/CHANGELOG.md +21 -0
- package/README.md +21 -4
- package/bin/gitmem.js +10 -0
- package/dist/commands/activate.d.ts +20 -0
- package/dist/commands/activate.js +606 -0
- package/dist/commands/deactivate.d.ts +10 -0
- package/dist/commands/deactivate.js +95 -0
- package/dist/commands/migrate-local.d.ts +71 -0
- package/dist/commands/migrate-local.js +317 -0
- package/dist/schemas/log.d.ts +2 -2
- package/dist/schemas/search.d.ts +2 -2
- package/dist/schemas/session-close.d.ts +12 -12
- package/dist/server.js +20 -2
- package/dist/services/analytics.d.ts +22 -0
- package/dist/services/analytics.js +68 -0
- package/dist/services/embedding.d.ts +7 -1
- package/dist/services/embedding.js +23 -5
- package/dist/services/license.d.ts +57 -0
- package/dist/services/license.js +200 -0
- package/dist/services/supabase-client.d.ts +6 -0
- package/dist/services/supabase-client.js +75 -22
- package/dist/services/tier.d.ts +13 -3
- package/dist/services/tier.js +38 -7
- package/dist/services/transcript-chunker.js +3 -2
- package/dist/services/variant-generation.js +3 -2
- package/dist/tools/recall.js +16 -4
- package/dist/tools/session-close.js +31 -5
- package/dist/tools/session-start.js +43 -5
- package/package.json +1 -1
- package/schema/setup.sql +489 -25
package/schema/setup.sql
CHANGED
|
@@ -24,11 +24,34 @@ CREATE TABLE IF NOT EXISTS gitmem_learnings (
|
|
|
24
24
|
embedding vector(1536),
|
|
25
25
|
project TEXT DEFAULT 'default',
|
|
26
26
|
source_date DATE DEFAULT CURRENT_DATE,
|
|
27
|
+
source_linear_issue TEXT,
|
|
28
|
+
persona_name TEXT,
|
|
29
|
+
why_this_matters TEXT,
|
|
30
|
+
action_protocol TEXT,
|
|
31
|
+
self_check_criteria TEXT,
|
|
32
|
+
is_active BOOLEAN DEFAULT true,
|
|
33
|
+
decay_multiplier FLOAT DEFAULT 1.0,
|
|
34
|
+
repeat_mistake BOOLEAN DEFAULT false,
|
|
35
|
+
related_scar_id UUID,
|
|
36
|
+
repeat_mistake_details JSONB,
|
|
27
37
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
28
38
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
29
39
|
);
|
|
30
40
|
|
|
31
|
-
--
|
|
41
|
+
-- Migration: ensure all columns exist (idempotent for upgrades)
|
|
42
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS embedding vector(1536);
|
|
43
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS source_linear_issue TEXT;
|
|
44
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS persona_name TEXT;
|
|
45
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS why_this_matters TEXT;
|
|
46
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS action_protocol TEXT;
|
|
47
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS self_check_criteria TEXT;
|
|
48
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true;
|
|
49
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS decay_multiplier FLOAT DEFAULT 1.0;
|
|
50
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS repeat_mistake BOOLEAN DEFAULT false;
|
|
51
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS related_scar_id UUID;
|
|
52
|
+
ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS repeat_mistake_details JSONB;
|
|
53
|
+
|
|
54
|
+
-- Indexes (created after migration ensures columns exist)
|
|
32
55
|
CREATE INDEX IF NOT EXISTS idx_gitmem_learnings_embedding
|
|
33
56
|
ON gitmem_learnings USING ivfflat (embedding vector_cosine_ops)
|
|
34
57
|
WITH (lists = 10);
|
|
@@ -48,14 +71,27 @@ CREATE TABLE IF NOT EXISTS gitmem_sessions (
|
|
|
48
71
|
session_date DATE DEFAULT CURRENT_DATE,
|
|
49
72
|
agent TEXT DEFAULT 'Unknown',
|
|
50
73
|
project TEXT DEFAULT 'default',
|
|
74
|
+
linear_issue TEXT,
|
|
75
|
+
recording_path TEXT,
|
|
76
|
+
transcript_path TEXT,
|
|
51
77
|
decisions TEXT[] DEFAULT '{}',
|
|
52
78
|
open_threads TEXT[] DEFAULT '{}',
|
|
53
79
|
closing_reflection JSONB,
|
|
80
|
+
close_compliance JSONB,
|
|
81
|
+
rapport_summary TEXT,
|
|
54
82
|
embedding vector(1536),
|
|
55
83
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
56
84
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
57
85
|
);
|
|
58
86
|
|
|
87
|
+
-- Migration: ensure all columns exist
|
|
88
|
+
ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS embedding vector(1536);
|
|
89
|
+
ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS linear_issue TEXT;
|
|
90
|
+
ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS recording_path TEXT;
|
|
91
|
+
ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS transcript_path TEXT;
|
|
92
|
+
ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS close_compliance JSONB;
|
|
93
|
+
ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS rapport_summary TEXT;
|
|
94
|
+
|
|
59
95
|
CREATE INDEX IF NOT EXISTS idx_gitmem_sessions_agent
|
|
60
96
|
ON gitmem_sessions (agent);
|
|
61
97
|
|
|
@@ -72,12 +108,21 @@ CREATE TABLE IF NOT EXISTS gitmem_decisions (
|
|
|
72
108
|
decision TEXT NOT NULL,
|
|
73
109
|
rationale TEXT NOT NULL,
|
|
74
110
|
alternatives_considered TEXT[] DEFAULT '{}',
|
|
111
|
+
personas_involved TEXT[] DEFAULT '{}',
|
|
112
|
+
docs_affected TEXT[] DEFAULT '{}',
|
|
113
|
+
linear_issue TEXT,
|
|
75
114
|
session_id UUID REFERENCES gitmem_sessions(id),
|
|
76
115
|
project TEXT DEFAULT 'default',
|
|
77
116
|
embedding vector(1536),
|
|
78
117
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
79
118
|
);
|
|
80
119
|
|
|
120
|
+
-- Migration: ensure all columns exist
|
|
121
|
+
ALTER TABLE gitmem_decisions ADD COLUMN IF NOT EXISTS embedding vector(1536);
|
|
122
|
+
ALTER TABLE gitmem_decisions ADD COLUMN IF NOT EXISTS personas_involved TEXT[] DEFAULT '{}';
|
|
123
|
+
ALTER TABLE gitmem_decisions ADD COLUMN IF NOT EXISTS docs_affected TEXT[] DEFAULT '{}';
|
|
124
|
+
ALTER TABLE gitmem_decisions ADD COLUMN IF NOT EXISTS linear_issue TEXT;
|
|
125
|
+
|
|
81
126
|
CREATE INDEX IF NOT EXISTS idx_gitmem_decisions_session
|
|
82
127
|
ON gitmem_decisions (session_id);
|
|
83
128
|
|
|
@@ -89,10 +134,15 @@ CREATE TABLE IF NOT EXISTS gitmem_scar_usage (
|
|
|
89
134
|
scar_id UUID REFERENCES gitmem_learnings(id),
|
|
90
135
|
session_id UUID REFERENCES gitmem_sessions(id),
|
|
91
136
|
agent TEXT DEFAULT 'Unknown',
|
|
137
|
+
issue_id TEXT,
|
|
138
|
+
issue_identifier TEXT,
|
|
92
139
|
reference_type TEXT CHECK (reference_type IN ('explicit', 'implicit', 'acknowledged', 'refuted', 'none')),
|
|
93
140
|
reference_context TEXT,
|
|
94
141
|
surfaced_at TIMESTAMPTZ,
|
|
142
|
+
acknowledged_at TIMESTAMPTZ,
|
|
143
|
+
referenced BOOLEAN,
|
|
95
144
|
execution_successful BOOLEAN,
|
|
145
|
+
variant_id UUID,
|
|
96
146
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
97
147
|
);
|
|
98
148
|
|
|
@@ -102,6 +152,135 @@ CREATE INDEX IF NOT EXISTS idx_gitmem_scar_usage_scar
|
|
|
102
152
|
CREATE INDEX IF NOT EXISTS idx_gitmem_scar_usage_session
|
|
103
153
|
ON gitmem_scar_usage (session_id);
|
|
104
154
|
|
|
155
|
+
-- Migration: add columns for older installs
|
|
156
|
+
ALTER TABLE gitmem_scar_usage ADD COLUMN IF NOT EXISTS issue_id TEXT;
|
|
157
|
+
ALTER TABLE gitmem_scar_usage ADD COLUMN IF NOT EXISTS issue_identifier TEXT;
|
|
158
|
+
ALTER TABLE gitmem_scar_usage ADD COLUMN IF NOT EXISTS acknowledged_at TIMESTAMPTZ;
|
|
159
|
+
ALTER TABLE gitmem_scar_usage ADD COLUMN IF NOT EXISTS referenced BOOLEAN;
|
|
160
|
+
ALTER TABLE gitmem_scar_usage ADD COLUMN IF NOT EXISTS variant_id UUID;
|
|
161
|
+
|
|
162
|
+
-- ============================================================================
|
|
163
|
+
-- Threads table (cross-session work tracking)
|
|
164
|
+
-- ============================================================================
|
|
165
|
+
CREATE TABLE IF NOT EXISTS gitmem_threads (
|
|
166
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
167
|
+
thread_id TEXT NOT NULL UNIQUE,
|
|
168
|
+
text TEXT NOT NULL,
|
|
169
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('emerging', 'active', 'cooling', 'dormant', 'archived', 'resolved')),
|
|
170
|
+
thread_class TEXT DEFAULT 'operational' CHECK (thread_class IN ('operational', 'backlog')),
|
|
171
|
+
vitality_score FLOAT DEFAULT 1.0,
|
|
172
|
+
last_touched_at TIMESTAMPTZ DEFAULT NOW(),
|
|
173
|
+
touch_count INT DEFAULT 1,
|
|
174
|
+
resolved_at TIMESTAMPTZ,
|
|
175
|
+
resolution_note TEXT,
|
|
176
|
+
source_session UUID REFERENCES gitmem_sessions(id),
|
|
177
|
+
resolved_by_session UUID REFERENCES gitmem_sessions(id),
|
|
178
|
+
related_issues TEXT[] DEFAULT '{}',
|
|
179
|
+
domain TEXT[] DEFAULT '{}',
|
|
180
|
+
project TEXT DEFAULT 'default',
|
|
181
|
+
metadata JSONB DEFAULT '{}',
|
|
182
|
+
embedding vector(1536),
|
|
183
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
184
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_gitmem_threads_status
|
|
188
|
+
ON gitmem_threads (status);
|
|
189
|
+
|
|
190
|
+
CREATE INDEX IF NOT EXISTS idx_gitmem_threads_project
|
|
191
|
+
ON gitmem_threads (project);
|
|
192
|
+
|
|
193
|
+
-- ============================================================================
|
|
194
|
+
-- Knowledge triples (knowledge graph)
|
|
195
|
+
-- ============================================================================
|
|
196
|
+
CREATE TABLE IF NOT EXISTS knowledge_triples (
|
|
197
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
198
|
+
subject TEXT NOT NULL,
|
|
199
|
+
predicate TEXT NOT NULL CHECK (predicate IN ('created_in', 'influenced_by', 'supersedes', 'demonstrates', 'affects_doc', 'created_thread', 'resolves_thread', 'relates_to_thread')),
|
|
200
|
+
object TEXT NOT NULL,
|
|
201
|
+
event_time TIMESTAMPTZ DEFAULT NOW(),
|
|
202
|
+
decay_weight FLOAT DEFAULT 1.0,
|
|
203
|
+
half_life_days INT DEFAULT 9999,
|
|
204
|
+
decay_floor FLOAT DEFAULT 0.1,
|
|
205
|
+
source_type TEXT,
|
|
206
|
+
source_id UUID,
|
|
207
|
+
source_linear_issue TEXT,
|
|
208
|
+
domain TEXT[] DEFAULT '{}',
|
|
209
|
+
project TEXT DEFAULT 'default',
|
|
210
|
+
created_by TEXT,
|
|
211
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
212
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
CREATE INDEX IF NOT EXISTS idx_gitmem_triples_subject
|
|
216
|
+
ON knowledge_triples (subject);
|
|
217
|
+
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_gitmem_triples_project
|
|
219
|
+
ON knowledge_triples (project);
|
|
220
|
+
|
|
221
|
+
-- ============================================================================
|
|
222
|
+
-- Query metrics (performance tracking)
|
|
223
|
+
-- ============================================================================
|
|
224
|
+
CREATE TABLE IF NOT EXISTS gitmem_query_metrics (
|
|
225
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
226
|
+
session_id UUID REFERENCES gitmem_sessions(id),
|
|
227
|
+
agent TEXT,
|
|
228
|
+
tool_name TEXT NOT NULL,
|
|
229
|
+
query_text TEXT,
|
|
230
|
+
tables_searched TEXT[],
|
|
231
|
+
latency_ms INT,
|
|
232
|
+
result_count INT,
|
|
233
|
+
similarity_scores FLOAT[],
|
|
234
|
+
context_bytes INT,
|
|
235
|
+
phase_tag TEXT,
|
|
236
|
+
linear_issue TEXT,
|
|
237
|
+
memories_surfaced UUID[],
|
|
238
|
+
metadata JSONB DEFAULT '{}',
|
|
239
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
-- ============================================================================
|
|
243
|
+
-- Scar enforcement variants (A/B testing)
|
|
244
|
+
-- ============================================================================
|
|
245
|
+
CREATE TABLE IF NOT EXISTS scar_enforcement_variants (
|
|
246
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
247
|
+
scar_id UUID NOT NULL REFERENCES gitmem_learnings(id),
|
|
248
|
+
variant_type TEXT NOT NULL,
|
|
249
|
+
variant_config JSONB,
|
|
250
|
+
metadata JSONB DEFAULT '{}',
|
|
251
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
252
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
-- ============================================================================
|
|
256
|
+
-- Lite views (exclude embedding columns for reduced context)
|
|
257
|
+
-- ============================================================================
|
|
258
|
+
CREATE OR REPLACE VIEW gitmem_learnings_lite AS
|
|
259
|
+
SELECT id, learning_type, title, description, severity, scar_type,
|
|
260
|
+
counter_arguments, problem_context, solution_approach, applies_when,
|
|
261
|
+
keywords, domain, project, source_date, source_linear_issue,
|
|
262
|
+
persona_name, is_active, decay_multiplier, created_at, updated_at
|
|
263
|
+
FROM gitmem_learnings;
|
|
264
|
+
|
|
265
|
+
CREATE OR REPLACE VIEW gitmem_sessions_lite AS
|
|
266
|
+
SELECT id, session_title, session_date, agent, project, linear_issue,
|
|
267
|
+
decisions, open_threads, closing_reflection, close_compliance,
|
|
268
|
+
rapport_summary, created_at, updated_at
|
|
269
|
+
FROM gitmem_sessions;
|
|
270
|
+
|
|
271
|
+
CREATE OR REPLACE VIEW gitmem_decisions_lite AS
|
|
272
|
+
SELECT id, decision_date, title, decision, rationale,
|
|
273
|
+
alternatives_considered, personas_involved, docs_affected,
|
|
274
|
+
linear_issue, session_id, project, created_at
|
|
275
|
+
FROM gitmem_decisions;
|
|
276
|
+
|
|
277
|
+
CREATE OR REPLACE VIEW gitmem_threads_lite AS
|
|
278
|
+
SELECT id, thread_id, text, status, thread_class, vitality_score,
|
|
279
|
+
last_touched_at, touch_count, resolved_at, resolution_note,
|
|
280
|
+
source_session, resolved_by_session, related_issues, domain,
|
|
281
|
+
project, metadata, created_at, updated_at
|
|
282
|
+
FROM gitmem_threads;
|
|
283
|
+
|
|
105
284
|
-- ============================================================================
|
|
106
285
|
-- Semantic search RPC function
|
|
107
286
|
-- ============================================================================
|
|
@@ -137,6 +316,106 @@ BEGIN
|
|
|
137
316
|
END;
|
|
138
317
|
$$;
|
|
139
318
|
|
|
319
|
+
-- ============================================================================
|
|
320
|
+
-- Scar search with temporal + behavioral decay weighting
|
|
321
|
+
-- ============================================================================
|
|
322
|
+
CREATE OR REPLACE FUNCTION gitmem_scar_search(
|
|
323
|
+
query_embedding vector(1536),
|
|
324
|
+
match_count INT DEFAULT 5,
|
|
325
|
+
similarity_threshold FLOAT DEFAULT 0.0
|
|
326
|
+
)
|
|
327
|
+
RETURNS TABLE (
|
|
328
|
+
id UUID,
|
|
329
|
+
title TEXT,
|
|
330
|
+
description TEXT,
|
|
331
|
+
severity TEXT,
|
|
332
|
+
scar_type TEXT,
|
|
333
|
+
counter_arguments TEXT[],
|
|
334
|
+
decay_multiplier FLOAT,
|
|
335
|
+
similarity FLOAT,
|
|
336
|
+
weighted_similarity FLOAT
|
|
337
|
+
)
|
|
338
|
+
LANGUAGE plpgsql
|
|
339
|
+
AS $$
|
|
340
|
+
DECLARE
|
|
341
|
+
decay_days INT;
|
|
342
|
+
temporal_weight FLOAT;
|
|
343
|
+
BEGIN
|
|
344
|
+
RETURN QUERY
|
|
345
|
+
SELECT
|
|
346
|
+
l.id,
|
|
347
|
+
l.title,
|
|
348
|
+
l.description,
|
|
349
|
+
l.severity,
|
|
350
|
+
l.scar_type,
|
|
351
|
+
l.counter_arguments,
|
|
352
|
+
COALESCE(l.decay_multiplier, 1.0) AS decay_multiplier,
|
|
353
|
+
(1 - (l.embedding <=> query_embedding))::FLOAT AS similarity,
|
|
354
|
+
-- Weighted similarity: raw * temporal_decay * behavioral_decay
|
|
355
|
+
(
|
|
356
|
+
(1 - (l.embedding <=> query_embedding)) *
|
|
357
|
+
-- Temporal decay based on scar_type
|
|
358
|
+
CASE
|
|
359
|
+
WHEN l.scar_type = 'process' THEN 1.0 -- permanent
|
|
360
|
+
WHEN l.scar_type = 'incident' THEN
|
|
361
|
+
GREATEST(0.1, 1.0 - 0.9 * (EXTRACT(EPOCH FROM (NOW() - l.created_at)) / (180.0 * 86400)))
|
|
362
|
+
WHEN l.scar_type = 'context' THEN
|
|
363
|
+
GREATEST(0.1, 1.0 - 0.9 * (EXTRACT(EPOCH FROM (NOW() - l.created_at)) / (30.0 * 86400)))
|
|
364
|
+
ELSE 1.0 -- default: no decay
|
|
365
|
+
END *
|
|
366
|
+
-- Behavioral decay multiplier
|
|
367
|
+
COALESCE(l.decay_multiplier, 1.0)
|
|
368
|
+
)::FLOAT AS weighted_similarity
|
|
369
|
+
FROM gitmem_learnings l
|
|
370
|
+
WHERE l.embedding IS NOT NULL
|
|
371
|
+
AND COALESCE(l.is_active, true) = true
|
|
372
|
+
AND (1 - (l.embedding <=> query_embedding)) > similarity_threshold
|
|
373
|
+
ORDER BY weighted_similarity DESC
|
|
374
|
+
LIMIT match_count;
|
|
375
|
+
END;
|
|
376
|
+
$$;
|
|
377
|
+
|
|
378
|
+
-- ============================================================================
|
|
379
|
+
-- Refresh behavioral decay scores from scar_usage patterns
|
|
380
|
+
-- Aggregates last 90 days of usage, computes dismiss rate,
|
|
381
|
+
-- updates decay_multiplier on learnings (minimum 3 surfacings required)
|
|
382
|
+
-- ============================================================================
|
|
383
|
+
CREATE OR REPLACE FUNCTION refresh_scar_behavioral_scores()
|
|
384
|
+
RETURNS TABLE (scars_updated INT, scars_scanned INT)
|
|
385
|
+
LANGUAGE plpgsql
|
|
386
|
+
AS $$
|
|
387
|
+
DECLARE
|
|
388
|
+
v_scanned INT := 0;
|
|
389
|
+
v_updated INT := 0;
|
|
390
|
+
BEGIN
|
|
391
|
+
-- Aggregate scar_usage from last 90 days, compute dismiss rate
|
|
392
|
+
WITH usage_stats AS (
|
|
393
|
+
SELECT
|
|
394
|
+
su.scar_id,
|
|
395
|
+
COUNT(*) AS times_surfaced,
|
|
396
|
+
COUNT(*) FILTER (WHERE su.reference_type IN ('none', 'refuted')) AS times_dismissed
|
|
397
|
+
FROM gitmem_scar_usage su
|
|
398
|
+
WHERE su.surfaced_at >= NOW() - INTERVAL '90 days'
|
|
399
|
+
GROUP BY su.scar_id
|
|
400
|
+
HAVING COUNT(*) >= 3 -- minimum surfacings for meaningful signal
|
|
401
|
+
)
|
|
402
|
+
UPDATE gitmem_learnings l
|
|
403
|
+
SET decay_multiplier = GREATEST(0.1, 1.0 - (us.times_dismissed::FLOAT / us.times_surfaced::FLOAT) * 0.8),
|
|
404
|
+
updated_at = NOW()
|
|
405
|
+
FROM usage_stats us
|
|
406
|
+
WHERE l.id = us.scar_id
|
|
407
|
+
AND COALESCE(l.is_active, true) = true;
|
|
408
|
+
|
|
409
|
+
GET DIAGNOSTICS v_updated = ROW_COUNT;
|
|
410
|
+
|
|
411
|
+
SELECT COUNT(DISTINCT scar_id) INTO v_scanned
|
|
412
|
+
FROM gitmem_scar_usage
|
|
413
|
+
WHERE surfaced_at >= NOW() - INTERVAL '90 days';
|
|
414
|
+
|
|
415
|
+
RETURN QUERY SELECT v_updated, v_scanned;
|
|
416
|
+
END;
|
|
417
|
+
$$;
|
|
418
|
+
|
|
140
419
|
-- ============================================================================
|
|
141
420
|
-- Row Level Security
|
|
142
421
|
-- ============================================================================
|
|
@@ -146,32 +425,55 @@ ALTER TABLE gitmem_learnings ENABLE ROW LEVEL SECURITY;
|
|
|
146
425
|
ALTER TABLE gitmem_sessions ENABLE ROW LEVEL SECURITY;
|
|
147
426
|
ALTER TABLE gitmem_decisions ENABLE ROW LEVEL SECURITY;
|
|
148
427
|
ALTER TABLE gitmem_scar_usage ENABLE ROW LEVEL SECURITY;
|
|
428
|
+
ALTER TABLE gitmem_threads ENABLE ROW LEVEL SECURITY;
|
|
429
|
+
ALTER TABLE knowledge_triples ENABLE ROW LEVEL SECURITY;
|
|
430
|
+
ALTER TABLE gitmem_query_metrics ENABLE ROW LEVEL SECURITY;
|
|
431
|
+
ALTER TABLE scar_enforcement_variants ENABLE ROW LEVEL SECURITY;
|
|
149
432
|
|
|
150
433
|
-- Service role has full access (used by the MCP server)
|
|
151
|
-
CREATE POLICY
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
FOR ALL USING (auth.role() = 'service_role');
|
|
156
|
-
|
|
157
|
-
CREATE POLICY "
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
CREATE POLICY "Service role full access" ON
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
--
|
|
164
|
-
|
|
165
|
-
FOR ALL USING (auth.role()
|
|
166
|
-
|
|
167
|
-
CREATE POLICY "Block anonymous access" ON
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
CREATE POLICY "
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
434
|
+
-- Drop-then-create for idempotency (CREATE POLICY has no IF NOT EXISTS)
|
|
435
|
+
DO $$ BEGIN
|
|
436
|
+
-- gitmem_learnings
|
|
437
|
+
DROP POLICY IF EXISTS "Service role full access" ON gitmem_learnings;
|
|
438
|
+
CREATE POLICY "Service role full access" ON gitmem_learnings FOR ALL USING (auth.role() = 'service_role');
|
|
439
|
+
DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_learnings;
|
|
440
|
+
CREATE POLICY "Block anonymous access" ON gitmem_learnings FOR ALL USING (auth.role() != 'anon');
|
|
441
|
+
-- gitmem_sessions
|
|
442
|
+
DROP POLICY IF EXISTS "Service role full access" ON gitmem_sessions;
|
|
443
|
+
CREATE POLICY "Service role full access" ON gitmem_sessions FOR ALL USING (auth.role() = 'service_role');
|
|
444
|
+
DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_sessions;
|
|
445
|
+
CREATE POLICY "Block anonymous access" ON gitmem_sessions FOR ALL USING (auth.role() != 'anon');
|
|
446
|
+
-- gitmem_decisions
|
|
447
|
+
DROP POLICY IF EXISTS "Service role full access" ON gitmem_decisions;
|
|
448
|
+
CREATE POLICY "Service role full access" ON gitmem_decisions FOR ALL USING (auth.role() = 'service_role');
|
|
449
|
+
DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_decisions;
|
|
450
|
+
CREATE POLICY "Block anonymous access" ON gitmem_decisions FOR ALL USING (auth.role() != 'anon');
|
|
451
|
+
-- gitmem_scar_usage
|
|
452
|
+
DROP POLICY IF EXISTS "Service role full access" ON gitmem_scar_usage;
|
|
453
|
+
CREATE POLICY "Service role full access" ON gitmem_scar_usage FOR ALL USING (auth.role() = 'service_role');
|
|
454
|
+
DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_scar_usage;
|
|
455
|
+
CREATE POLICY "Block anonymous access" ON gitmem_scar_usage FOR ALL USING (auth.role() != 'anon');
|
|
456
|
+
-- gitmem_threads
|
|
457
|
+
DROP POLICY IF EXISTS "Service role full access" ON gitmem_threads;
|
|
458
|
+
CREATE POLICY "Service role full access" ON gitmem_threads FOR ALL USING (auth.role() = 'service_role');
|
|
459
|
+
DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_threads;
|
|
460
|
+
CREATE POLICY "Block anonymous access" ON gitmem_threads FOR ALL USING (auth.role() != 'anon');
|
|
461
|
+
-- knowledge_triples
|
|
462
|
+
DROP POLICY IF EXISTS "Service role full access" ON knowledge_triples;
|
|
463
|
+
CREATE POLICY "Service role full access" ON knowledge_triples FOR ALL USING (auth.role() = 'service_role');
|
|
464
|
+
DROP POLICY IF EXISTS "Block anonymous access" ON knowledge_triples;
|
|
465
|
+
CREATE POLICY "Block anonymous access" ON knowledge_triples FOR ALL USING (auth.role() != 'anon');
|
|
466
|
+
-- gitmem_query_metrics
|
|
467
|
+
DROP POLICY IF EXISTS "Service role full access" ON gitmem_query_metrics;
|
|
468
|
+
CREATE POLICY "Service role full access" ON gitmem_query_metrics FOR ALL USING (auth.role() = 'service_role');
|
|
469
|
+
DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_query_metrics;
|
|
470
|
+
CREATE POLICY "Block anonymous access" ON gitmem_query_metrics FOR ALL USING (auth.role() != 'anon');
|
|
471
|
+
-- scar_enforcement_variants
|
|
472
|
+
DROP POLICY IF EXISTS "Service role full access" ON scar_enforcement_variants;
|
|
473
|
+
CREATE POLICY "Service role full access" ON scar_enforcement_variants FOR ALL USING (auth.role() = 'service_role');
|
|
474
|
+
DROP POLICY IF EXISTS "Block anonymous access" ON scar_enforcement_variants;
|
|
475
|
+
CREATE POLICY "Block anonymous access" ON scar_enforcement_variants FOR ALL USING (auth.role() != 'anon');
|
|
476
|
+
END $$;
|
|
175
477
|
|
|
176
478
|
-- ============================================================================
|
|
177
479
|
-- Auto-update timestamps
|
|
@@ -184,10 +486,172 @@ BEGIN
|
|
|
184
486
|
END;
|
|
185
487
|
$$ LANGUAGE plpgsql;
|
|
186
488
|
|
|
489
|
+
DROP TRIGGER IF EXISTS gitmem_learnings_updated ON gitmem_learnings;
|
|
187
490
|
CREATE TRIGGER gitmem_learnings_updated
|
|
188
491
|
BEFORE UPDATE ON gitmem_learnings
|
|
189
492
|
FOR EACH ROW EXECUTE FUNCTION gitmem_update_timestamp();
|
|
190
493
|
|
|
494
|
+
DROP TRIGGER IF EXISTS gitmem_sessions_updated ON gitmem_sessions;
|
|
191
495
|
CREATE TRIGGER gitmem_sessions_updated
|
|
192
496
|
BEFORE UPDATE ON gitmem_sessions
|
|
193
497
|
FOR EACH ROW EXECUTE FUNCTION gitmem_update_timestamp();
|
|
498
|
+
|
|
499
|
+
-- ============================================================================
|
|
500
|
+
-- License Management Tables (deployed on GitMem's infrastructure, not user's)
|
|
501
|
+
-- These tables are included here for reference and for our own deployment.
|
|
502
|
+
-- Users do NOT need to run these — they exist on gitmem-api.supabase.co
|
|
503
|
+
-- ============================================================================
|
|
504
|
+
|
|
505
|
+
CREATE TABLE IF NOT EXISTS gitmem_licenses (
|
|
506
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
507
|
+
api_key TEXT UNIQUE NOT NULL,
|
|
508
|
+
tier TEXT NOT NULL DEFAULT 'pro' CHECK (tier IN ('pro', 'dev')),
|
|
509
|
+
owner_email TEXT,
|
|
510
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
511
|
+
max_activations INTEGER NOT NULL DEFAULT 3,
|
|
512
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
513
|
+
expires_at TIMESTAMPTZ,
|
|
514
|
+
last_validated_at TIMESTAMPTZ,
|
|
515
|
+
metadata JSONB DEFAULT '{}'
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
CREATE TABLE IF NOT EXISTS gitmem_license_activations (
|
|
519
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
520
|
+
license_id UUID NOT NULL REFERENCES gitmem_licenses(id) ON DELETE CASCADE,
|
|
521
|
+
install_id TEXT NOT NULL,
|
|
522
|
+
activated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
523
|
+
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
524
|
+
device_info JSONB DEFAULT '{}',
|
|
525
|
+
UNIQUE(license_id, install_id)
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
-- RLS: service role only (these are admin tables on our infrastructure)
|
|
529
|
+
ALTER TABLE gitmem_licenses ENABLE ROW LEVEL SECURITY;
|
|
530
|
+
ALTER TABLE gitmem_license_activations ENABLE ROW LEVEL SECURITY;
|
|
531
|
+
|
|
532
|
+
CREATE POLICY "Service role full access" ON gitmem_licenses
|
|
533
|
+
FOR ALL USING (auth.role() = 'service_role');
|
|
534
|
+
|
|
535
|
+
CREATE POLICY "Service role full access" ON gitmem_license_activations
|
|
536
|
+
FOR ALL USING (auth.role() = 'service_role');
|
|
537
|
+
|
|
538
|
+
-- Index for fast key lookup
|
|
539
|
+
CREATE INDEX IF NOT EXISTS idx_gitmem_licenses_api_key
|
|
540
|
+
ON gitmem_licenses (api_key);
|
|
541
|
+
|
|
542
|
+
CREATE INDEX IF NOT EXISTS idx_gitmem_license_activations_license
|
|
543
|
+
ON gitmem_license_activations (license_id);
|
|
544
|
+
|
|
545
|
+
-- ============================================================================
|
|
546
|
+
-- License Validation RPC
|
|
547
|
+
-- Checks key validity, device limit, registers/updates activation
|
|
548
|
+
-- ============================================================================
|
|
549
|
+
CREATE OR REPLACE FUNCTION gitmem_validate_license(p_api_key TEXT, p_install_id TEXT)
|
|
550
|
+
RETURNS TABLE(tier TEXT, valid BOOLEAN, message TEXT)
|
|
551
|
+
LANGUAGE plpgsql
|
|
552
|
+
SECURITY DEFINER
|
|
553
|
+
AS $$
|
|
554
|
+
DECLARE
|
|
555
|
+
v_license_id UUID;
|
|
556
|
+
v_tier TEXT;
|
|
557
|
+
v_is_active BOOLEAN;
|
|
558
|
+
v_max_activations INTEGER;
|
|
559
|
+
v_expires_at TIMESTAMPTZ;
|
|
560
|
+
v_current_activations INTEGER;
|
|
561
|
+
BEGIN
|
|
562
|
+
-- Look up the license
|
|
563
|
+
SELECT l.id, l.tier, l.is_active, l.max_activations, l.expires_at
|
|
564
|
+
INTO v_license_id, v_tier, v_is_active, v_max_activations, v_expires_at
|
|
565
|
+
FROM gitmem_licenses l
|
|
566
|
+
WHERE l.api_key = p_api_key;
|
|
567
|
+
|
|
568
|
+
-- Key not found
|
|
569
|
+
IF v_license_id IS NULL THEN
|
|
570
|
+
RETURN QUERY SELECT NULL::TEXT, false, 'Invalid license key'::TEXT;
|
|
571
|
+
RETURN;
|
|
572
|
+
END IF;
|
|
573
|
+
|
|
574
|
+
-- Key deactivated
|
|
575
|
+
IF NOT v_is_active THEN
|
|
576
|
+
RETURN QUERY SELECT NULL::TEXT, false, 'License has been deactivated'::TEXT;
|
|
577
|
+
RETURN;
|
|
578
|
+
END IF;
|
|
579
|
+
|
|
580
|
+
-- Key expired
|
|
581
|
+
IF v_expires_at IS NOT NULL AND v_expires_at < NOW() THEN
|
|
582
|
+
RETURN QUERY SELECT NULL::TEXT, false, 'License has expired'::TEXT;
|
|
583
|
+
RETURN;
|
|
584
|
+
END IF;
|
|
585
|
+
|
|
586
|
+
-- Count current activations (excluding this install_id if already registered)
|
|
587
|
+
SELECT COUNT(*)
|
|
588
|
+
INTO v_current_activations
|
|
589
|
+
FROM gitmem_license_activations a
|
|
590
|
+
WHERE a.license_id = v_license_id
|
|
591
|
+
AND a.install_id != p_install_id;
|
|
592
|
+
|
|
593
|
+
-- Check device limit (if this is a new device)
|
|
594
|
+
IF v_current_activations >= v_max_activations THEN
|
|
595
|
+
-- Check if this install_id already exists (re-validation)
|
|
596
|
+
IF NOT EXISTS (
|
|
597
|
+
SELECT 1 FROM gitmem_license_activations a
|
|
598
|
+
WHERE a.license_id = v_license_id AND a.install_id = p_install_id
|
|
599
|
+
) THEN
|
|
600
|
+
RETURN QUERY SELECT NULL::TEXT, false,
|
|
601
|
+
format('Device limit reached (%s/%s). Deactivate another device or contact support.', v_current_activations, v_max_activations)::TEXT;
|
|
602
|
+
RETURN;
|
|
603
|
+
END IF;
|
|
604
|
+
END IF;
|
|
605
|
+
|
|
606
|
+
-- Register or update activation
|
|
607
|
+
INSERT INTO gitmem_license_activations (license_id, install_id, last_seen_at)
|
|
608
|
+
VALUES (v_license_id, p_install_id, NOW())
|
|
609
|
+
ON CONFLICT (license_id, install_id)
|
|
610
|
+
DO UPDATE SET last_seen_at = NOW();
|
|
611
|
+
|
|
612
|
+
-- Update last_validated_at on the license
|
|
613
|
+
UPDATE gitmem_licenses SET last_validated_at = NOW() WHERE id = v_license_id;
|
|
614
|
+
|
|
615
|
+
-- Success
|
|
616
|
+
RETURN QUERY SELECT v_tier, true, 'Valid'::TEXT;
|
|
617
|
+
END;
|
|
618
|
+
$$;
|
|
619
|
+
|
|
620
|
+
-- ============================================================================
|
|
621
|
+
-- Device Deactivation RPC
|
|
622
|
+
-- Removes a device activation for a license key + install_id pair.
|
|
623
|
+
-- Called by `gitmem-mcp deactivate` to free up a device slot.
|
|
624
|
+
-- ============================================================================
|
|
625
|
+
CREATE OR REPLACE FUNCTION gitmem_deactivate_device(p_api_key TEXT, p_install_id TEXT)
|
|
626
|
+
RETURNS TABLE(success BOOLEAN, message TEXT)
|
|
627
|
+
LANGUAGE plpgsql
|
|
628
|
+
SECURITY DEFINER
|
|
629
|
+
AS $$
|
|
630
|
+
DECLARE
|
|
631
|
+
v_license_id UUID;
|
|
632
|
+
v_deleted INTEGER;
|
|
633
|
+
BEGIN
|
|
634
|
+
-- Look up the license
|
|
635
|
+
SELECT l.id INTO v_license_id
|
|
636
|
+
FROM gitmem_licenses l
|
|
637
|
+
WHERE l.api_key = p_api_key;
|
|
638
|
+
|
|
639
|
+
IF v_license_id IS NULL THEN
|
|
640
|
+
RETURN QUERY SELECT false, 'Invalid license key'::TEXT;
|
|
641
|
+
RETURN;
|
|
642
|
+
END IF;
|
|
643
|
+
|
|
644
|
+
-- Delete the activation for this install_id
|
|
645
|
+
DELETE FROM gitmem_license_activations a
|
|
646
|
+
WHERE a.license_id = v_license_id
|
|
647
|
+
AND a.install_id = p_install_id;
|
|
648
|
+
|
|
649
|
+
GET DIAGNOSTICS v_deleted = ROW_COUNT;
|
|
650
|
+
|
|
651
|
+
IF v_deleted > 0 THEN
|
|
652
|
+
RETURN QUERY SELECT true, 'Device deactivated'::TEXT;
|
|
653
|
+
ELSE
|
|
654
|
+
RETURN QUERY SELECT true, 'Device was not registered (already deactivated)'::TEXT;
|
|
655
|
+
END IF;
|
|
656
|
+
END;
|
|
657
|
+
$$;
|