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.
- agent_code-1.0.5/CHANGELOG.md +69 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/PKG-INFO +1 -1
- {agent_code-1.0.4 → agent_code-1.0.5}/pyproject.toml +1 -1
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/__init__.py +1 -1
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/agent.py +198 -48
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/cli.py +18 -5
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/paths.py +32 -4
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/PKG-INFO +1 -1
- agent_code-1.0.4/CHANGELOG.md +0 -52
- {agent_code-1.0.4 → agent_code-1.0.5}/LICENSE +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/LICENSE-GPL +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/MANIFEST.in +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/README.md +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/setup.cfg +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/__main__.py +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/browser/skill.md +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/browser/skill.py +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/filesystem/skill.md +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/filesystem/skill.py +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/public_api/skill.md +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/public_api/skill.py +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/temp_mail/skill.md +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/data/skills/temp_mail/skill.py +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/loader.py +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code/skill_manager.py +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/SOURCES.txt +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/dependency_links.txt +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/entry_points.txt +0 -0
- {agent_code-1.0.4 → agent_code-1.0.5}/src/agent_code.egg-info/requires.txt +0 -0
- {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,2 +1,2 @@
|
|
|
1
1
|
"""Agent + Skill Store — local AI agent with headless browser and skills."""
|
|
2
|
-
__version__ = "1.0.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
398
|
+
for i in ip[1:]:
|
|
399
|
+
clean[i]["status"] = "pending"
|
|
400
|
+
|
|
364
401
|
self.items = clean
|
|
365
402
|
self._save()
|
|
366
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
r"
|
|
606
|
-
r"
|
|
607
|
-
|
|
608
|
-
r"
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if not
|
|
645
|
-
return f"BLOCKED:
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
import
|
|
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)")
|
|
14
|
-
|
|
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
|
-
|
|
2
|
-
import os
|
|
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
|
|
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
|
agent_code-1.0.4/CHANGELOG.md
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|