codegpt-ai 1.1.0 → 1.3.0
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.
- package/ai_cli/updater.py +48 -2
- package/chat.py +136 -54
- package/package.json +1 -1
package/ai_cli/updater.py
CHANGED
|
@@ -148,18 +148,64 @@ def force_update():
|
|
|
148
148
|
return
|
|
149
149
|
|
|
150
150
|
asset = exe_assets[0]
|
|
151
|
+
|
|
152
|
+
# Find checksum file in release assets
|
|
153
|
+
sha_assets = [a for a in release.get("assets", []) if a["name"].endswith(".sha256")]
|
|
154
|
+
expected_hash = None
|
|
155
|
+
if sha_assets:
|
|
156
|
+
try:
|
|
157
|
+
sha_resp = requests.get(sha_assets[0]["browser_download_url"], timeout=10)
|
|
158
|
+
# Parse certutil output: second line is the hash
|
|
159
|
+
lines = sha_resp.text.strip().splitlines()
|
|
160
|
+
for line in lines:
|
|
161
|
+
line = line.strip().replace(" ", "")
|
|
162
|
+
if len(line) == 64 and all(c in "0123456789abcdef" for c in line.lower()):
|
|
163
|
+
expected_hash = line.lower()
|
|
164
|
+
break
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
|
|
151
168
|
console.print(f" Downloading {asset['name']} ({latest_tag})...")
|
|
169
|
+
if expected_hash:
|
|
170
|
+
console.print(f" Expected SHA256: {expected_hash[:16]}...")
|
|
171
|
+
else:
|
|
172
|
+
console.print("[yellow] WARNING: No checksum file found. Cannot verify integrity.[/]")
|
|
152
173
|
|
|
153
174
|
try:
|
|
154
175
|
resp = requests.get(asset["browser_download_url"], stream=True, timeout=60)
|
|
155
176
|
resp.raise_for_status()
|
|
156
177
|
|
|
157
|
-
# Download to temp file
|
|
178
|
+
# Download to temp file and compute hash
|
|
179
|
+
import hashlib as _hashlib
|
|
180
|
+
sha256 = _hashlib.sha256()
|
|
158
181
|
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".exe")
|
|
159
182
|
for chunk in resp.iter_content(chunk_size=8192):
|
|
160
183
|
tmp.write(chunk)
|
|
184
|
+
sha256.update(chunk)
|
|
161
185
|
tmp.close()
|
|
162
186
|
|
|
187
|
+
actual_hash = sha256.hexdigest().lower()
|
|
188
|
+
console.print(f" Actual SHA256: {actual_hash[:16]}...")
|
|
189
|
+
|
|
190
|
+
# Verify checksum if available
|
|
191
|
+
if expected_hash and actual_hash != expected_hash:
|
|
192
|
+
console.print(Panel(
|
|
193
|
+
Text(
|
|
194
|
+
"CHECKSUM MISMATCH — download may be tampered with.\n"
|
|
195
|
+
f"Expected: {expected_hash}\n"
|
|
196
|
+
f"Got: {actual_hash}\n\n"
|
|
197
|
+
"Update aborted for your safety.",
|
|
198
|
+
style="bold red"
|
|
199
|
+
),
|
|
200
|
+
title="[bold red]SECURITY ALERT[/]",
|
|
201
|
+
border_style="red",
|
|
202
|
+
))
|
|
203
|
+
os.unlink(tmp.name)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if expected_hash:
|
|
207
|
+
console.print("[green] Checksum verified.[/]")
|
|
208
|
+
|
|
163
209
|
if _is_frozen():
|
|
164
210
|
# Replace the running exe
|
|
165
211
|
current_exe = sys.executable
|
|
@@ -172,7 +218,7 @@ def force_update():
|
|
|
172
218
|
shutil.move(tmp.name, current_exe)
|
|
173
219
|
|
|
174
220
|
console.print(Panel(
|
|
175
|
-
Text(f"Updated: v{current} -> {latest_tag}\nRestart to use the new version.", style="green"),
|
|
221
|
+
Text(f"Updated: v{current} -> {latest_tag}\nChecksum: {actual_hash[:16]}...\nRestart to use the new version.", style="green"),
|
|
176
222
|
border_style="green",
|
|
177
223
|
))
|
|
178
224
|
else:
|
package/chat.py
CHANGED
|
@@ -589,9 +589,29 @@ def audit_log(action, detail=""):
|
|
|
589
589
|
def is_shell_safe(cmd_text):
|
|
590
590
|
"""Check if a shell command is safe to run."""
|
|
591
591
|
cmd_lower = cmd_text.lower().strip()
|
|
592
|
+
|
|
593
|
+
# Blocklist check
|
|
592
594
|
for blocked in SHELL_BLOCKLIST:
|
|
593
595
|
if blocked in cmd_lower:
|
|
594
596
|
return False, blocked
|
|
597
|
+
|
|
598
|
+
# Block shell injection patterns
|
|
599
|
+
injection_patterns = [
|
|
600
|
+
r'[;&|`]', # Command chaining/injection
|
|
601
|
+
r'\$\(', # Command substitution
|
|
602
|
+
r'>\s*/dev/', # Device writes
|
|
603
|
+
r'\\x[0-9a-f]', # Hex escapes
|
|
604
|
+
r'\\u[0-9a-f]', # Unicode escapes
|
|
605
|
+
r'\brm\b.*-[rR]', # rm with recursive flag (any form)
|
|
606
|
+
]
|
|
607
|
+
for pattern in injection_patterns:
|
|
608
|
+
if re.search(pattern, cmd_text):
|
|
609
|
+
return False, f"blocked pattern: {pattern}"
|
|
610
|
+
|
|
611
|
+
# Max command length
|
|
612
|
+
if len(cmd_text) > 500:
|
|
613
|
+
return False, "command too long (500 char limit)"
|
|
614
|
+
|
|
595
615
|
return True, ""
|
|
596
616
|
|
|
597
617
|
|
|
@@ -783,8 +803,8 @@ def build_sidebar():
|
|
|
783
803
|
|
|
784
804
|
|
|
785
805
|
def print_with_sidebar(panel):
|
|
786
|
-
"""Print a panel with sidebar if enabled."""
|
|
787
|
-
if not sidebar_enabled or console.width < 80:
|
|
806
|
+
"""Print a panel with sidebar if enabled. Auto-disabled on small screens."""
|
|
807
|
+
if not sidebar_enabled or is_compact() or console.width < 80:
|
|
788
808
|
console.print(panel)
|
|
789
809
|
return
|
|
790
810
|
|
|
@@ -812,11 +832,16 @@ def tw():
|
|
|
812
832
|
return min(console.width, 100)
|
|
813
833
|
|
|
814
834
|
|
|
835
|
+
def is_compact():
|
|
836
|
+
"""Check if terminal is small (Termux, narrow window)."""
|
|
837
|
+
return console.width < 60
|
|
838
|
+
|
|
839
|
+
|
|
815
840
|
def clear_screen():
|
|
816
841
|
os.system("cls" if os.name == "nt" else "clear")
|
|
817
842
|
|
|
818
843
|
|
|
819
|
-
|
|
844
|
+
LOGO_FULL = """
|
|
820
845
|
[bright_cyan] ██████╗ ██████╗ ██████╗ ███████╗[/][bold white] ██████╗ ██████╗ ████████╗[/]
|
|
821
846
|
[bright_cyan] ██╔════╝██╔═══██╗██╔══██╗██╔════╝[/][bold white] ██╔════╝ ██╔══██╗╚══██╔══╝[/]
|
|
822
847
|
[bright_cyan] ██║ ██║ ██║██║ ██║█████╗ [/][bold white] ██║ ███╗██████╔╝ ██║ [/]
|
|
@@ -825,6 +850,14 @@ LOGO = """
|
|
|
825
850
|
[bright_cyan] ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝[/][bold white] ╚═════╝ ╚═╝ ╚═╝ [/]
|
|
826
851
|
[dim] Your Local AI Assistant — Powered by Ollama[/]"""
|
|
827
852
|
|
|
853
|
+
LOGO_COMPACT = """
|
|
854
|
+
[bold bright_cyan]╔═══════════════════════╗[/]
|
|
855
|
+
[bold bright_cyan]║[/] [bold white]C O D E[/][bold bright_cyan] G P T[/] [bold bright_cyan]║[/]
|
|
856
|
+
[bold bright_cyan]╚═══════════════════════╝[/]
|
|
857
|
+
[dim] Local AI · Ollama[/]"""
|
|
858
|
+
|
|
859
|
+
LOGO = LOGO_FULL
|
|
860
|
+
|
|
828
861
|
# --- Command Aliases ---
|
|
829
862
|
ALIASES = {
|
|
830
863
|
"/q": "/quit", "/x": "/quit", "/exit": "/quit",
|
|
@@ -872,30 +905,39 @@ HISTORY_FILE = Path.home() / ".codegpt" / "input_history"
|
|
|
872
905
|
def print_header(model):
|
|
873
906
|
clear_screen()
|
|
874
907
|
w = tw()
|
|
908
|
+
compact = is_compact()
|
|
875
909
|
console.print()
|
|
910
|
+
|
|
911
|
+
# Responsive logo
|
|
912
|
+
logo = LOGO_COMPACT if compact else LOGO_FULL
|
|
876
913
|
console.print(Panel(
|
|
877
|
-
Text.from_markup(
|
|
914
|
+
Text.from_markup(logo),
|
|
878
915
|
border_style="bright_cyan",
|
|
879
|
-
padding=(1, 2),
|
|
916
|
+
padding=(0 if compact else 1, 1 if compact else 2),
|
|
880
917
|
width=w,
|
|
881
918
|
))
|
|
882
919
|
|
|
883
|
-
# Status bar
|
|
920
|
+
# Status bar — compact version for small screens
|
|
884
921
|
now = datetime.now().strftime("%H:%M")
|
|
885
922
|
elapsed = int(time.time() - session_stats["start"])
|
|
886
923
|
uptime = f"{elapsed // 60}m"
|
|
887
924
|
tok = session_stats["tokens_out"]
|
|
888
925
|
|
|
889
926
|
bar = Text()
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
927
|
+
if compact:
|
|
928
|
+
bar.append(f" {model}", style="bright_cyan")
|
|
929
|
+
bar.append(f" {session_stats['messages']}msg", style="dim")
|
|
930
|
+
bar.append(f" {now}", style="dim")
|
|
931
|
+
else:
|
|
932
|
+
bar.append(f" {model}", style="bright_cyan")
|
|
933
|
+
bar.append(" | ", style="dim")
|
|
934
|
+
bar.append(f"{session_stats['messages']} msgs", style="dim")
|
|
935
|
+
bar.append(" | ", style="dim")
|
|
936
|
+
bar.append(f"{tok} tokens", style="dim")
|
|
937
|
+
bar.append(" | ", style="dim")
|
|
938
|
+
bar.append(f"{uptime}", style="dim")
|
|
939
|
+
bar.append(" | ", style="dim")
|
|
940
|
+
bar.append(now, style="dim")
|
|
899
941
|
|
|
900
942
|
console.print(Panel(bar, border_style="dim", padding=0, width=w))
|
|
901
943
|
console.print()
|
|
@@ -914,6 +956,8 @@ def print_welcome(model, available_models):
|
|
|
914
956
|
else:
|
|
915
957
|
greeting = "Good evening"
|
|
916
958
|
|
|
959
|
+
compact = is_compact()
|
|
960
|
+
|
|
917
961
|
console.print(Align.center(Text(f"\n{greeting}.\n", style="bold white")), width=w)
|
|
918
962
|
|
|
919
963
|
# Connection status bar
|
|
@@ -925,75 +969,101 @@ def print_welcome(model, available_models):
|
|
|
925
969
|
streak = profile.get("total_sessions", 0)
|
|
926
970
|
|
|
927
971
|
status = Text()
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
972
|
+
if compact:
|
|
973
|
+
status.append(f" {model}", style="bold bright_cyan")
|
|
974
|
+
status.append(f" {server_type}", style="green" if model_count > 0 else "red")
|
|
975
|
+
status.append(f" {model_count}m", style="dim")
|
|
976
|
+
else:
|
|
977
|
+
status.append(" ◈ ", style="bright_cyan")
|
|
978
|
+
status.append(f"{model}", style="bold bright_cyan")
|
|
979
|
+
status.append(" │ ", style="dim")
|
|
980
|
+
status.append(f"◇ {server_type}", style="green" if model_count > 0 else "red")
|
|
937
981
|
status.append(" │ ", style="dim")
|
|
938
|
-
status.append(f"
|
|
982
|
+
status.append(f"△ {model_count} models", style="dim")
|
|
983
|
+
status.append(" │ ", style="dim")
|
|
984
|
+
status.append(f"◇ {mem_count} memories", style="dim")
|
|
985
|
+
if streak > 1:
|
|
986
|
+
status.append(" │ ", style="dim")
|
|
987
|
+
status.append(f"▸ {streak} sessions", style="dim")
|
|
939
988
|
console.print(Panel(status, border_style="bright_black", padding=0, width=w))
|
|
940
989
|
|
|
941
|
-
# Suggestion chips
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
990
|
+
# Suggestion chips — fewer on compact
|
|
991
|
+
if compact:
|
|
992
|
+
console.print(Panel(
|
|
993
|
+
_build_suggestions(max_items=3),
|
|
994
|
+
title="[dim]Try[/]",
|
|
995
|
+
title_align="left",
|
|
996
|
+
border_style="bright_black",
|
|
997
|
+
padding=(0, 1),
|
|
998
|
+
width=w,
|
|
999
|
+
))
|
|
1000
|
+
else:
|
|
1001
|
+
console.print(Panel(
|
|
1002
|
+
_build_suggestions(),
|
|
1003
|
+
title="[dim]Suggestions (type a number)[/]",
|
|
1004
|
+
title_align="left",
|
|
1005
|
+
border_style="bright_black",
|
|
1006
|
+
padding=(1, 2),
|
|
1007
|
+
width=w,
|
|
1008
|
+
))
|
|
950
1009
|
|
|
951
1010
|
# Tip of the day
|
|
952
1011
|
tip = random.choice(TIPS)
|
|
953
|
-
console.print(
|
|
1012
|
+
console.print(Text(f" Tip: {tip}", style="dim italic"))
|
|
954
1013
|
console.print()
|
|
955
1014
|
|
|
956
1015
|
|
|
957
|
-
def _build_suggestions():
|
|
1016
|
+
def _build_suggestions(max_items=None):
|
|
958
1017
|
text = Text()
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1018
|
+
items = SUGGESTIONS[:max_items] if max_items else SUGGESTIONS
|
|
1019
|
+
for i, s in enumerate(items, 1):
|
|
1020
|
+
if is_compact():
|
|
1021
|
+
text.append(f" {i}.", style="bright_cyan bold")
|
|
1022
|
+
text.append(f" {s[:30]}\n", style="white")
|
|
1023
|
+
else:
|
|
1024
|
+
text.append(f" [{i}]", style="bright_cyan bold")
|
|
1025
|
+
text.append(f" {s}\n", style="white")
|
|
962
1026
|
return text
|
|
963
1027
|
|
|
964
1028
|
|
|
965
1029
|
def print_user_msg(text):
|
|
1030
|
+
pad = (0, 1) if is_compact() else (0, 2)
|
|
966
1031
|
console.print(Panel(
|
|
967
1032
|
Text(text, style="white"),
|
|
968
1033
|
title="[bold bright_cyan]You[/]",
|
|
969
1034
|
title_align="left",
|
|
970
1035
|
border_style="bright_cyan",
|
|
971
|
-
padding=
|
|
1036
|
+
padding=pad,
|
|
972
1037
|
width=tw(),
|
|
973
1038
|
))
|
|
974
1039
|
|
|
975
1040
|
|
|
976
1041
|
def print_ai_msg(text, stats=""):
|
|
1042
|
+
pad = (0, 1) if is_compact() else (0, 2)
|
|
1043
|
+
compact = is_compact()
|
|
977
1044
|
panel = Panel(
|
|
978
1045
|
Markdown(text),
|
|
979
1046
|
title="[bold bright_green]AI[/]",
|
|
980
1047
|
title_align="left",
|
|
981
1048
|
border_style="bright_green",
|
|
982
|
-
subtitle=stats,
|
|
1049
|
+
subtitle=stats if not compact else "",
|
|
983
1050
|
subtitle_align="right",
|
|
984
|
-
padding=
|
|
1051
|
+
padding=pad,
|
|
985
1052
|
width=tw(),
|
|
986
1053
|
)
|
|
987
1054
|
print_with_sidebar(panel)
|
|
988
1055
|
|
|
989
1056
|
|
|
990
1057
|
def print_sys(text):
|
|
991
|
-
|
|
992
|
-
Text(text, style="dim italic")
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1058
|
+
if is_compact():
|
|
1059
|
+
console.print(Text(f" {text}", style="dim italic"))
|
|
1060
|
+
else:
|
|
1061
|
+
console.print(Panel(
|
|
1062
|
+
Text(text, style="dim italic"),
|
|
1063
|
+
border_style="bright_black",
|
|
1064
|
+
padding=(0, 1),
|
|
1065
|
+
width=tw(),
|
|
1066
|
+
))
|
|
997
1067
|
|
|
998
1068
|
|
|
999
1069
|
def print_err(text):
|
|
@@ -1978,10 +2048,11 @@ def run_shell(cmd_text):
|
|
|
1978
2048
|
print_sys("Usage: /shell <command>\nExample: /shell dir")
|
|
1979
2049
|
return
|
|
1980
2050
|
|
|
1981
|
-
# Safety check
|
|
2051
|
+
# Safety check
|
|
1982
2052
|
safe, blocked = is_shell_safe(cmd_text)
|
|
1983
2053
|
if not safe:
|
|
1984
|
-
print_err(f"Blocked:
|
|
2054
|
+
print_err(f"Blocked: {blocked}")
|
|
2055
|
+
audit_log("SHELL_BLOCKED", f"{blocked}: {cmd_text[:80]}")
|
|
1985
2056
|
return
|
|
1986
2057
|
|
|
1987
2058
|
console.print(Panel(
|
|
@@ -1991,10 +2062,19 @@ def run_shell(cmd_text):
|
|
|
1991
2062
|
))
|
|
1992
2063
|
|
|
1993
2064
|
try:
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2065
|
+
# Use shlex.split for safer argument parsing on non-Windows
|
|
2066
|
+
if os.name != "nt":
|
|
2067
|
+
import shlex
|
|
2068
|
+
args = shlex.split(cmd_text)
|
|
2069
|
+
result = subprocess.run(
|
|
2070
|
+
args, capture_output=True, text=True, timeout=30,
|
|
2071
|
+
cwd=str(Path.home()),
|
|
2072
|
+
)
|
|
2073
|
+
else:
|
|
2074
|
+
result = subprocess.run(
|
|
2075
|
+
cmd_text, shell=True, capture_output=True, text=True, timeout=30,
|
|
2076
|
+
cwd=str(Path.home()),
|
|
2077
|
+
)
|
|
1998
2078
|
output = ""
|
|
1999
2079
|
if result.stdout:
|
|
2000
2080
|
output += result.stdout
|
|
@@ -3904,6 +3984,8 @@ def _bottom_toolbar():
|
|
|
3904
3984
|
mins = elapsed // 60
|
|
3905
3985
|
msgs = session_stats["messages"]
|
|
3906
3986
|
tok = session_stats["tokens_out"]
|
|
3987
|
+
if is_compact():
|
|
3988
|
+
return [("class:bottom-toolbar", f" {msgs}msg {tok}tok {mins}m │ / cmds ")]
|
|
3907
3989
|
return [("class:bottom-toolbar",
|
|
3908
3990
|
f" {msgs} msgs │ {tok} tok │ {mins}m │ / for commands │ Ctrl+C to exit ")]
|
|
3909
3991
|
|