Add ctx context manager, installer, and auto-detect Claude path

- ctx: SQLite-based cross-session context manager for Claude Code
- install.sh: automated installer (deps, files, symlinks, DB, .desktop)
- bterminal.py: replace hardcoded CLAUDE_PATH with auto-detection
- README.md: rewrite in English, document ctx and installation
- .gitignore: add CLAUDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DexterFromLab 2026-03-05 11:54:24 +01:00
parent edc13e2d27
commit 4268ead6a4
5 changed files with 640 additions and 34 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
__pycache__/ __pycache__/
*.pyc *.pyc
*.pyo *.pyo
CLAUDE.md

105
README.md
View file

@ -1,59 +1,98 @@
# BTerminal # BTerminal
Terminal z panelem sesji w stylu MobaXterm, zbudowany w GTK 3 + VTE. Catppuccin Mocha theme. Terminal with session panel (MobaXterm-style), built with GTK 3 + VTE. Catppuccin Mocha theme.
![BTerminal](screenshot.png) ![BTerminal](screenshot.png)
## Funkcje ## Features
- **Sesje SSH** — zapisywane konfiguracje (host, port, user, klucz, folder, kolor), CRUD z panelem bocznym - **SSH sessions** — saved configs (host, port, user, key, folder, color), CRUD with side panel
- **Claude Code** — zapisywane konfiguracje Claude Code z opcjami sudo, resume, skip-permissions i initial prompt - **Claude Code sessions** — saved Claude Code configs with sudo, resume, skip-permissions and initial prompt
- **Makra SSH** — wielokrokowe makra (text, key, delay) przypisane do sesji, uruchamiane z sidebara - **SSH macros** — multi-step macros (text, key, delay) assigned to sessions, run from sidebar
- **Zakładki** — wiele terminali w tabach, Ctrl+T nowy, Ctrl+Shift+W zamknij, Ctrl+PageUp/Down przełączaj - **Tabs** — multiple terminals in tabs, Ctrl+T new, Ctrl+Shift+W close, Ctrl+PageUp/Down switch
- **Sudo askpass** — Claude Code z sudo: hasło podawane raz, tymczasowy askpass helper, automatyczne czyszczenie - **Sudo askpass** — Claude Code with sudo: password entered once, temporary askpass helper, auto-cleanup
- **Grupowanie folderami** — sesje SSH i Claude Code mogą być grupowane w foldery na sidebarze - **Folder grouping** — SSH and Claude Code sessions can be grouped in folders on the sidebar
- **Catppuccin Mocha** — pełny theme: terminal, sidebar, taby, kolory sesji - **ctx — Context manager** — SQLite-based cross-session context database for Claude Code projects
- **Catppuccin Mocha** — full theme: terminal, sidebar, tabs, session colors
## Wymagania ## Installation
``` ```bash
python3 >= 3.8 git clone https://github.com/DexterFromLab/BTerminal.git
python3-gi cd BTerminal
gir1.2-gtk-3.0 ./install.sh
gir1.2-vte-2.91
``` ```
### Instalacja zależności (Debian/Ubuntu/Pop!_OS) The installer will:
1. Install system dependencies (python3-gi, GTK3, VTE)
2. Copy files to `~/.local/share/bterminal/`
3. Create symlinks: `bterminal` and `ctx` in `~/.local/bin/`
4. Initialize context database at `~/.claude-context/context.db`
5. Add desktop entry to application menu
### Manual dependency install (Debian/Ubuntu/Pop!_OS)
```bash ```bash
sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-vte-2.91 sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-vte-2.91
``` ```
## Uruchomienie ## Usage
```bash ```bash
python3 bterminal.py bterminal
``` ```
## Konfiguracja ## Context Manager (ctx)
Pliki konfiguracyjne w `~/.config/bterminal/`: `ctx` is a SQLite-based tool for managing persistent context across Claude Code sessions.
| Plik | Opis | ```bash
|------|------| ctx init myproject "Project description" /path/to/project
| `sessions.json` | Zapisane sesje SSH + makra | ctx get myproject # Load full context (shared + project)
| `claude_sessions.json` | Zapisane konfiguracje Claude Code | ctx set myproject key "value" # Save a context entry
ctx shared set preferences "value" # Save shared context (available in all projects)
ctx summary myproject "What was done" # Save session summary
ctx search "query" # Full-text search across everything
ctx list # List all projects
ctx history myproject # Show session history
ctx --help # All commands
```
## Skróty klawiszowe ### Integration with Claude Code
| Skrót | Akcja | Add a `CLAUDE.md` to your project root:
|-------|-------|
| `Ctrl+T` | Nowa zakładka (local shell) |
| `Ctrl+Shift+W` | Zamknij zakładkę |
| `Ctrl+Shift+C` | Kopiuj |
| `Ctrl+Shift+V` | Wklej |
| `Ctrl+PageUp/Down` | Poprzednia/następna zakładka |
## Licencja ```markdown
On session start, load context:
ctx get myproject
Save important discoveries: ctx set myproject <key> <value>
Before ending session: ctx summary myproject "<what was done>"
```
Claude Code reads `CLAUDE.md` automatically and will maintain the context database.
## Configuration
Config files in `~/.config/bterminal/`:
| File | Description |
|------|-------------|
| `sessions.json` | Saved SSH sessions + macros |
| `claude_sessions.json` | Saved Claude Code configs |
Context database: `~/.claude-context/context.db`
## Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| `Ctrl+T` | New tab (local shell) |
| `Ctrl+Shift+W` | Close tab |
| `Ctrl+Shift+C` | Copy |
| `Ctrl+Shift+V` | Paste |
| `Ctrl+PageUp/Down` | Previous/next tab |
## License
MIT MIT

View file

@ -20,7 +20,19 @@ CONFIG_DIR = os.path.expanduser("~/.config/bterminal")
SESSIONS_FILE = os.path.join(CONFIG_DIR, "sessions.json") SESSIONS_FILE = os.path.join(CONFIG_DIR, "sessions.json")
CLAUDE_SESSIONS_FILE = os.path.join(CONFIG_DIR, "claude_sessions.json") CLAUDE_SESSIONS_FILE = os.path.join(CONFIG_DIR, "claude_sessions.json")
SSH_PATH = "/usr/bin/ssh" SSH_PATH = "/usr/bin/ssh"
CLAUDE_PATH = "/home/bartek/.local/bin/claude"
def _find_claude_path():
for p in [
os.path.expanduser("~/.local/bin/claude"),
"/usr/local/bin/claude",
"/usr/bin/claude",
]:
if os.path.isfile(p) and os.access(p, os.X_OK):
return p
import shutil
return shutil.which("claude") or "claude"
CLAUDE_PATH = _find_claude_path()
FONT = "Monospace 11" FONT = "Monospace 11"
SCROLLBACK_LINES = 10000 SCROLLBACK_LINES = 10000

461
ctx Executable file
View file

@ -0,0 +1,461 @@
#!/usr/bin/env python3
"""
ctx — Cross-session context manager for Claude Code.
Stores project contexts and shared data in SQLite for instant access.
Usage: ctx <command> [args]
"""
import sqlite3
import sys
import os
import json
from datetime import datetime, timezone
from pathlib import Path
DB_PATH = Path.home() / ".claude-context" / "context.db"
def get_db():
db = sqlite3.connect(str(DB_PATH))
db.row_factory = sqlite3.Row
db.execute("PRAGMA journal_mode=WAL")
return db
def init_db():
db = get_db()
db.executescript("""
CREATE TABLE IF NOT EXISTS sessions (
name TEXT PRIMARY KEY,
description TEXT,
work_dir TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS contexts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(project, key)
);
CREATE TABLE IF NOT EXISTS shared (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project TEXT NOT NULL,
summary TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE VIRTUAL TABLE IF NOT EXISTS contexts_fts USING fts5(
project, key, value, content=contexts, content_rowid=id
);
CREATE VIRTUAL TABLE IF NOT EXISTS shared_fts USING fts5(
key, value, content=shared
);
-- Triggers to keep FTS in sync
CREATE TRIGGER IF NOT EXISTS contexts_ai AFTER INSERT ON contexts BEGIN
INSERT INTO contexts_fts(rowid, project, key, value)
VALUES (new.id, new.project, new.key, new.value);
END;
CREATE TRIGGER IF NOT EXISTS contexts_ad AFTER DELETE ON contexts BEGIN
INSERT INTO contexts_fts(contexts_fts, rowid, project, key, value)
VALUES ('delete', old.id, old.project, old.key, old.value);
END;
CREATE TRIGGER IF NOT EXISTS contexts_au AFTER UPDATE ON contexts BEGIN
INSERT INTO contexts_fts(contexts_fts, rowid, project, key, value)
VALUES ('delete', old.id, old.project, old.key, old.value);
INSERT INTO contexts_fts(rowid, project, key, value)
VALUES (new.id, new.project, new.key, new.value);
END;
""")
db.close()
# ─── Commands ───────────────────────────────────────────────────────────
def cmd_init(args):
"""Register a new project. Usage: ctx init <project> <description> [work_dir]"""
if len(args) < 2:
print("Usage: ctx init <project> <description> [work_dir]")
sys.exit(1)
name, desc = args[0], args[1]
work_dir = args[2] if len(args) > 2 else None
db = get_db()
db.execute(
"INSERT OR REPLACE INTO sessions (name, description, work_dir) VALUES (?, ?, ?)",
(name, desc, work_dir),
)
db.commit()
db.close()
print(f"Project '{name}' registered.")
def cmd_get(args):
"""Get full context for a project (shared + project-specific + recent summaries)."""
if len(args) < 1:
print("Usage: ctx get <project>")
sys.exit(1)
project = args[0]
db = get_db()
# Session info
session = db.execute("SELECT * FROM sessions WHERE name = ?", (project,)).fetchone()
# Shared context
shared = db.execute("SELECT key, value FROM shared ORDER BY key").fetchall()
# Project context
contexts = db.execute(
"SELECT key, value FROM contexts WHERE project = ? ORDER BY key", (project,)
).fetchall()
# Recent summaries (last 5)
summaries = db.execute(
"SELECT summary, created_at FROM summaries WHERE project = ? ORDER BY created_at DESC LIMIT 5",
(project,),
).fetchall()
# Output
print("=" * 60)
if session:
print(f"PROJECT: {session['name']} — {session['description']}")
if session["work_dir"]:
print(f"DIR: {session['work_dir']}")
else:
print(f"PROJECT: {project} (not registered, use: ctx init)")
print("=" * 60)
if shared:
print("\n--- Shared Context ---")
for row in shared:
print(f"\n[{row['key']}]")
print(row["value"])
if contexts:
print(f"\n--- {project} Context ---")
for row in contexts:
print(f"\n[{row['key']}]")
print(row["value"])
if summaries:
print("\n--- Recent Sessions ---")
for row in reversed(summaries):
print(f"\n[{row['created_at']}]")
print(row["summary"])
if not shared and not contexts and not summaries:
print("\nNo context stored yet. Use 'ctx set' or 'ctx shared set' to add.")
db.close()
def cmd_set(args):
"""Set a project context entry. Usage: ctx set <project> <key> <value>"""
if len(args) < 3:
print("Usage: ctx set <project> <key> <value>")
sys.exit(1)
project, key, value = args[0], args[1], " ".join(args[2:])
db = get_db()
db.execute(
"""INSERT INTO contexts (project, key, value, updated_at)
VALUES (?, ?, ?, datetime('now'))
ON CONFLICT(project, key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at""",
(project, key, value),
)
db.commit()
db.close()
print(f"[{project}] {key} = saved.")
def cmd_append(args):
"""Append to an existing context entry. Usage: ctx append <project> <key> <value>"""
if len(args) < 3:
print("Usage: ctx append <project> <key> <value>")
sys.exit(1)
project, key, new_value = args[0], args[1], " ".join(args[2:])
db = get_db()
existing = db.execute(
"SELECT value FROM contexts WHERE project = ? AND key = ?", (project, key)
).fetchone()
if existing:
value = existing["value"] + "\n" + new_value
else:
value = new_value
db.execute(
"""INSERT INTO contexts (project, key, value, updated_at)
VALUES (?, ?, ?, datetime('now'))
ON CONFLICT(project, key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at""",
(project, key, value),
)
db.commit()
db.close()
print(f"[{project}] {key} += appended.")
def cmd_shared(args):
"""Manage shared context. Usage: ctx shared get | ctx shared set <key> <value>"""
if len(args) < 1:
print("Usage: ctx shared get | ctx shared set <key> <value>")
sys.exit(1)
if args[0] == "get":
db = get_db()
rows = db.execute("SELECT key, value FROM shared ORDER BY key").fetchall()
db.close()
if rows:
for row in rows:
print(f"\n[{row['key']}]")
print(row["value"])
else:
print("No shared context yet.")
elif args[0] == "set":
if len(args) < 3:
print("Usage: ctx shared set <key> <value>")
sys.exit(1)
key, value = args[1], " ".join(args[2:])
db = get_db()
db.execute(
"""INSERT INTO shared (key, value, updated_at)
VALUES (?, ?, datetime('now'))
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at""",
(key, value),
)
db.commit()
db.close()
print(f"[shared] {key} = saved.")
elif args[0] == "delete":
if len(args) < 2:
print("Usage: ctx shared delete <key>")
sys.exit(1)
db = get_db()
db.execute("DELETE FROM shared WHERE key = ?", (args[1],))
db.commit()
db.close()
print(f"[shared] {args[1]} deleted.")
else:
print("Usage: ctx shared get | ctx shared set <key> <value> | ctx shared delete <key>")
def cmd_summary(args):
"""Save a session summary. Usage: ctx summary <project> <text>"""
if len(args) < 2:
print("Usage: ctx summary <project> <summary text>")
sys.exit(1)
project, summary = args[0], " ".join(args[1:])
db = get_db()
db.execute(
"INSERT INTO summaries (project, summary) VALUES (?, ?)", (project, summary)
)
# Keep last 20 summaries per project
db.execute(
"""DELETE FROM summaries WHERE project = ? AND id NOT IN (
SELECT id FROM summaries WHERE project = ? ORDER BY created_at DESC LIMIT 20
)""",
(project, project),
)
db.commit()
db.close()
print(f"[{project}] Summary saved.")
def cmd_history(args):
"""Show session history. Usage: ctx history <project> [limit]"""
if len(args) < 1:
print("Usage: ctx history <project> [limit]")
sys.exit(1)
project = args[0]
limit = int(args[1]) if len(args) > 1 else 10
db = get_db()
rows = db.execute(
"SELECT summary, created_at FROM summaries WHERE project = ? ORDER BY created_at DESC LIMIT ?",
(project, limit),
).fetchall()
db.close()
if rows:
for row in reversed(rows):
print(f"\n[{row['created_at']}]")
print(row["summary"])
else:
print(f"No history for '{project}'.")
def cmd_search(args):
"""Full-text search across all contexts. Usage: ctx search <query>"""
if len(args) < 1:
print("Usage: ctx search <query>")
sys.exit(1)
query = " ".join(args)
db = get_db()
# Search project contexts
results_ctx = db.execute(
"SELECT project, key, value FROM contexts_fts WHERE contexts_fts MATCH ?",
(query,),
).fetchall()
# Search shared contexts
results_shared = db.execute(
"SELECT key, value FROM shared_fts WHERE shared_fts MATCH ?", (query,)
).fetchall()
# Search summaries (simple LIKE since no FTS on summaries)
results_sum = db.execute(
"SELECT project, summary, created_at FROM summaries WHERE summary LIKE ?",
(f"%{query}%",),
).fetchall()
db.close()
total = len(results_ctx) + len(results_shared) + len(results_sum)
print(f"Found {total} result(s) for '{query}':\n")
if results_shared:
print("--- Shared ---")
for row in results_shared:
print(f" [{row['key']}] {row['value'][:100]}")
if results_ctx:
print("--- Project Contexts ---")
for row in results_ctx:
print(f" [{row['project']}:{row['key']}] {row['value'][:100]}")
if results_sum:
print("--- Summaries ---")
for row in results_sum:
print(f" [{row['project']} @ {row['created_at']}] {row['summary'][:100]}")
def cmd_list(args):
"""List all registered projects."""
db = get_db()
sessions = db.execute("SELECT * FROM sessions ORDER BY name").fetchall()
# Also find projects with context but no session registration
orphans = db.execute(
"""SELECT DISTINCT project FROM contexts
WHERE project NOT IN (SELECT name FROM sessions)
ORDER BY project"""
).fetchall()
db.close()
if sessions:
print("Registered projects:")
for s in sessions:
ctx_count = _count_contexts(s["name"])
print(f" {s['name']:25s} — {s['description']} ({ctx_count} entries)")
if orphans:
print("\nUnregistered (have context but no init):")
for o in orphans:
print(f" {o['project']}")
if not sessions and not orphans:
print("No projects yet. Use 'ctx init <name> <description>' to start.")
def cmd_delete(args):
"""Delete a project or specific key. Usage: ctx delete <project> [key]"""
if len(args) < 1:
print("Usage: ctx delete <project> [key]")
sys.exit(1)
project = args[0]
db = get_db()
if len(args) >= 2:
key = args[1]
db.execute(
"DELETE FROM contexts WHERE project = ? AND key = ?", (project, key)
)
db.commit()
print(f"[{project}] {key} deleted.")
else:
db.execute("DELETE FROM contexts WHERE project = ?", (project,))
db.execute("DELETE FROM summaries WHERE project = ?", (project,))
db.execute("DELETE FROM sessions WHERE name = ?", (project,))
db.commit()
print(f"Project '{project}' and all its data deleted.")
db.close()
def cmd_export(args):
"""Export all data as JSON. Usage: ctx export"""
db = get_db()
data = {
"sessions": [dict(r) for r in db.execute("SELECT * FROM sessions").fetchall()],
"shared": [dict(r) for r in db.execute("SELECT * FROM shared").fetchall()],
"contexts": [dict(r) for r in db.execute("SELECT * FROM contexts").fetchall()],
"summaries": [dict(r) for r in db.execute("SELECT * FROM summaries").fetchall()],
}
db.close()
print(json.dumps(data, indent=2, ensure_ascii=False))
def _count_contexts(project):
db = get_db()
row = db.execute(
"SELECT COUNT(*) as c FROM contexts WHERE project = ?", (project,)
).fetchone()
db.close()
return row["c"]
# ─── Main ───────────────────────────────────────────────────────────────
COMMANDS = {
"init": cmd_init,
"get": cmd_get,
"set": cmd_set,
"append": cmd_append,
"shared": cmd_shared,
"summary": cmd_summary,
"history": cmd_history,
"search": cmd_search,
"list": cmd_list,
"delete": cmd_delete,
"export": cmd_export,
}
def print_help():
print("ctx — Cross-session context manager for Claude Code\n")
print("Commands:")
print(" init <project> <desc> [dir] Register a new project")
print(" get <project> Load full context (shared + project)")
print(" set <project> <key> <value> Set project context entry")
print(" append <project> <key> <val> Append to existing entry")
print(" shared get|set|delete Manage shared context")
print(" summary <project> <text> Save session work summary")
print(" history <project> [limit] Show session history")
print(" search <query> Full-text search across everything")
print(" list List all projects")
print(" delete <project> [key] Delete project or entry")
print(" export Export all data as JSON")
if __name__ == "__main__":
init_db()
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "help"):
print_help()
sys.exit(0)
cmd = sys.argv[1]
if cmd not in COMMANDS:
print(f"Unknown command: {cmd}")
print_help()
sys.exit(1)
COMMANDS[cmd](sys.argv[2:])

93
install.sh Executable file
View file

@ -0,0 +1,93 @@
#!/bin/bash
set -euo pipefail
# BTerminal installer
# Installs BTerminal + ctx (Claude Code context manager)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_DIR="$HOME/.local/share/bterminal"
BIN_DIR="$HOME/.local/bin"
CONFIG_DIR="$HOME/.config/bterminal"
CTX_DIR="$HOME/.claude-context"
DESKTOP_DIR="$HOME/.local/share/applications"
echo "=== BTerminal Installer ==="
echo ""
# ─── System dependencies ───────────────────────────────────────────────
echo "[1/5] Checking system dependencies..."
MISSING=()
python3 -c "import gi" 2>/dev/null || MISSING+=("python3-gi")
python3 -c "import gi; gi.require_version('Gtk', '3.0'); from gi.repository import Gtk" 2>/dev/null || MISSING+=("gir1.2-gtk-3.0")
python3 -c "import gi; gi.require_version('Vte', '2.91'); from gi.repository import Vte" 2>/dev/null || MISSING+=("gir1.2-vte-2.91")
if [ ${#MISSING[@]} -gt 0 ]; then
echo " Missing: ${MISSING[*]}"
echo " Installing..."
sudo apt install -y "${MISSING[@]}"
else
echo " All dependencies OK."
fi
# ─── Install files ─────────────────────────────────────────────────────
echo "[2/5] Installing BTerminal..."
mkdir -p "$INSTALL_DIR" "$BIN_DIR" "$CONFIG_DIR" "$CTX_DIR"
cp "$SCRIPT_DIR/bterminal.py" "$INSTALL_DIR/bterminal.py"
cp "$SCRIPT_DIR/ctx" "$INSTALL_DIR/ctx"
chmod +x "$INSTALL_DIR/bterminal.py" "$INSTALL_DIR/ctx"
# ─── Symlinks ──────────────────────────────────────────────────────────
echo "[3/5] Creating symlinks in $BIN_DIR..."
ln -sf "$INSTALL_DIR/bterminal.py" "$BIN_DIR/bterminal"
ln -sf "$INSTALL_DIR/ctx" "$BIN_DIR/ctx"
echo " bterminal -> $INSTALL_DIR/bterminal.py"
echo " ctx -> $INSTALL_DIR/ctx"
# ─── Init ctx database ────────────────────────────────────────────────
echo "[4/5] Initializing context database..."
if [ -f "$CTX_DIR/context.db" ]; then
echo " Database already exists, skipping."
else
"$BIN_DIR/ctx" list >/dev/null 2>&1
echo " Created $CTX_DIR/context.db"
fi
# ─── Desktop file ──────────────────────────────────────────────────────
echo "[5/5] Creating desktop entry..."
mkdir -p "$DESKTOP_DIR"
cat > "$DESKTOP_DIR/bterminal.desktop" << EOF
[Desktop Entry]
Name=BTerminal
Comment=Terminal with SSH & Claude Code session management
Exec=$BIN_DIR/bterminal
Icon=utilities-terminal
Type=Application
Categories=System;TerminalEmulator;
Terminal=false
StartupNotify=true
EOF
echo ""
echo "=== Installation complete ==="
echo ""
echo "Run BTerminal:"
echo " bterminal"
echo ""
echo "Context manager:"
echo " ctx --help"
echo ""
echo "Make sure $BIN_DIR is in your PATH."
echo "If not, add to ~/.bashrc:"
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""