diff --git a/nexuscli/README.md b/nexuscli/README.md new file mode 100644 index 000000000..3a4a65cc9 --- /dev/null +++ b/nexuscli/README.md @@ -0,0 +1,162 @@ +# NexusCLI + +> **A fast, lightweight, Termux-optimized CLI/TUI for agent interactions.** +> Powered by `curl_cffi` and reverse-engineered DeepSeek API endpoints. + +--- + +## 🚀 Features + +- **Blazing Fast**: Uses `curl_cffi` for high-performance API calls. +- **Termux Optimized**: Designed for mobile/Termux environments. +- **Lightweight**: No Playwright or heavy dependencies. +- **Phased Architecture**: Modular design for instant execution. +- **Session Management**: Create, list, and manage chat sessions. +- **Interactive Chat**: Real-time streaming responses. +- **Export Capabilities**: Export conversations to Markdown or JSON. + +--- + +## 📦 Installation + +### 1. Clone the Repository + +```bash +cd ~/termux-monorepo +git checkout vibe/deepcode-cli_phased-fusion_fc54fa +``` + +### 2. Install Dependencies + +```bash +pip install curl_cffi rich +``` + +### 3. Set Up API Token + +```bash +export NEXUSCLI_TOKEN="your_deepseek_token_here" +# Or save in config +echo '{"token": "your_deepseek_token_here"}' > ~/.nexuscli/config.json +``` + +--- + +## 🎯 Usage + +### Basic Commands + +| Command | Description | +|---------|-------------| +| `nexuscli sessions` | List all sessions | +| `nexuscli new-session` | Create a new session | +| `nexuscli chat --last` | Start interactive chat | +| `nexuscli send --prompt "Hello" --last` | Send a single message | +| `nexuscli export --last --format markdown` | Export session history | + +### Examples + +```bash +# List all sessions +nexuscli sessions + +# Create a new expert session +nexuscli new-session --model expert --save + +# Start interactive chat with last session +nexuscli chat --last --thinking + +# Send a single message +nexuscli send --prompt "Explain Python decorators" --last + +# Export session to Markdown +nexuscli export --last --format markdown --output conversation.md +``` + +--- + +## 🏗️ Project Structure + +``` +nexuscli/ +├── __init__.py +├── core/ +│ ├── __init__.py +│ └── api.py # Core API wrapper (reverse-engineered) +├── cli/ +│ ├── __init__.py +│ └── main.py # CLI entry point +├── nexuscli.py # Launcher script +├── pow_solver.js # POW solver for API challenges +└── README.md +``` + +--- + +## 🔧 Configuration + +### Config File + +Located at `~/.nexuscli/config.json`: + +```json +{ + "token": "your_deepseek_token", + "last_session": "your_last_session_id" +} +``` + +### Environment Variables + +- `NEXUSCLI_TOKEN` or `DEEPSEEK_TOKEN`: API token +- `NEXUSCLI_BASE_URL`: Custom API base URL (default: `https://chat.deepseek.com`) + +--- + +## 🤖 API Endpoints + +The following endpoints are reverse-engineered and used: + +| Endpoint | Description | +|----------|-------------| +| `/api/v0/chat_session/create` | Create a new chat session | +| `/api/v0/chat_session/fetch_page` | Fetch all sessions | +| `/api/v0/chat/history_messages` | Get session history | +| `/api/v0/chat/create_pow_challenge` | Get POW challenge | +| `/api/v0/chat/completion` | Send message (streaming) | +| `/api/v0/file/upload_file` | Upload a file | +| `/api/v0/share/create` | Create a share | +| `/api/v0/share/fork` | Fork a conversation | + +--- + +## 📜 License + +MIT License. See [LICENSE](../../LICENSE) for details. + +--- + +## 🙌 Contributing + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +--- + +## 🔗 Related Projects + +- [termux-monorepo](https://github.com/timerloggedout-spec/termux-monorepo) +- [deepcli](https://github.com/timerloggedout-spec/termux-monorepo/tree/master/deepcli) +- [multi-ai-cli](https://github.com/timerloggedout-spec/termux-monorepo/tree/master/multi-ai-cli) +- [termux-multi-agent](https://github.com/timerloggedout-spec/termux-monorepo/tree/master/termux-multi-agent) + +--- + +## 🎨 Branding + +**NexusCLI** is part of the **Termux Monorepo** ecosystem. + +> **"All for One; and, One for All!"** 🚀 diff --git a/nexuscli/__init__.py b/nexuscli/__init__.py new file mode 100644 index 000000000..d21b443bd --- /dev/null +++ b/nexuscli/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +""" +NexusCLI +A fast, lightweight, Termux-optimized CLI/TUI for agent interactions. +Uses curl_cffi for high-performance API calls. +""" + +__version__ = "0.1.0" +__author__ = "Termux Monorepo Team" diff --git a/nexuscli/cli/__init__.py b/nexuscli/cli/__init__.py new file mode 100644 index 000000000..ffefa101d --- /dev/null +++ b/nexuscli/cli/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +""" +CLI module for DeepCode-CLI Phased Nexus. +""" + +from .main import main + +__all__ = ["main"] diff --git a/nexuscli/cli/main.py b/nexuscli/cli/main.py new file mode 100644 index 000000000..718384e6f --- /dev/null +++ b/nexuscli/cli/main.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +""" +Main CLI entry point for DeepCode-CLI Phased Nexus. +""" + +import argparse +import sys +import os +from pathlib import Path +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Prompt, Confirm + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from core.api import ( + get_token, + create_session, + fetch_sessions, + get_history, + stream_completion, + send_message, + export_markdown, + export_json, +) + +console = Console() + + +def print_banner(): + """Print the CLI banner.""" + banner = """ + ███╗ ██╗██╗ ██╗███████╗██╗ ██╗ + ████╗ ██║██║ ██╔╝██╔════╝╚██╗ ██╔╝ + ██╔██╗ ██║█████╔╝ █████╗ ╚████╔╝ + ██║╚██╗██║██╔═██╗ ██╔══╝ ╚██╔╝ + ██║ ╚████║██║ ██╗███████╗ ██║ + ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ ╚═╝ + """ + console.print(Panel.fit(banner, title="[bold cyan]NexusCLI[/]", border_style="cyan")) + + +def cmd_sessions(args): + """List all sessions.""" + token = get_token() + sessions = fetch_sessions(token) + if not sessions: + console.print("[yellow]No sessions found.[/]") + return + console.print("[bold]Sessions:[/]") + for session in sessions: + session_id = session.get("id", "N/A") + title = session.get("title", "Untitled") + console.print(f" - [cyan]{session_id}[/] | {title}") + + +def cmd_new_session(args): + """Create a new session.""" + token = get_token() + model_type = args.model or "expert" + session_id = create_session(token, model_type=model_type) + console.print(f"[green]New session created: {session_id}[/]") + if args.save: + cfg = {} + if os.path.exists(str(Path.home() / ".deepcode-cli" / "config.json")): + cfg = json.loads((Path.home() / ".deepcode-cli" / "config.json").read_text()) + cfg["last_session"] = session_id + (Path.home() / ".deepcode-cli" / "config.json").write_text(json.dumps(cfg, indent=2)) + console.print("[yellow]Saved as last_session.[/]") + + +def cmd_chat(args): + """Start an interactive chat.""" + token = get_token() + session_id = args.session_id or (args.last and get_last_session()) + if not session_id: + console.print("[red]No session ID provided. Use --session-id or --last.[/]") + return + + console.print(f"[bold]Chat Session: {session_id}[/]") + + # Load history + history = get_history(token, session_id) + for msg in history: + role = msg.get("role", "user") + content = msg.get("content", "") + if role == "user": + console.print(f"[blue]You:[/] {content}") + else: + console.print(f"[green]Assistant:[/] {content}") + + # Interactive loop + parent_message_id = None + while True: + try: + prompt = Prompt.ask("[bold cyan]You[/]") + if prompt.lower() in ["exit", "quit", "q"]: + break + + console.print("[bold green]Assistant:[/] ", end="") + stream_completion( + token=token, + prompt=prompt, + session_id=session_id, + parent_message_id=parent_message_id, + thinking=args.thinking, + search=args.search, + ) + console.print() + parent_message_id = None # Reset for next message + + except KeyboardInterrupt: + console.print("\n[yellow]Interrupted. Exiting...[/]") + break + + +def cmd_send(args): + """Send a single message.""" + token = get_token() + session_id = args.session_id or get_last_session() + if not session_id: + console.print("[red]No session ID provided.[/]") + return + + response = send_message( + token=token, + session_id=session_id, + prompt=args.prompt, + thinking=args.thinking, + search=args.search, + ) + console.print(f"[green]Response:[/] {response}") + + +def cmd_export(args): + """Export session history.""" + token = get_token() + session_id = args.session_id or get_last_session() + if not session_id: + console.print("[red]No session ID provided.[/]") + return + + if args.format == "markdown": + content = export_markdown(token, session_id) + else: + content = export_json(token, session_id) + + if args.output: + with open(args.output, "w") as f: + f.write(content) + console.print(f"[green]Exported to {args.output}[/]") + else: + console.print(content) + + +def get_last_session(): + """Get the last session ID from config.""" + cfg_path = Path.home() / ".nexuscli" / "config.json" + if cfg_path.exists(): + cfg = json.loads(cfg_path.read_text()) + return cfg.get("last_session") + return None + + +def main(): + """Main CLI entry point.""" + import json + + parser = argparse.ArgumentParser( + description="NexusCLI - A fast, lightweight CLI for agent interactions.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + nexuscli sessions List all sessions + nexuscli new-session Create a new session + nexuscli chat --last Start chat with last session + nexuscli send --prompt "Hello" --last + nexuscli export --last --format markdown + """ + ) + + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Sessions command + sessions_parser = subparsers.add_parser("sessions", help="List all sessions") + sessions_parser.set_defaults(func=cmd_sessions) + + # New session command + new_session_parser = subparsers.add_parser("new-session", help="Create a new session") + new_session_parser.add_argument("--model", type=str, default="expert", help="Model type (expert/instant)") + new_session_parser.add_argument("--save", action="store_true", help="Save as last session") + new_session_parser.set_defaults(func=cmd_new_session) + + # Chat command + chat_parser = subparsers.add_parser("chat", help="Start an interactive chat") + chat_parser.add_argument("--session-id", type=str, help="Session ID") + chat_parser.add_argument("--last", action="store_true", help="Use last session") + chat_parser.add_argument("--thinking", action="store_true", help="Enable thinking mode") + chat_parser.add_argument("--search", action="store_true", help="Enable search mode") + chat_parser.set_defaults(func=cmd_chat) + + # Send command + send_parser = subparsers.add_parser("send", help="Send a single message") + send_parser.add_argument("--prompt", type=str, required=True, help="Prompt to send") + send_parser.add_argument("--session-id", type=str, help="Session ID") + send_parser.add_argument("--last", action="store_true", help="Use last session") + send_parser.add_argument("--thinking", action="store_true", help="Enable thinking mode") + send_parser.add_argument("--search", action="store_true", help="Enable search mode") + send_parser.set_defaults(func=cmd_send) + + # Export command + export_parser = subparsers.add_parser("export", help="Export session history") + export_parser.add_argument("--session-id", type=str, help="Session ID") + export_parser.add_argument("--last", action="store_true", help="Use last session") + export_parser.add_argument("--format", type=str, default="markdown", choices=["markdown", "json"], help="Export format") + export_parser.add_argument("--output", type=str, help="Output file path") + export_parser.set_defaults(func=cmd_export) + + args = parser.parse_args() + + if not hasattr(args, "func"): + parser.print_help() + return + + print_banner() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/nexuscli/core/__init__.py b/nexuscli/core/__init__.py new file mode 100644 index 000000000..e05e4153e --- /dev/null +++ b/nexuscli/core/__init__.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +""" +Core API wrapper for NexusCLI. +Reverse-engineered from deepcli/core.py with optimizations for Termux. +""" + +from .api import ( + get_session, + solve_pow, + create_session, + fetch_sessions, + get_history, + get_pow_challenge, + upload_file, + wait_for_file, + branch_conversation, + stream_completion, + send_message, + chat_completion, + export_markdown, + export_json, +) + +__all__ = [ + "get_session", + "solve_pow", + "create_session", + "fetch_sessions", + "get_history", + "get_pow_challenge", + "upload_file", + "wait_for_file", + "branch_conversation", + "stream_completion", + "send_message", + "chat_completion", + "export_markdown", + "export_json", +] diff --git a/nexuscli/core/api.py b/nexuscli/core/api.py new file mode 100644 index 000000000..b884ab185 --- /dev/null +++ b/nexuscli/core/api.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python3 +""" +Core API wrapper for NexusCLI. +Optimized for Termux with curl_cffi and lightweight patterns. +""" + +import os +import json +import base64 +import time +import subprocess +import random +from pathlib import Path +from typing import Optional, List, Dict, Any +from curl_cffi import requests as curl_requests +import requests as http_requests +from rich.console import Console + +console = Console() + +# Configuration +CONFIG_DIR = Path.home() / ".nexuscli" +CONFIG_FILE = CONFIG_DIR / "config.json" +WASM_SOLVER = Path(__file__).parent.parent / "pow_solver.js" +BASE_URL = "https://chat.deepseek.com" + +CONFIG_DIR.mkdir(parents=True, exist_ok=True) + +# Persistent session (cookies preserved across API calls) +_session: Optional[curl_requests.Session] = None +_sessions: Dict[str, curl_requests.Session] = {} + +# ---------- Cache Helpers ---------- + +def _cache_path(session_id: str, account: str = "primary") -> str: + store_dir = os.path.join(os.path.expanduser("~/.nexuscli/session_store"), account) + os.makedirs(store_dir, exist_ok=True) + return os.path.join(store_dir, f"{session_id}.json") + + +def _cache_load(session_id: str, account: str = "primary") -> Optional[List[Dict[str, Any]]]: + path = _cache_path(session_id, account) + if os.path.exists(path): + with open(path) as f: + return json.load(f) + return None + + +def _cache_save(session_id: str, messages: List[Dict[str, Any]], account: str = "primary"): + path = _cache_path(session_id, account) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'w') as f: + json.dump(messages, f, indent=2) + + +# ---------- Config Helpers ---------- + +def load_config() -> Dict[str, Any]: + if CONFIG_FILE.exists(): + return json.loads(CONFIG_FILE.read_text()) + return {} + + +def save_config(cfg: Dict[str, Any]): + CONFIG_FILE.write_text(json.dumps(cfg, indent=2)) + + +def get_token() -> str: + token = os.environ.get("NEXUSCLI_TOKEN") or os.environ.get("DEEPSEEK_TOKEN") + if not token: + cfg = load_config() + token = cfg.get("token") + if not token: + console.print("[red]No token found. Run 'nexuscli import-session' or set NEXUSCLI_TOKEN[/]") + raise ValueError("No API token found") + return token + + +# ---------- HTTP Session ---------- + +def get_session(token: str, cookie: str = None) -> curl_requests.Session: + global _session, _sessions + cache_key = (token[:20] + '_' + (cookie or ''))[:30] + + if cache_key in _sessions: + _session = _sessions[cache_key] + _session.headers["Authorization"] = f"Bearer {token}" + else: + _session = curl_requests.Session() + _session.headers.update({ + "User-Agent": "Mozilla/5.0 (Linux; Android 10; Termux) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36", + "Accept": "*/*", + "Accept-Language": "en-US,en;q=0.5", + "Authorization": f"Bearer {token}", + "X-Client-Platform": "web", + "X-Client-Version": "1.3.0-nexuscli", + "X-App-Version": "20241129.1", + "X-Client-Locale": "en_US", + "Origin": BASE_URL, + "Referer": f"{BASE_URL}/", + "sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138"', + "sec-ch-ua-mobile": "?1", + "sec-ch-ua-platform": '"Android"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + }) + _sessions[cache_key] = _session + + if cookie: + _session.cookies.set("ds_session_id", cookie.split("=", 1)[1] if "=" in cookie else cookie) + return _session + + +# ---------- POW Solver ---------- + +def solve_pow(challenge: dict) -> str: + inp = json.dumps(challenge) + try: + proc = subprocess.run( + ["node", str(WASM_SOLVER)], + input=inp, + capture_output=True, + text=True, + timeout=10 + ) + if proc.returncode != 0: + raise RuntimeError(f"POW solver error: {proc.stderr.strip()}") + answer = int(proc.stdout.strip()) + except Exception as e: + console.print(f"[red]POW solving failed: {e}[/]") + raise + + payload = { + "algorithm": challenge.get("algorithm", "DeepSeekHashV1"), + "challenge": challenge["challenge"], + "salt": challenge["salt"], + "answer": answer, + "signature": challenge["signature"], + "target_path": challenge.get("target_path", "/api/v0/chat/completion") + } + return base64.b64encode(json.dumps(payload).encode()).decode() + + +# ---------- API Wrappers ---------- + +def create_session(token: str, model_type: str = "expert", cookie: str = None) -> str: + s = get_session(token, cookie=cookie) + if cookie: + console.print(f"[DEBUG] create_session using cookie: {cookie[:30]}...") + r = s.post(f"{BASE_URL}/api/v0/chat_session/create", json={"character_id": None, "model_type": model_type}) + console.print(f"[yellow]create_session status: {r.status_code}[/]") + r.raise_for_status() + return r.json()["data"]["biz_data"]["id"] + + +def fetch_sessions(token: str) -> List[Dict[str, Any]]: + s = get_session(token) + r = s.get(f"{BASE_URL}/api/v0/chat_session/fetch_page") + r.raise_for_status() + data = r.json()["data"]["biz_data"] + return data.get("chat_sessions", data.get("sessions", [])) + + +def get_history(token: str, session_id: str, force_refresh: bool = False, account: str = "primary") -> List[Dict[str, Any]]: + if not force_refresh: + cached = _cache_load(session_id, account) + if cached is not None: + return cached + s = get_session(token) + r = s.get(f"{BASE_URL}/api/v0/chat/history_messages?chat_session_id={session_id}") + if r.status_code != 200: + console.print(f"[red]Failed to fetch history (status {r.status_code}): {r.text[:200]}[/]") + r.raise_for_status() + data = r.json()["data"]["biz_data"]["chat_messages"] + _cache_save(session_id, data, account) + return data + + +def get_pow_challenge(token: str, target_path="/api/v0/chat/completion") -> dict: + s = get_session(token) + r = s.post(f"{BASE_URL}/api/v0/chat/create_pow_challenge", json={"target_path": target_path}) + r.raise_for_status() + return r.json()["data"]["biz_data"]["challenge"] + + +def upload_file(token: str, session_id: str, file_path: str) -> Optional[str]: + if not Path(file_path).exists(): + console.print(f"[red]File not found: {file_path}[/]") + return None + challenge = get_pow_challenge(token, "/api/v0/file/upload_file") + pow_header = solve_pow(challenge) + + s = get_session(token) + s.headers["X-Ds-Pow-Response"] = pow_header + with open(file_path, "rb") as f: + file_bytes = f.read() + upload_headers = {k: v for k, v in s.headers.items()} + upload_headers["X-Ds-Pow-Response"] = pow_header + r = http_requests.post( + f"{BASE_URL}/api/v0/file/upload_file", + files={"file": (Path(file_path).name, file_bytes, "application/octet-stream")}, + headers=upload_headers + ) + r.raise_for_status() + file_id = r.json().get("data", {}).get("biz_data", {}).get("id") or r.json().get("data", {}).get("file_id") + console.print(f"[green]File uploaded, ID: {file_id}[/]") + return file_id + + +def wait_for_file(token: str, file_id: str, timeout=30): + s = get_session(token) + start = time.time() + while time.time() - start < timeout: + r = s.get(f"{BASE_URL}/api/v0/file/fetch_files?file_ids={file_id}") + r.raise_for_status() + status = r.json()["data"]["biz_data"]["files"][0]["status"] + if status == "SUCCESS": + return True + time.sleep(1) + console.print(f"[yellow]File {file_id} processing timed out[/]") + return False + + +def branch_conversation(token: str, session_id: str, message_id: str) -> Optional[str]: + s = get_session(token) + session_referer = f"{BASE_URL}/a/chat/s/{session_id}" + history = get_history(token, session_id, force_refresh=True) + msg_map = {m["message_id"]: m for m in history} + target = msg_map.get(message_id) + if not target or target.get("role", "").upper() != "ASSISTANT": + console.print("[red]Branch target must be an ASSISTANT message.[/]") + return None + parent_id = target.get("parent_id") + if not parent_id or parent_id not in msg_map: + console.print("[red]Could not find parent USER message for branching.[/]") + return None + if msg_map[parent_id].get("role", "").upper() != "USER": + console.print("[red]Parent message is not a USER message. Cannot branch.[/]") + return None + + share_payload = {"chat_session_id": session_id, "message_ids": [parent_id, message_id]} + share_r = s.post(f"{BASE_URL}/api/v0/share/create", json=share_payload, headers={"Referer": session_referer}) + share_r.raise_for_status() + share_id = share_r.json()["data"]["biz_data"]["share_id"] + + fork_payload = {"share_id": share_id} + fork_r = s.post(f"{BASE_URL}/api/v0/share/fork", json=fork_payload, headers={"Referer": session_referer}) + fork_r.raise_for_status() + new_sid = fork_r.json()["data"]["biz_data"]["chat_session_id"] + console.print(f"[green]Branched to new session: {new_sid}[/]") + return new_sid + + +# ---------- Streaming Completion ---------- + +def stream_completion( + token: str, + prompt: str, + session_id: str, + parent_message_id: Optional[str] = None, + thinking: bool = False, + search: bool = False, + file_ids: Optional[List[str]] = None, + auto_retry: bool = True, + max_retries: int = 8, +): + challenge = get_pow_challenge(token, "/api/v0/chat/completion") + pow_header = solve_pow(challenge) + + payload = { + "chat_session_id": session_id, + "parent_message_id": int(parent_message_id) if parent_message_id else None, + "prompt": prompt, + "ref_file_ids": file_ids or [], + "thinking_enabled": thinking, + "search_enabled": search, + "stream": True + } + + base_sess = get_session(token) + headers = base_sess.headers.copy() + headers["X-Ds-Pow-Response"] = pow_header + headers["Content-Type"] = "application/json" + headers["Accept"] = "text/event-stream" + + url = f"{BASE_URL}/api/v0/chat/completion" + retries = 0 + base_delay = 2 + + while retries < max_retries: + try: + resp = base_sess.post(url, json=payload, headers=headers, stream=True) + body_preview = resp.text[:200] if hasattr(resp, 'text') else '' + + if 'Update to the latest version' in body_preview: + console.print("[yellow]Expert mode unavailable – retrying as instant.[/]") + payload['thinking_enabled'] = False + payload['search_enabled'] = False + time.sleep(1) + continue + + if resp.status_code in (403, 503): + retries += 1 + delay = base_delay * (2 ** min(retries, 5)) + random.uniform(0, base_delay) + delay = min(delay, 90) + console.print(f"[yellow]Server busy/blocked, retrying in {delay:.1f}s (attempt {retries})...[/]") + time.sleep(delay) + continue + + if resp.status_code != 200: + body = resp.text[:300] + retries += 1 + delay = base_delay * (2 ** min(retries, 5)) + random.uniform(0, 3) + delay = min(delay, 90) + console.print(f"[red]API error {resp.status_code}: {body}[/] retry in {delay:.1f}s") + if 'expert' in body.lower() or 'upgrade' in body.lower(): + console.print("[yellow]Expert mode unavailable – falling back to instant.[/]") + payload['thinking_enabled'] = False + payload['search_enabled'] = False + time.sleep(delay) + continue + + # Parse SSE manually + raw = resp.content.decode('utf-8', errors='replace') + for line in raw.split('\n'): + if not line.strip(): + continue + if line.startswith('data:'): + try: + data = json.loads(line[5:].strip()) + if isinstance(data, dict): + chunk = data.get("v") or data.get("content") + if chunk and isinstance(chunk, str) and chunk != "FINISHED": + console.print(chunk, end="") + except json.JSONDecodeError: + pass + return + + except Exception as e: + retries += 1 + delay = min(10 * retries, 60) + console.print(f"[red]Request error: {e}. Retrying in {delay}s...[/]") + time.sleep(delay) + + console.print("[red]Failed after multiple retries.[/]") + + +# ---------- Non-Streaming Send ---------- + +def send_message( + token: str, + session_id: str, + prompt: str, + parent_message_id: str = None, + thinking: bool = False, + search: bool = False, +) -> str: + challenge = get_pow_challenge(token, '/api/v0/chat/completion') + pow_header = solve_pow(challenge) + payload = { + 'chat_session_id': session_id, + 'parent_message_id': parent_message_id, + 'prompt': prompt, + 'ref_file_ids': [], + 'thinking_enabled': thinking, + 'search_enabled': search, + 'stream': False, + } + s = get_session(token) + headers = s.headers.copy() + headers['X-Ds-Pow-Response'] = pow_header + headers['Content-Type'] = 'application/json' + resp = http_requests.post(f'{BASE_URL}/api/v0/chat/completion', json=payload, headers=headers) + resp.raise_for_status() + data = resp.json() + return data['choices'][0]['message']['content'] + + +# ---------- Chat Completion Wrapper ---------- + +def chat_completion( + token: str, + prompt: str, + session_id: str, + parent_message_id: Optional[str] = None, + thinking: bool = False, + search: bool = False, + file_ids: Optional[List[str]] = None, + auto_continue: bool = True, + max_continues: int = 3, +) -> str: + import io + from contextlib import redirect_stdout + + full_output = "" + current_parent = parent_message_id + + for cont in range(max_continues + 1): + f = io.StringIO() + with redirect_stdout(f): + stream_completion( + token, prompt if cont == 0 else "", + session_id, current_parent, + thinking, search, file_ids, auto_retry=True + ) + chunk = f.getvalue() + full_output += chunk + + if not auto_continue: + break + + # Check if server asked for continue (simplified) + if "auto_resume" in chunk.lower(): + break + + return full_output + + +# ---------- Export Utilities ---------- + +def export_markdown(token: str, session_id: str) -> str: + messages = get_history(token, session_id) + md = "" + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + if role == "user": + md += f"### 👤 You\n{content}\n\n" + else: + md += f"### 🤖 DeepSeek\n{content}\n\n" + return md + + +def export_json(token: str, session_id: str) -> str: + messages = get_history(token, session_id) + return json.dumps(messages, indent=2) diff --git a/nexuscli/nexuscli.py b/nexuscli/nexuscli.py new file mode 100644 index 000000000..952754653 --- /dev/null +++ b/nexuscli/nexuscli.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +""" +NexusCLI Launcher +""" + +import sys +from pathlib import Path + +# Ensure the package is importable +sys.path.insert(0, str(Path(__file__).parent)) + +from cli.main import main + +if __name__ == "__main__": + main() diff --git a/nexuscli/pow_solver.js b/nexuscli/pow_solver.js new file mode 100644 index 000000000..8f477a339 --- /dev/null +++ b/nexuscli/pow_solver.js @@ -0,0 +1,52 @@ +// POW Solver for DeepSeek API +// Used by deepcode-cli to solve Proof-of-Work challenges + +const crypto = require('crypto'); + +function solveDeepSeekHashV1(challenge, salt, signature, targetPath) { + const target = targetPath || '/api/v0/chat/completion'; + let answer = 0; + const prefix = crypto.createHash('sha256') + .update(challenge + target + salt + signature) + .digest('hex') + .substring(0, 16); + + while (true) { + const input = prefix + answer.toString(); + const hash = crypto.createHash('sha256').update(input).digest('hex'); + if (hash.startsWith('000000')) { + return answer; + } + answer++; + } +} + +const input = JSON.parse(require('fs').readFileSync(0, 'utf-8').trim()); +const challenge = input.challenge; +const salt = input.salt; +const signature = input.signature; +const targetPath = input.target_path || '/api/v0/chat/completion'; +const algorithm = input.algorithm || 'DeepSeekHashV1'; + +let answer; +if (algorithm === 'DeepSeekHashV1') { + answer = solveDeepSeekHashV1(challenge, salt, signature, targetPath); +} else { + // Fallback for other algorithms + answer = 0; + const prefix = crypto.createHash('sha256') + .update(challenge + targetPath + salt + signature) + .digest('hex') + .substring(0, 16); + + while (true) { + const inputStr = prefix + answer.toString(); + const hash = crypto.createHash('sha256').update(inputStr).digest('hex'); + if (hash.startsWith('000000')) { + break; + } + answer++; + } +} + +console.log(answer); diff --git a/nexuscli/requirements.txt b/nexuscli/requirements.txt new file mode 100644 index 000000000..b58f96341 --- /dev/null +++ b/nexuscli/requirements.txt @@ -0,0 +1,14 @@ +# NexusCLI Dependencies +# Optimized for Termux and lightweight execution + +# Core dependencies +curl_cffi>=0.5.0 +rich>=13.0.0 + +# Optional: For file uploads +requests>=2.31.0 + +# Development dependencies (optional) +# pytest>=7.0.0 +# black>=23.0.0 +# flake8>=6.0.0