@jhizzard/termdeck 0.2.1 → 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 +27 -27
- package/config/config.example.yaml +10 -10
- package/package.json +1 -1
- package/packages/cli/src/index.js +17 -12
- package/packages/cli/src/init-rumen.js +6 -6
- package/packages/client/public/index.html +8 -8
- 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/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Enabling Flashback takes **one additional 15-minute setup step** — see Tier 2
|
|
|
26
26
|
|
|
27
27
|
## How Flashback works
|
|
28
28
|
|
|
29
|
-
When a panel's status transitions to `errored`, the server's output analyzer fires an event. The
|
|
29
|
+
When a panel's status transitions to `errored`, the server's output analyzer fires an event. The mnestra bridge takes the session context (type, project, last command, error tail) and queries your Mnestra memory store for the top similar match. If it finds one above the relevance threshold, the result is pushed to the panel's WebSocket as a `proactive_memory` message. The client renders it as a toast anchored to the panel, showing the match's project tag, source type, similarity score, and content snippet. You click the toast to expand into the Memory tab of that panel's drawer.
|
|
30
30
|
|
|
31
31
|
Rate-limited to once per 30 seconds per panel. Needs RAG enabled and credentials configured (Tier 2+). Fires on **any** terminal panel — shell, Claude Code, Python server, anything.
|
|
32
32
|
|
|
@@ -39,8 +39,8 @@ TermDeck is one piece of a three-tier memory stack. Each tier adds capability; e
|
|
|
39
39
|
| Tier | Install time | What you get |
|
|
40
40
|
|---|---|---|
|
|
41
41
|
| **1 — TermDeck alone** | 90 seconds | Full multiplexer, metadata, layouts, themes, tour, session history, command logging. Flashback silent. |
|
|
42
|
-
| **2 — +
|
|
43
|
-
| **3 — + Rumen async learning** | ~30 minutes | Async learning layer runs on a cron, synthesizes insights across projects, writes them back into
|
|
42
|
+
| **2 — + Mnestra memory store** | ~15 minutes | Flashback actively fires. Cross-session recall. "Ask about this terminal" queries real memories. Optional Mnestra-as-MCP for Claude Code / Cursor / Windsurf. |
|
|
43
|
+
| **3 — + Rumen async learning** | ~30 minutes | Async learning layer runs on a cron, synthesizes insights across projects, writes them back into Mnestra. Flashback starts surfacing cross-project patterns, not just direct matches. |
|
|
44
44
|
|
|
45
45
|
### Tier 1 — `npx @jhizzard/termdeck` (you're here)
|
|
46
46
|
|
|
@@ -61,22 +61,22 @@ What's included:
|
|
|
61
61
|
|
|
62
62
|
What's excluded at this tier: **Flashback is silent**, the "Ask about this terminal" input returns nothing, and no memories are surfaced. All other features work.
|
|
63
63
|
|
|
64
|
-
### Tier 2 — Add
|
|
64
|
+
### Tier 2 — Add Mnestra to light up Flashback
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Mnestra is a separate npm package — `@jhizzard/mnestra@0.2.0` — that ships a Postgres-backed persistent memory store with an MCP server, a webhook server, six search tools, and six SQL migrations. It can be consumed by TermDeck (for Flashback), by Claude Code (as an MCP memory layer), or by any tool that speaks the MCP protocol.
|
|
67
67
|
|
|
68
68
|
To enable Flashback in TermDeck:
|
|
69
69
|
|
|
70
70
|
1. **Provision Postgres with pgvector.** Easiest: create a free Supabase project at supabase.com. Copy the **Project URL** and the **service_role key** from Project Settings → API.
|
|
71
|
-
2. **Apply
|
|
71
|
+
2. **Apply Mnestra's migrations** to the database. Run each in order via the Supabase SQL Editor, or via `psql`:
|
|
72
72
|
```bash
|
|
73
|
-
npm install -g @jhizzard/
|
|
74
|
-
psql "$DATABASE_URL" -f node_modules/@jhizzard/
|
|
75
|
-
psql "$DATABASE_URL" -f node_modules/@jhizzard/
|
|
76
|
-
psql "$DATABASE_URL" -f node_modules/@jhizzard/
|
|
77
|
-
psql "$DATABASE_URL" -f node_modules/@jhizzard/
|
|
78
|
-
psql "$DATABASE_URL" -f node_modules/@jhizzard/
|
|
79
|
-
psql "$DATABASE_URL" -f node_modules/@jhizzard/
|
|
73
|
+
npm install -g @jhizzard/mnestra
|
|
74
|
+
psql "$DATABASE_URL" -f node_modules/@jhizzard/mnestra/migrations/001_mnestra_tables.sql
|
|
75
|
+
psql "$DATABASE_URL" -f node_modules/@jhizzard/mnestra/migrations/002_mnestra_search_function.sql
|
|
76
|
+
psql "$DATABASE_URL" -f node_modules/@jhizzard/mnestra/migrations/003_mnestra_event_webhook.sql
|
|
77
|
+
psql "$DATABASE_URL" -f node_modules/@jhizzard/mnestra/migrations/004_mnestra_match_count_cap_and_explain.sql
|
|
78
|
+
psql "$DATABASE_URL" -f node_modules/@jhizzard/mnestra/migrations/005_v0_1_to_v0_2_upgrade.sql
|
|
79
|
+
psql "$DATABASE_URL" -f node_modules/@jhizzard/mnestra/migrations/006_memory_status_rpc.sql
|
|
80
80
|
```
|
|
81
81
|
3. **Get an OpenAI API key** (text-embedding-3-large).
|
|
82
82
|
4. **Create `~/.termdeck/secrets.env`:**
|
|
@@ -93,18 +93,18 @@ To enable Flashback in TermDeck:
|
|
|
93
93
|
supabaseUrl: ${SUPABASE_URL}
|
|
94
94
|
supabaseKey: ${SUPABASE_SERVICE_ROLE_KEY}
|
|
95
95
|
openaiApiKey: ${OPENAI_API_KEY}
|
|
96
|
-
|
|
96
|
+
mnestraMode: direct
|
|
97
97
|
```
|
|
98
98
|
6. **Restart TermDeck** (`Ctrl+C`, then `npx @jhizzard/termdeck` again).
|
|
99
99
|
|
|
100
100
|
Flashback now fires when panels error. Initially your store is empty, so nothing surfaces. As you use TermDeck day-to-day, the output analyzer captures session events, command history, and error contexts — each one becomes a memory. After a few days of real work, Flashback starts surfacing real hits.
|
|
101
101
|
|
|
102
|
-
### Tier 2 bonus —
|
|
102
|
+
### Tier 2 bonus — Mnestra as a Claude Code MCP server
|
|
103
103
|
|
|
104
|
-
Independently of TermDeck, install
|
|
104
|
+
Independently of TermDeck, install Mnestra as an MCP server so Claude Code (and Cursor / Windsurf / Cline / Continue) have persistent memory across sessions pointing at the same database TermDeck uses:
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
npm install -g @jhizzard/
|
|
107
|
+
npm install -g @jhizzard/mnestra
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
Edit `~/.claude/mcp.json`:
|
|
@@ -112,8 +112,8 @@ Edit `~/.claude/mcp.json`:
|
|
|
112
112
|
```json
|
|
113
113
|
{
|
|
114
114
|
"mcpServers": {
|
|
115
|
-
"
|
|
116
|
-
"command": "
|
|
115
|
+
"mnestra": {
|
|
116
|
+
"command": "mnestra",
|
|
117
117
|
"env": {
|
|
118
118
|
"SUPABASE_URL": "https://your-project.supabase.co",
|
|
119
119
|
"SUPABASE_SERVICE_ROLE_KEY": "sb_secret_...",
|
|
@@ -125,11 +125,11 @@ Edit `~/.claude/mcp.json`:
|
|
|
125
125
|
}
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
Restart Claude Code. Six MCP tools appear: `memory_remember`, `memory_recall`, `memory_search`, `memory_forget`, `memory_status`, `memory_summarize_session`. Your AI assistant can now read and write memories that TermDeck's Flashback will later surface automatically. See [github.com/jhizzard/
|
|
128
|
+
Restart Claude Code. Six MCP tools appear: `memory_remember`, `memory_recall`, `memory_search`, `memory_forget`, `memory_status`, `memory_summarize_session`. Your AI assistant can now read and write memories that TermDeck's Flashback will later surface automatically. See [github.com/jhizzard/mnestra](https://github.com/jhizzard/mnestra) for full MCP reference.
|
|
129
129
|
|
|
130
130
|
### Tier 3 — Add Rumen for async learning
|
|
131
131
|
|
|
132
|
-
Rumen is a separate npm package — `@jhizzard/rumen@0.2.0` — that ships as a Supabase Edge Function designed to run on a 15-minute `pg_cron` schedule. It's the async reflection layer over
|
|
132
|
+
Rumen is a separate npm package — `@jhizzard/rumen@0.2.0` — that ships as a Supabase Edge Function designed to run on a 15-minute `pg_cron` schedule. It's the async reflection layer over Mnestra: it reads recent session memories, cross-references them with your entire historical corpus via hybrid search, synthesizes insights via Claude Haiku, and writes the results back into `rumen_insights` (a new table alongside Mnestra's `memory_items`). TermDeck's Flashback and Claude Code's `memory_recall` both automatically benefit because insights flow back into the same database.
|
|
133
133
|
|
|
134
134
|
Rumen v0.2 is the current shipped version. Setup is still manual — Sprint 3 (next) ships a `termdeck init --rumen` one-liner that automates the deploy. For now, follow [github.com/jhizzard/rumen#readme](https://github.com/jhizzard/rumen) to deploy via the Supabase CLI.
|
|
135
135
|
|
|
@@ -162,12 +162,12 @@ Honest limits, stated upfront so the skeptic has nothing to chase:
|
|
|
162
162
|
│ - Express + ws │
|
|
163
163
|
│ - node-pty per session │
|
|
164
164
|
│ - SQLite persistence │
|
|
165
|
-
│ -
|
|
165
|
+
│ - Mnestra bridge (direct/webhook/mcp) │
|
|
166
166
|
└─────────────────┬───────────────────────┘
|
|
167
167
|
│ hybrid search + embeddings
|
|
168
168
|
▼
|
|
169
169
|
┌─────────────────────────────────────────┐
|
|
170
|
-
│
|
|
170
|
+
│ Mnestra │
|
|
171
171
|
│ - Postgres + pgvector │
|
|
172
172
|
│ - MCP server (6 tools) │
|
|
173
173
|
│ - HTTP webhook │
|
|
@@ -183,7 +183,7 @@ Honest limits, stated upfront so the skeptic has nothing to chase:
|
|
|
183
183
|
└─────────────────────────────────────────┘
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
All three packages are independent. You can use
|
|
186
|
+
All three packages are independent. You can use Mnestra alone as a Claude Code memory layer. You can run Rumen on any pgvector store with compatible schema. You can use TermDeck with zero memory features. The stack is designed to be progressively adopted.
|
|
187
187
|
|
|
188
188
|
---
|
|
189
189
|
|
|
@@ -201,7 +201,7 @@ For users who want more than `npx` — cloning from source, building a macOS `.a
|
|
|
201
201
|
|
|
202
202
|
## Related packages
|
|
203
203
|
|
|
204
|
-
- **[@jhizzard/
|
|
204
|
+
- **[@jhizzard/mnestra](https://www.npmjs.com/package/@jhizzard/mnestra)** — persistent dev memory MCP server. pgvector + hybrid search + 3-layer progressive disclosure. Works standalone with any MCP client.
|
|
205
205
|
- **[@jhizzard/rumen](https://www.npmjs.com/package/@jhizzard/rumen)** — async learning layer. Extract/Relate/Synthesize loop over any pgvector store. Supabase Edge Function + `pg_cron`.
|
|
206
206
|
|
|
207
207
|
---
|
|
@@ -218,7 +218,7 @@ npm run dev
|
|
|
218
218
|
The server runs at `http://127.0.0.1:3000` with file-watch reload. Workspace layout:
|
|
219
219
|
|
|
220
220
|
- `packages/cli/src/` — the `termdeck` binary launcher
|
|
221
|
-
- `packages/server/src/` — Express + WebSocket + PTY +
|
|
221
|
+
- `packages/server/src/` — Express + WebSocket + PTY + Mnestra bridge + session analyzer
|
|
222
222
|
- `packages/client/public/` — vanilla-JS dashboard (single HTML file, no build step, loads xterm.js from CDN)
|
|
223
223
|
- `config/` — example `config.yaml` and `secrets.env.example`
|
|
224
224
|
- `docs/` — planning documents, launch strategy, install guide, followup items, ship checklist
|
|
@@ -258,7 +258,7 @@ MIT © Joshua Izzard. See [LICENSE](LICENSE).
|
|
|
258
258
|
- GitHub: [github.com/jhizzard/termdeck](https://github.com/jhizzard/termdeck)
|
|
259
259
|
- npm: [@jhizzard/termdeck](https://www.npmjs.com/package/@jhizzard/termdeck)
|
|
260
260
|
- Issues: [github.com/jhizzard/termdeck/issues](https://github.com/jhizzard/termdeck/issues)
|
|
261
|
-
-
|
|
261
|
+
- Mnestra: [github.com/jhizzard/mnestra](https://github.com/jhizzard/mnestra) · [npm](https://www.npmjs.com/package/@jhizzard/mnestra)
|
|
262
262
|
- Rumen: [github.com/jhizzard/rumen](https://github.com/jhizzard/rumen) · [npm](https://www.npmjs.com/package/@jhizzard/rumen)
|
|
263
263
|
|
|
264
264
|
Built because every LLM starts from zero, and every terminal starts from zero, and I got tired of re-debugging the same CORS error.
|
|
@@ -35,20 +35,20 @@ rag:
|
|
|
35
35
|
# developerId: your-username (defaults to OS username)
|
|
36
36
|
syncIntervalMs: 10000
|
|
37
37
|
|
|
38
|
-
#
|
|
38
|
+
# Mnestra bridge — how /api/ai/query talks to @jhizzard/mnestra.
|
|
39
39
|
# direct — server embeds + queries Supabase itself (default, preserves v0.1 behavior)
|
|
40
|
-
# webhook — POST to a running `
|
|
41
|
-
# mcp — spawn the local `
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
#
|
|
40
|
+
# webhook — POST to a running `mnestra serve` HTTP webhook (see mnestraWebhookUrl)
|
|
41
|
+
# mcp — spawn the local `mnestra` binary and talk JSON-RPC over stdio
|
|
42
|
+
mnestraMode: direct
|
|
43
|
+
mnestraWebhookUrl: http://localhost:37778/mnestra
|
|
44
|
+
# mnestraBinary: mnestra # override path to the @jhizzard/mnestra CLI for mcp mode
|
|
45
45
|
|
|
46
46
|
# Supabase table names (created by config/supabase-migration.sql)
|
|
47
47
|
tables:
|
|
48
|
-
session:
|
|
49
|
-
project:
|
|
50
|
-
developer:
|
|
51
|
-
commands:
|
|
48
|
+
session: mnestra_session_memory
|
|
49
|
+
project: mnestra_project_memory
|
|
50
|
+
developer: mnestra_developer_memory
|
|
51
|
+
commands: mnestra_commands
|
|
52
52
|
|
|
53
53
|
# Per-session markdown logs written to ~/.termdeck/sessions/ on session exit.
|
|
54
54
|
# Zero-config: works without Supabase / OpenAI. Optional LLM summary requires
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
|
|
5
5
|
"bin": {
|
|
6
6
|
"termdeck": "./packages/cli/src/index.js"
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
// TermDeck CLI launcher
|
|
4
4
|
// Usage:
|
|
5
5
|
// termdeck [--port 3000] [--no-open]
|
|
6
|
-
// termdeck init --
|
|
7
|
-
// termdeck init --rumen [flags]
|
|
6
|
+
// termdeck init --mnestra [flags] # Tier 2 memory setup (wired to init-mnestra.js)
|
|
7
|
+
// termdeck init --rumen [flags] # Tier 3 async learning deploy
|
|
8
|
+
//
|
|
9
|
+
// Note (Sprint 3): the `--mnestra` flag name matches the current init-mnestra.js
|
|
10
|
+
// filename. When the main orchestrator completes the Mnestra → Ingram rename
|
|
11
|
+
// sweep over this repo, both the flag name and the filename should flip to
|
|
12
|
+
// `--ingram` / `init-ingram.js` together.
|
|
8
13
|
|
|
9
14
|
const path = require('path');
|
|
10
15
|
const { execSync } = require('child_process');
|
|
@@ -12,10 +17,10 @@ const { execSync } = require('child_process');
|
|
|
12
17
|
// Parse CLI args
|
|
13
18
|
const args = process.argv.slice(2);
|
|
14
19
|
|
|
15
|
-
// Subcommand dispatch — handle `termdeck init --
|
|
16
|
-
// falling through to the default launcher's flag parsing.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
20
|
+
// Subcommand dispatch — handle `termdeck init --mnestra|--rumen` before
|
|
21
|
+
// falling through to the default launcher's flag parsing. The `require` of
|
|
22
|
+
// init-*.js is lazy so users running the normal `termdeck` command never pay
|
|
23
|
+
// the cost of loading pg / supabase helpers at startup.
|
|
19
24
|
if (args[0] === 'init') {
|
|
20
25
|
const mode = args[1];
|
|
21
26
|
const rest = args.slice(2);
|
|
@@ -23,9 +28,9 @@ if (args[0] === 'init') {
|
|
|
23
28
|
const fn = require(modPath);
|
|
24
29
|
return fn(rest).then((code) => process.exit(code || 0));
|
|
25
30
|
};
|
|
26
|
-
if (mode === '--
|
|
27
|
-
run(path.join(__dirname, 'init-
|
|
28
|
-
console.error('[cli] init --
|
|
31
|
+
if (mode === '--mnestra') {
|
|
32
|
+
run(path.join(__dirname, 'init-mnestra.js')).catch((err) => {
|
|
33
|
+
console.error('[cli] init --mnestra failed:', err && err.stack || err);
|
|
29
34
|
process.exit(1);
|
|
30
35
|
});
|
|
31
36
|
return;
|
|
@@ -37,8 +42,8 @@ if (args[0] === 'init') {
|
|
|
37
42
|
});
|
|
38
43
|
return;
|
|
39
44
|
}
|
|
40
|
-
console.error('Usage: termdeck init --
|
|
41
|
-
console.error(' termdeck init --
|
|
45
|
+
console.error('Usage: termdeck init --mnestra | --rumen');
|
|
46
|
+
console.error(' termdeck init --mnestra Configure Tier 2 memory (Supabase + Ingram)');
|
|
42
47
|
console.error(' termdeck init --rumen Deploy Tier 3 async learning (Rumen)');
|
|
43
48
|
process.exit(1);
|
|
44
49
|
}
|
|
@@ -61,7 +66,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
61
66
|
termdeck --port 8080 Start on custom port
|
|
62
67
|
termdeck --no-open Don't auto-open browser
|
|
63
68
|
termdeck --session-logs Write per-session markdown logs to ~/.termdeck/sessions/
|
|
64
|
-
termdeck init --
|
|
69
|
+
termdeck init --mnestra Configure Tier 2 memory (Supabase + Ingram)
|
|
65
70
|
termdeck init --rumen Deploy Tier 3 async learning (Rumen)
|
|
66
71
|
|
|
67
72
|
Keyboard shortcuts (in browser):
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
// `termdeck init --rumen` — interactive wizard for deploying Rumen as a
|
|
4
4
|
// Supabase Edge Function + pg_cron schedule against the same Supabase
|
|
5
|
-
// project that holds the
|
|
5
|
+
// project that holds the Mnemos store.
|
|
6
6
|
//
|
|
7
7
|
// Requirements checked at runtime:
|
|
8
8
|
// - `supabase` CLI on PATH
|
|
9
9
|
// - `deno` on PATH
|
|
10
10
|
// - `~/.termdeck/secrets.env` with SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY +
|
|
11
|
-
// DATABASE_URL + ANTHROPIC_API_KEY populated (run `termdeck init --
|
|
11
|
+
// DATABASE_URL + ANTHROPIC_API_KEY populated (run `termdeck init --mnemos` first)
|
|
12
12
|
//
|
|
13
13
|
// Steps:
|
|
14
14
|
// 1. Preflight: which supabase, which deno, read secrets.env
|
|
@@ -47,7 +47,7 @@ const HELP = [
|
|
|
47
47
|
' --skip-schedule Deploy the function but do not install the pg_cron schedule',
|
|
48
48
|
'',
|
|
49
49
|
'Requires: Supabase CLI and Deno already installed.',
|
|
50
|
-
'Requires: `termdeck init --
|
|
50
|
+
'Requires: `termdeck init --mnemos` has already run (needs secrets.env).',
|
|
51
51
|
''
|
|
52
52
|
].join('\n');
|
|
53
53
|
|
|
@@ -112,14 +112,14 @@ function preflight() {
|
|
|
112
112
|
}
|
|
113
113
|
ok();
|
|
114
114
|
|
|
115
|
-
step('Reading
|
|
115
|
+
step('Reading Mnemos config from ~/.termdeck/secrets.env...');
|
|
116
116
|
const secrets = dotenv.readSecrets();
|
|
117
117
|
const required = ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'DATABASE_URL', 'ANTHROPIC_API_KEY'];
|
|
118
118
|
const missing = required.filter((k) => !secrets[k]);
|
|
119
119
|
if (missing.length > 0) {
|
|
120
120
|
fail(`missing keys: ${missing.join(', ')}`);
|
|
121
121
|
process.stderr.write(
|
|
122
|
-
'\nRun `termdeck init --
|
|
122
|
+
'\nRun `termdeck init --mnemos` first — it writes the keys this wizard needs.\n'
|
|
123
123
|
);
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
@@ -363,7 +363,7 @@ Next steps:
|
|
|
363
363
|
1. Monitor: psql "$DATABASE_URL" -c "SELECT * FROM rumen_jobs ORDER BY started_at DESC LIMIT 5"
|
|
364
364
|
2. Store the service_role key in Supabase Vault as \`rumen_service_role_key\`
|
|
365
365
|
so the cron call in migrations/002_pg_cron_schedule.sql can authenticate.
|
|
366
|
-
3. Rumen insights flow back into
|
|
366
|
+
3. Rumen insights flow back into Mnemos's memory_items via rumen_insights.
|
|
367
367
|
4. TermDeck's Flashback will surface cross-project patterns automatically.
|
|
368
368
|
`);
|
|
369
369
|
}
|
|
@@ -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 = '';
|
|
@@ -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;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Mnestra bridge — routes TermDeck memory queries through one of three backends:
|
|
2
2
|
// - direct: talk to Supabase + OpenAI from the server (pre-bridge behavior)
|
|
3
|
-
// - webhook: POST to
|
|
4
|
-
// - mcp: spawn the @jhizzard/
|
|
3
|
+
// - webhook: POST to Mnestra's HTTP webhook server (T3.1) at rag.mnestraWebhookUrl
|
|
4
|
+
// - mcp: spawn the @jhizzard/mnestra binary and talk JSON-RPC over stdio
|
|
5
5
|
//
|
|
6
6
|
// All three modes return the same shape:
|
|
7
7
|
// { memories: Array<{ content, source_type, project, similarity, created_at }>, total }
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
const { spawn } = require('child_process');
|
|
12
12
|
|
|
13
13
|
function createBridge(config) {
|
|
14
|
-
const mode = config.rag?.
|
|
14
|
+
const mode = config.rag?.mnestraMode || 'direct';
|
|
15
15
|
const state = { mcpChild: null, mcpQueue: [], mcpNextId: 1, mcpBuffer: '' };
|
|
16
16
|
|
|
17
17
|
async function queryDirect({ question, project, searchAll }) {
|
|
@@ -40,7 +40,7 @@ function createBridge(config) {
|
|
|
40
40
|
});
|
|
41
41
|
if (!embeddingRes.ok) {
|
|
42
42
|
const err = await embeddingRes.text();
|
|
43
|
-
console.error('[
|
|
43
|
+
console.error('[mnestra-bridge:direct] embedding failed:', err);
|
|
44
44
|
throw new Error('Embedding generation failed');
|
|
45
45
|
}
|
|
46
46
|
const embeddingData = await embeddingRes.json();
|
|
@@ -68,7 +68,7 @@ function createBridge(config) {
|
|
|
68
68
|
});
|
|
69
69
|
if (!searchRes.ok) {
|
|
70
70
|
const err = await searchRes.text();
|
|
71
|
-
console.error('[
|
|
71
|
+
console.error('[mnestra-bridge:direct] supabase search failed:', err);
|
|
72
72
|
throw new Error('Memory search failed');
|
|
73
73
|
}
|
|
74
74
|
const rows = await searchRes.json();
|
|
@@ -85,7 +85,7 @@ function createBridge(config) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
async function queryWebhook({ question, project, searchAll }) {
|
|
88
|
-
const url = config.rag?.
|
|
88
|
+
const url = config.rag?.mnestraWebhookUrl || 'http://localhost:37778/mnestra';
|
|
89
89
|
const res = await fetch(url, {
|
|
90
90
|
method: 'POST',
|
|
91
91
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -98,8 +98,8 @@ function createBridge(config) {
|
|
|
98
98
|
});
|
|
99
99
|
if (!res.ok) {
|
|
100
100
|
const err = await res.text();
|
|
101
|
-
console.error('[
|
|
102
|
-
throw new Error(`
|
|
101
|
+
console.error('[mnestra-bridge:webhook] request failed:', err);
|
|
102
|
+
throw new Error(`Mnestra webhook returned ${res.status}`);
|
|
103
103
|
}
|
|
104
104
|
const data = await res.json();
|
|
105
105
|
const rows = data.memories || [];
|
|
@@ -118,7 +118,7 @@ function createBridge(config) {
|
|
|
118
118
|
function ensureMcpChild() {
|
|
119
119
|
if (state.mcpChild && !state.mcpChild.killed) return state.mcpChild;
|
|
120
120
|
|
|
121
|
-
const bin = config.rag?.
|
|
121
|
+
const bin = config.rag?.mnestraBinary || 'mnestra';
|
|
122
122
|
const child = spawn(bin, ['serve', '--stdio'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
123
123
|
state.mcpChild = child;
|
|
124
124
|
state.mcpBuffer = '';
|
|
@@ -135,24 +135,24 @@ function createBridge(config) {
|
|
|
135
135
|
const pending = state.mcpQueue.find((p) => p.id === msg.id);
|
|
136
136
|
if (pending) {
|
|
137
137
|
state.mcpQueue = state.mcpQueue.filter((p) => p !== pending);
|
|
138
|
-
if (msg.error) pending.reject(new Error(msg.error.message || '
|
|
138
|
+
if (msg.error) pending.reject(new Error(msg.error.message || 'Mnestra MCP error'));
|
|
139
139
|
else pending.resolve(msg.result);
|
|
140
140
|
}
|
|
141
141
|
} catch (err) {
|
|
142
|
-
console.error('[
|
|
142
|
+
console.error('[mnestra-bridge:mcp] parse error:', err.message, line);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
child.stderr.on('data', (chunk) => {
|
|
148
|
-
console.error('[
|
|
148
|
+
console.error('[mnestra-bridge:mcp]', chunk.toString('utf-8').trim());
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
child.on('exit', (code, signal) => {
|
|
152
|
-
console.warn(`[
|
|
152
|
+
console.warn(`[mnestra-bridge:mcp] child exited (code=${code}, signal=${signal}); will respawn on next call`);
|
|
153
153
|
state.mcpChild = null;
|
|
154
154
|
for (const pending of state.mcpQueue) {
|
|
155
|
-
pending.reject(new Error('
|
|
155
|
+
pending.reject(new Error('Mnestra MCP child exited'));
|
|
156
156
|
}
|
|
157
157
|
state.mcpQueue = [];
|
|
158
158
|
});
|
|
@@ -177,7 +177,7 @@ function createBridge(config) {
|
|
|
177
177
|
const pending = state.mcpQueue.find((p) => p.id === id);
|
|
178
178
|
if (pending) {
|
|
179
179
|
state.mcpQueue = state.mcpQueue.filter((p) => p !== pending);
|
|
180
|
-
pending.reject(new Error('
|
|
180
|
+
pending.reject(new Error('Mnestra MCP call timed out'));
|
|
181
181
|
}
|
|
182
182
|
}, 15000);
|
|
183
183
|
});
|
|
@@ -214,7 +214,7 @@ function createBridge(config) {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
async function
|
|
217
|
+
async function queryMnestra({ question, project, searchAll }) {
|
|
218
218
|
switch (mode) {
|
|
219
219
|
case 'webhook':
|
|
220
220
|
return queryWebhook({ question, project, searchAll });
|
|
@@ -226,7 +226,7 @@ function createBridge(config) {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
return { mode,
|
|
229
|
+
return { mode, queryMnestra };
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
module.exports = { createBridge };
|
|
@@ -16,10 +16,10 @@ class RAGIntegration {
|
|
|
16
16
|
|
|
17
17
|
// Table configuration matching Josh's multi-layer schema
|
|
18
18
|
this.tables = {
|
|
19
|
-
sessionMemory: config.rag?.tables?.session || '
|
|
20
|
-
projectMemory: config.rag?.tables?.project || '
|
|
21
|
-
developerMemory: config.rag?.tables?.developer || '
|
|
22
|
-
commandLog: config.rag?.tables?.commands || '
|
|
19
|
+
sessionMemory: config.rag?.tables?.session || 'mnestra_session_memory',
|
|
20
|
+
projectMemory: config.rag?.tables?.project || 'mnestra_project_memory',
|
|
21
|
+
developerMemory: config.rag?.tables?.developer || 'mnestra_developer_memory',
|
|
22
|
+
commandLog: config.rag?.tables?.commands || 'mnestra_commands'
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
if (this.enabled) {
|
|
@@ -120,7 +120,7 @@ class RAGIntegration {
|
|
|
120
120
|
}
|
|
121
121
|
} catch (err) {
|
|
122
122
|
// Will be retried by sync loop
|
|
123
|
-
console.error('[
|
|
123
|
+
console.error('[mnestra] Push failed:', err.message);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -163,7 +163,7 @@ class RAGIntegration {
|
|
|
163
163
|
markRagEventsSynced(this.db, synced);
|
|
164
164
|
}
|
|
165
165
|
} catch (err) {
|
|
166
|
-
console.error('[
|
|
166
|
+
console.error('[mnestra] Sync cycle error:', err.message);
|
|
167
167
|
}
|
|
168
168
|
}, this.syncInterval);
|
|
169
169
|
}
|