@jhizzard/termdeck 1.0.0 → 1.0.2
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/package.json +2 -1
- package/packages/cli/src/init-mnestra.js +149 -1
- package/packages/cli/src/init-rumen.js +370 -35
- package/packages/server/src/setup/audit-upgrade.js +425 -0
- package/packages/server/src/setup/index.js +2 -1
- package/packages/server/src/setup/mnestra-migrations/013_reclassify_uncertain.sql +39 -0
- package/packages/server/src/setup/mnestra-migrations/014_explicit_grants.sql +46 -0
- package/packages/server/src/setup/mnestra-migrations/015_source_agent.sql +51 -0
- package/packages/server/src/setup/mnestra-migrations/016_mnestra_doctor_probes.sql +117 -0
- package/packages/server/src/setup/mnestra-migrations/017_memory_sessions_session_metadata.sql +94 -0
- package/packages/server/src/setup/preconditions.js +36 -4
- package/packages/server/src/setup/rumen/functions/graph-inference/index.ts +6 -2
- package/packages/server/src/setup/rumen/functions/rumen-tick/index.ts +6 -2
- package/packages/stack-installer/README.md +73 -0
- package/packages/stack-installer/assets/hooks/README.md +172 -0
- package/packages/stack-installer/assets/hooks/memory-session-end.js +740 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
-- Mnestra v0.4.1 — `mnestra doctor` SECURITY DEFINER probe wrappers
|
|
2
|
+
--
|
|
3
|
+
-- Sprint 51.5 T2 (TermDeck). Adds five SECURITY DEFINER helper functions
|
|
4
|
+
-- so the `mnestra doctor` subcommand (running under the supabase
|
|
5
|
+
-- service_role) can probe `cron.job_run_details`, `cron.job`, `vault.secrets`,
|
|
6
|
+
-- `information_schema.columns`, and `pg_proc` without granting raw schema
|
|
7
|
+
-- access to service_role.
|
|
8
|
+
--
|
|
9
|
+
-- Why SECURITY DEFINER: by default `cron.*` and `vault.*` are owned by
|
|
10
|
+
-- the `postgres` role and unreadable from service_role. The classical
|
|
11
|
+
-- alternative is to `grant usage on schema cron to service_role; grant
|
|
12
|
+
-- select on cron.job_run_details to service_role; …` — broader privilege
|
|
13
|
+
-- expansion than this lane needs. SECURITY DEFINER lets us read exactly
|
|
14
|
+
-- what the doctor needs without exposing the entire cron/vault surface.
|
|
15
|
+
--
|
|
16
|
+
-- Idempotent: every function uses CREATE OR REPLACE; every GRANT is
|
|
17
|
+
-- safe to re-run. No data mutation. Safe to re-apply on every install.
|
|
18
|
+
|
|
19
|
+
-- ── 1. cron.job_run_details lookup ───────────────────────────────────────
|
|
20
|
+
--
|
|
21
|
+
-- Returns the most recent N runs of a named cron job, projecting only
|
|
22
|
+
-- the columns the doctor needs (status, start/end timestamps, return
|
|
23
|
+
-- message). The doctor parses `return_message` for the rumen-tick /
|
|
24
|
+
-- graph-inference all-zeros pattern.
|
|
25
|
+
|
|
26
|
+
create or replace function mnestra_doctor_cron_runs(
|
|
27
|
+
p_jobname text,
|
|
28
|
+
p_limit int default 10
|
|
29
|
+
)
|
|
30
|
+
returns table (
|
|
31
|
+
jobname text,
|
|
32
|
+
status text,
|
|
33
|
+
start_time timestamptz,
|
|
34
|
+
end_time timestamptz,
|
|
35
|
+
return_message text
|
|
36
|
+
)
|
|
37
|
+
language sql
|
|
38
|
+
security definer
|
|
39
|
+
set search_path = cron, public
|
|
40
|
+
as $$
|
|
41
|
+
select j.jobname, d.status, d.start_time, d.end_time, d.return_message
|
|
42
|
+
from cron.job_run_details d
|
|
43
|
+
join cron.job j on j.jobid = d.jobid
|
|
44
|
+
where j.jobname = p_jobname
|
|
45
|
+
order by d.start_time desc
|
|
46
|
+
limit greatest(coalesce(p_limit, 10), 1);
|
|
47
|
+
$$;
|
|
48
|
+
|
|
49
|
+
-- ── 2. column existence probe (public schema only) ───────────────────────
|
|
50
|
+
|
|
51
|
+
create or replace function mnestra_doctor_column_exists(
|
|
52
|
+
p_table text,
|
|
53
|
+
p_column text
|
|
54
|
+
)
|
|
55
|
+
returns boolean
|
|
56
|
+
language sql
|
|
57
|
+
security definer
|
|
58
|
+
set search_path = public
|
|
59
|
+
as $$
|
|
60
|
+
select exists (
|
|
61
|
+
select 1
|
|
62
|
+
from information_schema.columns
|
|
63
|
+
where table_schema = 'public'
|
|
64
|
+
and table_name = p_table
|
|
65
|
+
and column_name = p_column
|
|
66
|
+
);
|
|
67
|
+
$$;
|
|
68
|
+
|
|
69
|
+
-- ── 3. RPC / function existence probe ────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
create or replace function mnestra_doctor_rpc_exists(p_name text)
|
|
72
|
+
returns boolean
|
|
73
|
+
language sql
|
|
74
|
+
security definer
|
|
75
|
+
set search_path = public
|
|
76
|
+
as $$
|
|
77
|
+
select exists (
|
|
78
|
+
select 1
|
|
79
|
+
from pg_proc p
|
|
80
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
81
|
+
where p.proname = p_name
|
|
82
|
+
and n.nspname = 'public'
|
|
83
|
+
);
|
|
84
|
+
$$;
|
|
85
|
+
|
|
86
|
+
-- ── 4. cron.job existence probe (does the named job exist at all) ───────
|
|
87
|
+
|
|
88
|
+
create or replace function mnestra_doctor_cron_job_exists(p_jobname text)
|
|
89
|
+
returns boolean
|
|
90
|
+
language sql
|
|
91
|
+
security definer
|
|
92
|
+
set search_path = cron, public
|
|
93
|
+
as $$
|
|
94
|
+
select exists (select 1 from cron.job where jobname = p_jobname);
|
|
95
|
+
$$;
|
|
96
|
+
|
|
97
|
+
-- ── 5. vault.secrets existence probe (no value disclosure) ──────────────
|
|
98
|
+
--
|
|
99
|
+
-- Existence-only — never returns the secret value. The doctor only needs
|
|
100
|
+
-- to know whether the named vault entry was created during stack install.
|
|
101
|
+
|
|
102
|
+
create or replace function mnestra_doctor_vault_secret_exists(p_name text)
|
|
103
|
+
returns boolean
|
|
104
|
+
language sql
|
|
105
|
+
security definer
|
|
106
|
+
set search_path = vault, public
|
|
107
|
+
as $$
|
|
108
|
+
select exists (select 1 from vault.secrets where name = p_name);
|
|
109
|
+
$$;
|
|
110
|
+
|
|
111
|
+
-- ── 6. Grants ────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
grant execute on function mnestra_doctor_cron_runs(text, int) to service_role;
|
|
114
|
+
grant execute on function mnestra_doctor_column_exists(text, text) to service_role;
|
|
115
|
+
grant execute on function mnestra_doctor_rpc_exists(text) to service_role;
|
|
116
|
+
grant execute on function mnestra_doctor_cron_job_exists(text) to service_role;
|
|
117
|
+
grant execute on function mnestra_doctor_vault_secret_exists(text) to service_role;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
-- Migration 017 — memory_sessions session metadata columns.
|
|
2
|
+
--
|
|
3
|
+
-- Sprint 51.6 T3 (TermDeck v1.0.2 hotfix wave). Brings the canonical engram
|
|
4
|
+
-- memory_sessions schema in line with the rag-system writer's column set so
|
|
5
|
+
-- TermDeck's bundled session-end hook can write a uniform shape on both
|
|
6
|
+
-- fresh-canonical installs and Joshua's daily-driver petvetbid (where the
|
|
7
|
+
-- columns were already added by hand when rag-system bootstrap ran).
|
|
8
|
+
--
|
|
9
|
+
-- Why: until v1.0.2 the bundled hook only wrote memory_items. The actual
|
|
10
|
+
-- memory_sessions writer was Joshua's PRIOR personal hook at
|
|
11
|
+
-- ~/Documents/Graciella/rag-system/hooks/memory-session-end.js, which spawned
|
|
12
|
+
-- ~/Documents/Graciella/rag-system/src/scripts/process-session.ts; that script
|
|
13
|
+
-- INSERTed memory_sessions rows with this richer column set. When the
|
|
14
|
+
-- TermDeck stack-installer overwrote the personal hook on 2026-05-02, the
|
|
15
|
+
-- writer disappeared and memory_sessions stopped accumulating. v1.0.2's
|
|
16
|
+
-- bundled hook gains a memory_sessions write path; this migration ensures
|
|
17
|
+
-- the schema it expects exists everywhere.
|
|
18
|
+
--
|
|
19
|
+
-- Idempotent — safe on:
|
|
20
|
+
-- 1. petvetbid (where these columns are already present from hand-applied
|
|
21
|
+
-- DDL Joshua ran when setting up rag-system; the IF NOT EXISTS guards
|
|
22
|
+
-- no-op on every column).
|
|
23
|
+
-- 2. Fresh canonical installs that ran migrations 001-016 only (the canonical
|
|
24
|
+
-- engram set, which left memory_sessions at the minimal mig-001 shape).
|
|
25
|
+
-- 3. Re-runs of this migration — every operation is guarded.
|
|
26
|
+
--
|
|
27
|
+
-- The unique constraint on session_id is wrapped in a do-block because
|
|
28
|
+
-- ADD CONSTRAINT does not support IF NOT EXISTS in PostgreSQL. Joshua's
|
|
29
|
+
-- petvetbid already has the constraint as memory_sessions_session_id_key
|
|
30
|
+
-- (auto-named by the rag-system bootstrap); this block detects that name
|
|
31
|
+
-- and skips re-adding.
|
|
32
|
+
--
|
|
33
|
+
-- session_id is added NULLABLE on canonical installs even though petvetbid's
|
|
34
|
+
-- existing constraint is NOT NULL. Adding NOT NULL via ALTER TABLE on a
|
|
35
|
+
-- table with existing rows would fail; the bundled hook always supplies
|
|
36
|
+
-- session_id at write time, so nullability is non-blocking. A future sprint
|
|
37
|
+
-- may tighten to NOT NULL with a DEFAULT after a backfill pass.
|
|
38
|
+
|
|
39
|
+
-- Defensive: vector extension must already be installed (migration 001
|
|
40
|
+
-- requires it for memory_items.embedding). If it's somehow missing this
|
|
41
|
+
-- ADD COLUMN errors, surfacing the real environment issue rather than
|
|
42
|
+
-- silently skipping the embedding column.
|
|
43
|
+
|
|
44
|
+
alter table public.memory_sessions
|
|
45
|
+
add column if not exists session_id text,
|
|
46
|
+
add column if not exists summary_embedding vector(1536),
|
|
47
|
+
add column if not exists started_at timestamptz,
|
|
48
|
+
add column if not exists ended_at timestamptz,
|
|
49
|
+
add column if not exists duration_minutes integer,
|
|
50
|
+
add column if not exists messages_count integer default 0,
|
|
51
|
+
add column if not exists facts_extracted integer default 0,
|
|
52
|
+
add column if not exists files_changed jsonb default '[]'::jsonb,
|
|
53
|
+
add column if not exists topics jsonb default '[]'::jsonb,
|
|
54
|
+
add column if not exists transcript_path text;
|
|
55
|
+
|
|
56
|
+
-- Unique constraint on session_id. Skip if any unique constraint on
|
|
57
|
+
-- (session_id) is already in place — covers both the canonical name
|
|
58
|
+
-- memory_sessions_session_id_key and any alternate name from a manual
|
|
59
|
+
-- ALTER TABLE Joshua may have run on petvetbid.
|
|
60
|
+
do $$
|
|
61
|
+
declare
|
|
62
|
+
has_unique boolean;
|
|
63
|
+
begin
|
|
64
|
+
select exists (
|
|
65
|
+
select 1
|
|
66
|
+
from pg_constraint c
|
|
67
|
+
join pg_class t on t.oid = c.conrelid
|
|
68
|
+
join pg_namespace n on n.oid = t.relnamespace
|
|
69
|
+
where n.nspname = 'public'
|
|
70
|
+
and t.relname = 'memory_sessions'
|
|
71
|
+
and c.contype = 'u'
|
|
72
|
+
and (
|
|
73
|
+
select array_agg(att.attname order by att.attnum)
|
|
74
|
+
from unnest(c.conkey) as colnum
|
|
75
|
+
join pg_attribute att on att.attrelid = c.conrelid and att.attnum = colnum
|
|
76
|
+
) = ARRAY['session_id']::name[]
|
|
77
|
+
) into has_unique;
|
|
78
|
+
|
|
79
|
+
if not has_unique then
|
|
80
|
+
alter table public.memory_sessions
|
|
81
|
+
add constraint memory_sessions_session_id_key unique (session_id);
|
|
82
|
+
end if;
|
|
83
|
+
end $$;
|
|
84
|
+
|
|
85
|
+
-- HNSW index on summary_embedding for future similarity search. Idempotent.
|
|
86
|
+
-- Cost on insert is negligible; cost on backfill is one-time.
|
|
87
|
+
create index if not exists memory_sessions_summary_embedding_hnsw_idx
|
|
88
|
+
on public.memory_sessions using hnsw (summary_embedding vector_cosine_ops)
|
|
89
|
+
with (m = 16, ef_construction = 64);
|
|
90
|
+
|
|
91
|
+
-- Helpful covering index for time-range scans (used by Flashback / rumen
|
|
92
|
+
-- queries that filter by ended_at). Idempotent.
|
|
93
|
+
create index if not exists memory_sessions_ended_at_idx
|
|
94
|
+
on public.memory_sessions(ended_at desc nulls last);
|
|
@@ -52,6 +52,23 @@ function extensionsDashboardUrl(secrets) {
|
|
|
52
52
|
return `https://supabase.com/dashboard/project/${parsed.projectRef}/database/extensions`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Sprint 51.5 T3: Build a SQL-Editor deeplink that pre-fills a
|
|
56
|
+
// vault.create_secret() call. Used when the audit's vault probe finds the
|
|
57
|
+
// secret missing and the wizard's auto-apply step (init-rumen.js
|
|
58
|
+
// `ensureVaultSecrets`) was unable to create it via pgRunner. The Vault
|
|
59
|
+
// dashboard panel was quietly removed/relocated in current Supabase UIs
|
|
60
|
+
// (Brad 2026-05-03 takeaway #2; INSTALLER-PITFALLS.md Class B), so SQL
|
|
61
|
+
// Editor is the working manual surface.
|
|
62
|
+
function vaultSqlEditorUrl(secrets, secretName, secretValue) {
|
|
63
|
+
if (!secrets || !secrets.SUPABASE_URL) return null;
|
|
64
|
+
const parsed = supabaseUrlHelper.parseProjectUrl(secrets.SUPABASE_URL);
|
|
65
|
+
if (!parsed.ok) return null;
|
|
66
|
+
const value = String(secretValue == null ? '' : secretValue).replace(/'/g, "''");
|
|
67
|
+
const name = String(secretName == null ? '' : secretName).replace(/'/g, "''");
|
|
68
|
+
const sql = `select vault.create_secret('${value}', '${name}');`;
|
|
69
|
+
return `https://supabase.com/dashboard/project/${parsed.projectRef}/sql/new?content=${encodeURIComponent(sql)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
// Render a single gap into 2-3 lines of CLI output (one indented hint per
|
|
56
73
|
// non-empty `hint` line). Format aligned with the rest of the wizard's
|
|
57
74
|
// step lines.
|
|
@@ -206,14 +223,28 @@ async function auditRumenPreconditions({ secrets, env, _pgClient } = {}) {
|
|
|
206
223
|
'If permission is denied, the Vault is not accessible to this connection — double-check secrets.env.'
|
|
207
224
|
});
|
|
208
225
|
} else if (!vault.ok) {
|
|
226
|
+
// Sprint 51.5 T3: the Supabase Vault dashboard panel was quietly removed
|
|
227
|
+
// / relocated in current Supabase UIs (Brad 2026-05-03 takeaway #2;
|
|
228
|
+
// INSTALLER-PITFALLS.md Class B). The wizard's `ensureVaultSecrets`
|
|
229
|
+
// step (init-rumen.js) auto-creates this key via pgRunner when the
|
|
230
|
+
// user's connection has vault.create_secret privileges; this hint only
|
|
231
|
+
// fires when auto-apply also failed, in which case the SQL Editor is
|
|
232
|
+
// the working manual surface. Build a project-specific deeplink when
|
|
233
|
+
// we can derive the project ref so the user gets one click instead of
|
|
234
|
+
// a "find the SQL Editor yourself" instruction.
|
|
235
|
+
const sqlEditorUrl = vaultSqlEditorUrl(secrets, 'rumen_service_role_key',
|
|
236
|
+
'<paste your service_role JWT from Project Settings → API>');
|
|
237
|
+
const sqlEditorLine = sqlEditorUrl
|
|
238
|
+
? ` Open: ${sqlEditorUrl}\n Replace the placeholder with your service_role key from Project Settings → API, then click Run.`
|
|
239
|
+
: ' Open the Supabase SQL Editor and run:\n' +
|
|
240
|
+
" select vault.create_secret('<service_role JWT>', 'rumen_service_role_key');";
|
|
209
241
|
gaps.push({
|
|
210
242
|
key: 'rumen_service_role_key',
|
|
211
243
|
message: 'Vault secret "rumen_service_role_key" is missing',
|
|
212
244
|
hint:
|
|
213
|
-
|
|
214
|
-
'
|
|
215
|
-
|
|
216
|
-
' Value: your service_role key from Project Settings → API\n' +
|
|
245
|
+
"The wizard tries to auto-create this via vault.create_secret() and only surfaces this hint when auto-apply also failed.\n" +
|
|
246
|
+
'Create it manually via the SQL Editor (the Vault dashboard panel was removed in current Supabase UIs):\n' +
|
|
247
|
+
sqlEditorLine + '\n' +
|
|
217
248
|
'(The pg_cron schedule calls the Edge Function with this key as the bearer token.)'
|
|
218
249
|
});
|
|
219
250
|
}
|
|
@@ -384,6 +415,7 @@ module.exports = {
|
|
|
384
415
|
printAuditReport,
|
|
385
416
|
printVerifyReport,
|
|
386
417
|
extensionsDashboardUrl,
|
|
418
|
+
vaultSqlEditorUrl,
|
|
387
419
|
// Test surface
|
|
388
420
|
_probeSupabaseAuth: probeSupabaseAuth,
|
|
389
421
|
_safeQuery: safeQuery
|
|
@@ -343,9 +343,13 @@ export async function runGraphInference(sql: Sql): Promise<InferenceSummary> {
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
serve(async (_req: Request) => {
|
|
346
|
-
|
|
346
|
+
// Supabase Edge Runtime auto-injects SUPABASE_DB_URL as a built-in env var.
|
|
347
|
+
// Falling back to it removes one whole category of "where do I get the DB
|
|
348
|
+
// connection string" from the install wizard. Brad surfaced this 2026-05-03
|
|
349
|
+
// after hand-patching all four of his deployed copies.
|
|
350
|
+
const url = Deno.env.get('DATABASE_URL') ?? Deno.env.get('SUPABASE_DB_URL');
|
|
347
351
|
if (!url) {
|
|
348
|
-
console.error('[graph-inference] DATABASE_URL not set in Edge Function secrets');
|
|
352
|
+
console.error('[graph-inference] DATABASE_URL / SUPABASE_DB_URL not set in Edge Function secrets');
|
|
349
353
|
return new Response(
|
|
350
354
|
JSON.stringify({ ok: false, error: 'DATABASE_URL not set' }),
|
|
351
355
|
{ status: 500, headers: { 'Content-Type': 'application/json' } },
|
|
@@ -31,9 +31,13 @@ import { runRumenJob, createPoolFromUrl } from 'npm:@jhizzard/rumen@__RUMEN_VERS
|
|
|
31
31
|
declare const Deno: { env: { get: (k: string) => string | undefined } };
|
|
32
32
|
|
|
33
33
|
serve(async (_req: Request) => {
|
|
34
|
-
|
|
34
|
+
// Supabase Edge Runtime auto-injects SUPABASE_DB_URL as a built-in env var.
|
|
35
|
+
// Falling back to it removes one whole category of "where do I get the DB
|
|
36
|
+
// connection string" from the install wizard. Brad surfaced this 2026-05-03
|
|
37
|
+
// after hand-patching all four of his deployed copies.
|
|
38
|
+
const url = Deno.env.get('DATABASE_URL') ?? Deno.env.get('SUPABASE_DB_URL');
|
|
35
39
|
if (!url) {
|
|
36
|
-
console.error('[rumen] DATABASE_URL not set in Edge Function secrets');
|
|
40
|
+
console.error('[rumen] DATABASE_URL / SUPABASE_DB_URL not set in Edge Function secrets');
|
|
37
41
|
return new Response(
|
|
38
42
|
JSON.stringify({
|
|
39
43
|
ok: false,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @jhizzard/termdeck-stack
|
|
2
|
+
|
|
3
|
+
One-command installer for the TermDeck developer memory stack.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx @jhizzard/termdeck-stack
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What gets installed
|
|
10
|
+
|
|
11
|
+
| Layer | Package | What it does |
|
|
12
|
+
|-------|---------|--------------|
|
|
13
|
+
| 1 | `@jhizzard/termdeck` | Browser terminal multiplexer with metadata overlays and Flashback recall toasts |
|
|
14
|
+
| 2 | `@jhizzard/mnestra` | pgvector memory store + MCP server. Lights up Flashback. Provides `memory_*` tools to Claude Code, Cursor, Windsurf |
|
|
15
|
+
| 3 | `@jhizzard/rumen` | Async learning loop on a Supabase Edge Function cron. Synthesizes cross-project insights |
|
|
16
|
+
| 4 | `@supabase/mcp-server-supabase` | MCP that lets the TermDeck setup wizard provision your Supabase project automatically |
|
|
17
|
+
|
|
18
|
+
The wizard:
|
|
19
|
+
|
|
20
|
+
1. Prints the four-layer overview so you see what you're agreeing to.
|
|
21
|
+
2. Detects which pieces are already on your machine.
|
|
22
|
+
3. Asks which tier you want (default: 4 — full stack).
|
|
23
|
+
4. Runs `npm install -g` for the missing pieces.
|
|
24
|
+
5. Merges Mnestra and Supabase MCP entries into `~/.claude/mcp.json` — preserving any existing MCP servers.
|
|
25
|
+
6. Prints the next steps (Supabase PAT, credentials, `termdeck` to start).
|
|
26
|
+
|
|
27
|
+
## Modes
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
npx @jhizzard/termdeck-stack # interactive
|
|
31
|
+
npx @jhizzard/termdeck-stack --tier 4 # unattended
|
|
32
|
+
npx @jhizzard/termdeck-stack --dry-run # print plan, don't install
|
|
33
|
+
npx @jhizzard/termdeck-stack --yes # accept defaults (combine with --tier)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Known limitations
|
|
37
|
+
|
|
38
|
+
Tier 3 (Rumen) currently still requires one manual command after the
|
|
39
|
+
installer finishes:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
termdeck init --rumen
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That command deploys the Rumen Supabase Edge Function, applies the
|
|
46
|
+
migration, and installs the `pg_cron` schedule. Auto-running it from
|
|
47
|
+
the meta-installer is queued — until then the wizard prints it as an
|
|
48
|
+
explicit next step.
|
|
49
|
+
|
|
50
|
+
## Version vs. the rest of the stack
|
|
51
|
+
|
|
52
|
+
This package's version tracks the meta-installer surface, not the
|
|
53
|
+
underlying packages. Each layer ships on its own release cadence:
|
|
54
|
+
|
|
55
|
+
| Package | Where to look |
|
|
56
|
+
|---------|---------------|
|
|
57
|
+
| `@jhizzard/termdeck` | https://www.npmjs.com/package/@jhizzard/termdeck |
|
|
58
|
+
| `@jhizzard/mnestra` | https://www.npmjs.com/package/@jhizzard/mnestra |
|
|
59
|
+
| `@jhizzard/rumen` | https://www.npmjs.com/package/@jhizzard/rumen |
|
|
60
|
+
|
|
61
|
+
The installer always pulls each layer's `latest` dist-tag, so a fresh
|
|
62
|
+
`npx @jhizzard/termdeck-stack` run picks up the most recent published
|
|
63
|
+
version of every layer regardless of this package's own version.
|
|
64
|
+
|
|
65
|
+
## Why this exists
|
|
66
|
+
|
|
67
|
+
The TermDeck stack used to be a 15-step install: provision Supabase, run six SQL migrations, mint API keys, paste them into `secrets.env`, edit `config.yaml`, install Mnestra globally, deploy Rumen, install the Supabase MCP, wire `~/.claude/mcp.json`. Most testers bounced before step 5.
|
|
68
|
+
|
|
69
|
+
This installer collapses every step that's a `npm install -g` into one command, then drops the user at the doorstep of the in-browser setup wizard (which handles credentials).
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# TermDeck session-end memory hook
|
|
2
|
+
|
|
3
|
+
The `@jhizzard/termdeck-stack` installer can drop `memory-session-end.js`
|
|
4
|
+
into `~/.claude/hooks/` and wire it into `~/.claude/settings.json` under
|
|
5
|
+
`hooks.Stop`. The installer prompts you before doing this; default is
|
|
6
|
+
yes.
|
|
7
|
+
|
|
8
|
+
## What the hook does
|
|
9
|
+
|
|
10
|
+
On every Claude Code session close, Claude Code fires its `Stop` hook
|
|
11
|
+
with a JSON payload on stdin:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{ "transcript_path": "/path/to/session.jsonl", "cwd": "/path/where/you/were/working", "session_id": "..." }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The hook:
|
|
18
|
+
|
|
19
|
+
1. Skips transcripts smaller than 5 KB (no signal in tiny sessions —
|
|
20
|
+
override via `TERMDECK_HOOK_MIN_BYTES`).
|
|
21
|
+
2. Validates env vars (`SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`,
|
|
22
|
+
`OPENAI_API_KEY`); if any are missing, logs the missing list and
|
|
23
|
+
exits cleanly without blocking the session close.
|
|
24
|
+
3. Detects the project from `cwd` against a built-in regex table; falls
|
|
25
|
+
back to `"global"` when nothing matches. **The default table is
|
|
26
|
+
intentionally empty** — see "Customizing the project map" below to
|
|
27
|
+
add your own entries.
|
|
28
|
+
4. Builds a coarse session summary from the last ~30 messages of the
|
|
29
|
+
transcript (~7 KB cap to stay inside OpenAI's embedding-input
|
|
30
|
+
budget).
|
|
31
|
+
5. Embeds the summary via OpenAI `text-embedding-3-small` (1,536-dim).
|
|
32
|
+
6. POSTs **one row** to Supabase `/rest/v1/memory_items` with
|
|
33
|
+
`source_type='session_summary'`.
|
|
34
|
+
7. Logs every step to `~/.claude/hooks/memory-hook.log`.
|
|
35
|
+
|
|
36
|
+
The hook is **fail-soft**: any error (network, parse, env-var-missing,
|
|
37
|
+
malformed transcript) is logged and the hook exits 0. Claude Code's
|
|
38
|
+
session close is never blocked.
|
|
39
|
+
|
|
40
|
+
## Required environment
|
|
41
|
+
|
|
42
|
+
The hook needs three env vars at run time:
|
|
43
|
+
|
|
44
|
+
| Var | What | How to set |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `SUPABASE_URL` | Your Supabase project URL (e.g. `https://abc.supabase.co`) | `~/.termdeck/secrets.env` (Tier 2) |
|
|
47
|
+
| `SUPABASE_SERVICE_ROLE_KEY` | Service-role key with INSERT on `memory_items`. **Not the anon key.** | `~/.termdeck/secrets.env` |
|
|
48
|
+
| `OPENAI_API_KEY` | OpenAI key for embedding inference | `~/.termdeck/secrets.env` or your shell |
|
|
49
|
+
|
|
50
|
+
Claude Code propagates the parent shell's environment into hook
|
|
51
|
+
processes, so anything in your shell init or
|
|
52
|
+
`~/.termdeck/secrets.env` (sourced by `scripts/start.sh` /
|
|
53
|
+
`npx @jhizzard/termdeck`) is visible to the hook.
|
|
54
|
+
|
|
55
|
+
**From v0.17.0**, the TermDeck server also merges
|
|
56
|
+
`~/.termdeck/secrets.env` directly into every PTY-spawned shell — so any
|
|
57
|
+
Claude Code panel launched inside TermDeck inherits `SUPABASE_URL` /
|
|
58
|
+
`SUPABASE_SERVICE_ROLE_KEY` / `OPENAI_API_KEY` even if the user's
|
|
59
|
+
parent shell never sourced the file. Concrete values in
|
|
60
|
+
`process.env` still win (parent-shell env takes precedence over the
|
|
61
|
+
file fallback). Standalone Claude Code launches outside TermDeck
|
|
62
|
+
still rely on the parent shell having sourced the file — for those,
|
|
63
|
+
the wizard can offer a one-line `~/.zshrc` source addition.
|
|
64
|
+
|
|
65
|
+
If any of the three is missing the log line will name them:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
[2026-04-27T21:30:00.000Z] env-var-missing: OPENAI_API_KEY — set these in ~/.termdeck/secrets.env or your shell to enable Mnestra ingestion. Skipping.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Customizing the project map
|
|
72
|
+
|
|
73
|
+
The hook ships with an **empty `PROJECT_MAP`** by default — every
|
|
74
|
+
session lands under `project: 'global'` until you add entries. To add
|
|
75
|
+
your own:
|
|
76
|
+
|
|
77
|
+
1. Open `~/.claude/hooks/memory-session-end.js` after the installer
|
|
78
|
+
has dropped it.
|
|
79
|
+
2. Find the `PROJECT_MAP` array near the top of the file.
|
|
80
|
+
3. Add one entry per project; each entry is `{ pattern, project }`
|
|
81
|
+
where `pattern` is a regex matched against `cwd`.
|
|
82
|
+
|
|
83
|
+
### Order matters: most-specific-first
|
|
84
|
+
|
|
85
|
+
`detectProject(cwd)` returns the **first** matching entry. If a deep
|
|
86
|
+
project lives under a broader parent dir, the deep pattern must come
|
|
87
|
+
first or the parent will swallow it. This bug bit the TermDeck team in
|
|
88
|
+
Sprint 41 — every cwd under a `ChopinNashville/` parent was getting
|
|
89
|
+
tagged `chopin-nashville` because the parent-dir pattern came before
|
|
90
|
+
each sub-project's specific pattern.
|
|
91
|
+
|
|
92
|
+
Example showing the right ordering:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
const PROJECT_MAP = [
|
|
96
|
+
// Specific code projects under a common parent — these MUST appear
|
|
97
|
+
// before the parent-dir catch-all below.
|
|
98
|
+
{ pattern: /\/MyOrg\/SideProjects\/widget-app/i, project: 'widget-app' },
|
|
99
|
+
{ pattern: /\/MyOrg\/SideProjects\/scheduler/i, project: 'scheduler' },
|
|
100
|
+
{ pattern: /\/MyOrg\/2026\/festival\/podium/i, project: 'podium' },
|
|
101
|
+
{ pattern: /\/MyOrg\/2026\/festival/i, project: 'festival' },
|
|
102
|
+
|
|
103
|
+
// Other top-level projects.
|
|
104
|
+
{ pattern: /\/PVB\//i, project: 'pvb' },
|
|
105
|
+
|
|
106
|
+
// Catch-all for the parent dir — only matches when no specific
|
|
107
|
+
// project above matched first.
|
|
108
|
+
{ pattern: /\/MyOrg(\/|$)/i, project: 'myorg-ops' },
|
|
109
|
+
];
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For a worked example of a real production taxonomy (with explicit
|
|
113
|
+
priority ordering, alias documentation, and a structural-invariant
|
|
114
|
+
test), see [`docs/PROJECT-TAXONOMY.md`](https://github.com/jhizzard/termdeck/blob/main/docs/PROJECT-TAXONOMY.md)
|
|
115
|
+
in the TermDeck repo.
|
|
116
|
+
|
|
117
|
+
### Other rules
|
|
118
|
+
|
|
119
|
+
- The map is local-only — it's never sent to any service. Editing it
|
|
120
|
+
takes effect on the next Claude Code session close (no restart
|
|
121
|
+
needed).
|
|
122
|
+
- Anything that doesn't match falls through to `'global'`.
|
|
123
|
+
- Adopt the module-export contract (`module.exports = { detectProject, PROJECT_MAP }`)
|
|
124
|
+
if you want to write a unit test that exercises your taxonomy. The
|
|
125
|
+
bundled hook already does this; if you copy-paste a custom hook,
|
|
126
|
+
preserve the `if (require.main === module)` guard around the stdin
|
|
127
|
+
reader so `require()` doesn't hang.
|
|
128
|
+
|
|
129
|
+
## Coexistence with Joshua's `rag-system` hook
|
|
130
|
+
|
|
131
|
+
If you have Joshua's private `rag-system` repo and his rag-system-based
|
|
132
|
+
session hook installed, this bundled hook and that one can coexist:
|
|
133
|
+
|
|
134
|
+
- The bundled hook writes `source_type='session_summary'` — one row
|
|
135
|
+
per session, summary-only.
|
|
136
|
+
- The `rag-system` hook writes `source_type='fact'` — multiple rows
|
|
137
|
+
per session via Claude Haiku fact extraction + dedup.
|
|
138
|
+
|
|
139
|
+
Different `source_type` values mean the two paths don't dedup against
|
|
140
|
+
each other. If both are installed at the same path
|
|
141
|
+
(`~/.claude/hooks/memory-session-end.js`) the installer will prompt
|
|
142
|
+
before overwriting; choose accordingly.
|
|
143
|
+
|
|
144
|
+
## How to disable
|
|
145
|
+
|
|
146
|
+
Two options:
|
|
147
|
+
|
|
148
|
+
1. Edit `~/.claude/settings.json` and remove the entry under
|
|
149
|
+
`hooks.Stop` that references `memory-session-end.js`. Leave the
|
|
150
|
+
file in place; it simply won't fire.
|
|
151
|
+
2. Or delete `~/.claude/hooks/memory-session-end.js` AND remove the
|
|
152
|
+
`settings.json` entry. (Removing only the file leaves a broken
|
|
153
|
+
`command` in settings — Claude Code will log a missing-file error
|
|
154
|
+
on every session close.)
|
|
155
|
+
|
|
156
|
+
Re-running `npx @jhizzard/termdeck-stack` after disabling will
|
|
157
|
+
re-prompt to install. Decline at the prompt to stay opted out.
|
|
158
|
+
|
|
159
|
+
## Optional flags
|
|
160
|
+
|
|
161
|
+
| Env var | Effect |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `TERMDECK_HOOK_DEBUG=1` | Verbose `[debug]` lines in the log |
|
|
164
|
+
| `TERMDECK_HOOK_MIN_BYTES=10000` | Override the 5 KB skip threshold |
|
|
165
|
+
|
|
166
|
+
## Log file
|
|
167
|
+
|
|
168
|
+
`~/.claude/hooks/memory-hook.log` accumulates one line per session
|
|
169
|
+
event (skips, errors, ingests). The hook never rotates it. If it
|
|
170
|
+
grows unwieldy you can truncate it
|
|
171
|
+
(`: > ~/.claude/hooks/memory-hook.log`) without affecting hook
|
|
172
|
+
behavior.
|