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.
Files changed (3) hide show
  1. package/ai_cli/updater.py +48 -2
  2. package/chat.py +136 -54
  3. 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
- LOGO = """
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(LOGO),
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
- bar.append(f" {model}", style="bright_cyan")
891
- bar.append(" | ", style="dim")
892
- bar.append(f"{session_stats['messages']} msgs", style="dim")
893
- bar.append(" | ", style="dim")
894
- bar.append(f"{tok} tokens", style="dim")
895
- bar.append(" | ", style="dim")
896
- bar.append(f"{uptime}", style="dim")
897
- bar.append(" | ", style="dim")
898
- bar.append(now, style="dim")
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
- status.append(" ◈ ", style="bright_cyan")
929
- status.append(f"{model}", style="bold bright_cyan")
930
- status.append("", style="dim")
931
- status.append(f" {server_type}", style="green" if model_count > 0 else "red")
932
- status.append(" │ ", style="dim")
933
- status.append(f" {model_count} models", style="dim")
934
- status.append("", style="dim")
935
- status.append(f"◇ {mem_count} memories", style="dim")
936
- if streak > 1:
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" {streak} sessions", style="dim")
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
- console.print(Panel(
943
- _build_suggestions(),
944
- title="[dim]Suggestions (type a number)[/]",
945
- title_align="left",
946
- border_style="bright_black",
947
- padding=(1, 2),
948
- width=w,
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(Align.center(Text(f"Tip: {tip}", style="dim italic")), width=w)
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
- for i, s in enumerate(SUGGESTIONS, 1):
960
- text.append(f" [{i}]", style="bright_cyan bold")
961
- text.append(f" {s}\n", style="white")
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=(0, 2),
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=(0, 2),
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
- console.print(Panel(
992
- Text(text, style="dim italic"),
993
- border_style="bright_black",
994
- padding=(0, 1),
995
- width=tw(),
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 — use the global blocklist
2051
+ # Safety check
1982
2052
  safe, blocked = is_shell_safe(cmd_text)
1983
2053
  if not safe:
1984
- print_err(f"Blocked: dangerous command detected ({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
- result = subprocess.run(
1995
- cmd_text, shell=True, capture_output=True, text=True, timeout=30,
1996
- cwd=str(Path.home()),
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegpt-ai",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Local AI Assistant Hub — 80+ commands, 29 tools, 8 agents, training, security",
5
5
  "author": "ArukuX",
6
6
  "license": "MIT",