diff --git a/README.md b/README.md index bc2eaeb..ca21547 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,32 @@ finish usage # tokens & cost Bash ≥4 or Zsh ≥5, curl, jq, bc. Optional: bash-completion. +```json +{ + "provider": "lmstudio", + "model": "darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2", + "endpoint": "http://plato:1234/v1/chat/completions", + "temperature": 0.0, + "api_prompt_cost": 0.0, + "api_completion_cost": 0.0, + "max_history_commands": 20, + "max_recent_files": 20, + "cache_size": 100 +} +``` +```json +{ + "provider": "ollama", + "model": "llama3:latest", + "endpoint": "http://localhost:11434/api/chat", + "temperature": 0.2, + "api_prompt_cost": 0.0, + "api_completion_cost": 0.0, + "max_history_commands": 20, + "max_recent_files": 20, + "cache_size": 100 +} +``` ## License BSD 2-Clause. diff --git a/queen.txt b/queen.txt new file mode 100644 index 0000000..07d2a6f --- /dev/null +++ b/queen.txt @@ -0,0 +1,8 @@ + + * ________/ + * / \ + * / \ + * | | + * | | + * Queen of Hearts + diff --git a/src/finish.py b/src/finish.py index 394437f..67ba00d 100755 --- a/src/finish.py +++ b/src/finish.py @@ -73,37 +73,91 @@ def _recent_files() -> str: except Exception: return "" -def _help_for(cmd: str) -> str: - try: - return subprocess.check_output([cmd, "--help"], stderr=subprocess.STDOUT, timeout=2).decode() - except Exception: - return f"{cmd} --help not available" +def _installed_tools() -> str: + """Check for commonly used tools that might be relevant""" + tools = ["curl", "wget", "jq", "grep", "awk", "sed", "python3", "node", "docker", "git", "nvcc", "nvidia-smi"] + available = [] + for tool in tools: + try: + subprocess.run(["which", tool], capture_output=True, timeout=0.5, check=True) + available.append(tool) + except Exception: + pass + return ", ".join(available) if available else "standard shell tools" + +def _get_context_info() -> dict: + """Gather comprehensive shell context""" + return { + "user": os.getenv("USER", "unknown"), + "pwd": os.getcwd(), + "home": os.getenv("HOME", ""), + "hostname": os.getenv("HOSTNAME", os.getenv("HOST", "localhost")), + "shell": os.getenv("SHELL", "bash"), + "tools": _installed_tools(), + } def build_prompt(user_input: str) -> str: + """Build an enhanced prompt with better context and instructions""" c = cfg() - term_info = f"""User: {os.getenv("USER")} -PWD: {os.getcwd()} -HOME: {os.getenv("HOME")} -HOST: {os.getenv("HOSTNAME")} -SHELL: bash""" - prompt = f"""You are a helpful bash-completion script. -Generate 2–5 concise, valid bash commands that complete the user’s intent. + ctx = _get_context_info() -Reply **only** JSON: {{"completions":["cmd1","cmd2",...]}} + # Analyze user intent + user_words = user_input.lower().split() + intent_hints = [] + if any(word in user_words for word in ["resolve", "get", "fetch", "download", "curl", "wget"]): + intent_hints.append("The user wants to fetch/download data") + if any(word in user_words for word in ["website", "url", "http", "html", "title"]): + intent_hints.append("The user is working with web content") + if any(word in user_words for word in ["parse", "extract", "grep", "find"]): + intent_hints.append("The user wants to extract/parse data") + if any(word in user_words for word in ["create", "make", "new", "write", "edit"]): + intent_hints.append("The user wants to create or edit files") + if any(word in user_words for word in ["file", "document", "text", "story", "about"]): + intent_hints.append("The user is working with files/documents") + if any(word in user_words for word in ["install", "setup", "configure", "apt", "pip"]): + intent_hints.append("The user wants to install or configure software") + if any(word in user_words for word in ["gpu", "nvidia", "cuda", "memory", "cpu"]): + intent_hints.append("The user wants system/hardware information") -User command: {user_input} + intent_context = "\n".join(f"- {hint}" for hint in intent_hints) if intent_hints else "- General command completion" -Terminal context: -{term_info} + prompt = f"""You are an intelligent bash completion assistant. Analyze the user's intent and provide practical, executable commands. -History: +USER INPUT: {user_input} + +DETECTED INTENT: +{intent_context} + +SHELL CONTEXT: +- User: {ctx['user']} +- Working directory: {ctx['pwd']} +- Hostname: {ctx['hostname']} +- Available tools: {ctx['tools']} + +RECENT COMMAND HISTORY: {_sanitise_history()} -Recent files: -{_recent_files()} +INSTRUCTIONS: +1. Understand what the user wants to accomplish (not just the literal words) +2. Generate 2-5 practical bash commands that achieve the user's goal +3. Prefer common tools (curl, wget, grep, awk, sed, jq, python) +4. For file creation: use echo with heredoc, cat, or nano/vim +5. For web tasks: use curl with headers, pipe to grep/sed/awk for parsing +6. For complex parsing: suggest one-liners with proper escaping +7. Commands should be copy-paste ready and executable +8. Order by most likely to least likely intent + +EXAMPLES: +- "resolve title of website example.com" → curl -s https://example.com | grep -oP '\\K[^<]+' +- "show gpu status" → nvidia-smi +- "download json from api.com" → curl -s https://api.com/data | jq . +- "make a new file story.txt" → nano story.txt +- "create file and write hello" → echo "hello" > file.txt +- "write about dogs to file" → cat > dogs.txt << 'EOF' (then Ctrl+D to finish) + +OUTPUT FORMAT (JSON only, no other text): +{{"completions":["command1", "command2", "command3"]}}""" -Help: -{_help_for(user_input.split()[0])}""" return prompt # --------------------------------------------------------------------------- # @@ -155,9 +209,9 @@ async def llm_complete(prompt: str) -> List[str]: f.write(f"\n=== {time.strftime('%Y-%m-%d %H:%M:%S')} ===\n") f.write(f"Raw response:\n{raw}\n") - # try json first + # try json first - use strict=False to be more lenient try: - result = json.loads(raw)["completions"] + result = json.loads(raw, strict=False)["completions"] if os.getenv("FINISH_DEBUG"): with open(LOG_FILE, "a") as f: f.write(f"Parsed completions: {result}\n") @@ -166,8 +220,28 @@ async def llm_complete(prompt: str) -> List[str]: if os.getenv("FINISH_DEBUG"): with open(LOG_FILE, "a") as f: f.write(f"JSON parse failed: {e}\n") + f.write(f"Attempting manual extraction...\n") + + # Try to extract JSON array manually using regex + try: + match = re.search(r'"completions"\s*:\s*\[(.*?)\]', raw, re.DOTALL) + if match: + # Extract commands from the array - handle escaped quotes + array_content = match.group(1) + # Find all quoted strings + commands = re.findall(r'"([^"\\]*(?:\\.[^"\\]*)*)"', array_content) + if commands: + if os.getenv("FINISH_DEBUG"): + with open(LOG_FILE, "a") as f: + f.write(f"Manual extraction succeeded: {commands}\n") + return commands[:5] + except Exception as e2: + if os.getenv("FINISH_DEBUG"): + with open(LOG_FILE, "a") as f: + f.write(f"Manual extraction failed: {e2}\n") + # fallback: grep command-like lines - fallback = [ln for ln in raw.splitlines() if re.match(r"^(ls|cd|find|cat|grep|echo|mkdir|rm|cp|mv|pwd|chmod|chown)\b", ln)][:5] + fallback = [ln for ln in raw.splitlines() if re.match(r"^(ls|cd|find|cat|grep|echo|mkdir|rm|cp|mv|pwd|chmod|chown|nano|vim|touch)\b", ln)][:5] if os.getenv("FINISH_DEBUG"): with open(LOG_FILE, "a") as f: f.write(f"Fallback completions: {fallback}\n")