agent-code 1.0.4__tar.gz → 1.0.5__tar.gz

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 (30) hide show
  1. agent_code-1.0.5/CHANGELOG.md +69 -0
  2. {agent_code-1.0.4 → agent_code-1.0.5}/PKG-INFO +1 -1
  3. {agent_code-1.0.4 → agent_code-1.0.5}/pyproject.toml +1 -1
  4. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/__init__.py +1 -1
  5. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/agent.py +198 -48
  6. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/cli.py +18 -5
  7. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/paths.py +32 -4
  8. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/PKG-INFO +1 -1
  9. agent_code-1.0.4/CHANGELOG.md +0 -52
  10. {agent_code-1.0.4 → agent_code-1.0.5}/LICENSE +0 -0
  11. {agent_code-1.0.4 → agent_code-1.0.5}/LICENSE-GPL +0 -0
  12. {agent_code-1.0.4 → agent_code-1.0.5}/MANIFEST.in +0 -0
  13. {agent_code-1.0.4 → agent_code-1.0.5}/README.md +0 -0
  14. {agent_code-1.0.4 → agent_code-1.0.5}/setup.cfg +0 -0
  15. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/__main__.py +0 -0
  16. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/browser/skill.md +0 -0
  17. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/browser/skill.py +0 -0
  18. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/filesystem/skill.md +0 -0
  19. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/filesystem/skill.py +0 -0
  20. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/public_api/skill.md +0 -0
  21. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/public_api/skill.py +0 -0
  22. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/temp_mail/skill.md +0 -0
  23. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/temp_mail/skill.py +0 -0
  24. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/loader.py +0 -0
  25. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/skill_manager.py +0 -0
  26. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/SOURCES.txt +0 -0
  27. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/dependency_links.txt +0 -0
  28. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/entry_points.txt +0 -0
  29. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/requires.txt +0 -0
  30. {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/top_level.txt +0 -0
@@ -0,0 +1,69 @@
1
+ # Changelog
2
+
3
+ ## [1.0.5] - 2026-10-09
4
+
5
+ ### Added
6
+ - Global folder mode: `agent-code run --global` (or
7
+ `AGENT_CODE_GLOBAL=1`, or `"use_global_paths": true` in config)
8
+ uses ~/.agent-code for skills, workspace and chats instead of the
9
+ current working directory.
10
+ - High-risk shell denylist — every command is now allowed except
11
+ shutdown / reboot / disk / bootloader / privilege-escalation /
12
+ account / destructive-delete patterns. Toggle strict mode with
13
+ `"permissive_shell": false` in agent_config.json.
14
+ - Force stop: pressing Stop a second time abandons the running turn
15
+ and re-enables the UI immediately.
16
+ - Paste guard: entry truncates pastes > 20 000 chars.
17
+ - Log guard: lines > 4 000 chars are truncated; widget trimmed to
18
+ the last 2 500 lines so long sessions stay responsive.
19
+ - LLM timeout: 120 s per request (was infinite).
20
+
21
+ ### Fixed
22
+ - TodoList.write() rejected `todos=` because the signature only
23
+ accepted `items`. It now accepts `todos`, `items` and `tasks`
24
+ aliases, normalizes status synonyms (done → completed,
25
+ active → in_progress), and reports malformed items instead of
26
+ silently dropping them.
27
+ - tool_shell_run: pipes, redirects, `&&` and `;` are now supported
28
+ in permissive mode.
29
+
30
+ ## [1.0.4] - 2026-10-09
31
+
32
+ ### Fixed
33
+ - agent.py: make_client passed a callable where the OpenAI SDK
34
+ expected a string; multi-key rotation now works via a property
35
+ override on a subclass instead of a lambda.
36
+ - agent.py: _safe_path used str.startswith() for the workspace jail,
37
+ which let a sibling directory named "workspace-foo" escape. Now
38
+ uses Path.relative_to().
39
+ - agent.py: removed duplicate load_skills / discover_skills; the
40
+ loader is now imported from loader.py (single source of truth).
41
+ - agent.py: removed the six browser_* tools from CORE_SCHEMAS and
42
+ all_callables — the browser skill owns them now.
43
+ - agent.py: Skills dialog called self.app._save_disabled_skills(),
44
+ which did not exist. Added the method.
45
+ - agent.py: todo_write schema now describes items (content + enum
46
+ status), so LLMs reliably produce valid input.
47
+ - skill_manager.py: _do_request crashed on empty / 204 responses.
48
+ - skill_manager.py: extract_zip_safe missed Windows-style ".."
49
+ entries and drive letters; now normalizes to PurePosixPath.
50
+ - __init__.py: __version__ was stuck at 1.0.2 while pyproject was
51
+ at 1.0.3. Both are 1.0.5 now.
52
+ - pyproject.toml: added data/site/*.html to package-data.
53
+ - MANIFEST.in: include docs/ and CHANGELOG.md.
54
+
55
+ ### Improved
56
+ - Todo list: header now shows "N/M complete" with a rule.
57
+ - Agent log: boot banner lists keyboard shortcuts.
58
+ - Keyboard shortcuts: Ctrl+L, Ctrl+Enter, Ctrl+S, Ctrl+E, Ctrl+K,
59
+ Ctrl+,, F1, Esc.
60
+ - README: replaced the broken docs/skills.html link.
61
+
62
+ ## [1.0.3] - 2026-10-03
63
+
64
+ ### Added
65
+ - skills/browser/ - DrissionPage wrapped as a proper skill folder.
66
+
67
+ ### Fixed
68
+ - terminal.py API-key rotation and other small bugs.
69
+ - Moved skills/loader.py -> loader.py for clean pip import.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-code
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: Local AI agent with headless browser, sandboxed shell, and plug-and-play skills
5
5
  Author: minecraftbefile-maker
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-code"
7
- version = "1.0.4"
7
+ version = "1.0.5"
8
8
  description = "Local AI agent with headless browser, sandboxed shell, and plug-and-play skills"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,2 +1,2 @@
1
1
  """Agent + Skill Store — local AI agent with headless browser and skills."""
2
- __version__ = "1.0.4"
2
+ __version__ = "1.0.5"
@@ -233,6 +233,11 @@ MAX_TURNS = 40
233
233
  SHELL_TIMEOUT = 60
234
234
  MAX_READ_LINES = 2000
235
235
  MAX_SUBAGENT_TURNS = 12
236
+ MAX_INPUT_CHARS = 20000
237
+ MAX_LOG_LINE_CHARS = 4000
238
+ MAX_LOG_LINES = 2500
239
+ MAX_USER_MESSAGE_CHARS = 100000
240
+ LLM_TIMEOUT = 120
236
241
 
237
242
  PERMISSION_READONLY = "readonly"
238
243
  PERMISSION_ASK = "ask"
@@ -346,24 +351,59 @@ class TodoList:
346
351
  except Exception:
347
352
  pass
348
353
 
349
- def write(self, items):
354
+ _STATUS_ALIASES = {
355
+ "pending": "pending", "todo": "pending", "open": "pending",
356
+ "in_progress": "in_progress", "active": "in_progress",
357
+ "working": "in_progress", "started": "in_progress", "doing": "in_progress",
358
+ "completed": "completed", "complete": "completed", "done": "completed",
359
+ "finished": "completed", "closed": "completed",
360
+ }
361
+
362
+ def write(self, todos=None, items=None, tasks=None, **_ignored):
363
+ # The schema advertises `todos`, but models sometimes send `items`
364
+ # or `tasks`. Accept all three; any other kwargs are ignored.
365
+ raw = todos if todos is not None else (
366
+ items if items is not None else tasks)
367
+
368
+ if raw is None:
369
+ return "ERROR: missing required field 'todos' (list of {content, status})"
370
+ if isinstance(raw, dict):
371
+ raw = [raw]
372
+ if not isinstance(raw, list):
373
+ return (f"ERROR: 'todos' must be a list of "
374
+ f"{{content, status}} objects, got {type(raw).__name__}")
375
+
350
376
  clean = []
351
- for x in items or []:
377
+ skipped = []
378
+ for x in raw:
352
379
  if not isinstance(x, dict):
380
+ skipped.append(repr(x)[:60])
353
381
  continue
354
- content = str(x.get("content", "")).strip()
355
- status = str(x.get("status", "pending")).lower()
356
- if status not in ("pending", "in_progress", "completed"):
357
- status = "pending"
382
+ content = str(
383
+ x.get("content") or x.get("task") or x.get("text") or ""
384
+ ).strip()
385
+ raw_status = str(x.get("status", "pending")).lower().strip()
386
+ status = self._STATUS_ALIASES.get(raw_status, "pending")
358
387
  if content:
359
388
  clean.append({"content": content, "status": status})
389
+ else:
390
+ skipped.append(repr(x)[:60])
391
+
392
+ if not clean:
393
+ return ("ERROR: no valid items. Each item needs a 'content' "
394
+ "string and a 'status' of pending/in_progress/completed.")
395
+
396
+ # Only one item may be in_progress
360
397
  ip = [i for i, x in enumerate(clean) if x["status"] == "in_progress"]
361
- if len(ip) > 1:
362
- for i in ip[1:]:
363
- clean[i]["status"] = "pending"
398
+ for i in ip[1:]:
399
+ clean[i]["status"] = "pending"
400
+
364
401
  self.items = clean
365
402
  self._save()
366
- return self.render()
403
+ out = self.render()
404
+ if skipped:
405
+ out += f"\n(skipped {len(skipped)} malformed item(s))"
406
+ return out
367
407
 
368
408
  def render(self):
369
409
  if not self.items:
@@ -595,19 +635,49 @@ def tool_grep_files(pattern: str, path: str = ".", glob: str = "",
595
635
  # Shell
596
636
  # ===========================================================================
597
637
 
598
- ALLOWED_CMDS = {
599
- "ls", "pwd", "cd", "cat", "head", "tail", "grep", "find", "wc", "echo",
600
- "date", "whoami", "git", "python", "python3", "pip", "pip3", "node", "npm",
601
- "curl", "wget", "sed", "awk", "sort", "uniq", "diff", "tree", "dir",
602
- "type", "where", "mkdir", "copy", "move", "touch",
603
- }
604
- BLOCKED_PATTERNS = [
605
- r"rm\s+-rf\s+/", r":\(\)\s*\{", r"mkfs\.", r"dd\s+if=",
606
- r">\s*/dev/sd", r"format\s+[a-z]:", r"del\s+/[sq]", r"rd\s+/s",
607
- r"shutdown", r"reboot", r"reg\s+delete", r"regedit",
608
- r"net\s+user\s+.*\s+/add", r"runas",
638
+ # In permissive mode (default) every command is allowed EXCEPT patterns that
639
+ # can brick the machine, escalate privileges, or delete the OS. Toggle with
640
+ # { "permissive_shell": false } in agent_config.json
641
+ DEFAULT_PERMISSIVE_SHELL = True
642
+
643
+ HIGH_RISK_PATTERNS = [
644
+ # Shutdown / reboot / power
645
+ r"\bshutdown\b", r"\breboot\b", r"\bhalt\b", r"\bpoweroff\b",
646
+ r"\binit\s+0\b", r"\bStop-Computer\b", r"\bRestart-Computer\b",
647
+ # Disk / partition / bootloader
648
+ r"\bmkfs(\.\w+)?\b", r"\bformat\s+[a-zA-Z]:", r"\bdiskpart\b",
649
+ r"\bfdisk\b", r"\bparted\b", r"\bdd\s+.*of\s*=\s*/dev/",
650
+ r"\bbootsect\b", r"\bbcdedit\b", r"\bdiskutil\s+eraseDisk\b",
651
+ # Windows registry / services / scheduler
652
+ r"\breg\s+delete\b", r"\bregedit\b", r"\bsc\s+delete\b",
653
+ r"\bschtasks\s+/(create|delete|change)\b",
654
+ r"\bwmic\s+.*\s+delete\b",
655
+ # Privilege escalation
656
+ r"\brunas\b", r"\bsudo\b", r"\bdoas\b", r"\bpkexec\b", r"\bgsudo\b",
657
+ # User / account manipulation
658
+ r"\bnet\s+user\s+.*\s+/add\b", r"\bnet\s+localgroup\b",
659
+ r"\buseradd\b", r"\buserdel\b", r"\badduser\b", r"\bdeluser\b",
660
+ r"\bchpasswd\b",
661
+ # System permission changes
662
+ r"\bchmod\s+(--?[A-Za-z]+\s+)*[-=]?[0-7]*777\s+/(?:\s|$)",
663
+ r"\bchown\s+.*\s+/(?:\s|$)",
664
+ r"\bicacls\s+.*\s+/grant\b", r"\btakeown\b",
665
+ # Destructive deletes on absolute paths
666
+ r"\brm\s+-[rRf]*[rRf][rRf]*\s+/",
667
+ r"\brm\s+-[rRf]+\s+~(?:\s|$|/)",
668
+ r"\bRemove-Item\b.*-Recurse.*-Force.*[a-zA-Z]:[\\/]*\s*$",
669
+ # Device writes
670
+ r">\s*/dev/(sd|hd|nvme|disk)",
671
+ # Fork bombs
672
+ r":\(\)\s*\{",
673
+ # PATH hijack
674
+ r"\b(export|setx)\s+PATH\s*=",
609
675
  ]
610
676
 
677
+ # Backwards-compat aliases
678
+ BLOCKED_PATTERNS = HIGH_RISK_PATTERNS
679
+ ALLOWED_CMDS = set()
680
+
611
681
 
612
682
  def _arg_safe(arg: str) -> bool:
613
683
  if arg.startswith(("http://", "https://")):
@@ -625,39 +695,69 @@ def _arg_safe(arg: str) -> bool:
625
695
  return True
626
696
 
627
697
 
628
- def tool_shell_run(command: str, cwd: str = ".") -> str:
629
- for pat in BLOCKED_PATTERNS:
698
+ def tool_shell_run(command: str, cwd: str = ".", permissive: bool = None) -> str:
699
+ """Run a shell command. CWD is always jailed to the workspace.
700
+
701
+ permissive=True — denylist only (default). Any command except high-risk.
702
+ permissive=False — the legacy allowlist + no-metacharacters mode.
703
+ """
704
+ if permissive is None:
705
+ try:
706
+ permissive = bool(
707
+ load_agent_config().get("permissive_shell",
708
+ DEFAULT_PERMISSIVE_SHELL))
709
+ except Exception:
710
+ permissive = DEFAULT_PERMISSIVE_SHELL
711
+
712
+ if not command or not command.strip():
713
+ return "BLOCKED: empty command"
714
+
715
+ # High-risk patterns are ALWAYS blocked, in both modes.
716
+ for pat in HIGH_RISK_PATTERNS:
630
717
  if re.search(pat, command, re.IGNORECASE):
631
- return f"BLOCKED: pattern {pat}"
632
- if re.search(r"[|&;<>`$]", command):
633
- return ("BLOCKED: shell metacharacters not allowed.")
634
- try:
635
- parts = shlex.split(command, posix=False)
636
- except ValueError as e:
637
- return f"BLOCKED: parse error {e}"
638
- if not parts:
639
- return "BLOCKED: empty"
640
- base = os.path.basename(parts[0].strip('"')).lower()
641
- if base not in ALLOWED_CMDS:
642
- return f"BLOCKED: '{base}' not allowed"
643
- for arg in parts[1:]:
644
- if not _arg_safe(arg):
645
- return f"BLOCKED: arg escapes workspace: {arg}"
718
+ return f"BLOCKED: high-risk pattern: {pat}"
719
+
720
+ if not permissive:
721
+ # Legacy strict path
722
+ if re.search(r"[|&;<>`$]", command):
723
+ return "BLOCKED: shell metacharacters not allowed in strict mode"
724
+ try:
725
+ parts = shlex.split(command, posix=False)
726
+ except ValueError as e:
727
+ return f"BLOCKED: parse error {e}"
728
+ if not parts:
729
+ return "BLOCKED: empty"
730
+ base = os.path.basename(parts[0].strip('"')).lower()
731
+ if base not in ALLOWED_CMDS:
732
+ return f"BLOCKED: '{base}' not in allowlist (strict mode)"
733
+ for arg in parts[1:]:
734
+ if not _arg_safe(arg):
735
+ return f"BLOCKED: arg escapes workspace: {arg}"
736
+
646
737
  try:
647
738
  run_cwd = _safe_path(cwd) if cwd else WORKSPACE
648
739
  except ValueError as e:
649
740
  return f"BLOCKED: cwd: {e}"
741
+
650
742
  try:
651
- r = subprocess.run(
652
- parts, capture_output=True, text=True,
653
- encoding="utf-8", errors="replace",
654
- timeout=SHELL_TIMEOUT, cwd=str(run_cwd), shell=False)
743
+ if permissive:
744
+ # Real shell: pipes, redirects, &&, ; all work.
745
+ r = subprocess.run(
746
+ command, capture_output=True, text=True,
747
+ encoding="utf-8", errors="replace",
748
+ timeout=SHELL_TIMEOUT, cwd=str(run_cwd), shell=True)
749
+ else:
750
+ parts = shlex.split(command, posix=False)
751
+ r = subprocess.run(
752
+ parts, capture_output=True, text=True,
753
+ encoding="utf-8", errors="replace",
754
+ timeout=SHELL_TIMEOUT, cwd=str(run_cwd), shell=False)
655
755
  out = ((r.stdout or "") + (r.stderr or "")).strip()
656
756
  return out[:8000] or "(no output)"
657
757
  except subprocess.TimeoutExpired:
658
758
  return f"ERROR: timeout {SHELL_TIMEOUT}s"
659
759
  except FileNotFoundError:
660
- return f"ERROR: command not found: {base}"
760
+ return "ERROR: command not found"
661
761
  except Exception as e:
662
762
  return f"ERROR: {e}"
663
763
 
@@ -1614,6 +1714,8 @@ class App:
1614
1714
  self.entry = ttk.Entry(inner, font=("Segoe UI", 11))
1615
1715
  self.entry.pack(side="left", fill="x", expand=True, ipady=3)
1616
1716
  self.entry.bind("<Return>", lambda e: self.on_send())
1717
+ self.entry.bind("<<Paste>>", self._on_paste)
1718
+ self.entry.bind("<Control-v>", self._on_paste)
1617
1719
 
1618
1720
  self.send_btn = ttk.Button(inner, text="Send ➤",
1619
1721
  style="Send.TButton",
@@ -1656,6 +1758,25 @@ class App:
1656
1758
  self._bind_shortcuts()
1657
1759
  self.root.protocol("WM_DELETE_WINDOW", self._on_close)
1658
1760
 
1761
+ # ---- input guard ----
1762
+
1763
+ def _on_paste(self, event=None):
1764
+ """Truncate huge pastes so the entry never freezes the UI."""
1765
+ try:
1766
+ text = self.root.clipboard_get()
1767
+ except Exception:
1768
+ return "break"
1769
+ if not isinstance(text, str):
1770
+ return "break"
1771
+ if len(text) > MAX_INPUT_CHARS:
1772
+ text = text[:MAX_INPUT_CHARS]
1773
+ self.log_line(
1774
+ f"[input] paste truncated to {MAX_INPUT_CHARS:,} chars",
1775
+ "dim")
1776
+ self.entry.delete(0, "end")
1777
+ self.entry.insert(0, text)
1778
+ return "break"
1779
+
1659
1780
  # ---- shortcuts ----
1660
1781
 
1661
1782
  def _bind_shortcuts(self):
@@ -1822,7 +1943,7 @@ class App:
1822
1943
  resp = self.client.chat.completions.create(
1823
1944
  model=self.model, messages=sub_messages,
1824
1945
  tools=self.all_schemas, tool_choice="auto",
1825
- temperature=0.2)
1946
+ temperature=0.2, timeout=LLM_TIMEOUT)
1826
1947
  except Exception as e:
1827
1948
  return f"SUBAGENT ERROR: {e}"
1828
1949
 
@@ -2048,7 +2169,7 @@ class App:
2048
2169
  def _set_running(self, running):
2049
2170
  if running:
2050
2171
  self.send_btn.configure(state="disabled")
2051
- self.stop_btn.configure(state="normal")
2172
+ self.stop_btn.configure(state="normal", text="Stop")
2052
2173
  self.continue_btn.configure(state="disabled")
2053
2174
  self.clear_btn.configure(state="disabled")
2054
2175
  self.status_lbl.configure(text="running…")
@@ -2061,9 +2182,20 @@ class App:
2061
2182
  self._update_header()
2062
2183
 
2063
2184
  def _on_stop(self):
2185
+ if self._stop_event.is_set():
2186
+ # Second press within the same turn: force-detach the worker.
2187
+ self.log_line("[stop] force — abandoning current turn", "error")
2188
+ self._worker = None
2189
+ self._stopped_last = True
2190
+ self._stop_event.clear()
2191
+ self._set_running(False)
2192
+ return
2064
2193
  self._stop_event.set()
2065
- self.log_line("[stop] requested", "error")
2066
- self.stop_btn.configure(state="disabled")
2194
+ self.log_line("[stop] requested — press Stop again to force", "error")
2195
+ try:
2196
+ self.stop_btn.configure(text="Force stop")
2197
+ except Exception:
2198
+ pass
2067
2199
 
2068
2200
  def _on_continue(self):
2069
2201
  if self._worker and self._worker.is_alive():
@@ -2116,6 +2248,8 @@ class App:
2116
2248
  try:
2117
2249
  while True:
2118
2250
  text, tag = self._log_q.get_nowait()
2251
+ if isinstance(text, str) and len(text) > MAX_LOG_LINE_CHARS:
2252
+ text = text[:MAX_LOG_LINE_CHARS] + " …(truncated)"
2119
2253
  self.log.configure(state="normal")
2120
2254
  if tag == "agent_md":
2121
2255
  self.log.insert("end", "AGENT ▸ ", "agent")
@@ -2129,6 +2263,17 @@ class App:
2129
2263
  self.log.configure(state="disabled")
2130
2264
  except queue.Empty:
2131
2265
  pass
2266
+
2267
+ # Trim the widget so the UI never lags on very long sessions.
2268
+ try:
2269
+ total = int(self.log.index("end-1c").split(".")[0])
2270
+ if total > MAX_LOG_LINES:
2271
+ self.log.configure(state="normal")
2272
+ self.log.delete("1.0", f"{total - MAX_LOG_LINES}.0")
2273
+ self.log.configure(state="disabled")
2274
+ except Exception:
2275
+ pass
2276
+
2132
2277
  self.root.after(60, self._poll_log)
2133
2278
 
2134
2279
  # ---- input ----
@@ -2223,6 +2368,11 @@ class App:
2223
2368
 
2224
2369
  def _agent_loop(self, task):
2225
2370
  if task is not None:
2371
+ if len(task) > MAX_USER_MESSAGE_CHARS:
2372
+ self.log_line(
2373
+ f"[input] user message truncated to "
2374
+ f"{MAX_USER_MESSAGE_CHARS:,} chars", "dim")
2375
+ task = task[:MAX_USER_MESSAGE_CHARS]
2226
2376
  self.messages.append({"role": "user", "content": task})
2227
2377
  if len(self.messages) > 60:
2228
2378
  self.messages = [self.messages[0]] + self.messages[-50:]
@@ -2238,7 +2388,7 @@ class App:
2238
2388
  resp = self.client.chat.completions.create(
2239
2389
  model=self.model, messages=self.messages,
2240
2390
  tools=self.all_schemas, tool_choice="auto",
2241
- temperature=0.2)
2391
+ temperature=0.2, timeout=LLM_TIMEOUT)
2242
2392
  except Exception as e:
2243
2393
  self.log_line(f"LLM ERROR: {e}", "error")
2244
2394
  return
@@ -1,5 +1,7 @@
1
- """CLI entry point. Subcommands: run, manage, serve, skills."""
2
- import sys, argparse
1
+ """CLI entry point. Subcommands: run, manage, serve, skills."""
2
+ import os
3
+ import sys
4
+ import argparse
3
5
  from . import __version__
4
6
 
5
7
  def main(argv=None):
@@ -10,8 +12,15 @@ def main(argv=None):
10
12
  version=f"agent-code {__version__}")
11
13
  sub = ap.add_subparsers(dest="cmd")
12
14
 
13
- sub.add_parser("run", help="run the agent (default)").set_defaults(func=_run)
14
- sub.add_parser("manage", help="skill manager GUI").set_defaults(func=_manage)
15
+ p = sub.add_parser("run", help="run the agent (default)")
16
+ p.add_argument("--global", dest="global_paths", action="store_true",
17
+ help="use ~/.agent-code for skills, workspace and chats")
18
+ p.set_defaults(func=_run)
19
+
20
+ p = sub.add_parser("manage", help="skill manager GUI")
21
+ p.add_argument("--global", dest="global_paths", action="store_true",
22
+ help="install skills into ~/.agent-code/skills")
23
+ p.set_defaults(func=_manage)
15
24
 
16
25
  p = sub.add_parser("serve", help="serve the landing page")
17
26
  p.add_argument("--host", default=None)
@@ -29,10 +38,14 @@ def main(argv=None):
29
38
  return args.func(args)
30
39
 
31
40
  def _run(args):
41
+ if getattr(args, "global_paths", False):
42
+ os.environ["AGENT_CODE_GLOBAL"] = "1"
32
43
  from .agent import main as m
33
44
  m(); return 0
34
45
 
35
46
  def _manage(args):
47
+ if getattr(args, "global_paths", False):
48
+ os.environ["AGENT_CODE_GLOBAL"] = "1"
36
49
  from .skill_manager import main as m
37
50
  m(); return 0
38
51
 
@@ -66,4 +79,4 @@ def _skills(args):
66
79
  return 0
67
80
 
68
81
  if __name__ == "__main__":
69
- sys.exit(main())
82
+ sys.exit(main())
@@ -1,5 +1,7 @@
1
- """Path resolution for pip-installed use."""
2
- import os, shutil
1
+ """Path resolution for pip-installed use."""
2
+ import os
3
+ import json
4
+ import shutil
3
5
  from pathlib import Path
4
6
 
5
7
  PACKAGE_ROOT = Path(__file__).parent.resolve()
@@ -21,7 +23,33 @@ def user_config_dir() -> Path:
21
23
  def user_config_file() -> Path:
22
24
  return user_config_dir() / "agent_config.json"
23
25
 
24
- def project_dir() -> Path: return Path.cwd()
26
+ def global_data_dir() -> Path:
27
+ p = Path.home() / ".agent-code"
28
+ p.mkdir(parents=True, exist_ok=True)
29
+ return p
30
+
31
+
32
+ def _cfg() -> dict:
33
+ p = user_config_file()
34
+ if p.exists():
35
+ try:
36
+ return json.loads(p.read_text(encoding="utf-8"))
37
+ except Exception:
38
+ return {}
39
+ return {}
40
+
41
+
42
+ def use_global_paths() -> bool:
43
+ env = os.environ.get("AGENT_CODE_GLOBAL")
44
+ if env is not None:
45
+ return env.strip().lower() in ("1", "true", "yes", "on")
46
+ return bool(_cfg().get("use_global_paths"))
47
+
48
+
49
+ def project_dir() -> Path:
50
+ if use_global_paths():
51
+ return global_data_dir()
52
+ return Path.cwd()
25
53
  def project_workspace_dir() -> Path:
26
54
  p = project_dir() / "workspace"; p.mkdir(parents=True, exist_ok=True); return p
27
55
  def project_skills_dir() -> Path:
@@ -51,4 +79,4 @@ def install_bundled_skills(overwrite: bool = False) -> list:
51
79
  return written
52
80
 
53
81
  def bundled_site_dir() -> Path:
54
- return BUNDLED_SITE
82
+ return BUNDLED_SITE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-code
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: Local AI agent with headless browser, sandboxed shell, and plug-and-play skills
5
5
  Author: minecraftbefile-maker
6
6
  License-Expression: MIT
@@ -1,52 +0,0 @@
1
- # Changelog
2
-
3
- ## [1.0.4] - 2026-10-09
4
-
5
- ### Fixed
6
- - agent.py: make_client passed a callable where the OpenAI SDK
7
- expected a string; multi-key rotation now works via a property
8
- override on a subclass instead of a lambda.
9
- - agent.py: _safe_path used str.startswith() for the workspace jail,
10
- which let a sibling directory named "workspace-foo" escape. Now
11
- uses Path.relative_to().
12
- - agent.py: removed duplicate load_skills / discover_skills; the
13
- loader is now imported from loader.py (single source of truth).
14
- - agent.py: removed the six browser_* tools from CORE_SCHEMAS and
15
- all_callables — the browser skill owns them now, matching the
16
- 1.0.3 CHANGELOG.
17
- - agent.py: Skills dialog called self.app._save_disabled_skills(),
18
- which did not exist. Added the method.
19
- - agent.py: todo_write schema now describes items (content + enum
20
- status), so LLMs reliably produce valid input.
21
- - skill_manager.py: _do_request crashed on empty / 204 responses
22
- (e.g. DELETE /skills/{name}); now returns {}.
23
- - skill_manager.py: extract_zip_safe missed Windows-style ".."
24
- entries and drive letters; now normalizes to PurePosixPath.
25
- - __init__.py: __version__ was stuck at 1.0.2 while pyproject was
26
- at 1.0.3. Both are 1.0.4 now.
27
- - pyproject.toml: added data/site/*.html to package-data.
28
- - MANIFEST.in: include docs/ and CHANGELOG.md.
29
-
30
- ### Improved
31
- - Todo list: header now shows "N/M complete" with a rule.
32
- - Agent log: boot banner lists keyboard shortcuts.
33
- - Keyboard shortcuts: Ctrl+L (clear), Ctrl+Enter (send),
34
- Ctrl+S (save), Ctrl+E (export), Ctrl+K (skills),
35
- Ctrl+, (settings), F1 (help), Esc (stop running loop).
36
- - README: replaced the broken docs/skills.html link with a
37
- pointer to the loader module.
38
-
39
- ## [1.0.3] - 2026-10-03
40
-
41
- ### Added
42
- - skills/browser/ - DrissionPage wrapped as a proper skill folder.
43
- Six tools: browser_navigate, browser_snapshot, browser_click,
44
- browser_type, browser_links, browser_scroll, plus /browser-close.
45
-
46
- ### Fixed
47
- - terminal.py API-key rotation crashed after 1000 x len(KEYS) requests.
48
- - terminal.py passed a callable where the OpenAI SDK expects a string.
49
- - terminal.py /reset printed "memory cleared" but kept no memory.
50
- - terminal.py shell_run prompted even in readonly mode.
51
- - terminal.py stray import in the module body.
52
- - Moved skills/loader.py -> loader.py for clean pip import.
File without changes
File without changes
File without changes
File without changes
File without changes