@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.
Files changed (23) hide show
  1. package/README.md +191 -169
  2. package/package.json +7 -6
  3. package/packages/cli/src/index.js +39 -1
  4. package/packages/cli/src/init-engram.js +344 -0
  5. package/packages/cli/src/init-rumen.js +425 -0
  6. package/packages/client/public/index.html +2 -2
  7. package/packages/server/src/setup/dotenv-io.js +116 -0
  8. package/packages/server/src/setup/engram-migrations/001_engram_tables.sql +116 -0
  9. package/packages/server/src/setup/engram-migrations/002_engram_search_function.sql +141 -0
  10. package/packages/server/src/setup/engram-migrations/003_engram_event_webhook.sql +28 -0
  11. package/packages/server/src/setup/engram-migrations/004_engram_match_count_cap_and_explain.sql +176 -0
  12. package/packages/server/src/setup/engram-migrations/005_v0_1_to_v0_2_upgrade.sql +23 -0
  13. package/packages/server/src/setup/engram-migrations/006_memory_status_rpc.sql +58 -0
  14. package/packages/server/src/setup/index.js +14 -0
  15. package/packages/server/src/setup/migrations.js +80 -0
  16. package/packages/server/src/setup/pg-runner.js +113 -0
  17. package/packages/server/src/setup/prompts.js +177 -0
  18. package/packages/server/src/setup/rumen/functions/rumen-tick/index.ts +85 -0
  19. package/packages/server/src/setup/rumen/functions/rumen-tick/tsconfig.json +14 -0
  20. package/packages/server/src/setup/rumen/migrations/001_rumen_tables.sql +91 -0
  21. package/packages/server/src/setup/rumen/migrations/002_pg_cron_schedule.sql +40 -0
  22. package/packages/server/src/setup/supabase-url.js +114 -0
  23. package/packages/server/src/setup/yaml-io.js +99 -0
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+
3
+ // `termdeck init --engram` — interactive wizard for TermDeck's Tier 2 memory
4
+ // layer. Wraps the six manual Engram setup steps into one command.
5
+ //
6
+ // Steps:
7
+ // 1. Collect Supabase URL, service_role key, direct DB URL, OpenAI + Anthropic keys
8
+ // 2. Connect via `pg` using the direct URL
9
+ // 3. Apply the six bundled Engram migrations in order
10
+ // 4. Write ~/.termdeck/secrets.env (merge-aware, preserves existing values)
11
+ // 5. Update ~/.termdeck/config.yaml to enable RAG + point at ${VAR} refs
12
+ // 6. Verify with a memory_status_aggregation() call
13
+ //
14
+ // Flags:
15
+ // --help Print usage and exit
16
+ // --yes Accept defaults, skip confirmations (still prompts for secrets)
17
+ // --dry-run Print what the wizard would do; don't touch the DB or filesystem
18
+ // --skip-verify Skip the final memory_status_aggregation() check
19
+ //
20
+ // All control flow lives in this file. All prompts, file IO, and pg work
21
+ // delegate to packages/server/src/setup/**.
22
+
23
+ const path = require('path');
24
+ const fs = require('fs');
25
+
26
+ const SETUP_DIR = path.join(__dirname, '..', '..', 'server', 'src', 'setup');
27
+ const {
28
+ prompts,
29
+ dotenv,
30
+ yaml,
31
+ supabaseUrl: urlHelper,
32
+ migrations,
33
+ pgRunner
34
+ } = require(SETUP_DIR);
35
+
36
+ const HELP = [
37
+ '',
38
+ 'TermDeck Engram Setup',
39
+ '',
40
+ 'Usage: termdeck init --engram [flags]',
41
+ '',
42
+ 'Flags:',
43
+ ' --help Print this message and exit',
44
+ ' --yes Assume "yes" on confirmations (still prompts for secret values)',
45
+ ' --dry-run Print the plan without touching the database or filesystem',
46
+ ' --skip-verify Skip the final memory_status_aggregation() sanity call',
47
+ '',
48
+ 'What this does:',
49
+ ' 1. Prompts for Supabase URL, service_role key, direct Postgres connection',
50
+ ' string, OpenAI API key, and (optional) Anthropic API key.',
51
+ ' 2. Applies the six Engram schema + RPC migrations via node-postgres.',
52
+ ' 3. Writes ~/.termdeck/secrets.env (merge-aware, preserves existing values).',
53
+ ' 4. Updates ~/.termdeck/config.yaml to enable RAG and reference ${VAR} keys.',
54
+ ' 5. Verifies the Engram store is reachable via memory_status_aggregation().',
55
+ '',
56
+ 'Every secret stays on your machine. Nothing is ever printed once entered.',
57
+ ''
58
+ ].join('\n');
59
+
60
+ function parseFlags(argv) {
61
+ const out = { help: false, yes: false, dryRun: false, skipVerify: false };
62
+ for (const a of argv) {
63
+ if (a === '--help' || a === '-h') out.help = true;
64
+ else if (a === '--yes' || a === '-y') out.yes = true;
65
+ else if (a === '--dry-run') out.dryRun = true;
66
+ else if (a === '--skip-verify') out.skipVerify = true;
67
+ }
68
+ return out;
69
+ }
70
+
71
+ function printBanner() {
72
+ process.stdout.write(`
73
+ TermDeck Engram Setup
74
+ ─────────────────────
75
+
76
+ This wizard configures TermDeck's Tier 2 memory layer (Engram) by:
77
+ 1. Asking for your Supabase URL and service_role key
78
+ 2. Asking for a direct Postgres connection string
79
+ 3. Applying six SQL migrations to the database
80
+ 4. Asking for an OpenAI API key (embeddings)
81
+ 5. Asking for an Anthropic API key (optional, summaries)
82
+ 6. Writing ~/.termdeck/secrets.env
83
+ 7. Updating ~/.termdeck/config.yaml to enable RAG
84
+ 8. Verifying the connection with a memory_status call
85
+
86
+ Press Ctrl+C at any time to cancel.
87
+
88
+ `);
89
+ }
90
+
91
+ function step(msg) { process.stdout.write(`→ ${msg}`); }
92
+ function ok(suffix = '') { process.stdout.write(` ✓${suffix ? ' ' + suffix : ''}\n`); }
93
+ function fail(err) { process.stdout.write(` ✗\n ${err}\n`); }
94
+
95
+ async function collectInputs({ yes }) {
96
+ const projectUrlStr = await prompts.askRequired(
97
+ '? Supabase Project URL (e.g. https://xyz.supabase.co)',
98
+ {
99
+ validate: (v) => {
100
+ const parsed = urlHelper.parseProjectUrl(v);
101
+ return parsed.ok ? null : parsed.error;
102
+ }
103
+ }
104
+ );
105
+ const projectUrl = urlHelper.parseProjectUrl(projectUrlStr);
106
+
107
+ process.stdout.write('? Supabase service_role key (starts sb_secret_ or eyJ): ');
108
+ const serviceRoleKey = await promptSecretWithValidation(
109
+ urlHelper.looksLikeServiceRole
110
+ );
111
+
112
+ process.stdout.write(
113
+ '? Direct Postgres connection string\n' +
114
+ ` (Supabase dashboard → Project Settings → Database → Connection String → Transaction pooler)\n` +
115
+ ' postgres://postgres.REF:PW@... '
116
+ );
117
+ const databaseUrl = await promptSecretWithValidation(urlHelper.looksLikePostgresUrl);
118
+
119
+ process.stdout.write('? OpenAI API key (starts sk-proj- or sk-): ');
120
+ const openaiKey = await promptSecretWithValidation(urlHelper.looksLikeOpenAiKey);
121
+
122
+ process.stdout.write('? Anthropic API key (optional, for session summaries): ');
123
+ const anthropicKeyRaw = await prompts.askSecret('');
124
+ const anthropicKey = anthropicKeyRaw || null;
125
+ if (anthropicKey) {
126
+ const shapeErr = urlHelper.looksLikeAnthropicKey(anthropicKey);
127
+ if (shapeErr) {
128
+ process.stdout.write(` (warning: ${shapeErr} — storing anyway)\n`);
129
+ }
130
+ }
131
+
132
+ if (!yes) {
133
+ process.stdout.write('\n');
134
+ const go = await prompts.confirm(`Proceed with setup for project ${projectUrl.projectRef}?`);
135
+ if (!go) {
136
+ process.stdout.write('Cancelled.\n');
137
+ process.exit(0);
138
+ }
139
+ }
140
+
141
+ return {
142
+ projectUrl,
143
+ serviceRoleKey,
144
+ databaseUrl,
145
+ openaiKey,
146
+ anthropicKey
147
+ };
148
+ }
149
+
150
+ // Wrapper that re-prompts on a shape mismatch instead of aborting. Max 3 tries.
151
+ async function promptSecretWithValidation(validator) {
152
+ for (let i = 0; i < 3; i++) {
153
+ const value = await prompts.askSecret('');
154
+ if (!value) {
155
+ process.stdout.write(' (required)\n? ');
156
+ continue;
157
+ }
158
+ const err = validator(value);
159
+ if (err) {
160
+ process.stdout.write(` (${err})\n? `);
161
+ continue;
162
+ }
163
+ return value;
164
+ }
165
+ throw new Error('Too many invalid attempts — cancelling.');
166
+ }
167
+
168
+ async function applyMigrations(client, dryRun) {
169
+ const files = migrations.listEngramMigrations();
170
+ if (files.length === 0) {
171
+ throw new Error('No Engram migrations found. TermDeck install looks corrupted.');
172
+ }
173
+
174
+ for (const file of files) {
175
+ const base = path.basename(file);
176
+ step(`Applying migration ${base}...`);
177
+ if (dryRun) { ok('(dry-run)'); continue; }
178
+ const result = await pgRunner.applyFile(client, file);
179
+ if (result.ok) {
180
+ ok(`(${result.elapsedMs}ms)`);
181
+ } else {
182
+ fail(result.error);
183
+ throw new Error(`Migration failed: ${base}`);
184
+ }
185
+ }
186
+ }
187
+
188
+ async function checkExistingStore(client) {
189
+ step('Checking for existing memory_items table...');
190
+ try {
191
+ const result = await pgRunner.run(
192
+ client,
193
+ `SELECT COUNT(*)::bigint AS n FROM information_schema.tables
194
+ WHERE table_schema = 'public' AND table_name = 'memory_items'`
195
+ );
196
+ const exists = result.rows[0] && Number(result.rows[0].n) > 0;
197
+ if (!exists) {
198
+ ok('not found (will create)');
199
+ return { exists: false, rows: 0 };
200
+ }
201
+ const countResult = await pgRunner.run(client, 'SELECT COUNT(*)::bigint AS n FROM memory_items');
202
+ const rows = Number(countResult.rows[0].n);
203
+ ok(`found (${rows.toLocaleString()} rows)`);
204
+ return { exists: true, rows };
205
+ } catch (err) {
206
+ fail(err.message);
207
+ throw err;
208
+ }
209
+ }
210
+
211
+ async function verifyStatus(client) {
212
+ step('Verifying memory_status_aggregation()...');
213
+ try {
214
+ const result = await pgRunner.run(client, 'SELECT * FROM memory_status_aggregation()');
215
+ const row = result.rows && result.rows[0];
216
+ if (!row) {
217
+ fail('RPC returned no rows');
218
+ return false;
219
+ }
220
+ const total = Number(row.total_active || 0);
221
+ ok(`(${total.toLocaleString()} active memories found)`);
222
+ return true;
223
+ } catch (err) {
224
+ fail(err.message);
225
+ return false;
226
+ }
227
+ }
228
+
229
+ function writeLocalConfig(inputs, dryRun) {
230
+ step('Writing ~/.termdeck/secrets.env...');
231
+ if (dryRun) { ok('(dry-run)'); }
232
+ else {
233
+ dotenv.writeSecrets({
234
+ SUPABASE_URL: inputs.projectUrl.url,
235
+ SUPABASE_SERVICE_ROLE_KEY: inputs.serviceRoleKey,
236
+ DATABASE_URL: inputs.databaseUrl,
237
+ OPENAI_API_KEY: inputs.openaiKey,
238
+ ...(inputs.anthropicKey ? { ANTHROPIC_API_KEY: inputs.anthropicKey } : {})
239
+ });
240
+ ok();
241
+ }
242
+
243
+ step('Updating ~/.termdeck/config.yaml (rag.enabled: true)...');
244
+ if (dryRun) { ok('(dry-run)'); }
245
+ else {
246
+ const r = yaml.updateRagConfig({
247
+ enabled: true,
248
+ supabaseUrl: '${SUPABASE_URL}',
249
+ supabaseKey: '${SUPABASE_SERVICE_ROLE_KEY}',
250
+ openaiApiKey: '${OPENAI_API_KEY}',
251
+ anthropicApiKey: '${ANTHROPIC_API_KEY}'
252
+ });
253
+ if (r.backup) ok(`(backup: ${path.basename(r.backup)})`);
254
+ else ok();
255
+ }
256
+ }
257
+
258
+ function printNextSteps() {
259
+ process.stdout.write(`
260
+ Engram is configured.
261
+
262
+ Next steps:
263
+ 1. Restart TermDeck: termdeck
264
+ 2. Flashback will fire automatically on panel errors
265
+ 3. Use the "Ask about this terminal" input to query memories
266
+ 4. Want async learning? Run: termdeck init --rumen
267
+ `);
268
+ }
269
+
270
+ async function main(argv) {
271
+ const flags = parseFlags(argv || []);
272
+ if (flags.help) {
273
+ process.stdout.write(HELP);
274
+ return 0;
275
+ }
276
+
277
+ printBanner();
278
+
279
+ let inputs;
280
+ try {
281
+ inputs = await collectInputs({ yes: flags.yes });
282
+ } catch (err) {
283
+ process.stderr.write(`\n[init --engram] ${err.message}\n`);
284
+ return 2;
285
+ }
286
+
287
+ process.stdout.write('\n');
288
+ step('Connecting to Supabase...');
289
+ if (flags.dryRun) {
290
+ ok('(dry-run, skipped)');
291
+ await applyMigrations(null, true);
292
+ writeLocalConfig(inputs, true);
293
+ process.stdout.write('\nDry run complete. No changes were made.\n');
294
+ return 0;
295
+ }
296
+
297
+ let client;
298
+ try {
299
+ client = await pgRunner.connect(inputs.databaseUrl);
300
+ ok();
301
+ } catch (err) {
302
+ fail(err.message);
303
+ process.stderr.write(
304
+ '\nDouble-check the connection string from Supabase → Project Settings → Database → Connection String.\n'
305
+ );
306
+ return 3;
307
+ }
308
+
309
+ try {
310
+ await checkExistingStore(client);
311
+ await applyMigrations(client, false);
312
+ writeLocalConfig(inputs, false);
313
+ if (!flags.skipVerify) {
314
+ const verified = await verifyStatus(client);
315
+ if (!verified) {
316
+ process.stdout.write(
317
+ '\nMigrations applied, but memory_status_aggregation() is not responding yet.\n' +
318
+ 'This usually means the RPC was just created — give it a second and run:\n' +
319
+ ' psql "$DATABASE_URL" -c "SELECT * FROM memory_status_aggregation();"\n'
320
+ );
321
+ return 4;
322
+ }
323
+ }
324
+ } catch (err) {
325
+ process.stderr.write(`\n[init --engram] ${err.message}\n`);
326
+ return 5;
327
+ } finally {
328
+ try { await client.end(); } catch (_err) { /* ignore */ }
329
+ }
330
+
331
+ printNextSteps();
332
+ return 0;
333
+ }
334
+
335
+ if (require.main === module) {
336
+ main(process.argv.slice(2))
337
+ .then((code) => process.exit(code || 0))
338
+ .catch((err) => {
339
+ process.stderr.write(`\n[init --engram] unexpected error: ${err && err.stack || err}\n`);
340
+ process.exit(1);
341
+ });
342
+ }
343
+
344
+ module.exports = main;