@jhizzard/termdeck 0.2.0 → 0.2.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/README.md +191 -169
- package/config/config.example.yaml +10 -10
- package/package.json +7 -6
- package/packages/cli/src/index.js +44 -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 +10 -10
- package/packages/server/src/config.js +8 -8
- package/packages/server/src/index.js +10 -10
- package/packages/server/src/{engram-bridge → mnestra-bridge}/index.js +18 -18
- package/packages/server/src/rag.js +6 -6
- 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,425 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// `termdeck init --rumen` — interactive wizard for deploying Rumen as a
|
|
4
|
+
// Supabase Edge Function + pg_cron schedule against the same Supabase
|
|
5
|
+
// project that holds the Mnemos store.
|
|
6
|
+
//
|
|
7
|
+
// Requirements checked at runtime:
|
|
8
|
+
// - `supabase` CLI on PATH
|
|
9
|
+
// - `deno` on PATH
|
|
10
|
+
// - `~/.termdeck/secrets.env` with SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY +
|
|
11
|
+
// DATABASE_URL + ANTHROPIC_API_KEY populated (run `termdeck init --mnemos` first)
|
|
12
|
+
//
|
|
13
|
+
// Steps:
|
|
14
|
+
// 1. Preflight: which supabase, which deno, read secrets.env
|
|
15
|
+
// 2. Derive project ref from SUPABASE_URL; confirm with user
|
|
16
|
+
// 3. supabase link --project-ref <ref>
|
|
17
|
+
// 4. Apply rumen migration 001 via pg
|
|
18
|
+
// 5. supabase functions deploy rumen-tick --no-verify-jwt
|
|
19
|
+
// 6. supabase secrets set DATABASE_URL=... ANTHROPIC_API_KEY=...
|
|
20
|
+
// 7. Test the function with a manual POST (fetch)
|
|
21
|
+
// 8. Apply pg_cron schedule migration (002) with project ref substituted
|
|
22
|
+
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const { execSync, spawnSync } = require('child_process');
|
|
27
|
+
|
|
28
|
+
const SETUP_DIR = path.join(__dirname, '..', '..', 'server', 'src', 'setup');
|
|
29
|
+
const {
|
|
30
|
+
prompts,
|
|
31
|
+
dotenv,
|
|
32
|
+
supabaseUrl: urlHelper,
|
|
33
|
+
migrations,
|
|
34
|
+
pgRunner
|
|
35
|
+
} = require(SETUP_DIR);
|
|
36
|
+
|
|
37
|
+
const HELP = [
|
|
38
|
+
'',
|
|
39
|
+
'TermDeck Rumen Setup',
|
|
40
|
+
'',
|
|
41
|
+
'Usage: termdeck init --rumen [flags]',
|
|
42
|
+
'',
|
|
43
|
+
'Flags:',
|
|
44
|
+
' --help Print this message and exit',
|
|
45
|
+
' --yes Skip the project-ref confirmation',
|
|
46
|
+
' --dry-run Print what would run; touch nothing',
|
|
47
|
+
' --skip-schedule Deploy the function but do not install the pg_cron schedule',
|
|
48
|
+
'',
|
|
49
|
+
'Requires: Supabase CLI and Deno already installed.',
|
|
50
|
+
'Requires: `termdeck init --mnemos` has already run (needs secrets.env).',
|
|
51
|
+
''
|
|
52
|
+
].join('\n');
|
|
53
|
+
|
|
54
|
+
function parseFlags(argv) {
|
|
55
|
+
const out = { help: false, yes: false, dryRun: false, skipSchedule: false };
|
|
56
|
+
for (const a of argv) {
|
|
57
|
+
if (a === '--help' || a === '-h') out.help = true;
|
|
58
|
+
else if (a === '--yes' || a === '-y') out.yes = true;
|
|
59
|
+
else if (a === '--dry-run') out.dryRun = true;
|
|
60
|
+
else if (a === '--skip-schedule') out.skipSchedule = true;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printBanner() {
|
|
66
|
+
process.stdout.write(`
|
|
67
|
+
TermDeck Rumen Setup
|
|
68
|
+
────────────────────
|
|
69
|
+
|
|
70
|
+
This wizard deploys Rumen as a Supabase Edge Function with a pg_cron schedule.
|
|
71
|
+
Requires: Supabase CLI + Deno already installed.
|
|
72
|
+
|
|
73
|
+
Press Ctrl+C at any time to cancel.
|
|
74
|
+
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function step(msg) { process.stdout.write(`→ ${msg}`); }
|
|
79
|
+
function ok(suffix = '') { process.stdout.write(` ✓${suffix ? ' ' + suffix : ''}\n`); }
|
|
80
|
+
function fail(err) { process.stdout.write(` ✗\n ${err}\n`); }
|
|
81
|
+
|
|
82
|
+
function which(bin) {
|
|
83
|
+
const r = spawnSync('which', [bin], { encoding: 'utf-8' });
|
|
84
|
+
if (r.status !== 0) return null;
|
|
85
|
+
return (r.stdout || '').trim() || null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function preflight() {
|
|
89
|
+
step('Checking for supabase CLI...');
|
|
90
|
+
const sb = which('supabase');
|
|
91
|
+
if (!sb) {
|
|
92
|
+
fail('not found on PATH');
|
|
93
|
+
process.stderr.write(
|
|
94
|
+
'\nInstall it first:\n' +
|
|
95
|
+
' macOS: brew install supabase/tap/supabase\n' +
|
|
96
|
+
' other: https://supabase.com/docs/guides/local-development/cli/getting-started\n'
|
|
97
|
+
);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
ok();
|
|
101
|
+
|
|
102
|
+
step('Checking for deno...');
|
|
103
|
+
const deno = which('deno');
|
|
104
|
+
if (!deno) {
|
|
105
|
+
fail('not found on PATH');
|
|
106
|
+
process.stderr.write(
|
|
107
|
+
'\nInstall it first:\n' +
|
|
108
|
+
' macOS: brew install deno\n' +
|
|
109
|
+
' other: https://deno.com/#installation\n'
|
|
110
|
+
);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
ok();
|
|
114
|
+
|
|
115
|
+
step('Reading Mnemos config from ~/.termdeck/secrets.env...');
|
|
116
|
+
const secrets = dotenv.readSecrets();
|
|
117
|
+
const required = ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'DATABASE_URL', 'ANTHROPIC_API_KEY'];
|
|
118
|
+
const missing = required.filter((k) => !secrets[k]);
|
|
119
|
+
if (missing.length > 0) {
|
|
120
|
+
fail(`missing keys: ${missing.join(', ')}`);
|
|
121
|
+
process.stderr.write(
|
|
122
|
+
'\nRun `termdeck init --mnemos` first — it writes the keys this wizard needs.\n'
|
|
123
|
+
);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
ok();
|
|
127
|
+
|
|
128
|
+
return { supabaseBin: sb, denoBin: deno, secrets };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function deriveProjectRef(secrets) {
|
|
132
|
+
const parsed = urlHelper.parseProjectUrl(secrets.SUPABASE_URL);
|
|
133
|
+
if (!parsed.ok) {
|
|
134
|
+
throw new Error(`SUPABASE_URL is not a valid Supabase project URL: ${parsed.error}`);
|
|
135
|
+
}
|
|
136
|
+
return parsed.projectRef;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Shell out with inherited stdio so the user sees real-time supabase CLI
|
|
140
|
+
// output. Returns `{ ok, code }`.
|
|
141
|
+
function runShell(command, args, opts = {}) {
|
|
142
|
+
const r = spawnSync(command, args, {
|
|
143
|
+
stdio: 'inherit',
|
|
144
|
+
encoding: 'utf-8',
|
|
145
|
+
...opts
|
|
146
|
+
});
|
|
147
|
+
return { ok: r.status === 0, code: r.status };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Same but capture stdout for things we want to inspect programmatically.
|
|
151
|
+
function runShellCaptured(command, args, opts = {}) {
|
|
152
|
+
const r = spawnSync(command, args, {
|
|
153
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
154
|
+
encoding: 'utf-8',
|
|
155
|
+
...opts
|
|
156
|
+
});
|
|
157
|
+
return { ok: r.status === 0, code: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function link(projectRef, dryRun) {
|
|
161
|
+
step(`Running: supabase link --project-ref ${projectRef}...`);
|
|
162
|
+
if (dryRun) { ok('(dry-run)'); return true; }
|
|
163
|
+
const r = runShellCaptured('supabase', ['link', '--project-ref', projectRef]);
|
|
164
|
+
if (!r.ok) {
|
|
165
|
+
fail(`supabase link failed (exit ${r.code})`);
|
|
166
|
+
if (r.stderr) process.stderr.write(r.stderr + '\n');
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
ok();
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function applyRumenTables(secrets, dryRun) {
|
|
174
|
+
step('Applying rumen tables migration...');
|
|
175
|
+
if (dryRun) { ok('(dry-run)'); return true; }
|
|
176
|
+
const files = migrations.listRumenMigrations();
|
|
177
|
+
const tableFile = files.find((f) => /001.*rumen_tables/.test(path.basename(f)));
|
|
178
|
+
if (!tableFile) {
|
|
179
|
+
fail('bundled 001_rumen_tables.sql is missing');
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
let client;
|
|
183
|
+
try {
|
|
184
|
+
client = await pgRunner.connect(secrets.DATABASE_URL);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
fail(err.message);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const result = await pgRunner.applyFile(client, tableFile);
|
|
191
|
+
if (!result.ok) { fail(result.error); return false; }
|
|
192
|
+
ok(`(${result.elapsedMs}ms)`);
|
|
193
|
+
return true;
|
|
194
|
+
} finally {
|
|
195
|
+
try { await client.end(); } catch (_err) { /* ignore */ }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function deployFunction(dryRun) {
|
|
200
|
+
step('Running: supabase functions deploy rumen-tick --no-verify-jwt...');
|
|
201
|
+
if (dryRun) { ok('(dry-run)'); return true; }
|
|
202
|
+
|
|
203
|
+
// We need the supabase command to run against a repo layout with
|
|
204
|
+
// `supabase/functions/rumen-tick/`. The TermDeck install does NOT include
|
|
205
|
+
// a `supabase/` directory at the project root, so we stage a tiny working
|
|
206
|
+
// directory under `os.tmpdir()` that mirrors what the CLI expects.
|
|
207
|
+
const stage = stageRumenFunction();
|
|
208
|
+
if (!stage) {
|
|
209
|
+
fail('could not stage rumen-tick function source');
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const r = runShell('supabase', ['functions', 'deploy', 'rumen-tick', '--no-verify-jwt'], {
|
|
214
|
+
cwd: stage
|
|
215
|
+
});
|
|
216
|
+
if (!r.ok) { fail(`deploy failed (exit ${r.code})`); return false; }
|
|
217
|
+
ok();
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create a staging directory containing:
|
|
222
|
+
// <stage>/supabase/functions/rumen-tick/{index.ts, tsconfig.json}
|
|
223
|
+
// Also write a minimal `supabase/config.toml` so `supabase functions deploy`
|
|
224
|
+
// doesn't complain about a missing project root.
|
|
225
|
+
function stageRumenFunction() {
|
|
226
|
+
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'termdeck-rumen-stage-'));
|
|
227
|
+
const functionSrc = migrations.rumenFunctionDir();
|
|
228
|
+
if (!fs.existsSync(functionSrc)) return null;
|
|
229
|
+
|
|
230
|
+
const dest = path.join(stage, 'supabase', 'functions', 'rumen-tick');
|
|
231
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
232
|
+
for (const f of fs.readdirSync(functionSrc)) {
|
|
233
|
+
fs.copyFileSync(path.join(functionSrc, f), path.join(dest, f));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const configToml = `# staged by termdeck init --rumen
|
|
237
|
+
project_id = "termdeck-rumen-stage"
|
|
238
|
+
|
|
239
|
+
[functions.rumen-tick]
|
|
240
|
+
verify_jwt = false
|
|
241
|
+
`;
|
|
242
|
+
fs.writeFileSync(path.join(stage, 'supabase', 'config.toml'), configToml);
|
|
243
|
+
|
|
244
|
+
return stage;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function setFunctionSecrets(secrets, dryRun) {
|
|
248
|
+
step('Setting function secrets (DATABASE_URL, ANTHROPIC_API_KEY)...');
|
|
249
|
+
if (dryRun) { ok('(dry-run)'); return true; }
|
|
250
|
+
const r = runShellCaptured('supabase', [
|
|
251
|
+
'secrets', 'set',
|
|
252
|
+
`DATABASE_URL=${secrets.DATABASE_URL}`,
|
|
253
|
+
`ANTHROPIC_API_KEY=${secrets.ANTHROPIC_API_KEY}`
|
|
254
|
+
]);
|
|
255
|
+
if (!r.ok) {
|
|
256
|
+
fail(`secrets set failed (exit ${r.code})`);
|
|
257
|
+
if (r.stderr) process.stderr.write(r.stderr + '\n');
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
ok();
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function testFunction(projectRef, secrets, dryRun) {
|
|
265
|
+
step('Testing function with a manual POST...');
|
|
266
|
+
if (dryRun) { ok('(dry-run)'); return true; }
|
|
267
|
+
|
|
268
|
+
const functionUrl = `https://${projectRef}.supabase.co/functions/v1/rumen-tick`;
|
|
269
|
+
let response;
|
|
270
|
+
try {
|
|
271
|
+
response = await fetch(functionUrl, {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: {
|
|
274
|
+
'Authorization': `Bearer ${secrets.SUPABASE_SERVICE_ROLE_KEY}`,
|
|
275
|
+
'Content-Type': 'application/json'
|
|
276
|
+
},
|
|
277
|
+
body: '{}'
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
fail(`network error: ${err.message}`);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let body;
|
|
285
|
+
try { body = await response.json(); } catch (_err) { body = {}; }
|
|
286
|
+
|
|
287
|
+
if (response.status !== 200 || !body || body.ok !== true) {
|
|
288
|
+
fail(`function returned ${response.status} — ${JSON.stringify(body).slice(0, 200)}`);
|
|
289
|
+
process.stderr.write(
|
|
290
|
+
'\nCheck the function logs in the Supabase dashboard: ' +
|
|
291
|
+
`https://supabase.com/dashboard/project/${projectRef}/functions/rumen-tick\n`
|
|
292
|
+
);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const summary = body.summary || {};
|
|
297
|
+
ok(`(job_id: ${summary.job_id || '?'}, extracted: ${summary.extracted ?? '?'}, surfaced: ${summary.insights_generated ?? summary.surfaced ?? '?'})`);
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function applySchedule(projectRef, secrets, dryRun) {
|
|
302
|
+
step('Applying pg_cron schedule (every 15 minutes)...');
|
|
303
|
+
if (dryRun) { ok('(dry-run)'); return true; }
|
|
304
|
+
|
|
305
|
+
const files = migrations.listRumenMigrations();
|
|
306
|
+
const scheduleFile = files.find((f) => /002.*pg_cron/.test(path.basename(f)));
|
|
307
|
+
if (!scheduleFile) { fail('bundled 002_pg_cron_schedule.sql is missing'); return false; }
|
|
308
|
+
|
|
309
|
+
const raw = migrations.readFile(scheduleFile);
|
|
310
|
+
// Substitute the project ref into the schedule body. The bundled migration
|
|
311
|
+
// ships with the placeholder `<project-ref>` per Rumen's deploy docs; we
|
|
312
|
+
// also accept `{{PROJECT_REF}}` for robustness.
|
|
313
|
+
const substituted = raw
|
|
314
|
+
.replace(/<project-ref>/g, projectRef)
|
|
315
|
+
.replace(/\{\{PROJECT_REF\}\}/g, projectRef);
|
|
316
|
+
|
|
317
|
+
// The shipped migration uses Supabase Vault (`vault.decrypted_secrets`) to
|
|
318
|
+
// pull the service-role key. If the user hasn't stored the key in Vault the
|
|
319
|
+
// cron call will fail. We leave that as a post-install step and print a
|
|
320
|
+
// reminder below.
|
|
321
|
+
|
|
322
|
+
let client;
|
|
323
|
+
try {
|
|
324
|
+
client = await pgRunner.connect(secrets.DATABASE_URL);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
fail(err.message);
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
// Run the substituted SQL directly rather than applying the original file.
|
|
331
|
+
try {
|
|
332
|
+
await pgRunner.run(client, substituted);
|
|
333
|
+
ok();
|
|
334
|
+
return true;
|
|
335
|
+
} catch (err) {
|
|
336
|
+
fail(err.message);
|
|
337
|
+
process.stderr.write(
|
|
338
|
+
'\nThe schedule SQL failed — the most common cause is that pg_cron or pg_net\n' +
|
|
339
|
+
'is not enabled in the Supabase project. Enable them in Dashboard → Database\n' +
|
|
340
|
+
'→ Extensions, then re-run `termdeck init --rumen --skip-schedule=false`.\n'
|
|
341
|
+
);
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
} finally {
|
|
345
|
+
try { await client.end(); } catch (_err) { /* ignore */ }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function printNextSteps(projectRef) {
|
|
350
|
+
const functionUrl = `https://${projectRef}.supabase.co/functions/v1/rumen-tick`;
|
|
351
|
+
const now = new Date();
|
|
352
|
+
// Round up to the next 15-minute mark so the hint is accurate.
|
|
353
|
+
const next = new Date(now.getTime());
|
|
354
|
+
next.setUTCMinutes(Math.ceil((now.getUTCMinutes() + 1) / 15) * 15, 0, 0);
|
|
355
|
+
process.stdout.write(`
|
|
356
|
+
Rumen is deployed.
|
|
357
|
+
|
|
358
|
+
Schedule: every 15 minutes via pg_cron
|
|
359
|
+
First scheduled run: ${next.toISOString().replace(/\.\d+Z$/, 'Z')}
|
|
360
|
+
Edge Function URL: ${functionUrl}
|
|
361
|
+
|
|
362
|
+
Next steps:
|
|
363
|
+
1. Monitor: psql "$DATABASE_URL" -c "SELECT * FROM rumen_jobs ORDER BY started_at DESC LIMIT 5"
|
|
364
|
+
2. Store the service_role key in Supabase Vault as \`rumen_service_role_key\`
|
|
365
|
+
so the cron call in migrations/002_pg_cron_schedule.sql can authenticate.
|
|
366
|
+
3. Rumen insights flow back into Mnemos's memory_items via rumen_insights.
|
|
367
|
+
4. TermDeck's Flashback will surface cross-project patterns automatically.
|
|
368
|
+
`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function main(argv) {
|
|
372
|
+
const flags = parseFlags(argv || []);
|
|
373
|
+
if (flags.help) {
|
|
374
|
+
process.stdout.write(HELP);
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
printBanner();
|
|
379
|
+
|
|
380
|
+
const pf = preflight();
|
|
381
|
+
if (!pf) return 2;
|
|
382
|
+
const { secrets } = pf;
|
|
383
|
+
|
|
384
|
+
let projectRef;
|
|
385
|
+
try {
|
|
386
|
+
projectRef = deriveProjectRef(secrets);
|
|
387
|
+
process.stdout.write(`→ Deriving project ref from SUPABASE_URL... ✓ ${projectRef}\n`);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
process.stderr.write(`\n[init --rumen] ${err.message}\n`);
|
|
390
|
+
return 3;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!flags.yes) {
|
|
394
|
+
const go = await prompts.confirm(`? Proceed with deploy to project ${projectRef}?`);
|
|
395
|
+
if (!go) {
|
|
396
|
+
process.stdout.write('Cancelled.\n');
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!(await link(projectRef, flags.dryRun))) return 4;
|
|
402
|
+
if (!(await applyRumenTables(secrets, flags.dryRun))) return 5;
|
|
403
|
+
if (!deployFunction(flags.dryRun)) return 6;
|
|
404
|
+
if (!setFunctionSecrets(secrets, flags.dryRun)) return 7;
|
|
405
|
+
if (!(await testFunction(projectRef, secrets, flags.dryRun))) return 8;
|
|
406
|
+
if (!flags.skipSchedule) {
|
|
407
|
+
if (!(await applySchedule(projectRef, secrets, flags.dryRun))) return 9;
|
|
408
|
+
} else {
|
|
409
|
+
process.stdout.write('→ Skipping pg_cron schedule (per --skip-schedule) ✓\n');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
printNextSteps(projectRef);
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (require.main === module) {
|
|
417
|
+
main(process.argv.slice(2))
|
|
418
|
+
.then((code) => process.exit(code || 0))
|
|
419
|
+
.catch((err) => {
|
|
420
|
+
process.stderr.write(`\n[init --rumen] unexpected error: ${err && err.stack || err}\n`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = main;
|
|
@@ -1300,7 +1300,7 @@
|
|
|
1300
1300
|
<button id="btn-status">status</button>
|
|
1301
1301
|
<button id="btn-config">config</button>
|
|
1302
1302
|
<button id="btn-how" title="Walkthrough of every TermDeck feature">how this works</button>
|
|
1303
|
-
<button id="btn-help" title="Open the TermDeck documentation" onclick="window.open('https://
|
|
1303
|
+
<button id="btn-help" title="Open the TermDeck documentation" onclick="window.open('https://termdeck-docs.vercel.app','_blank','noopener')">help</button>
|
|
1304
1304
|
</div>
|
|
1305
1305
|
</div>
|
|
1306
1306
|
|
|
@@ -1901,7 +1901,7 @@
|
|
|
1901
1901
|
|
|
1902
1902
|
toast.innerHTML = `
|
|
1903
1903
|
<button class="t-dismiss" aria-label="Dismiss">×</button>
|
|
1904
|
-
<div class="t-title">
|
|
1904
|
+
<div class="t-title">Mnestra — possible match</div>
|
|
1905
1905
|
<div class="t-body">Found a similar error in <b>${proj}</b>${score ? ` · ${score}` : ''} — click to see.</div>
|
|
1906
1906
|
<div class="t-meta">${snippet}</div>
|
|
1907
1907
|
`;
|
|
@@ -2596,7 +2596,7 @@
|
|
|
2596
2596
|
// Early return if AI queries are not available
|
|
2597
2597
|
if (!state.config.aiQueryAvailable) {
|
|
2598
2598
|
entry.terminal.write(
|
|
2599
|
-
'\r\n\x1b[33m[
|
|
2599
|
+
'\r\n\x1b[33m[mnestra] AI queries are not available.\x1b[0m\r\n' +
|
|
2600
2600
|
'\x1b[33mTo enable, add the following to ~/.termdeck/config.yaml:\x1b[0m\r\n' +
|
|
2601
2601
|
'\x1b[90m rag:\r\n' +
|
|
2602
2602
|
' supabaseUrl: https://your-project.supabase.co\r\n' +
|
|
@@ -2618,7 +2618,7 @@
|
|
|
2618
2618
|
});
|
|
2619
2619
|
|
|
2620
2620
|
if (result.error) {
|
|
2621
|
-
entry.terminal.write(`\r\n\x1b[33m[
|
|
2621
|
+
entry.terminal.write(`\r\n\x1b[33m[mnestra] ${result.error}\x1b[0m\r\n`);
|
|
2622
2622
|
} else if (result.memories && result.memories.length > 0) {
|
|
2623
2623
|
// Cache hits for the Memory tab
|
|
2624
2624
|
if (!entry.memoryHits) entry.memoryHits = [];
|
|
@@ -2651,7 +2651,7 @@
|
|
|
2651
2651
|
return lines;
|
|
2652
2652
|
};
|
|
2653
2653
|
|
|
2654
|
-
entry.terminal.write(`\r\n\x1b[36m━━━
|
|
2654
|
+
entry.terminal.write(`\r\n\x1b[36m━━━ Mnestra: ${result.total} memories found ━━━\x1b[0m\r\n`);
|
|
2655
2655
|
for (const m of result.memories) {
|
|
2656
2656
|
const score = m.similarity ? `${(m.similarity * 100).toFixed(0)}%` : '';
|
|
2657
2657
|
const proj = m.project ? m.project : '';
|
|
@@ -2663,11 +2663,11 @@
|
|
|
2663
2663
|
}
|
|
2664
2664
|
entry.terminal.write(`\r\n\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\r\n\r\n`);
|
|
2665
2665
|
} else {
|
|
2666
|
-
entry.terminal.write(`\r\n\x1b[33m[
|
|
2666
|
+
entry.terminal.write(`\r\n\x1b[33m[mnestra] No relevant memories found.\x1b[0m\r\n`);
|
|
2667
2667
|
}
|
|
2668
2668
|
} catch (err) {
|
|
2669
2669
|
console.error('[client] AI query failed:', err);
|
|
2670
|
-
entry.terminal.write(`\r\n\x1b[31m[
|
|
2670
|
+
entry.terminal.write(`\r\n\x1b[31m[mnestra] Query failed: ${err.message}\x1b[0m\r\n`);
|
|
2671
2671
|
}
|
|
2672
2672
|
|
|
2673
2673
|
inputEl.value = '';
|
|
@@ -3048,7 +3048,7 @@
|
|
|
3048
3048
|
{
|
|
3049
3049
|
targets: ['#btn-how', '#btn-help'],
|
|
3050
3050
|
title: 'How this works and help',
|
|
3051
|
-
body: `Click <strong>how this works</strong> any time to replay this tour. <strong>help</strong> opens the full TermDeck documentation
|
|
3051
|
+
body: `Click <strong>how this works</strong> any time to replay this tour. <strong>help</strong> opens the full TermDeck documentation in a new tab.`,
|
|
3052
3052
|
},
|
|
3053
3053
|
{
|
|
3054
3054
|
target: '.panel-header',
|
|
@@ -3073,14 +3073,14 @@
|
|
|
3073
3073
|
{
|
|
3074
3074
|
target: '.ctrl-input',
|
|
3075
3075
|
title: 'Ask about this terminal',
|
|
3076
|
-
body: `Type a question here and TermDeck queries your <strong>
|
|
3076
|
+
body: `Type a question here and TermDeck queries your <strong>Mnestra memory store</strong> for relevant context — scoped to the current panel's project. Prefix with <kbd>all:</kbd> to search every project. Results render inline in the terminal with similarity scores.`,
|
|
3077
3077
|
onEnter: async () => { await openFirstPanelDrawer('overview'); },
|
|
3078
3078
|
fallback: '#topbarQuickLaunch',
|
|
3079
3079
|
},
|
|
3080
3080
|
{
|
|
3081
3081
|
target: null,
|
|
3082
3082
|
title: 'Flashback — proactive recall',
|
|
3083
|
-
body: `When a panel errors out, TermDeck <strong>automatically</strong> queries
|
|
3083
|
+
body: `When a panel errors out, TermDeck <strong>automatically</strong> queries Mnestra for similar past errors and surfaces the top match as a toast. You don't have to ask. Rate-limited to one per 30 seconds per panel. Click the toast to open the Memory tab with the full hit expanded.`,
|
|
3084
3084
|
},
|
|
3085
3085
|
{
|
|
3086
3086
|
target: '.prompt-bar',
|
|
@@ -141,13 +141,13 @@ function defaultConfig() {
|
|
|
141
141
|
anthropicApiKey: null,
|
|
142
142
|
developerId: os.userInfo().username,
|
|
143
143
|
syncIntervalMs: 10000,
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
mnestraMode: 'direct',
|
|
145
|
+
mnestraWebhookUrl: 'http://localhost:37778/mnestra',
|
|
146
146
|
tables: {
|
|
147
|
-
session: '
|
|
148
|
-
project: '
|
|
149
|
-
developer: '
|
|
150
|
-
commands: '
|
|
147
|
+
session: 'mnestra_session_memory',
|
|
148
|
+
project: 'mnestra_project_memory',
|
|
149
|
+
developer: 'mnestra_developer_memory',
|
|
150
|
+
commands: 'mnestra_commands'
|
|
151
151
|
}
|
|
152
152
|
},
|
|
153
153
|
sessionLogs: {
|
|
@@ -194,8 +194,8 @@ rag:
|
|
|
194
194
|
openaiApiKey: \${OPENAI_API_KEY}
|
|
195
195
|
anthropicApiKey: \${ANTHROPIC_API_KEY}
|
|
196
196
|
syncIntervalMs: 10000
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
mnestraMode: direct
|
|
198
|
+
mnestraWebhookUrl: http://localhost:37778/mnestra
|
|
199
199
|
|
|
200
200
|
sessionLogs:
|
|
201
201
|
enabled: false
|
|
@@ -17,7 +17,7 @@ try { Database = require('better-sqlite3'); } catch { Database = null; }
|
|
|
17
17
|
const { SessionManager } = require('./session');
|
|
18
18
|
const { initDatabase, logCommand, getSessionHistory, getProjectSessions } = require('./database');
|
|
19
19
|
const { RAGIntegration } = require('./rag');
|
|
20
|
-
const { createBridge } = require('./
|
|
20
|
+
const { createBridge } = require('./mnestra-bridge');
|
|
21
21
|
const { writeSessionLog } = require('./session-logger');
|
|
22
22
|
const { themes, statusColors } = require('./themes');
|
|
23
23
|
const { loadConfig, addProject } = require('./config');
|
|
@@ -54,10 +54,10 @@ function createServer(config) {
|
|
|
54
54
|
// Initialize session manager
|
|
55
55
|
const sessions = new SessionManager(db);
|
|
56
56
|
|
|
57
|
-
// Initialize RAG +
|
|
57
|
+
// Initialize RAG + Mnestra bridge
|
|
58
58
|
const rag = new RAGIntegration(config, db);
|
|
59
|
-
const
|
|
60
|
-
console.log(`[
|
|
59
|
+
const mnestraBridge = createBridge(config);
|
|
60
|
+
console.log(`[mnestra-bridge] mode=${mnestraBridge.mode}`);
|
|
61
61
|
|
|
62
62
|
// Wire RAG to session events
|
|
63
63
|
sessions.on('session:created', (s) => rag.onSessionCreated(s));
|
|
@@ -171,11 +171,11 @@ function createServer(config) {
|
|
|
171
171
|
rag.onStatusChanged(sess, oldStatus, newStatus);
|
|
172
172
|
};
|
|
173
173
|
|
|
174
|
-
// Proactive
|
|
174
|
+
// Proactive Mnestra queries on error — fire-and-forget, respects rag.enabled
|
|
175
175
|
session.onErrorDetected = (sess, ctx) => {
|
|
176
176
|
if (!rag.enabled) return;
|
|
177
177
|
const question = `${sess.meta.type} error ${ctx.lastCommand || ''} ${ctx.tail || ''}`.trim();
|
|
178
|
-
|
|
178
|
+
mnestraBridge.queryMnestra({
|
|
179
179
|
question,
|
|
180
180
|
project: sess.meta.project,
|
|
181
181
|
searchAll: false,
|
|
@@ -196,7 +196,7 @@ function createServer(config) {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
}).catch((err) => {
|
|
199
|
-
console.warn('[
|
|
199
|
+
console.warn('[mnestra-bridge] proactive query failed:', err.message);
|
|
200
200
|
});
|
|
201
201
|
};
|
|
202
202
|
|
|
@@ -415,7 +415,7 @@ function createServer(config) {
|
|
|
415
415
|
});
|
|
416
416
|
});
|
|
417
417
|
|
|
418
|
-
// POST /api/ai/query - query
|
|
418
|
+
// POST /api/ai/query - query Mnestra memory via the bridge (direct|webhook|mcp)
|
|
419
419
|
app.post('/api/ai/query', async (req, res) => {
|
|
420
420
|
let { question, sessionId, project } = req.body;
|
|
421
421
|
if (!question) return res.status(400).json({ error: 'Missing question' });
|
|
@@ -435,7 +435,7 @@ function createServer(config) {
|
|
|
435
435
|
} : null;
|
|
436
436
|
|
|
437
437
|
try {
|
|
438
|
-
const { memories, total } = await
|
|
438
|
+
const { memories, total } = await mnestraBridge.queryMnestra({
|
|
439
439
|
question,
|
|
440
440
|
project,
|
|
441
441
|
searchAll,
|
|
@@ -455,7 +455,7 @@ function createServer(config) {
|
|
|
455
455
|
total
|
|
456
456
|
});
|
|
457
457
|
} catch (err) {
|
|
458
|
-
console.error('[
|
|
458
|
+
console.error('[mnestra-bridge] query failed:', err.message);
|
|
459
459
|
// Config-shaped errors are 503, everything else 502
|
|
460
460
|
const msg = err.message || 'Query failed';
|
|
461
461
|
const status = /not configured|OPENAI_API_KEY/i.test(msg) ? 503 : 502;
|