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:
parent
edc13e2d27
commit
4268ead6a4
5 changed files with 640 additions and 34 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
|
CLAUDE.md
|
||||||
|
|
|
||||||
105
README.md
105
README.md
|
|
@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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
|
||||||
|
|
|
||||||
14
bterminal.py
14
bterminal.py
|
|
@ -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
461
ctx
Executable 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
93
install.sh
Executable 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\""
|
||||||
Loading…
Add table
Add a link
Reference in a new issue