@jhizzard/termdeck 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -169
- package/package.json +7 -6
- package/packages/cli/src/index.js +39 -1
- package/packages/cli/src/init-engram.js +344 -0
- package/packages/cli/src/init-rumen.js +425 -0
- package/packages/client/public/index.html +2 -2
- package/packages/server/src/setup/dotenv-io.js +116 -0
- package/packages/server/src/setup/engram-migrations/001_engram_tables.sql +116 -0
- package/packages/server/src/setup/engram-migrations/002_engram_search_function.sql +141 -0
- package/packages/server/src/setup/engram-migrations/003_engram_event_webhook.sql +28 -0
- package/packages/server/src/setup/engram-migrations/004_engram_match_count_cap_and_explain.sql +176 -0
- package/packages/server/src/setup/engram-migrations/005_v0_1_to_v0_2_upgrade.sql +23 -0
- package/packages/server/src/setup/engram-migrations/006_memory_status_rpc.sql +58 -0
- package/packages/server/src/setup/index.js +14 -0
- package/packages/server/src/setup/migrations.js +80 -0
- package/packages/server/src/setup/pg-runner.js +113 -0
- package/packages/server/src/setup/prompts.js +177 -0
- package/packages/server/src/setup/rumen/functions/rumen-tick/index.ts +85 -0
- package/packages/server/src/setup/rumen/functions/rumen-tick/tsconfig.json +14 -0
- package/packages/server/src/setup/rumen/migrations/001_rumen_tables.sql +91 -0
- package/packages/server/src/setup/rumen/migrations/002_pg_cron_schedule.sql +40 -0
- package/packages/server/src/setup/supabase-url.js +114 -0
- package/packages/server/src/setup/yaml-io.js +99 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
-- Engram v0.1 — memory_hybrid_search
|
|
2
|
+
--
|
|
3
|
+
-- Reciprocal rank fusion over full-text and semantic search, with the
|
|
4
|
+
-- three SQL-side fixes from RAG-MEMORY-IMPROVEMENTS-AND-TERMDECK-STRATEGY.md:
|
|
5
|
+
--
|
|
6
|
+
-- Fix 1 — Tiered recency decay by source_type. Architectural decisions
|
|
7
|
+
-- decay on a one-year half-life; bug fixes on a 30-day half-life;
|
|
8
|
+
-- session summaries and document chunks on a 14-day half-life.
|
|
9
|
+
--
|
|
10
|
+
-- Fix 3 — Source_type weighting. Decisions and architecture outrank
|
|
11
|
+
-- raw document chunks in the final fused score.
|
|
12
|
+
--
|
|
13
|
+
-- Fix 5 — Project affinity scoring. Exact project match multiplies the
|
|
14
|
+
-- score by 1.5x; mismatches are penalised 0.7x.
|
|
15
|
+
|
|
16
|
+
create or replace function memory_hybrid_search (
|
|
17
|
+
query_text text,
|
|
18
|
+
query_embedding vector(1536),
|
|
19
|
+
match_count int default 20,
|
|
20
|
+
full_text_weight float default 1.0,
|
|
21
|
+
semantic_weight float default 1.0,
|
|
22
|
+
rrf_k int default 60,
|
|
23
|
+
filter_project text default null,
|
|
24
|
+
filter_source_type text default null
|
|
25
|
+
)
|
|
26
|
+
returns table (
|
|
27
|
+
id uuid,
|
|
28
|
+
content text,
|
|
29
|
+
source_type text,
|
|
30
|
+
category text,
|
|
31
|
+
project text,
|
|
32
|
+
metadata jsonb,
|
|
33
|
+
score float,
|
|
34
|
+
created_at timestamptz
|
|
35
|
+
)
|
|
36
|
+
language sql stable
|
|
37
|
+
as $$
|
|
38
|
+
with candidates as (
|
|
39
|
+
select
|
|
40
|
+
m.id,
|
|
41
|
+
m.content,
|
|
42
|
+
m.source_type,
|
|
43
|
+
m.category,
|
|
44
|
+
m.project,
|
|
45
|
+
m.metadata,
|
|
46
|
+
m.created_at,
|
|
47
|
+
m.embedding,
|
|
48
|
+
ts_rank_cd(to_tsvector('english', m.content), plainto_tsquery('english', query_text))
|
|
49
|
+
as ft_rank,
|
|
50
|
+
1 - (m.embedding <=> query_embedding) as sem_rank,
|
|
51
|
+
extract(epoch from (now() - m.created_at))::float as age_seconds
|
|
52
|
+
from memory_items m
|
|
53
|
+
where m.is_active = true
|
|
54
|
+
and m.archived = false
|
|
55
|
+
and m.superseded_by is null
|
|
56
|
+
and m.embedding is not null
|
|
57
|
+
and (filter_project is null or m.project = filter_project)
|
|
58
|
+
and (filter_source_type is null or m.source_type = filter_source_type)
|
|
59
|
+
),
|
|
60
|
+
ft_ranked as (
|
|
61
|
+
select id, row_number() over (order by ft_rank desc nulls last) as rank
|
|
62
|
+
from candidates where ft_rank > 0
|
|
63
|
+
),
|
|
64
|
+
sem_ranked as (
|
|
65
|
+
select id, row_number() over (order by sem_rank desc nulls last) as rank
|
|
66
|
+
from candidates
|
|
67
|
+
),
|
|
68
|
+
fused as (
|
|
69
|
+
select
|
|
70
|
+
c.id,
|
|
71
|
+
c.content,
|
|
72
|
+
c.source_type,
|
|
73
|
+
c.category,
|
|
74
|
+
c.project,
|
|
75
|
+
c.metadata,
|
|
76
|
+
c.created_at,
|
|
77
|
+
c.age_seconds,
|
|
78
|
+
-- RRF base score
|
|
79
|
+
coalesce(full_text_weight / (rrf_k + ft.rank), 0.0) +
|
|
80
|
+
coalesce(semantic_weight / (rrf_k + sr.rank), 0.0) as base_score
|
|
81
|
+
from candidates c
|
|
82
|
+
left join ft_ranked ft on ft.id = c.id
|
|
83
|
+
left join sem_ranked sr on sr.id = c.id
|
|
84
|
+
),
|
|
85
|
+
scored as (
|
|
86
|
+
select
|
|
87
|
+
f.id,
|
|
88
|
+
f.content,
|
|
89
|
+
f.source_type,
|
|
90
|
+
f.category,
|
|
91
|
+
f.project,
|
|
92
|
+
f.metadata,
|
|
93
|
+
f.created_at,
|
|
94
|
+
f.base_score
|
|
95
|
+
-- Fix 1: tiered recency decay by source_type
|
|
96
|
+
* case f.source_type
|
|
97
|
+
when 'decision' then 1.0 / (1.0 + f.age_seconds / (365.0 * 86400.0))
|
|
98
|
+
when 'architecture' then 1.0 / (1.0 + f.age_seconds / (365.0 * 86400.0))
|
|
99
|
+
when 'preference' then 1.0 / (1.0 + f.age_seconds / (365.0 * 86400.0))
|
|
100
|
+
when 'fact' then 1.0 / (1.0 + f.age_seconds / ( 90.0 * 86400.0))
|
|
101
|
+
when 'convention' then 1.0 / (1.0 + f.age_seconds / ( 90.0 * 86400.0))
|
|
102
|
+
when 'bug_fix' then 1.0 / (1.0 + f.age_seconds / ( 30.0 * 86400.0))
|
|
103
|
+
when 'debugging' then 1.0 / (1.0 + f.age_seconds / ( 30.0 * 86400.0))
|
|
104
|
+
when 'session_summary' then 1.0 / (1.0 + f.age_seconds / ( 14.0 * 86400.0))
|
|
105
|
+
when 'document_chunk' then 1.0 / (1.0 + f.age_seconds / ( 14.0 * 86400.0))
|
|
106
|
+
when 'code_context' then 1.0 / (1.0 + f.age_seconds / ( 14.0 * 86400.0))
|
|
107
|
+
else 1.0 / (1.0 + f.age_seconds / ( 30.0 * 86400.0))
|
|
108
|
+
end
|
|
109
|
+
-- Fix 3: source_type weighting
|
|
110
|
+
* case f.source_type
|
|
111
|
+
when 'decision' then 1.5
|
|
112
|
+
when 'architecture' then 1.4
|
|
113
|
+
when 'bug_fix' then 1.3
|
|
114
|
+
when 'preference' then 1.2
|
|
115
|
+
when 'fact' then 1.0
|
|
116
|
+
when 'document_chunk' then 0.6
|
|
117
|
+
else 1.0
|
|
118
|
+
end
|
|
119
|
+
-- Fix 5: project affinity scoring
|
|
120
|
+
* case
|
|
121
|
+
when filter_project is null then 1.0
|
|
122
|
+
when f.project = filter_project then 1.5
|
|
123
|
+
when f.project = 'global' then 1.0
|
|
124
|
+
else 0.7
|
|
125
|
+
end
|
|
126
|
+
as score
|
|
127
|
+
from fused f
|
|
128
|
+
)
|
|
129
|
+
select
|
|
130
|
+
s.id,
|
|
131
|
+
s.content,
|
|
132
|
+
s.source_type,
|
|
133
|
+
s.category,
|
|
134
|
+
s.project,
|
|
135
|
+
s.metadata,
|
|
136
|
+
s.score,
|
|
137
|
+
s.created_at
|
|
138
|
+
from scored s
|
|
139
|
+
order by s.score desc
|
|
140
|
+
limit match_count;
|
|
141
|
+
$$;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- Engram v0.1 — real-time event webhook (Fix 6)
|
|
2
|
+
--
|
|
3
|
+
-- Fix 6 from RAG-MEMORY-IMPROVEMENTS-AND-TERMDECK-STRATEGY.md is a
|
|
4
|
+
-- real-time event intake path so TermDeck (or any other client) can
|
|
5
|
+
-- POST terminal events — "server started on :8080", "tests failing",
|
|
6
|
+
-- "error detected" — and have them land in memory immediately.
|
|
7
|
+
--
|
|
8
|
+
-- That intake is implemented as an HTTP endpoint inside the Engram MCP
|
|
9
|
+
-- server process, not as a SQL trigger. This file exists as a placeholder
|
|
10
|
+
-- so the migration history is explicit and future database-side changes
|
|
11
|
+
-- (e.g. an events queue table for async ingestion) have a home.
|
|
12
|
+
--
|
|
13
|
+
-- If you want to add a durable event queue later, add it below this line.
|
|
14
|
+
|
|
15
|
+
-- Example future shape (commented out — not applied):
|
|
16
|
+
--
|
|
17
|
+
-- create table if not exists memory_events (
|
|
18
|
+
-- id uuid primary key default gen_random_uuid(),
|
|
19
|
+
-- project text not null,
|
|
20
|
+
-- source text not null,
|
|
21
|
+
-- event_type text not null,
|
|
22
|
+
-- payload jsonb not null default '{}'::jsonb,
|
|
23
|
+
-- processed boolean not null default false,
|
|
24
|
+
-- created_at timestamptz not null default now()
|
|
25
|
+
-- );
|
|
26
|
+
--
|
|
27
|
+
-- create index if not exists memory_events_unprocessed_idx
|
|
28
|
+
-- on memory_events(created_at) where processed = false;
|
package/packages/server/src/setup/engram-migrations/004_engram_match_count_cap_and_explain.sql
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
-- Engram v0.2 — match_count cap + EXPLAIN variant
|
|
2
|
+
--
|
|
3
|
+
-- Two changes to the search surface:
|
|
4
|
+
--
|
|
5
|
+
-- 1. memory_hybrid_search gains a configurable cap on `match_count`.
|
|
6
|
+
-- Default cap: 200. The original function was unbounded, which risks
|
|
7
|
+
-- runaway queries at scale (10k+ rows pulled per call).
|
|
8
|
+
--
|
|
9
|
+
-- Override per-database: ALTER DATABASE your_db SET engram.max_match_count = 500;
|
|
10
|
+
-- Override per-session: SET engram.max_match_count = 500;
|
|
11
|
+
-- Leave unset: cap defaults to 200.
|
|
12
|
+
--
|
|
13
|
+
-- 2. A new function `memory_hybrid_search_explain` that returns
|
|
14
|
+
-- EXPLAIN (ANALYZE, BUFFERS) output for an equivalent call. Used by
|
|
15
|
+
-- `engram diagnose` to troubleshoot slow recall queries.
|
|
16
|
+
--
|
|
17
|
+
-- Rerun-safe: CREATE OR REPLACE on both.
|
|
18
|
+
|
|
19
|
+
-- ── memory_hybrid_search ─────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
create or replace function memory_hybrid_search (
|
|
22
|
+
query_text text,
|
|
23
|
+
query_embedding vector(1536),
|
|
24
|
+
match_count int default 20,
|
|
25
|
+
full_text_weight float default 1.0,
|
|
26
|
+
semantic_weight float default 1.0,
|
|
27
|
+
rrf_k int default 60,
|
|
28
|
+
filter_project text default null,
|
|
29
|
+
filter_source_type text default null
|
|
30
|
+
)
|
|
31
|
+
returns table (
|
|
32
|
+
id uuid,
|
|
33
|
+
content text,
|
|
34
|
+
source_type text,
|
|
35
|
+
category text,
|
|
36
|
+
project text,
|
|
37
|
+
metadata jsonb,
|
|
38
|
+
score float,
|
|
39
|
+
created_at timestamptz
|
|
40
|
+
)
|
|
41
|
+
language sql stable
|
|
42
|
+
as $$
|
|
43
|
+
with candidates as (
|
|
44
|
+
select
|
|
45
|
+
m.id,
|
|
46
|
+
m.content,
|
|
47
|
+
m.source_type,
|
|
48
|
+
m.category,
|
|
49
|
+
m.project,
|
|
50
|
+
m.metadata,
|
|
51
|
+
m.created_at,
|
|
52
|
+
m.embedding,
|
|
53
|
+
ts_rank_cd(to_tsvector('english', m.content), plainto_tsquery('english', query_text))
|
|
54
|
+
as ft_rank,
|
|
55
|
+
1 - (m.embedding <=> query_embedding) as sem_rank,
|
|
56
|
+
extract(epoch from (now() - m.created_at))::float as age_seconds
|
|
57
|
+
from memory_items m
|
|
58
|
+
where m.is_active = true
|
|
59
|
+
and m.archived = false
|
|
60
|
+
and m.superseded_by is null
|
|
61
|
+
and m.embedding is not null
|
|
62
|
+
and (filter_project is null or m.project = filter_project)
|
|
63
|
+
and (filter_source_type is null or m.source_type = filter_source_type)
|
|
64
|
+
),
|
|
65
|
+
ft_ranked as (
|
|
66
|
+
select id, row_number() over (order by ft_rank desc nulls last) as rank
|
|
67
|
+
from candidates where ft_rank > 0
|
|
68
|
+
),
|
|
69
|
+
sem_ranked as (
|
|
70
|
+
select id, row_number() over (order by sem_rank desc nulls last) as rank
|
|
71
|
+
from candidates
|
|
72
|
+
),
|
|
73
|
+
fused as (
|
|
74
|
+
select
|
|
75
|
+
c.id,
|
|
76
|
+
c.content,
|
|
77
|
+
c.source_type,
|
|
78
|
+
c.category,
|
|
79
|
+
c.project,
|
|
80
|
+
c.metadata,
|
|
81
|
+
c.created_at,
|
|
82
|
+
c.age_seconds,
|
|
83
|
+
coalesce(full_text_weight / (rrf_k + ft.rank), 0.0) +
|
|
84
|
+
coalesce(semantic_weight / (rrf_k + sr.rank), 0.0) as base_score
|
|
85
|
+
from candidates c
|
|
86
|
+
left join ft_ranked ft on ft.id = c.id
|
|
87
|
+
left join sem_ranked sr on sr.id = c.id
|
|
88
|
+
),
|
|
89
|
+
scored as (
|
|
90
|
+
select
|
|
91
|
+
f.id,
|
|
92
|
+
f.content,
|
|
93
|
+
f.source_type,
|
|
94
|
+
f.category,
|
|
95
|
+
f.project,
|
|
96
|
+
f.metadata,
|
|
97
|
+
f.created_at,
|
|
98
|
+
f.base_score
|
|
99
|
+
* case f.source_type
|
|
100
|
+
when 'decision' then 1.0 / (1.0 + f.age_seconds / (365.0 * 86400.0))
|
|
101
|
+
when 'architecture' then 1.0 / (1.0 + f.age_seconds / (365.0 * 86400.0))
|
|
102
|
+
when 'preference' then 1.0 / (1.0 + f.age_seconds / (365.0 * 86400.0))
|
|
103
|
+
when 'fact' then 1.0 / (1.0 + f.age_seconds / ( 90.0 * 86400.0))
|
|
104
|
+
when 'convention' then 1.0 / (1.0 + f.age_seconds / ( 90.0 * 86400.0))
|
|
105
|
+
when 'bug_fix' then 1.0 / (1.0 + f.age_seconds / ( 30.0 * 86400.0))
|
|
106
|
+
when 'debugging' then 1.0 / (1.0 + f.age_seconds / ( 30.0 * 86400.0))
|
|
107
|
+
when 'session_summary' then 1.0 / (1.0 + f.age_seconds / ( 14.0 * 86400.0))
|
|
108
|
+
when 'document_chunk' then 1.0 / (1.0 + f.age_seconds / ( 14.0 * 86400.0))
|
|
109
|
+
when 'code_context' then 1.0 / (1.0 + f.age_seconds / ( 14.0 * 86400.0))
|
|
110
|
+
else 1.0 / (1.0 + f.age_seconds / ( 30.0 * 86400.0))
|
|
111
|
+
end
|
|
112
|
+
* case f.source_type
|
|
113
|
+
when 'decision' then 1.5
|
|
114
|
+
when 'architecture' then 1.4
|
|
115
|
+
when 'bug_fix' then 1.3
|
|
116
|
+
when 'preference' then 1.2
|
|
117
|
+
when 'fact' then 1.0
|
|
118
|
+
when 'document_chunk' then 0.6
|
|
119
|
+
else 1.0
|
|
120
|
+
end
|
|
121
|
+
* case
|
|
122
|
+
when filter_project is null then 1.0
|
|
123
|
+
when f.project = filter_project then 1.5
|
|
124
|
+
when f.project = 'global' then 1.0
|
|
125
|
+
else 0.7
|
|
126
|
+
end
|
|
127
|
+
as score
|
|
128
|
+
from fused f
|
|
129
|
+
)
|
|
130
|
+
select
|
|
131
|
+
s.id,
|
|
132
|
+
s.content,
|
|
133
|
+
s.source_type,
|
|
134
|
+
s.category,
|
|
135
|
+
s.project,
|
|
136
|
+
s.metadata,
|
|
137
|
+
s.score,
|
|
138
|
+
s.created_at
|
|
139
|
+
from scored s
|
|
140
|
+
order by s.score desc
|
|
141
|
+
limit least(
|
|
142
|
+
greatest(match_count, 1),
|
|
143
|
+
coalesce(nullif(current_setting('engram.max_match_count', true), '')::int, 200)
|
|
144
|
+
);
|
|
145
|
+
$$;
|
|
146
|
+
|
|
147
|
+
-- ── memory_hybrid_search_explain ─────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
create or replace function memory_hybrid_search_explain (
|
|
150
|
+
query_text text,
|
|
151
|
+
query_embedding vector(1536),
|
|
152
|
+
match_count int default 20,
|
|
153
|
+
full_text_weight float default 1.0,
|
|
154
|
+
semantic_weight float default 1.0,
|
|
155
|
+
rrf_k int default 60,
|
|
156
|
+
filter_project text default null,
|
|
157
|
+
filter_source_type text default null
|
|
158
|
+
)
|
|
159
|
+
returns setof text
|
|
160
|
+
language plpgsql
|
|
161
|
+
as $$
|
|
162
|
+
begin
|
|
163
|
+
return query execute
|
|
164
|
+
'explain (analyze, buffers, format text) '
|
|
165
|
+
|| 'select * from memory_hybrid_search($1, $2, $3, $4, $5, $6, $7, $8)'
|
|
166
|
+
using
|
|
167
|
+
query_text,
|
|
168
|
+
query_embedding,
|
|
169
|
+
match_count,
|
|
170
|
+
full_text_weight,
|
|
171
|
+
semantic_weight,
|
|
172
|
+
rrf_k,
|
|
173
|
+
filter_project,
|
|
174
|
+
filter_source_type;
|
|
175
|
+
end;
|
|
176
|
+
$$;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Engram v0.2 — minimal additive delta for stores provisioned against the
|
|
2
|
+
-- original rag-system schema.
|
|
3
|
+
--
|
|
4
|
+
-- This migration is idempotent and non-destructive:
|
|
5
|
+
-- • adds only the single column (`archived`) that Engram v0.2 reads/writes
|
|
6
|
+
-- but which is absent from the original rag-system `memory_items` table;
|
|
7
|
+
-- • re-creates the two partial indexes under v2 names so they cannot
|
|
8
|
+
-- collide with any pre-existing same-named index that uses a different
|
|
9
|
+
-- `where` predicate;
|
|
10
|
+
-- • does NOT replace `memory_hybrid_search`, `match_memories`, or any
|
|
11
|
+
-- other SQL function — those remain on their production versions.
|
|
12
|
+
--
|
|
13
|
+
-- Apply once, in the Supabase SQL editor, against any existing store that
|
|
14
|
+
-- pre-dates Engram v0.2's `archived` soft-delete column.
|
|
15
|
+
|
|
16
|
+
alter table memory_items
|
|
17
|
+
add column if not exists archived boolean not null default false;
|
|
18
|
+
|
|
19
|
+
create index if not exists memory_items_project_idx_v2
|
|
20
|
+
on memory_items(project) where is_active = true and archived = false;
|
|
21
|
+
|
|
22
|
+
create index if not exists memory_items_source_type_idx_v2
|
|
23
|
+
on memory_items(source_type) where is_active = true and archived = false;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- Engram migration 006 — memory_status_aggregation RPC
|
|
2
|
+
--
|
|
3
|
+
-- Why: `memoryStatus()` previously did a plain
|
|
4
|
+
-- supabase.from('memory_items').select('project, source_type, category')
|
|
5
|
+
-- which hits PostgREST's default 1000-row cap. On a store with 3,397 active
|
|
6
|
+
-- rows the `by_project` / `by_source_type` / `by_category` histograms only
|
|
7
|
+
-- summed to ~1000 even though `total_active` was correct. Pushing the GROUP
|
|
8
|
+
-- BY server-side eliminates the cap and saves the round-trip of streaming
|
|
9
|
+
-- every row to the client just to count them.
|
|
10
|
+
--
|
|
11
|
+
-- Safe to re-run — CREATE OR REPLACE.
|
|
12
|
+
|
|
13
|
+
create or replace function memory_status_aggregation()
|
|
14
|
+
returns table (
|
|
15
|
+
total_active bigint,
|
|
16
|
+
sessions bigint,
|
|
17
|
+
by_project jsonb,
|
|
18
|
+
by_source_type jsonb,
|
|
19
|
+
by_category jsonb
|
|
20
|
+
)
|
|
21
|
+
language sql
|
|
22
|
+
stable
|
|
23
|
+
as $$
|
|
24
|
+
select
|
|
25
|
+
(select count(*)::bigint from memory_items
|
|
26
|
+
where is_active = true and archived = false) as total_active,
|
|
27
|
+
(select count(*)::bigint from memory_sessions) as sessions,
|
|
28
|
+
coalesce(
|
|
29
|
+
(select jsonb_object_agg(project, c) from (
|
|
30
|
+
select project, count(*)::bigint as c
|
|
31
|
+
from memory_items
|
|
32
|
+
where is_active = true and archived = false
|
|
33
|
+
group by project
|
|
34
|
+
) p),
|
|
35
|
+
'{}'::jsonb
|
|
36
|
+
) as by_project,
|
|
37
|
+
coalesce(
|
|
38
|
+
(select jsonb_object_agg(source_type, c) from (
|
|
39
|
+
select source_type, count(*)::bigint as c
|
|
40
|
+
from memory_items
|
|
41
|
+
where is_active = true and archived = false
|
|
42
|
+
group by source_type
|
|
43
|
+
) s),
|
|
44
|
+
'{}'::jsonb
|
|
45
|
+
) as by_source_type,
|
|
46
|
+
coalesce(
|
|
47
|
+
(select jsonb_object_agg(coalesce(category, 'uncategorized'), c) from (
|
|
48
|
+
select category, count(*)::bigint as c
|
|
49
|
+
from memory_items
|
|
50
|
+
where is_active = true and archived = false
|
|
51
|
+
group by category
|
|
52
|
+
) cat),
|
|
53
|
+
'{}'::jsonb
|
|
54
|
+
) as by_category;
|
|
55
|
+
$$;
|
|
56
|
+
|
|
57
|
+
-- Ensure the service role (and anon, if you allow it) can call the RPC.
|
|
58
|
+
grant execute on function memory_status_aggregation() to anon, authenticated, service_role;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Aggregate export for the `termdeck init` setup helpers.
|
|
2
|
+
//
|
|
3
|
+
// The init wizards live in packages/cli/src/ but all the heavy lifting
|
|
4
|
+
// (prompting, reading/writing config, applying migrations) lives here so
|
|
5
|
+
// the CLI files can stay short, linear, and easy to audit.
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
prompts: require('./prompts'),
|
|
9
|
+
dotenv: require('./dotenv-io'),
|
|
10
|
+
yaml: require('./yaml-io'),
|
|
11
|
+
supabaseUrl: require('./supabase-url'),
|
|
12
|
+
migrations: require('./migrations'),
|
|
13
|
+
pgRunner: require('./pg-runner')
|
|
14
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Discover the SQL migration files that ship bundled inside the TermDeck
|
|
2
|
+
// package. Both init wizards call this — init-engram for the six Engram
|
|
3
|
+
// migrations, init-rumen for the two Rumen migrations.
|
|
4
|
+
//
|
|
5
|
+
// The wizards intentionally do NOT fall back to a sibling `../../engram`
|
|
6
|
+
// working copy. Resolution order:
|
|
7
|
+
//
|
|
8
|
+
// 1. Files bundled at `packages/server/src/setup/engram-migrations/*.sql`
|
|
9
|
+
// (this directory is covered by the root package.json `files` glob).
|
|
10
|
+
// 2. Files at `node_modules/@jhizzard/engram/migrations/*.sql` if that
|
|
11
|
+
// package is installed alongside TermDeck (future-proof path — shipping
|
|
12
|
+
// `@jhizzard/engram` as an optional peer would let us drop the bundled
|
|
13
|
+
// copy).
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const SETUP_DIR = __dirname;
|
|
19
|
+
|
|
20
|
+
function listBundled(subdir) {
|
|
21
|
+
const dir = path.join(SETUP_DIR, subdir);
|
|
22
|
+
if (!fs.existsSync(dir)) return [];
|
|
23
|
+
return fs.readdirSync(dir)
|
|
24
|
+
.filter((f) => f.toLowerCase().endsWith('.sql'))
|
|
25
|
+
.sort()
|
|
26
|
+
.map((f) => path.join(dir, f));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tryNodeModules(packageName, migrationSubdir = 'migrations') {
|
|
30
|
+
try {
|
|
31
|
+
// Resolve the package's main file, then look for a migrations sibling dir.
|
|
32
|
+
const pkgJsonPath = require.resolve(`${packageName}/package.json`, {
|
|
33
|
+
paths: [process.cwd(), SETUP_DIR]
|
|
34
|
+
});
|
|
35
|
+
const pkgDir = path.dirname(pkgJsonPath);
|
|
36
|
+
const migrationDir = path.join(pkgDir, migrationSubdir);
|
|
37
|
+
if (!fs.existsSync(migrationDir)) return [];
|
|
38
|
+
return fs.readdirSync(migrationDir)
|
|
39
|
+
.filter((f) => f.toLowerCase().endsWith('.sql'))
|
|
40
|
+
.sort()
|
|
41
|
+
.map((f) => path.join(migrationDir, f));
|
|
42
|
+
} catch (_err) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function listEngramMigrations() {
|
|
48
|
+
const fromNm = tryNodeModules('@jhizzard/engram');
|
|
49
|
+
if (fromNm.length > 0) return fromNm;
|
|
50
|
+
return listBundled('engram-migrations');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listRumenMigrations() {
|
|
54
|
+
const fromNm = tryNodeModules('@jhizzard/rumen');
|
|
55
|
+
if (fromNm.length > 0) return fromNm;
|
|
56
|
+
return listBundled(path.join('rumen', 'migrations'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rumenFunctionDir() {
|
|
60
|
+
// Same resolution order.
|
|
61
|
+
try {
|
|
62
|
+
const pkgJsonPath = require.resolve('@jhizzard/rumen/package.json', {
|
|
63
|
+
paths: [process.cwd(), SETUP_DIR]
|
|
64
|
+
});
|
|
65
|
+
const candidate = path.join(path.dirname(pkgJsonPath), 'supabase', 'functions', 'rumen-tick');
|
|
66
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
67
|
+
} catch (_err) { /* fallthrough */ }
|
|
68
|
+
return path.join(SETUP_DIR, 'rumen', 'functions', 'rumen-tick');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readFile(filepath) {
|
|
72
|
+
return fs.readFileSync(filepath, 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
listEngramMigrations,
|
|
77
|
+
listRumenMigrations,
|
|
78
|
+
rumenFunctionDir,
|
|
79
|
+
readFile
|
|
80
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Thin wrapper around node-postgres for applying SQL migration files.
|
|
2
|
+
//
|
|
3
|
+
// `pg` is a runtime dep of the top-level package and must be present when the
|
|
4
|
+
// init wizards run. If `require('pg')` fails we throw an actionable error
|
|
5
|
+
// pointing the user at `npm install` — TermDeck's normal install path pulls
|
|
6
|
+
// `pg` in automatically, so this only fires on partial / broken installs.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
function loadPg() {
|
|
12
|
+
try {
|
|
13
|
+
return require('pg');
|
|
14
|
+
} catch (err) {
|
|
15
|
+
const e = new Error(
|
|
16
|
+
"Could not load node-postgres ('pg'). TermDeck's init wizards need it to " +
|
|
17
|
+
'apply migrations. Run `npm install` (or reinstall TermDeck with `npm install -g @jhizzard/termdeck`) and try again.'
|
|
18
|
+
);
|
|
19
|
+
e.cause = err;
|
|
20
|
+
throw e;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Connect to the given postgres URL and return a live Client. The caller is
|
|
25
|
+
// responsible for `await client.end()`.
|
|
26
|
+
async function connect(databaseUrl) {
|
|
27
|
+
const { Client } = loadPg();
|
|
28
|
+
const client = new Client({
|
|
29
|
+
connectionString: databaseUrl,
|
|
30
|
+
// Supabase pooler endpoints present a TLS cert that Node's default CA
|
|
31
|
+
// store accepts, but some users hit verification issues behind corporate
|
|
32
|
+
// proxies. Enable TLS with rejectUnauthorized=false so the wizard works
|
|
33
|
+
// in those environments — we're talking to a Supabase host we already
|
|
34
|
+
// trust by project ref, and the connection is still encrypted.
|
|
35
|
+
ssl: { rejectUnauthorized: false },
|
|
36
|
+
// Keep connection attempts bounded so a misconfigured URL fails fast.
|
|
37
|
+
connectionTimeoutMillis: 15000
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
await client.connect();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const friendly = mapConnectError(err);
|
|
43
|
+
const e = new Error(`Could not connect to Postgres: ${friendly}`);
|
|
44
|
+
e.cause = err;
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
return client;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mapConnectError(err) {
|
|
51
|
+
const msg = err && err.message ? err.message : String(err);
|
|
52
|
+
if (/ENOTFOUND/.test(msg)) return 'host not found (check the project URL)';
|
|
53
|
+
if (/ECONNREFUSED/.test(msg)) return 'connection refused (wrong port?)';
|
|
54
|
+
if (/password authentication failed/i.test(msg)) return 'password authentication failed (check the service_role / db password)';
|
|
55
|
+
if (/SASL/i.test(msg)) return 'authentication failed (check username, it should be `postgres` or `postgres.<project-ref>`)';
|
|
56
|
+
if (/timeout/i.test(msg)) return 'connect timed out after 15s (network issue or wrong host)';
|
|
57
|
+
return msg;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Read a migration file and execute it as a single batched query. Returns
|
|
61
|
+
// `{ ok, file, elapsedMs, rowCount, skipped?, error? }`.
|
|
62
|
+
//
|
|
63
|
+
// A migration "skipped" means the file body included a marker the runner
|
|
64
|
+
// detected as already-applied (reserved for future use — currently not wired).
|
|
65
|
+
async function applyFile(client, filepath) {
|
|
66
|
+
const started = Date.now();
|
|
67
|
+
const sql = fs.readFileSync(filepath, 'utf-8');
|
|
68
|
+
const base = path.basename(filepath);
|
|
69
|
+
try {
|
|
70
|
+
const result = await client.query(sql);
|
|
71
|
+
const rowCount = Array.isArray(result)
|
|
72
|
+
? result.reduce((sum, r) => sum + (r && r.rowCount ? r.rowCount : 0), 0)
|
|
73
|
+
: (result && result.rowCount) || 0;
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
file: base,
|
|
77
|
+
elapsedMs: Date.now() - started,
|
|
78
|
+
rowCount
|
|
79
|
+
};
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
file: base,
|
|
84
|
+
elapsedMs: Date.now() - started,
|
|
85
|
+
error: err && err.message ? err.message : String(err)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Apply a list of migration files in order. Returns an array of per-file
|
|
91
|
+
// results. Stops on first failure unless `continueOnError: true` is passed.
|
|
92
|
+
async function applyAll(client, files, { continueOnError = false } = {}) {
|
|
93
|
+
const results = [];
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const result = await applyFile(client, file);
|
|
96
|
+
results.push(result);
|
|
97
|
+
if (!result.ok && !continueOnError) break;
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Run a single SQL string (not a file). Useful for parameterized checks like
|
|
103
|
+
// `SELECT COUNT(*) FROM memory_items`.
|
|
104
|
+
async function run(client, sql, params = []) {
|
|
105
|
+
return client.query(sql, params);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
connect,
|
|
110
|
+
applyFile,
|
|
111
|
+
applyAll,
|
|
112
|
+
run
|
|
113
|
+
};
|