Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
mini-copilot-login # Authenticate with GitHub via device flow (saves token to ~/.config/mini-copilot/config.json)
mini-copilot # Start the interactive CLI REPL
leanclaw-login # Authenticate with GitHub via device flow (saves token to ~/.config/leanclaw/config.json)
leanclaw # Start the interactive CLI REPL
ruff check . # Lint code
ruff format . # Format code
```
Expand All @@ -16,11 +16,11 @@ Pre-commit hooks run automatically on commit: ruff format.

## Architecture

This is a Python CLI package (mini-copilot) that provides an interactive terminal REPL for chatting with GitHub Copilot.
This is a Python CLI package (leanclaw) that provides an interactive terminal REPL for chatting with GitHub Copilot.

**Authentication flow:**
1. `mini-copilot-login` runs GitHub OAuth Device Flow, saves the GitHub token to `~/.config/mini-copilot/config.json`
2. On startup, `mini-copilot` reads the GitHub token from `~/.config/mini-copilot/config.json`, then exchanges it for a short-lived Copilot token via `https://api.github.com/copilot_internal/v2/token`
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`
3. The Copilot token is refreshed every ~24 minutes during the session

**API endpoints:**
Expand All @@ -30,7 +30,7 @@ This is a Python CLI package (mini-copilot) that provides an interactive termina
- `https://api.githubcopilot.com/chat/completions` — Chat completions (GPT-4o model)

**Key files:**
- `mini_copilot/main.py` — Interactive REPL: loads token, maintains conversation history, calls Copilot API
- `mini_copilot/login.py` — CLI login utility
- `leanclaw/main.py` — Interactive REPL: loads token, maintains conversation history, calls Copilot API
- `leanclaw/login.py` — CLI login utility
- `pyproject.toml` — Package metadata and entry points
- `~/.config/mini-copilot/config.json` — Generated by login, not in repo; contains `{ github_token, created_at }`
- `~/.config/leanclaw/config.json` — Generated by login, not in repo; contains `{ github_token, created_at }`
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# mini-copilot
# leanclaw

An interactive terminal REPL for chatting with GitHub Copilot, built with Python.

Expand All @@ -14,22 +14,22 @@ An interactive terminal REPL for chatting with GitHub Copilot, built with Python
## Installation

```bash
git clone https://github.com/lzwjava/mini-copilot
cd mini-copilot
git clone https://github.com/lzwjava/leanclaw
cd leanclaw
pip install -e .
```

## Usage

1. **Authenticate with GitHub** (once):
```bash
mini-copilot-login
leanclaw-login
```
This runs the GitHub device authorization flow and saves your token to `~/.config/mini-copilot/config.json`.
This runs the GitHub device authorization flow and saves your token to `~/.config/leanclaw/config.json`.

2. **Start the REPL**:
```bash
mini-copilot
leanclaw
```

### CLI Commands
Expand Down Expand Up @@ -58,7 +58,7 @@ python3 -m coverage report -m

### Project Structure
```
mini_copilot/
leanclaw/
├── commands/ # Modular CLI command handlers
├── main.py # Core REPL loop and tool definitions
├── github_api.py # GitHub/Copilot API communication
Expand Down
14 changes: 7 additions & 7 deletions README_CN.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# mini-copilot (中文说明)
# leanclaw (中文说明)

这是一个基于 Python 的交互式终端 REPL,用于在终端中与 GitHub Copilot 进行对话。

Expand All @@ -14,22 +14,22 @@
## 安装步骤

```bash
git clone https://github.com/lzwjava/mini-copilot
cd mini-copilot
git clone https://github.com/lzwjava/leanclaw
cd leanclaw
pip install -e .
```

## 使用说明

1. **GitHub 认证(仅需一次)**:
```bash
mini-copilot-login
leanclaw-login
```
这将启动 GitHub 设备授权流程,并将您的令牌保存到 `~/.config/mini-copilot/config.json`。
这将启动 GitHub 设备授权流程,并将您的令牌保存到 `~/.config/leanclaw/config.json`。

2. **启动 REPL**:
```bash
mini-copilot
leanclaw
```

### 终端命令
Expand All @@ -56,7 +56,7 @@ python3 -m coverage report -m

### 项目结构
```
mini_copilot/
leanclaw/
├── commands/ # 模块化 CLI 命令处理器
├── main.py # 核心 REPL 循环和工具定义
├── github_api.py # GitHub/Copilot API 通信
Expand Down
2 changes: 1 addition & 1 deletion integration_tests/test_copilot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest
import os
import requests
from mini_copilot import github_api
from leanclaw import github_api

class TestCopilotIntegration(unittest.TestCase):
def setUp(self):
Expand Down
4 changes: 2 additions & 2 deletions integration_tests/test_edit_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 mini-copilot process
# Start the leanclaw process
process = subprocess.Popen(
[sys.executable, "-m", "mini_copilot.main"],
[sys.executable, "-m", "leanclaw.main"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down
4 changes: 2 additions & 2 deletions integration_tests/test_exec_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ def run_integration_test():
env = os.environ.copy()
env["PYTHONPATH"] = os.getcwd() + ":" + env.get("PYTHONPATH", "")

# Start the mini-copilot process
# Start the leanclaw process
process = subprocess.Popen(
[sys.executable, "-m", "mini_copilot.main"],
[sys.executable, "-m", "leanclaw.main"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # Combine stdout and stderr
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import sys
from datetime import datetime, timezone
from mini_copilot.login import get_device_code, poll_for_access_token
from leanclaw.login import get_device_code, poll_for_access_token

def handle_login_command(config_path, token_refresh_interval):
try:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import sys
from mini_copilot.github_api import get_models
from leanclaw.github_api import get_models

def handle_model_command(copilot_token, current_model):
try:
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
16 changes: 8 additions & 8 deletions mini_copilot/main.py → leanclaw/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ def completer(text, state):
except ImportError:
pass

from mini_copilot.github_api import chat, get_copilot_token
from mini_copilot.web_search import web_search
from mini_copilot.exec_tool import exec_command as exec
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 tools.edit_tool import EditTool
from mini_copilot.commands.auth import handle_login_command
from mini_copilot.commands.model import handle_model_command
from mini_copilot.commands.search_provider import handle_search_provider_command
from mini_copilot.commands.utils import handle_copy_command
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

COMMANDS_HELP = [
("/login", "Authenticate with GitHub"),
Expand All @@ -36,7 +36,7 @@ def completer(text, state):
(".exit", "Quit"),
]

CONFIG_PATH = Path.home() / ".config" / "mini-copilot" / "config.json"
CONFIG_PATH = Path.home() / ".config" / "leanclaw" / "config.json"
TOKEN_REFRESH_INTERVAL = 24 * 60 # seconds

WEB_SEARCH_TOOL = {
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions openclaw/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion openclaw/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "mini-copilot",
"name": "leanclaw",
"version": "1.0.0",
"description": "",
"main": "github_auth.js",
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mini-copilot"
name = "leanclaw"
version = "0.1.0"
description = "Interactive CLI REPL for chatting with GitHub Copilot"
readme = "README.md"
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]
mini-copilot = "mini_copilot.main:main"
leanclaw = "leanclaw.main:main"
14 changes: 7 additions & 7 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import unittest
from unittest.mock import MagicMock, patch
from mini_copilot.commands import model, search_provider, auth, utils
from leanclaw.commands import model, search_provider, auth, utils

class TestCommands(unittest.TestCase):
@patch("mini_copilot.commands.model.get_models")
@patch("mini_copilot.commands.model.input", return_value="1")
@patch("leanclaw.commands.model.get_models")
@patch("leanclaw.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("mini_copilot.commands.search_provider.input", return_value="1")
@patch("leanclaw.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("mini_copilot.commands.auth.get_device_code")
@patch("mini_copilot.commands.auth.poll_for_access_token")
@patch("leanclaw.commands.auth.get_device_code")
@patch("leanclaw.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"
Expand All @@ -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("mini_copilot.commands.utils.sys")
@patch("leanclaw.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()
Expand Down
2 changes: 1 addition & 1 deletion tests/test_exec_tool.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import unittest
from mini_copilot.exec_tool import exec_command
from leanclaw.exec_tool import exec_command


class TestExecTool(unittest.TestCase):
Expand Down
10 changes: 5 additions & 5 deletions tests/test_github_api.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import unittest
from unittest.mock import MagicMock, patch

from mini_copilot import github_api
from leanclaw import github_api

class TestGithubApi(unittest.TestCase):
@patch("mini_copilot.github_api.requests.get")
@patch("leanclaw.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("mini_copilot.github_api.requests.get")
@patch("leanclaw.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("mini_copilot.github_api.requests.get")
@patch("leanclaw.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("mini_copilot.github_api.requests.post")
@patch("leanclaw.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")
Expand Down
6 changes: 3 additions & 3 deletions tests/test_login.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import unittest
from unittest.mock import MagicMock, patch
from mini_copilot import login
from leanclaw import login

class TestLogin(unittest.TestCase):
@patch("mini_copilot.login.requests.post")
@patch("leanclaw.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("mini_copilot.login.time.sleep"):
with patch("leanclaw.login.time.sleep"):
self.assertEqual(login.poll_for_access_token("dc", 1), "at")

if __name__ == "__main__":
Expand Down
12 changes: 6 additions & 6 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import unittest
from unittest.mock import MagicMock, patch
from mini_copilot import main
from leanclaw import main

class TestMain(unittest.TestCase):
@patch("mini_copilot.main.chat")
@patch("mini_copilot.main.load_github_token")
@patch("mini_copilot.main.input")
@patch("leanclaw.main.chat")
@patch("leanclaw.main.load_github_token")
@patch("leanclaw.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("mini_copilot.main.get_copilot_token"), patch("mini_copilot.main.time.monotonic", return_value=0):
with patch("sys.stdout"), patch("leanclaw.main.get_copilot_token"), patch("leanclaw.main.time.monotonic", return_value=0):
main.main()

def test_load_github_token(self):
with patch("mini_copilot.main.CONFIG_PATH") as mp:
with patch("leanclaw.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")
Expand Down
Loading
Loading