diff --git a/CLAUDE.md b/CLAUDE.md index 5f89e5e..1c5222c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,8 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ```bash pip install -e . # Install package in editable mode (dev) -leanclaw-login # Authenticate with GitHub via device flow (saves token to ~/.config/leanclaw/config.json) -leanclaw # Start the interactive CLI REPL +iclaw-login # Authenticate with GitHub via device flow (saves token to ~/.config/iclaw/config.json) +iclaw # Start the interactive CLI REPL ruff check . # Lint code ruff format . # Format code ``` @@ -16,11 +16,11 @@ Pre-commit hooks run automatically on commit: ruff format. ## Architecture -This is a Python CLI package (leanclaw) that provides an interactive terminal REPL for chatting with GitHub Copilot. +This is a Python CLI package (iclaw) that provides an interactive terminal REPL for chatting with GitHub Copilot. **Authentication flow:** -1. `leanclaw-login` runs GitHub OAuth Device Flow, saves the GitHub token to `~/.config/leanclaw/config.json` -2. On startup, `leanclaw` reads the GitHub token from `~/.config/leanclaw/config.json`, then exchanges it for a short-lived Copilot token via `https://api.github.com/copilot_internal/v2/token` +1. `iclaw-login` runs GitHub OAuth Device Flow, saves the GitHub token to `~/.config/iclaw/config.json` +2. On startup, `iclaw` reads the GitHub token from `~/.config/iclaw/config.json`, then exchanges it for a short-lived Copilot token via `https://api.github.com/copilot_internal/v2/token` 3. The Copilot token is refreshed every ~24 minutes during the session **API endpoints:** @@ -30,7 +30,7 @@ This is a Python CLI package (leanclaw) that provides an interactive terminal RE - `https://api.githubcopilot.com/chat/completions` — Chat completions (GPT-4o model) **Key files:** -- `leanclaw/main.py` — Interactive REPL: loads token, maintains conversation history, calls Copilot API -- `leanclaw/login.py` — CLI login utility +- `iclaw/main.py` — Interactive REPL: loads token, maintains conversation history, calls Copilot API +- `iclaw/login.py` — CLI login utility - `pyproject.toml` — Package metadata and entry points -- `~/.config/leanclaw/config.json` — Generated by login, not in repo; contains `{ github_token, created_at }` +- `~/.config/iclaw/config.json` — Generated by login, not in repo; contains `{ github_token, created_at }` diff --git a/README.md b/README.md index 0408bd2..644a057 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# leanclaw +# iclaw An interactive terminal REPL for chatting with GitHub Copilot, built with Python. @@ -14,8 +14,8 @@ An interactive terminal REPL for chatting with GitHub Copilot, built with Python ## Installation ```bash -git clone https://github.com/lzwjava/leanclaw -cd leanclaw +git clone https://github.com/lzwjava/iclaw +cd iclaw pip install -e . ``` @@ -23,13 +23,13 @@ pip install -e . 1. **Authenticate with GitHub** (once): ```bash - leanclaw-login + iclaw-login ``` - This runs the GitHub device authorization flow and saves your token to `~/.config/leanclaw/config.json`. + This runs the GitHub device authorization flow and saves your token to `~/.config/iclaw/config.json`. 2. **Start the REPL**: ```bash - leanclaw + iclaw ``` ### CLI Commands @@ -58,7 +58,7 @@ python3 -m coverage report -m ### Project Structure ``` -leanclaw/ +iclaw/ ├── commands/ # Modular CLI command handlers ├── main.py # Core REPL loop and tool definitions ├── github_api.py # GitHub/Copilot API communication diff --git a/README_CN.md b/README_CN.md index e854e95..4779ec9 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,4 +1,4 @@ -# leanclaw (中文说明) +# iclaw (中文说明) 这是一个基于 Python 的交互式终端 REPL,用于在终端中与 GitHub Copilot 进行对话。 @@ -14,8 +14,8 @@ ## 安装步骤 ```bash -git clone https://github.com/lzwjava/leanclaw -cd leanclaw +git clone https://github.com/lzwjava/iclaw +cd iclaw pip install -e . ``` @@ -23,13 +23,13 @@ pip install -e . 1. **GitHub 认证(仅需一次)**: ```bash - leanclaw-login + iclaw-login ``` - 这将启动 GitHub 设备授权流程,并将您的令牌保存到 `~/.config/leanclaw/config.json`。 + 这将启动 GitHub 设备授权流程,并将您的令牌保存到 `~/.config/iclaw/config.json`。 2. **启动 REPL**: ```bash - leanclaw + iclaw ``` ### 终端命令 @@ -56,7 +56,7 @@ python3 -m coverage report -m ### 项目结构 ``` -leanclaw/ +iclaw/ ├── commands/ # 模块化 CLI 命令处理器 ├── main.py # 核心 REPL 循环和工具定义 ├── github_api.py # GitHub/Copilot API 通信 diff --git a/leanclaw/__init__.py b/iclaw/__init__.py similarity index 100% rename from leanclaw/__init__.py rename to iclaw/__init__.py diff --git a/leanclaw/commands/auth.py b/iclaw/commands/auth.py similarity index 91% rename from leanclaw/commands/auth.py rename to iclaw/commands/auth.py index 78f56c3..d4a59bb 100644 --- a/leanclaw/commands/auth.py +++ b/iclaw/commands/auth.py @@ -1,7 +1,7 @@ import json import sys from datetime import datetime, timezone -from leanclaw.login import get_device_code, poll_for_access_token +from iclaw.login import get_device_code, poll_for_access_token def handle_login_command(config_path, token_refresh_interval): try: diff --git a/leanclaw/commands/model.py b/iclaw/commands/model.py similarity index 97% rename from leanclaw/commands/model.py rename to iclaw/commands/model.py index c3923f5..ea81ecd 100644 --- a/leanclaw/commands/model.py +++ b/iclaw/commands/model.py @@ -1,5 +1,5 @@ import sys -from leanclaw.github_api import get_models +from iclaw.github_api import get_models def handle_model_command(copilot_token, current_model): try: diff --git a/leanclaw/commands/search_provider.py b/iclaw/commands/search_provider.py similarity index 100% rename from leanclaw/commands/search_provider.py rename to iclaw/commands/search_provider.py diff --git a/leanclaw/commands/utils.py b/iclaw/commands/utils.py similarity index 100% rename from leanclaw/commands/utils.py rename to iclaw/commands/utils.py diff --git a/leanclaw/exec_tool.py b/iclaw/exec_tool.py similarity index 100% rename from leanclaw/exec_tool.py rename to iclaw/exec_tool.py diff --git a/leanclaw/github_api.py b/iclaw/github_api.py similarity index 100% rename from leanclaw/github_api.py rename to iclaw/github_api.py diff --git a/leanclaw/login.py b/iclaw/login.py similarity index 100% rename from leanclaw/login.py rename to iclaw/login.py diff --git a/leanclaw/main.py b/iclaw/main.py similarity index 94% rename from leanclaw/main.py rename to iclaw/main.py index c5981e4..e935226 100644 --- a/leanclaw/main.py +++ b/iclaw/main.py @@ -18,14 +18,14 @@ def completer(text, state): except ImportError: pass -from leanclaw.github_api import chat, get_copilot_token -from leanclaw.web_search import web_search -from leanclaw.exec_tool import exec_command as exec +from iclaw.github_api import chat, get_copilot_token +from iclaw.web_search import web_search +from iclaw.exec_tool import exec_command as exec from tools.edit_tool import EditTool -from leanclaw.commands.auth import handle_login_command -from leanclaw.commands.model import handle_model_command -from leanclaw.commands.search_provider import handle_search_provider_command -from leanclaw.commands.utils import handle_copy_command +from iclaw.commands.auth import handle_login_command +from iclaw.commands.model import handle_model_command +from iclaw.commands.search_provider import handle_search_provider_command +from iclaw.commands.utils import handle_copy_command COMMANDS_HELP = [ ("/login", "Authenticate with GitHub"), @@ -36,7 +36,7 @@ def completer(text, state): (".exit", "Quit"), ] -CONFIG_PATH = Path.home() / ".config" / "leanclaw" / "config.json" +CONFIG_PATH = Path.home() / ".config" / "iclaw" / "config.json" TOKEN_REFRESH_INTERVAL = 24 * 60 # seconds WEB_SEARCH_TOOL = { diff --git a/leanclaw/web_search.py b/iclaw/web_search.py similarity index 100% rename from leanclaw/web_search.py rename to iclaw/web_search.py diff --git a/integration_tests/test_copilot.py b/integration_tests/test_copilot.py index 46ce3b3..25ba9b3 100644 --- a/integration_tests/test_copilot.py +++ b/integration_tests/test_copilot.py @@ -1,7 +1,7 @@ import unittest import os import requests -from leanclaw import github_api +from iclaw import github_api class TestCopilotIntegration(unittest.TestCase): def setUp(self): diff --git a/integration_tests/test_edit_integration.py b/integration_tests/test_edit_integration.py index 55ee6b6..05444df 100644 --- a/integration_tests/test_edit_integration.py +++ b/integration_tests/test_edit_integration.py @@ -16,9 +16,9 @@ def run_integration_test(): with open(test_filename, "w") as f: f.write("Line 1\nLine 2\nLine 3\n") - # Start the leanclaw process + # Start the iclaw process process = subprocess.Popen( - [sys.executable, "-m", "leanclaw.main"], + [sys.executable, "-m", "iclaw.main"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, diff --git a/integration_tests/test_exec_integration.py b/integration_tests/test_exec_integration.py index 6aba46c..0bd7299 100644 --- a/integration_tests/test_exec_integration.py +++ b/integration_tests/test_exec_integration.py @@ -11,9 +11,9 @@ def run_integration_test(): env = os.environ.copy() env["PYTHONPATH"] = os.getcwd() + ":" + env.get("PYTHONPATH", "") - # Start the leanclaw process + # Start the iclaw process process = subprocess.Popen( - [sys.executable, "-m", "leanclaw.main"], + [sys.executable, "-m", "iclaw.main"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # Combine stdout and stderr diff --git a/openclaw/package-lock.json b/openclaw/package-lock.json index 0f7eb6c..7950a4e 100644 --- a/openclaw/package-lock.json +++ b/openclaw/package-lock.json @@ -1,11 +1,11 @@ { - "name": "leanclaw", + "name": "iclaw", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "leanclaw", + "name": "iclaw", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/openclaw/package.json b/openclaw/package.json index 4208a70..9ffdbc1 100644 --- a/openclaw/package.json +++ b/openclaw/package.json @@ -1,5 +1,5 @@ { - "name": "leanclaw", + "name": "iclaw", "version": "1.0.0", "description": "", "main": "github_auth.js", diff --git a/pyproject.toml b/pyproject.toml index 01f4056..462d8b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "leanclaw" +name = "iclaw" version = "0.1.0" description = "Interactive CLI REPL for chatting with GitHub Copilot" readme = "README.md" @@ -11,4 +11,4 @@ requires-python = ">=3.8" dependencies = ["requests>=2.32.0", "pyperclip>=1.8.0", "beautifulsoup4>=4.12.0", "readability-lxml>=0.8.1", "lxml>=5.0.0"] [project.scripts] -leanclaw = "leanclaw.main:main" +iclaw = "iclaw.main:main" diff --git a/tests/test_commands.py b/tests/test_commands.py index c0ca4d6..a7d3542 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,24 +1,24 @@ import unittest from unittest.mock import MagicMock, patch -from leanclaw.commands import model, search_provider, auth, utils +from iclaw.commands import model, search_provider, auth, utils class TestCommands(unittest.TestCase): - @patch("leanclaw.commands.model.get_models") - @patch("leanclaw.commands.model.input", return_value="1") + @patch("iclaw.commands.model.get_models") + @patch("iclaw.commands.model.input", return_value="1") def test_handle_model_command(self, mock_input, mock_models): mock_models.return_value = [{"id": "m1", "owned_by": "o1"}] with patch("sys.stdout"): res = model.handle_model_command("t", "curr") self.assertEqual(res, "m1") - @patch("leanclaw.commands.search_provider.input", return_value="1") + @patch("iclaw.commands.search_provider.input", return_value="1") def test_handle_search_provider_command(self, mock_input): with patch("sys.stdout"): res = search_provider.handle_search_provider_command("duckduckgo") self.assertEqual(res, "duckduckgo") - @patch("leanclaw.commands.auth.get_device_code") - @patch("leanclaw.commands.auth.poll_for_access_token") + @patch("iclaw.commands.auth.get_device_code") + @patch("iclaw.commands.auth.poll_for_access_token") def test_handle_login_command(self, mock_poll, mock_device): mock_device.return_value = {"device_code": "dc"} mock_poll.return_value = "at" @@ -27,7 +27,7 @@ def test_handle_login_command(self, mock_poll, mock_device): res = auth.handle_login_command(mock_path, 1) self.assertEqual(res, "at") - @patch("leanclaw.commands.utils.sys") + @patch("iclaw.commands.utils.sys") def test_handle_copy_command(self, mock_sys): # We'll patch pyperclip inside sys.modules because it's imported inside the function mock_py = MagicMock() diff --git a/tests/test_exec_tool.py b/tests/test_exec_tool.py index 61a96c4..d19a8a6 100644 --- a/tests/test_exec_tool.py +++ b/tests/test_exec_tool.py @@ -1,5 +1,5 @@ import unittest -from leanclaw.exec_tool import exec_command +from iclaw.exec_tool import exec_command class TestExecTool(unittest.TestCase): diff --git a/tests/test_github_api.py b/tests/test_github_api.py index b0a05c4..a6bee2b 100644 --- a/tests/test_github_api.py +++ b/tests/test_github_api.py @@ -1,26 +1,26 @@ import unittest from unittest.mock import MagicMock, patch -from leanclaw import github_api +from iclaw import github_api class TestGithubApi(unittest.TestCase): - @patch("leanclaw.github_api.requests.get") + @patch("iclaw.github_api.requests.get") def test_get_copilot_token_success(self, mock_get): mock_get.return_value = MagicMock(ok=True, json=lambda: {"token": "t"}) self.assertEqual(github_api.get_copilot_token("gt"), "t") - @patch("leanclaw.github_api.requests.get") + @patch("iclaw.github_api.requests.get") def test_get_copilot_token_failure(self, mock_get): mock_get.return_value = MagicMock(ok=False, status_code=401) with self.assertRaises(RuntimeError): github_api.get_copilot_token("it") - @patch("leanclaw.github_api.requests.get") + @patch("iclaw.github_api.requests.get") def test_get_models(self, mock_get): mock_get.return_value = MagicMock(ok=True, json=lambda: {"data": [{"id": "m"}]}) self.assertEqual(github_api.get_models("t")[0]["id"], "m") - @patch("leanclaw.github_api.requests.post") + @patch("iclaw.github_api.requests.post") def test_chat(self, mock_post): mock_post.return_value = MagicMock(ok=True, json=lambda: {"choices": [{"message": {"content": "h"}}]}) self.assertEqual(github_api.chat([], "t")["content"], "h") diff --git a/tests/test_login.py b/tests/test_login.py index 5dbcf71..b7a9c89 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -1,15 +1,15 @@ import unittest from unittest.mock import MagicMock, patch -from leanclaw import login +from iclaw import login class TestLogin(unittest.TestCase): - @patch("leanclaw.login.requests.post") + @patch("iclaw.login.requests.post") def test_login_flow(self, mock_post): mock_post.return_value = MagicMock(ok=True, json=lambda: {"device_code": "dc", "user_code": "uc", "verification_uri": "v"}) self.assertEqual(login.get_device_code()["device_code"], "dc") mock_post.side_effect = [MagicMock(ok=True, json=lambda: {"access_token": "at"})] - with patch("leanclaw.login.time.sleep"): + with patch("iclaw.login.time.sleep"): self.assertEqual(login.poll_for_access_token("dc", 1), "at") if __name__ == "__main__": diff --git a/tests/test_main.py b/tests/test_main.py index 32a3133..f29e048 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,19 +1,19 @@ import unittest from unittest.mock import MagicMock, patch -from leanclaw import main +from iclaw import main class TestMain(unittest.TestCase): - @patch("leanclaw.main.chat") - @patch("leanclaw.main.load_github_token") - @patch("leanclaw.main.input") + @patch("iclaw.main.chat") + @patch("iclaw.main.load_github_token") + @patch("iclaw.main.input") def test_main_cli(self, mock_input, mock_load, mock_chat): mock_load.return_value = "gt" mock_input.side_effect = [".exit"] - with patch("sys.stdout"), patch("leanclaw.main.get_copilot_token"), patch("leanclaw.main.time.monotonic", return_value=0): + with patch("sys.stdout"), patch("iclaw.main.get_copilot_token"), patch("iclaw.main.time.monotonic", return_value=0): main.main() def test_load_github_token(self): - with patch("leanclaw.main.CONFIG_PATH") as mp: + with patch("iclaw.main.CONFIG_PATH") as mp: mp.exists.return_value = True mp.read_text.return_value = '{"github_token": "t"}' self.assertEqual(main.load_github_token(), "t") diff --git a/tests/test_web_search.py b/tests/test_web_search.py index 2d2a3bf..c29f907 100644 --- a/tests/test_web_search.py +++ b/tests/test_web_search.py @@ -1,10 +1,10 @@ import unittest from unittest.mock import MagicMock, patch -from leanclaw import web_search +from iclaw import web_search class TestWebSearch(unittest.TestCase): - @patch("leanclaw.web_search.requests.get") + @patch("iclaw.web_search.requests.get") def test_search_ddg(self, mock_get): # Match the .result__title .result__a selector mock_get.return_value = MagicMock( @@ -15,7 +15,7 @@ def test_search_ddg(self, mock_get): self.assertEqual(len(results), 1) self.assertEqual(results[0]["url"], "u") - @patch("leanclaw.web_search.requests.get") + @patch("iclaw.web_search.requests.get") def test_search_startpage(self, mock_get): # Match the .result and a.result-link, .wgl-title selectors mock_get.return_value = MagicMock( @@ -27,7 +27,7 @@ def test_search_startpage(self, mock_get): self.assertEqual(results[0]["url"], "http://u") self.assertEqual(results[0]["title"], "T") - @patch("leanclaw.web_search.requests.Session") + @patch("iclaw.web_search.requests.Session") def test_search_bing(self, mock_session): # Match the li.b_algo and h2 a selectors mock_s = MagicMock() @@ -41,7 +41,7 @@ def test_search_bing(self, mock_session): self.assertEqual(results[0]["url"], "http://u") self.assertEqual(results[0]["title"], "T") - @patch("leanclaw.web_search.requests.Session") + @patch("iclaw.web_search.requests.Session") def test_extract_text(self, mock_session): mock_s = MagicMock() mock_session.return_value = mock_s