refineo-cli 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- refineo_cli/__init__.py +16 -0
- refineo_cli/api.py +237 -0
- refineo_cli/cli.py +293 -0
- refineo_cli/config.py +80 -0
- refineo_cli-0.0.1.dist-info/METADATA +148 -0
- refineo_cli-0.0.1.dist-info/RECORD +8 -0
- refineo_cli-0.0.1.dist-info/WHEEL +4 -0
- refineo_cli-0.0.1.dist-info/entry_points.txt +2 -0
refineo_cli/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Refineo AI Text Humanizer CLI."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .api import humanize, get_usage, start_device_code_flow, poll_for_token
|
|
6
|
+
from .config import load_credentials, save_credentials, clear_credentials
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"humanize",
|
|
10
|
+
"get_usage",
|
|
11
|
+
"start_device_code_flow",
|
|
12
|
+
"poll_for_token",
|
|
13
|
+
"load_credentials",
|
|
14
|
+
"save_credentials",
|
|
15
|
+
"clear_credentials",
|
|
16
|
+
]
|
refineo_cli/api.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""API client for Refineo."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import urllib.request
|
|
6
|
+
import urllib.error
|
|
7
|
+
from typing import Optional, Callable, TypedDict, Any
|
|
8
|
+
|
|
9
|
+
from .config import (
|
|
10
|
+
API_BASE_URL,
|
|
11
|
+
Credentials,
|
|
12
|
+
load_credentials,
|
|
13
|
+
save_credentials,
|
|
14
|
+
is_token_expired,
|
|
15
|
+
get_platform_info,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DeviceCodeResponse(TypedDict):
|
|
20
|
+
device_code: str
|
|
21
|
+
user_code: str
|
|
22
|
+
verification_uri: str
|
|
23
|
+
verification_uri_complete: str
|
|
24
|
+
expires_in: int
|
|
25
|
+
interval: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HumanizeResult(TypedDict):
|
|
29
|
+
humanizedText: str
|
|
30
|
+
wordCount: int
|
|
31
|
+
model: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UsageStats(TypedDict):
|
|
35
|
+
tier: str
|
|
36
|
+
used: int
|
|
37
|
+
limit: int
|
|
38
|
+
remaining: int
|
|
39
|
+
resetDate: Optional[str]
|
|
40
|
+
wordLimit: int
|
|
41
|
+
rateLimit: Optional[int]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
USER_AGENT = get_platform_info()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _make_request(
|
|
48
|
+
path: str,
|
|
49
|
+
method: str = "GET",
|
|
50
|
+
data: Optional[dict[str, Any]] = None,
|
|
51
|
+
headers: Optional[dict[str, str]] = None,
|
|
52
|
+
) -> Any:
|
|
53
|
+
"""Make an HTTP request."""
|
|
54
|
+
url = f"{API_BASE_URL}{path}"
|
|
55
|
+
|
|
56
|
+
req_headers = {
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
"User-Agent": USER_AGENT,
|
|
59
|
+
}
|
|
60
|
+
if headers:
|
|
61
|
+
req_headers.update(headers)
|
|
62
|
+
|
|
63
|
+
body = None
|
|
64
|
+
if data is not None:
|
|
65
|
+
body = json.dumps(data).encode("utf-8")
|
|
66
|
+
|
|
67
|
+
req = urllib.request.Request(url, data=body, headers=req_headers, method=method)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
with urllib.request.urlopen(req, timeout=120) as response:
|
|
71
|
+
return json.loads(response.read().decode("utf-8"))
|
|
72
|
+
except urllib.error.HTTPError as e:
|
|
73
|
+
error_body = e.read().decode("utf-8")
|
|
74
|
+
try:
|
|
75
|
+
error_data = json.loads(error_body)
|
|
76
|
+
msg = (
|
|
77
|
+
error_data.get("message")
|
|
78
|
+
or error_data.get("error_description")
|
|
79
|
+
or error_data.get("error")
|
|
80
|
+
or f"HTTP {e.code}"
|
|
81
|
+
)
|
|
82
|
+
raise Exception(msg) from e
|
|
83
|
+
except json.JSONDecodeError:
|
|
84
|
+
raise Exception(f"HTTP {e.code}: {error_body}") from e
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _api_request(
|
|
88
|
+
path: str,
|
|
89
|
+
method: str = "GET",
|
|
90
|
+
data: Optional[dict[str, Any]] = None,
|
|
91
|
+
) -> Any:
|
|
92
|
+
"""Make an authenticated API request."""
|
|
93
|
+
credentials = load_credentials()
|
|
94
|
+
|
|
95
|
+
if not credentials:
|
|
96
|
+
raise Exception("Not logged in. Run: refineo login")
|
|
97
|
+
|
|
98
|
+
# Refresh token if expired
|
|
99
|
+
token = credentials["accessToken"]
|
|
100
|
+
if is_token_expired(credentials):
|
|
101
|
+
refreshed = _refresh_token(credentials["refreshToken"])
|
|
102
|
+
if refreshed:
|
|
103
|
+
token = refreshed["accessToken"]
|
|
104
|
+
else:
|
|
105
|
+
raise Exception("Session expired. Run: refineo login")
|
|
106
|
+
|
|
107
|
+
return _make_request(
|
|
108
|
+
path,
|
|
109
|
+
method=method,
|
|
110
|
+
data=data,
|
|
111
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _refresh_token(refresh_token: str) -> Optional[Credentials]:
|
|
116
|
+
"""Refresh access token."""
|
|
117
|
+
try:
|
|
118
|
+
data = _make_request(
|
|
119
|
+
"/api/auth/device/refresh",
|
|
120
|
+
method="POST",
|
|
121
|
+
data={
|
|
122
|
+
"refresh_token": refresh_token,
|
|
123
|
+
"grant_type": "refresh_token",
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
old_credentials = load_credentials()
|
|
128
|
+
|
|
129
|
+
credentials: Credentials = {
|
|
130
|
+
"accessToken": data["access_token"],
|
|
131
|
+
"refreshToken": data["refresh_token"],
|
|
132
|
+
"expiresAt": data["expires_at"],
|
|
133
|
+
"user": old_credentials["user"] if old_credentials else {"email": "", "tier": "", "name": None},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
save_credentials(credentials)
|
|
137
|
+
return credentials
|
|
138
|
+
except Exception:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def start_device_code_flow() -> DeviceCodeResponse:
|
|
143
|
+
"""Start device code flow."""
|
|
144
|
+
return _make_request("/api/auth/device/code", method="POST")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def poll_for_token(
|
|
148
|
+
device_code: str,
|
|
149
|
+
interval: int,
|
|
150
|
+
expires_in: int,
|
|
151
|
+
on_poll: Optional[Callable[[], None]] = None,
|
|
152
|
+
) -> Credentials:
|
|
153
|
+
"""Poll for device code authorization."""
|
|
154
|
+
start_time = time.time()
|
|
155
|
+
timeout = expires_in
|
|
156
|
+
|
|
157
|
+
while time.time() - start_time < timeout:
|
|
158
|
+
if on_poll:
|
|
159
|
+
on_poll()
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
data = _make_request(
|
|
163
|
+
"/api/auth/device/token",
|
|
164
|
+
method="POST",
|
|
165
|
+
data={
|
|
166
|
+
"device_code": device_code,
|
|
167
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Success!
|
|
172
|
+
credentials: Credentials = {
|
|
173
|
+
"accessToken": data["access_token"],
|
|
174
|
+
"refreshToken": data["refresh_token"],
|
|
175
|
+
"expiresAt": data["expires_at"],
|
|
176
|
+
"user": data.get("user", {"email": "", "tier": "", "name": None}),
|
|
177
|
+
}
|
|
178
|
+
save_credentials(credentials)
|
|
179
|
+
return credentials
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
error_msg = str(e)
|
|
183
|
+
|
|
184
|
+
if "authorization_pending" in error_msg:
|
|
185
|
+
time.sleep(interval)
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
if "slow_down" in error_msg:
|
|
189
|
+
time.sleep(interval + 5)
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if "access_denied" in error_msg:
|
|
193
|
+
raise Exception(
|
|
194
|
+
"Access denied. CLI requires Pro or Ultra subscription."
|
|
195
|
+
) from e
|
|
196
|
+
|
|
197
|
+
if "expired_token" in error_msg:
|
|
198
|
+
raise Exception("Login timed out. Please try again.") from e
|
|
199
|
+
|
|
200
|
+
raise
|
|
201
|
+
|
|
202
|
+
raise Exception("Login timed out. Please try again.")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def humanize(
|
|
206
|
+
text: str,
|
|
207
|
+
model: str = "enhanced",
|
|
208
|
+
) -> HumanizeResult:
|
|
209
|
+
"""Humanize text."""
|
|
210
|
+
api_model = "BALANCE" if model == "standard" else "ENHANCED"
|
|
211
|
+
|
|
212
|
+
result = _api_request(
|
|
213
|
+
"/api/humanize",
|
|
214
|
+
method="POST",
|
|
215
|
+
data={"text": text, "model": api_model},
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"humanizedText": result["data"]["humanizedText"],
|
|
220
|
+
"wordCount": result["data"]["wordCount"],
|
|
221
|
+
"model": result["data"]["model"],
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_usage() -> UsageStats:
|
|
226
|
+
"""Get usage stats."""
|
|
227
|
+
result = _api_request("/api/usage")
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"tier": result["tier"],
|
|
231
|
+
"used": result["used"],
|
|
232
|
+
"limit": result["limit"],
|
|
233
|
+
"remaining": result["remaining"],
|
|
234
|
+
"resetDate": result.get("resetDate"),
|
|
235
|
+
"wordLimit": result["wordLimit"],
|
|
236
|
+
"rateLimit": result.get("rateLimit"),
|
|
237
|
+
}
|
refineo_cli/cli.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Refineo CLI entry point."""
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import platform
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .config import load_credentials, clear_credentials
|
|
10
|
+
from .api import start_device_code_flow, poll_for_token, humanize, get_usage
|
|
11
|
+
|
|
12
|
+
VERSION = "0.1.0"
|
|
13
|
+
|
|
14
|
+
# ANSI colors
|
|
15
|
+
RESET = "\033[0m"
|
|
16
|
+
BOLD = "\033[1m"
|
|
17
|
+
DIM = "\033[2m"
|
|
18
|
+
GREEN = "\033[32m"
|
|
19
|
+
YELLOW = "\033[33m"
|
|
20
|
+
BLUE = "\033[34m"
|
|
21
|
+
CYAN = "\033[36m"
|
|
22
|
+
RED = "\033[31m"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def print_error(message: str) -> None:
|
|
26
|
+
"""Print error message."""
|
|
27
|
+
print(f"{RED}Error:{RESET} {message}", file=sys.stderr)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def print_success(message: str) -> None:
|
|
31
|
+
"""Print success message."""
|
|
32
|
+
print(f"{GREEN}✓{RESET} {message}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def open_browser(url: str) -> None:
|
|
36
|
+
"""Open URL in default browser."""
|
|
37
|
+
system = platform.system()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
if system == "Darwin":
|
|
41
|
+
subprocess.run(["open", url], check=True, capture_output=True)
|
|
42
|
+
elif system == "Windows":
|
|
43
|
+
subprocess.run(["start", "", url], check=True, capture_output=True, shell=True)
|
|
44
|
+
else:
|
|
45
|
+
subprocess.run(["xdg-open", url], check=True, capture_output=True)
|
|
46
|
+
except Exception:
|
|
47
|
+
print(f"Please open this URL in your browser: {url}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def login_command() -> None:
|
|
51
|
+
"""Login command."""
|
|
52
|
+
existing = load_credentials()
|
|
53
|
+
if existing:
|
|
54
|
+
print(f"Already logged in as {CYAN}{existing['user']['email']}{RESET}")
|
|
55
|
+
print(f"Tier: {BOLD}{existing['user']['tier']}{RESET}")
|
|
56
|
+
print(f"\nRun {DIM}refineo logout{RESET} to switch accounts.")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
print(f"{BOLD}Refineo CLI Login{RESET}\n")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
device_code = start_device_code_flow()
|
|
63
|
+
|
|
64
|
+
print(f"Your code: {BOLD}{CYAN}{device_code['user_code']}{RESET}\n")
|
|
65
|
+
print("Opening browser to authorize...")
|
|
66
|
+
print(f"{DIM}{device_code['verification_uri_complete']}{RESET}\n")
|
|
67
|
+
|
|
68
|
+
open_browser(device_code["verification_uri_complete"])
|
|
69
|
+
|
|
70
|
+
print("Waiting for authorization...", end="", flush=True)
|
|
71
|
+
|
|
72
|
+
dots = [0]
|
|
73
|
+
|
|
74
|
+
def on_poll() -> None:
|
|
75
|
+
dots[0] = (dots[0] % 3) + 1
|
|
76
|
+
print(f"\rWaiting for authorization{'.' * dots[0]} ", end="", flush=True)
|
|
77
|
+
|
|
78
|
+
credentials = poll_for_token(
|
|
79
|
+
device_code["device_code"],
|
|
80
|
+
device_code["interval"],
|
|
81
|
+
device_code["expires_in"],
|
|
82
|
+
on_poll,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
print("\r \r", end="")
|
|
86
|
+
print_success(f"Logged in as {CYAN}{credentials['user']['email']}{RESET}")
|
|
87
|
+
print(f"Tier: {BOLD}{credentials['user']['tier']}{RESET}")
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print()
|
|
91
|
+
print_error(str(e))
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def logout_command() -> None:
|
|
96
|
+
"""Logout command."""
|
|
97
|
+
credentials = load_credentials()
|
|
98
|
+
|
|
99
|
+
if not credentials:
|
|
100
|
+
print("Not logged in.")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
clear_credentials()
|
|
104
|
+
print_success(f"Logged out from {credentials['user']['email']}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def stats_command() -> None:
|
|
108
|
+
"""Stats command."""
|
|
109
|
+
credentials = load_credentials()
|
|
110
|
+
|
|
111
|
+
if not credentials:
|
|
112
|
+
print_error("Not logged in. Run: refineo login")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
usage = get_usage()
|
|
117
|
+
|
|
118
|
+
print(f"{BOLD}Refineo Usage{RESET}\n")
|
|
119
|
+
print(f"Account: {CYAN}{credentials['user']['email']}{RESET}")
|
|
120
|
+
print(f"Plan: {BOLD}{usage['tier']}{RESET}")
|
|
121
|
+
print()
|
|
122
|
+
|
|
123
|
+
if usage["limit"] == -1:
|
|
124
|
+
print(f"Requests: {GREEN}Unlimited{RESET}")
|
|
125
|
+
if usage.get("rateLimit"):
|
|
126
|
+
print(f"Rate limit: {usage['rateLimit']} requests/hour")
|
|
127
|
+
else:
|
|
128
|
+
percentage = round((usage["used"] / usage["limit"]) * 100)
|
|
129
|
+
color = RED if percentage >= 90 else YELLOW if percentage >= 70 else GREEN
|
|
130
|
+
print(f"Requests: {color}{usage['used']}{RESET} / {usage['limit']} ({percentage}%)")
|
|
131
|
+
print(f"Remaining: {usage['remaining']}")
|
|
132
|
+
|
|
133
|
+
print(f"Word limit: {usage['wordLimit']} words/request")
|
|
134
|
+
|
|
135
|
+
if usage.get("resetDate"):
|
|
136
|
+
print(f"Resets: {usage['resetDate']}")
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print_error(str(e))
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def humanize_command(args: list[str]) -> None:
|
|
144
|
+
"""Humanize command."""
|
|
145
|
+
credentials = load_credentials()
|
|
146
|
+
|
|
147
|
+
if not credentials:
|
|
148
|
+
print_error("Not logged in. Run: refineo login")
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
# Parse arguments
|
|
152
|
+
text = ""
|
|
153
|
+
model = "enhanced"
|
|
154
|
+
input_file = ""
|
|
155
|
+
output_file = ""
|
|
156
|
+
|
|
157
|
+
i = 0
|
|
158
|
+
while i < len(args):
|
|
159
|
+
arg = args[i]
|
|
160
|
+
|
|
161
|
+
if arg in ("--model", "-m"):
|
|
162
|
+
i += 1
|
|
163
|
+
if i < len(args):
|
|
164
|
+
value = args[i]
|
|
165
|
+
if value in ("standard", "enhanced"):
|
|
166
|
+
model = value
|
|
167
|
+
else:
|
|
168
|
+
print_error('Model must be "standard" or "enhanced"')
|
|
169
|
+
sys.exit(1)
|
|
170
|
+
elif arg in ("--file", "-f"):
|
|
171
|
+
i += 1
|
|
172
|
+
if i < len(args):
|
|
173
|
+
input_file = args[i]
|
|
174
|
+
elif arg in ("--output", "-o"):
|
|
175
|
+
i += 1
|
|
176
|
+
if i < len(args):
|
|
177
|
+
output_file = args[i]
|
|
178
|
+
elif not arg.startswith("-"):
|
|
179
|
+
text = arg
|
|
180
|
+
|
|
181
|
+
i += 1
|
|
182
|
+
|
|
183
|
+
# Read from file if specified
|
|
184
|
+
if input_file:
|
|
185
|
+
path = Path(input_file)
|
|
186
|
+
if not path.exists():
|
|
187
|
+
print_error(f"File not found: {input_file}")
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
text = path.read_text()
|
|
190
|
+
|
|
191
|
+
# Read from stdin if no text provided
|
|
192
|
+
if not text:
|
|
193
|
+
if sys.stdin.isatty():
|
|
194
|
+
print_error(
|
|
195
|
+
'No text provided. Usage: refineo humanize "your text" or echo "text" | refineo humanize'
|
|
196
|
+
)
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
text = sys.stdin.read().strip()
|
|
199
|
+
|
|
200
|
+
if not text:
|
|
201
|
+
print_error("No text provided")
|
|
202
|
+
sys.exit(1)
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
result = humanize(text, model)
|
|
206
|
+
|
|
207
|
+
if output_file:
|
|
208
|
+
Path(output_file).write_text(result["humanizedText"])
|
|
209
|
+
print_success(f"Output written to {output_file}")
|
|
210
|
+
else:
|
|
211
|
+
print(result["humanizedText"])
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
print_error(str(e))
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def help_command() -> None:
|
|
219
|
+
"""Help command."""
|
|
220
|
+
print(f"""
|
|
221
|
+
{BOLD}Refineo CLI{RESET} - AI Text Humanizer
|
|
222
|
+
Version {VERSION}
|
|
223
|
+
|
|
224
|
+
{BOLD}Usage:{RESET}
|
|
225
|
+
refineo <command> [options]
|
|
226
|
+
|
|
227
|
+
{BOLD}Commands:{RESET}
|
|
228
|
+
login Authenticate with your Refineo account
|
|
229
|
+
logout Clear stored credentials
|
|
230
|
+
stats Show usage statistics
|
|
231
|
+
humanize <text> Humanize AI-generated text
|
|
232
|
+
|
|
233
|
+
{BOLD}Humanize Options:{RESET}
|
|
234
|
+
-m, --model <model> Model: "standard" or "enhanced" (default: enhanced)
|
|
235
|
+
-f, --file <path> Read input from file
|
|
236
|
+
-o, --output <path> Write output to file
|
|
237
|
+
|
|
238
|
+
{BOLD}Examples:{RESET}
|
|
239
|
+
{DIM}# Login to your account{RESET}
|
|
240
|
+
refineo login
|
|
241
|
+
|
|
242
|
+
{DIM}# Humanize text directly{RESET}
|
|
243
|
+
refineo humanize "The results indicate a significant correlation."
|
|
244
|
+
|
|
245
|
+
{DIM}# Use standard model{RESET}
|
|
246
|
+
refineo humanize "Text here" --model standard
|
|
247
|
+
|
|
248
|
+
{DIM}# Read from file{RESET}
|
|
249
|
+
refineo humanize --file input.txt --output output.txt
|
|
250
|
+
|
|
251
|
+
{DIM}# Pipe from stdin{RESET}
|
|
252
|
+
echo "AI-generated text" | refineo humanize
|
|
253
|
+
|
|
254
|
+
{DIM}# Check usage{RESET}
|
|
255
|
+
refineo stats
|
|
256
|
+
|
|
257
|
+
{BOLD}More Info:{RESET}
|
|
258
|
+
https://refineo.app/docs/cli
|
|
259
|
+
""")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def version_command() -> None:
|
|
263
|
+
"""Version command."""
|
|
264
|
+
print(f"refineo {VERSION}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main() -> None:
|
|
268
|
+
"""Main entry point."""
|
|
269
|
+
args = sys.argv[1:]
|
|
270
|
+
command = args[0] if args else None
|
|
271
|
+
|
|
272
|
+
if command == "login":
|
|
273
|
+
login_command()
|
|
274
|
+
elif command == "logout":
|
|
275
|
+
logout_command()
|
|
276
|
+
elif command == "stats":
|
|
277
|
+
stats_command()
|
|
278
|
+
elif command == "humanize":
|
|
279
|
+
humanize_command(args[1:])
|
|
280
|
+
elif command in ("help", "--help", "-h"):
|
|
281
|
+
help_command()
|
|
282
|
+
elif command in ("version", "--version", "-v"):
|
|
283
|
+
version_command()
|
|
284
|
+
elif command is None:
|
|
285
|
+
help_command()
|
|
286
|
+
else:
|
|
287
|
+
print_error(f"Unknown command: {command}")
|
|
288
|
+
print(f"Run {DIM}refineo help{RESET} for usage.")
|
|
289
|
+
sys.exit(1)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
if __name__ == "__main__":
|
|
293
|
+
main()
|
refineo_cli/config.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Configuration and credentials management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TypedDict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UserInfo(TypedDict):
|
|
11
|
+
email: str
|
|
12
|
+
name: Optional[str]
|
|
13
|
+
tier: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Credentials(TypedDict):
|
|
17
|
+
accessToken: str
|
|
18
|
+
refreshToken: str
|
|
19
|
+
expiresAt: int
|
|
20
|
+
user: UserInfo
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
CONFIG_DIR = Path.home() / ".refineo"
|
|
24
|
+
CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
|
|
25
|
+
|
|
26
|
+
API_BASE_URL = os.environ.get("REFINEO_API_URL", "https://refineo.app")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def ensure_config_dir() -> None:
|
|
30
|
+
"""Ensure config directory exists with proper permissions."""
|
|
31
|
+
CONFIG_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_credentials() -> Optional[Credentials]:
|
|
35
|
+
"""Load credentials from disk."""
|
|
36
|
+
try:
|
|
37
|
+
if not CREDENTIALS_FILE.exists():
|
|
38
|
+
return None
|
|
39
|
+
data = json.loads(CREDENTIALS_FILE.read_text())
|
|
40
|
+
return data
|
|
41
|
+
except Exception:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def save_credentials(credentials: Credentials) -> None:
|
|
46
|
+
"""Save credentials to disk."""
|
|
47
|
+
ensure_config_dir()
|
|
48
|
+
CREDENTIALS_FILE.write_text(json.dumps(credentials, indent=2))
|
|
49
|
+
CREDENTIALS_FILE.chmod(0o600)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def clear_credentials() -> None:
|
|
53
|
+
"""Clear credentials from disk."""
|
|
54
|
+
try:
|
|
55
|
+
if CREDENTIALS_FILE.exists():
|
|
56
|
+
CREDENTIALS_FILE.unlink()
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_token_expired(credentials: Credentials) -> bool:
|
|
62
|
+
"""Check if credentials are expired (with 1 minute buffer)."""
|
|
63
|
+
import time
|
|
64
|
+
now = int(time.time())
|
|
65
|
+
return credentials["expiresAt"] <= now + 60
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_platform_info() -> str:
|
|
69
|
+
"""Get current platform info for User-Agent."""
|
|
70
|
+
system = platform.system()
|
|
71
|
+
arch = platform.machine()
|
|
72
|
+
py_version = platform.python_version()
|
|
73
|
+
|
|
74
|
+
os_name = {
|
|
75
|
+
"Darwin": "macOS",
|
|
76
|
+
"Windows": "Windows",
|
|
77
|
+
"Linux": "Linux",
|
|
78
|
+
}.get(system, "Unknown")
|
|
79
|
+
|
|
80
|
+
return f"refineo-cli/0.1.0 ({os_name}; {arch}) Python/{py_version}"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: refineo-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Refineo AI Text Humanizer CLI - Transform AI-generated text into natural human writing
|
|
5
|
+
Project-URL: Homepage, https://refineo.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/refineo/refineo-ai-tools
|
|
7
|
+
Project-URL: Issues, https://github.com/refineo/refineo-ai-tools/issues
|
|
8
|
+
Author: Refineo
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai,cli,humanizer,mcp,refineo,text,writing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Text Processing
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.13.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Refineo AI Tools
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/refineo-cli/)
|
|
31
|
+
[](https://www.npmjs.com/package/@refineo/cli)
|
|
32
|
+
|
|
33
|
+
CLI and MCP tools for [Refineo](https://refineo.app) - Transform AI-generated text into natural human writing.
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### Node.js / TypeScript
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Using bunx (recommended)
|
|
41
|
+
bunx @refineo/cli login
|
|
42
|
+
|
|
43
|
+
# Using npx
|
|
44
|
+
npx @refineo/cli login
|
|
45
|
+
|
|
46
|
+
# Global install
|
|
47
|
+
npm i -g @refineo/cli && refineo login
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Python
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Using uvx (recommended)
|
|
54
|
+
uvx refineo-cli login
|
|
55
|
+
|
|
56
|
+
# Using pipx
|
|
57
|
+
pipx run refineo-cli login
|
|
58
|
+
|
|
59
|
+
# Global install
|
|
60
|
+
pip install refineo-cli && refineo login
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Commands
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
refineo login # Authenticate with your account
|
|
67
|
+
refineo logout # Clear stored credentials
|
|
68
|
+
refineo stats # Show usage statistics
|
|
69
|
+
refineo humanize "text" # Humanize AI-generated text
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Humanize Options
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
refineo humanize "text" --model enhanced # Use enhanced model (default)
|
|
76
|
+
refineo humanize "text" --model standard # Use standard model
|
|
77
|
+
refineo humanize --file input.txt # Read from file
|
|
78
|
+
refineo humanize --file input.txt --output output.txt # Write to file
|
|
79
|
+
echo "text" | refineo humanize # Read from stdin
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Requirements
|
|
83
|
+
|
|
84
|
+
- **Pro or Ultra subscription** - CLI/MCP access is a Pro+ feature
|
|
85
|
+
- Node.js 18+ (for Node CLI)
|
|
86
|
+
- Python 3.10+ (for Python CLI)
|
|
87
|
+
|
|
88
|
+
## Authentication
|
|
89
|
+
|
|
90
|
+
The CLI uses device code flow for secure authentication:
|
|
91
|
+
|
|
92
|
+
1. Run `refineo login`
|
|
93
|
+
2. A browser opens to authorize the device
|
|
94
|
+
3. Enter the code shown in your terminal
|
|
95
|
+
4. Credentials are stored securely in `~/.refineo/`
|
|
96
|
+
|
|
97
|
+
## MCP Integration
|
|
98
|
+
|
|
99
|
+
Refineo provides an MCP (Model Context Protocol) endpoint for integration with Claude, Cursor, and other AI tools.
|
|
100
|
+
|
|
101
|
+
### Tools Available
|
|
102
|
+
|
|
103
|
+
- **humanize** - Transform AI-generated text into natural human writing
|
|
104
|
+
- **get_usage** - Check remaining quota for current billing period
|
|
105
|
+
|
|
106
|
+
### Configuration
|
|
107
|
+
|
|
108
|
+
Add to your Claude/Cursor MCP config:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"refineo": {
|
|
114
|
+
"url": "https://refineo.app/api/mcp",
|
|
115
|
+
"transport": "http",
|
|
116
|
+
"authentication": {
|
|
117
|
+
"type": "bearer",
|
|
118
|
+
"token": "<your-access-token>"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Get your access token by running `refineo login` and checking `~/.refineo/credentials.json`.
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
### Node CLI
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
cd node
|
|
133
|
+
npm install
|
|
134
|
+
npm run build
|
|
135
|
+
npm run test
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Python CLI
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cd python
|
|
142
|
+
pip install -e ".[dev]"
|
|
143
|
+
pytest
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
refineo_cli/__init__.py,sha256=BIzN5T8plbx6FDib_3JXo9kMzlk-8FskiOx0OeznLEI,385
|
|
2
|
+
refineo_cli/api.py,sha256=U_lrTu56pW-y33rvDsUb5npNEoyNpU_VQNKjlCB3o8E,6248
|
|
3
|
+
refineo_cli/cli.py,sha256=LOKuMWP0n2C5gVtMwTkyjT36oKLYr35Ujqc2wApYBkQ,8033
|
|
4
|
+
refineo_cli/config.py,sha256=EXiRtiaWbJ_NhFsGSDQD3igrtezpsmgwrd-Pi5V-n6A,1976
|
|
5
|
+
refineo_cli-0.0.1.dist-info/METADATA,sha256=S7xQgz1Zm6VOdBE59gpwjKAmxNTn-9TSCpF8n7XXJlQ,3714
|
|
6
|
+
refineo_cli-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
refineo_cli-0.0.1.dist-info/entry_points.txt,sha256=0ZfAhBoW3AWkwIZDD_ZXcHbB4KwsBI3-dWB_lHxO6_g,49
|
|
8
|
+
refineo_cli-0.0.1.dist-info/RECORD,,
|