@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.
@@ -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
- 'Create it in the Supabase dashboard:\n' +
214
- ' Project Settings Vault New secret\n' +
215
- ' Name: rumen_service_role_key (exact, case-sensitive)\n' +
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
- const url = Deno.env.get('DATABASE_URL');
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
- const url = Deno.env.get('DATABASE_URL');
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.