forgegrit-cli 1.0.0__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.
- forgegrit_cli/__init__.py +1 -0
- forgegrit_cli/__main__.py +3 -0
- forgegrit_cli/main.py +408 -0
- forgegrit_cli-1.0.0.dist-info/LICENSE +21 -0
- forgegrit_cli-1.0.0.dist-info/METADATA +64 -0
- forgegrit_cli-1.0.0.dist-info/RECORD +9 -0
- forgegrit_cli-1.0.0.dist-info/WHEEL +5 -0
- forgegrit_cli-1.0.0.dist-info/entry_points.txt +2 -0
- forgegrit_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
forgegrit_cli/main.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""ForgeGrit CLI — talk to the ForgeGrit API from a terminal.
|
|
2
|
+
|
|
3
|
+
Auth (pick one):
|
|
4
|
+
forgegrit login log in with your ForgeGrit email + password
|
|
5
|
+
forgegrit register create a new ForgeGrit account
|
|
6
|
+
forgegrit api KEY use an API key you already have
|
|
7
|
+
|
|
8
|
+
Then:
|
|
9
|
+
forgegrit chat interactive chat (streams replies)
|
|
10
|
+
forgegrit ask "..." one question, one answer
|
|
11
|
+
forgegrit models list models
|
|
12
|
+
forgegrit usage requests used today
|
|
13
|
+
forgegrit whoami show the saved login
|
|
14
|
+
forgegrit logout forget the saved key
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import getpass
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import stat
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
import requests
|
|
25
|
+
|
|
26
|
+
from . import __version__
|
|
27
|
+
|
|
28
|
+
DEFAULT_URL = "https://api-forgegrit.serveousercontent.com"
|
|
29
|
+
DEFAULT_MODEL = "claude-opus-5"
|
|
30
|
+
CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".forgegrit")
|
|
31
|
+
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
|
|
32
|
+
TIMEOUT = 300
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------- terminal colours
|
|
35
|
+
_COLOR = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _c(code, text):
|
|
39
|
+
return f"\033[{code}m{text}\033[0m" if _COLOR else text
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def orange(t): return _c("38;5;208", t)
|
|
43
|
+
def green(t): return _c("32", t)
|
|
44
|
+
def red(t): return _c("31", t)
|
|
45
|
+
def dim(t): return _c("2", t)
|
|
46
|
+
def bold(t): return _c("1", t)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def die(msg, code=1):
|
|
50
|
+
print(red("error: ") + msg, file=sys.stderr)
|
|
51
|
+
sys.exit(code)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------- config
|
|
55
|
+
def load_config():
|
|
56
|
+
try:
|
|
57
|
+
with open(CONFIG_FILE, encoding="utf-8") as f:
|
|
58
|
+
return json.load(f)
|
|
59
|
+
except (OSError, ValueError):
|
|
60
|
+
return {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def save_config(cfg):
|
|
64
|
+
os.makedirs(CONFIG_DIR, exist_ok=True)
|
|
65
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
66
|
+
json.dump(cfg, f, indent=2)
|
|
67
|
+
try: # the file holds an API key — keep it private
|
|
68
|
+
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR)
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def base_url(args, cfg):
|
|
74
|
+
return (getattr(args, "url", None) or os.environ.get("FORGEGRIT_URL")
|
|
75
|
+
or cfg.get("url") or DEFAULT_URL).rstrip("/")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def api_key(cfg):
|
|
79
|
+
key = os.environ.get("FORGEGRIT_API_KEY") or cfg.get("api_key")
|
|
80
|
+
if not key:
|
|
81
|
+
die("not logged in. Run `forgegrit login`, `forgegrit register`, "
|
|
82
|
+
"or `forgegrit api YOUR_KEY` first.")
|
|
83
|
+
return key
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def mask(key):
|
|
87
|
+
return key[:8] + "…" + key[-4:] if key and len(key) > 14 else "(none)"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------- HTTP
|
|
91
|
+
def request(method, url, **kw):
|
|
92
|
+
kw.setdefault("timeout", TIMEOUT)
|
|
93
|
+
try:
|
|
94
|
+
r = requests.request(method, url, **kw)
|
|
95
|
+
except requests.ConnectionError:
|
|
96
|
+
die(f"can't reach {url.split('/v1')[0].split('/auth')[0]}. "
|
|
97
|
+
"The server or its tunnel may be offline.")
|
|
98
|
+
except requests.Timeout:
|
|
99
|
+
die("the server took too long to answer.")
|
|
100
|
+
return r
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def body_of(r):
|
|
104
|
+
try:
|
|
105
|
+
return r.json()
|
|
106
|
+
except ValueError:
|
|
107
|
+
if r.status_code == 502 and not r.text.strip():
|
|
108
|
+
die("the tunnel returned an empty 502 — usually a brief hiccup. Try again.")
|
|
109
|
+
die(f"unexpected response (HTTP {r.status_code}): {r.text[:200]}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def explain(r, data, auth=False):
|
|
113
|
+
msg = data.get("error") or f"HTTP {r.status_code}"
|
|
114
|
+
if auth: # login/register: a 401 means bad credentials, not a bad key
|
|
115
|
+
die(msg + (f"\n {data['how']}" if data.get("how") else ""))
|
|
116
|
+
hint = {
|
|
117
|
+
401: "Your key was rejected. Log in again or run `forgegrit api NEW_KEY`.",
|
|
118
|
+
403: None,
|
|
119
|
+
429: "Daily limit reached. It resets at midnight.",
|
|
120
|
+
502: "The model backend is down. Try again shortly.",
|
|
121
|
+
}.get(r.status_code)
|
|
122
|
+
if data.get("how"):
|
|
123
|
+
hint = data["how"]
|
|
124
|
+
die(msg + (f"\n {hint}" if hint else ""))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def show_notice(data):
|
|
128
|
+
if data.get("notice"):
|
|
129
|
+
print(dim("note: " + data["notice"]))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def prompt_credentials():
|
|
133
|
+
email = input("Email: ").strip()
|
|
134
|
+
password = getpass.getpass("Password: ")
|
|
135
|
+
if not email or not password:
|
|
136
|
+
die("email and password are both required.")
|
|
137
|
+
return email, password
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------- auth commands
|
|
141
|
+
def cmd_login(args):
|
|
142
|
+
cfg = load_config()
|
|
143
|
+
url = base_url(args, cfg)
|
|
144
|
+
email, password = (args.email, args.password) if args.email and args.password \
|
|
145
|
+
else prompt_credentials()
|
|
146
|
+
payload = {"email": email, "password": password}
|
|
147
|
+
|
|
148
|
+
for _ in range(3):
|
|
149
|
+
r = request("POST", f"{url}/authenticate/email", json=payload)
|
|
150
|
+
data = body_of(r)
|
|
151
|
+
if r.ok:
|
|
152
|
+
break
|
|
153
|
+
if data.get("requires_2fa"):
|
|
154
|
+
if data.get("code_reused"):
|
|
155
|
+
print(dim("That code was already used — wait for the next one."))
|
|
156
|
+
payload["code"] = input("2FA code: ").strip()
|
|
157
|
+
continue
|
|
158
|
+
explain(r, data, auth=True)
|
|
159
|
+
else:
|
|
160
|
+
die("too many failed 2FA attempts.")
|
|
161
|
+
|
|
162
|
+
cfg.update(api_key=data["api_key"], email=email, url=url, method="login")
|
|
163
|
+
save_config(cfg)
|
|
164
|
+
print(green("✓ Logged in") + f" as {bold(email)}")
|
|
165
|
+
print(dim(f" key {mask(data['api_key'])} saved to {CONFIG_FILE}"))
|
|
166
|
+
show_notice(data)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cmd_register(args):
|
|
170
|
+
cfg = load_config()
|
|
171
|
+
url = base_url(args, cfg)
|
|
172
|
+
email, password = (args.email, args.password) if args.email and args.password \
|
|
173
|
+
else prompt_credentials()
|
|
174
|
+
if not (args.email and args.password):
|
|
175
|
+
if getpass.getpass("Confirm password: ") != password:
|
|
176
|
+
die("passwords don't match.")
|
|
177
|
+
|
|
178
|
+
r = request("POST", f"{url}/register", json={"email": email, "password": password})
|
|
179
|
+
data = body_of(r)
|
|
180
|
+
if not r.ok:
|
|
181
|
+
explain(r, data, auth=True)
|
|
182
|
+
print(green("✓ Account created") + f" for {bold(email)}")
|
|
183
|
+
show_notice(data)
|
|
184
|
+
|
|
185
|
+
# log straight in so the user can chat immediately
|
|
186
|
+
r = request("POST", f"{url}/authenticate/email",
|
|
187
|
+
json={"email": email, "password": password})
|
|
188
|
+
data = body_of(r)
|
|
189
|
+
if not r.ok:
|
|
190
|
+
explain(r, data)
|
|
191
|
+
cfg.update(api_key=data["api_key"], email=email, url=url, method="register")
|
|
192
|
+
save_config(cfg)
|
|
193
|
+
print(dim(f" key {mask(data['api_key'])} saved to {CONFIG_FILE}"))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def cmd_api(args):
|
|
197
|
+
cfg = load_config()
|
|
198
|
+
url = base_url(args, cfg)
|
|
199
|
+
key = args.key or getpass.getpass("API key: ").strip()
|
|
200
|
+
if not key:
|
|
201
|
+
die("no key given.")
|
|
202
|
+
r = request("GET", f"{url}/v1/models", headers={"Authorization": f"Bearer {key}"})
|
|
203
|
+
data = body_of(r)
|
|
204
|
+
if not r.ok:
|
|
205
|
+
explain(r, data)
|
|
206
|
+
cfg.update(api_key=key, url=url, method="api")
|
|
207
|
+
cfg.pop("email", None)
|
|
208
|
+
save_config(cfg)
|
|
209
|
+
print(green("✓ API key saved") + dim(f" ({mask(key)}, {len(data.get('data', []))} models available)"))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def cmd_logout(args):
|
|
213
|
+
cfg = load_config()
|
|
214
|
+
for k in ("api_key", "email", "method"):
|
|
215
|
+
cfg.pop(k, None)
|
|
216
|
+
save_config(cfg)
|
|
217
|
+
print("Logged out. Your key is still valid on the server — rotate it on the site if it leaked.")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def cmd_whoami(args):
|
|
221
|
+
cfg = load_config()
|
|
222
|
+
key = os.environ.get("FORGEGRIT_API_KEY") or cfg.get("api_key")
|
|
223
|
+
print(f"server : {base_url(args, cfg)}")
|
|
224
|
+
print(f"account: {cfg.get('email') or dim('(API key only)')}")
|
|
225
|
+
print(f"method : {cfg.get('method', '—')}")
|
|
226
|
+
print(f"key : {mask(key)}")
|
|
227
|
+
print(f"model : {cfg.get('model', DEFAULT_MODEL)}")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------- info commands
|
|
231
|
+
def cmd_models(args):
|
|
232
|
+
cfg = load_config()
|
|
233
|
+
r = request("GET", f"{base_url(args, cfg)}/v1/models",
|
|
234
|
+
headers={"Authorization": f"Bearer {api_key(cfg)}"})
|
|
235
|
+
data = body_of(r)
|
|
236
|
+
if not r.ok:
|
|
237
|
+
explain(r, data)
|
|
238
|
+
current = cfg.get("model", DEFAULT_MODEL)
|
|
239
|
+
for m in sorted(data.get("data", []), key=lambda m: m["id"]):
|
|
240
|
+
mark = green("●") if m["id"] == current else " "
|
|
241
|
+
print(f" {mark} {m['id']}")
|
|
242
|
+
print(dim(f"\n{len(data.get('data', []))} models · default: {current} "
|
|
243
|
+
"· change with `forgegrit model ID`"))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def cmd_model(args):
|
|
247
|
+
cfg = load_config()
|
|
248
|
+
cfg["model"] = args.id
|
|
249
|
+
save_config(cfg)
|
|
250
|
+
print(green("✓ Default model: ") + args.id)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def cmd_usage(args):
|
|
254
|
+
cfg = load_config()
|
|
255
|
+
r = request("GET", f"{base_url(args, cfg)}/v1/usage",
|
|
256
|
+
headers={"Authorization": f"Bearer {api_key(cfg)}"})
|
|
257
|
+
data = body_of(r)
|
|
258
|
+
if not r.ok:
|
|
259
|
+
explain(r, data)
|
|
260
|
+
used, limit = data.get("requests_today", 0), data.get("rpd_limit", 0)
|
|
261
|
+
print(f"{used:,} / {limit:,} requests today · {data.get('remaining', 0):,} left")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---------------------------------------------------------------- chat
|
|
265
|
+
def stream_reply(url, key, model, messages):
|
|
266
|
+
"""Send a chat request with streaming on; print tokens as they arrive.
|
|
267
|
+
Returns the full reply text."""
|
|
268
|
+
r = request("POST", f"{url}/v1/chat/completions",
|
|
269
|
+
headers={"Authorization": f"Bearer {key}"},
|
|
270
|
+
json={"model": model, "messages": messages, "stream": True},
|
|
271
|
+
stream=True)
|
|
272
|
+
if not r.ok:
|
|
273
|
+
explain(r, body_of(r))
|
|
274
|
+
|
|
275
|
+
out = []
|
|
276
|
+
for raw in r.iter_lines(decode_unicode=True):
|
|
277
|
+
if not raw or not raw.startswith("data:"):
|
|
278
|
+
continue
|
|
279
|
+
chunk = raw[5:].strip()
|
|
280
|
+
if chunk == "[DONE]":
|
|
281
|
+
break
|
|
282
|
+
try:
|
|
283
|
+
piece = json.loads(chunk)["choices"][0].get("delta", {}).get("content") or ""
|
|
284
|
+
except (ValueError, KeyError, IndexError):
|
|
285
|
+
continue
|
|
286
|
+
out.append(piece)
|
|
287
|
+
sys.stdout.write(piece)
|
|
288
|
+
sys.stdout.flush()
|
|
289
|
+
print()
|
|
290
|
+
return "".join(out)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def cmd_ask(args):
|
|
294
|
+
cfg = load_config()
|
|
295
|
+
prompt = " ".join(args.prompt) if args.prompt else sys.stdin.read()
|
|
296
|
+
if not prompt.strip():
|
|
297
|
+
die("nothing to ask.")
|
|
298
|
+
msgs = []
|
|
299
|
+
if args.system:
|
|
300
|
+
msgs.append({"role": "system", "content": args.system})
|
|
301
|
+
msgs.append({"role": "user", "content": prompt})
|
|
302
|
+
stream_reply(base_url(args, cfg), api_key(cfg),
|
|
303
|
+
args.model or cfg.get("model", DEFAULT_MODEL), msgs)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
CHAT_HELP = """commands:
|
|
307
|
+
/model ID switch model
|
|
308
|
+
/system TEXT set a system prompt (clears history)
|
|
309
|
+
/clear start a new conversation
|
|
310
|
+
/exit quit (or Ctrl+D)"""
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def cmd_chat(args):
|
|
314
|
+
cfg = load_config()
|
|
315
|
+
url, key = base_url(args, cfg), api_key(cfg)
|
|
316
|
+
model = args.model or cfg.get("model", DEFAULT_MODEL)
|
|
317
|
+
system = args.system
|
|
318
|
+
history = []
|
|
319
|
+
|
|
320
|
+
print(orange("⚡ ForgeGrit") + dim(f" · {model} · /help for commands"))
|
|
321
|
+
while True:
|
|
322
|
+
try:
|
|
323
|
+
line = input(bold("you › "))
|
|
324
|
+
except (EOFError, KeyboardInterrupt):
|
|
325
|
+
print()
|
|
326
|
+
return
|
|
327
|
+
text = line.strip()
|
|
328
|
+
if not text:
|
|
329
|
+
continue
|
|
330
|
+
if text in ("/exit", "/quit"):
|
|
331
|
+
return
|
|
332
|
+
if text == "/help":
|
|
333
|
+
print(dim(CHAT_HELP)); continue
|
|
334
|
+
if text == "/clear":
|
|
335
|
+
history = []; print(dim("(new conversation)")); continue
|
|
336
|
+
if text.startswith("/model"):
|
|
337
|
+
parts = text.split(maxsplit=1)
|
|
338
|
+
if len(parts) == 2:
|
|
339
|
+
model = parts[1]; print(dim(f"(model: {model})"))
|
|
340
|
+
else:
|
|
341
|
+
print(dim(f"(model: {model})"))
|
|
342
|
+
continue
|
|
343
|
+
if text.startswith("/system"):
|
|
344
|
+
system = text[7:].strip() or None
|
|
345
|
+
history = []
|
|
346
|
+
print(dim("(system prompt set, history cleared)" if system else "(system prompt cleared)"))
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
history.append({"role": "user", "content": text})
|
|
350
|
+
msgs = ([{"role": "system", "content": system}] if system else []) + history
|
|
351
|
+
sys.stdout.write(orange(f"{model} › "))
|
|
352
|
+
sys.stdout.flush()
|
|
353
|
+
try:
|
|
354
|
+
reply = stream_reply(url, key, model, msgs)
|
|
355
|
+
except KeyboardInterrupt:
|
|
356
|
+
print(dim("\n(stopped)"))
|
|
357
|
+
history.pop()
|
|
358
|
+
continue
|
|
359
|
+
history.append({"role": "assistant", "content": reply})
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ---------------------------------------------------------------- entry point
|
|
363
|
+
def main(argv=None):
|
|
364
|
+
p = argparse.ArgumentParser(
|
|
365
|
+
prog="forgegrit",
|
|
366
|
+
description="ForgeGrit CLI — free frontier-model API from your terminal.",
|
|
367
|
+
epilog="Get an account at https://trey16885.github.io/forgegrit")
|
|
368
|
+
p.add_argument("--url", help=f"API base URL (default {DEFAULT_URL}, or $FORGEGRIT_URL)")
|
|
369
|
+
p.add_argument("-V", "--version", action="version", version=f"forgegrit-cli {__version__}")
|
|
370
|
+
sub = p.add_subparsers(dest="cmd", metavar="COMMAND")
|
|
371
|
+
|
|
372
|
+
s = sub.add_parser("login", help="log in with your ForgeGrit email and password")
|
|
373
|
+
s.add_argument("--email"); s.add_argument("--password")
|
|
374
|
+
s.set_defaults(func=cmd_login)
|
|
375
|
+
|
|
376
|
+
s = sub.add_parser("register", help="create a new ForgeGrit account")
|
|
377
|
+
s.add_argument("--email"); s.add_argument("--password")
|
|
378
|
+
s.set_defaults(func=cmd_register)
|
|
379
|
+
|
|
380
|
+
s = sub.add_parser("api", help="use an existing API key")
|
|
381
|
+
s.add_argument("key", nargs="?", help="your fgk_… key (prompted if omitted)")
|
|
382
|
+
s.set_defaults(func=cmd_api)
|
|
383
|
+
|
|
384
|
+
s = sub.add_parser("chat", help="interactive chat")
|
|
385
|
+
s.add_argument("-m", "--model"); s.add_argument("-s", "--system")
|
|
386
|
+
s.set_defaults(func=cmd_chat)
|
|
387
|
+
|
|
388
|
+
s = sub.add_parser("ask", help="ask one question (or pipe text in)")
|
|
389
|
+
s.add_argument("prompt", nargs="*")
|
|
390
|
+
s.add_argument("-m", "--model"); s.add_argument("-s", "--system")
|
|
391
|
+
s.set_defaults(func=cmd_ask)
|
|
392
|
+
|
|
393
|
+
s = sub.add_parser("models", help="list available models"); s.set_defaults(func=cmd_models)
|
|
394
|
+
s = sub.add_parser("model", help="set your default model")
|
|
395
|
+
s.add_argument("id"); s.set_defaults(func=cmd_model)
|
|
396
|
+
s = sub.add_parser("usage", help="requests used today"); s.set_defaults(func=cmd_usage)
|
|
397
|
+
s = sub.add_parser("whoami", help="show the saved login"); s.set_defaults(func=cmd_whoami)
|
|
398
|
+
s = sub.add_parser("logout", help="forget the saved key"); s.set_defaults(func=cmd_logout)
|
|
399
|
+
|
|
400
|
+
args = p.parse_args(argv)
|
|
401
|
+
if not getattr(args, "func", None):
|
|
402
|
+
p.print_help()
|
|
403
|
+
return
|
|
404
|
+
args.func(args)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 treyleo16
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: forgegrit-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Command-line client for the ForgeGrit API: log in, register, and chat with every ForgeGrit model from your terminal.
|
|
5
|
+
Author: treyleo16
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://trey16885.github.io/forgegrit
|
|
8
|
+
Keywords: forgegrit,llm,ai,cli,chat,api
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Requires-Dist: requests>=2.20
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
|
|
18
|
+
# ForgeGrit CLI
|
|
19
|
+
|
|
20
|
+
Chat with every ForgeGrit model from your terminal. Free: no credit card, no trial credits.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install forgegrit-cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Sign in — pick one
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
forgegrit login # use your ForgeGrit email + password (2FA supported)
|
|
30
|
+
forgegrit register # create a new ForgeGrit account
|
|
31
|
+
forgegrit api fgk_yourkey # use an API key you already have
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Your key is saved to `~/.forgegrit/config.json`, readable only by you.
|
|
35
|
+
|
|
36
|
+
## Use it
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
forgegrit chat # interactive chat, replies stream in
|
|
40
|
+
forgegrit chat -m gpt-6-astra # pick a model
|
|
41
|
+
forgegrit ask "explain recursion" # one-shot question
|
|
42
|
+
cat notes.txt | forgegrit ask # pipe text in
|
|
43
|
+
forgegrit models # list models
|
|
44
|
+
forgegrit model claude-sonnet-5-5 # set your default model
|
|
45
|
+
forgegrit usage # requests used today
|
|
46
|
+
forgegrit whoami # show the saved login
|
|
47
|
+
forgegrit logout # forget the saved key
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Inside `chat`: `/model ID`, `/system TEXT`, `/clear`, `/help`, `/exit`.
|
|
51
|
+
|
|
52
|
+
## Settings
|
|
53
|
+
|
|
54
|
+
| Variable | Does |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `FORGEGRIT_API_KEY` | Use this key instead of the saved one |
|
|
57
|
+
| `FORGEGRIT_URL` | Point at a different ForgeGrit server |
|
|
58
|
+
| `NO_COLOR` | Turn off colours |
|
|
59
|
+
|
|
60
|
+
## Note
|
|
61
|
+
|
|
62
|
+
Chats sent through ForgeGrit are logged by the server with your account, the time, your IP address, the model, your messages and the reply.
|
|
63
|
+
|
|
64
|
+
Get an account and read more at **https://trey16885.github.io/forgegrit**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
forgegrit_cli/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
|
|
2
|
+
forgegrit_cli/__main__.py,sha256=vBQ82334kX06ImDbFlPFgiBRiLIinwNk3z8Khs6hd74,31
|
|
3
|
+
forgegrit_cli/main.py,sha256=1wdLD6dGiPN4neEQByHHxwR0CO_M_AA44PKwFgzOd5I,14032
|
|
4
|
+
forgegrit_cli-1.0.0.dist-info/METADATA,sha256=tdKzjavky22s2OvxGxC01AuzprYstyqqBHBfiUzvs1U,2130
|
|
5
|
+
forgegrit_cli-1.0.0.dist-info/WHEEL,sha256=yp6aTco6OR3LM3n7UjmqeZ-4FXtQoVHarWCrFD1CyGU,87
|
|
6
|
+
forgegrit_cli-1.0.0.dist-info/entry_points.txt,sha256=jJyJqMYJ09tiee4e4LSgZK6oxUQDE7Mi8zvzMncJOqI,54
|
|
7
|
+
forgegrit_cli-1.0.0.dist-info/top_level.txt,sha256=9k0302y9bxuLzWIMOpgF0wxhq2ZEMp0eiQBVaVTusjM,14
|
|
8
|
+
forgegrit_cli-1.0.0.dist-info/LICENSE,sha256=bhsq1yPHxHBjccjl5zSTiNYwJN-har17OCw6DwKzJHw,1066
|
|
9
|
+
forgegrit_cli-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
forgegrit_cli
|