agent-toggle 0.16.0__tar.gz → 0.16.2__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_toggle-0.16.0 → agent_toggle-0.16.2}/PKG-INFO +7 -7
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/README.md +5 -5
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/__init__.py +1 -1
- agent_toggle-0.16.2/agent_toggle/update_check.py +209 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/update_prompt.py +5 -5
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle.egg-info/PKG-INFO +7 -7
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle.egg-info/requires.txt +1 -1
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/pyproject.toml +1 -1
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_cli.py +7 -7
- agent_toggle-0.16.2/tests/test_update_check.py +240 -0
- agent_toggle-0.16.0/agent_toggle/update_check.py +0 -361
- agent_toggle-0.16.0/tests/test_update_check.py +0 -423
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/LICENSE +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/__main__.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/backends/__init__.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/backends/flag_json.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/backends/mcp_json.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/backends/mcp_toml.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/backends/plugin_cli.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/cli.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/companions.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/completions.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/config.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/cost.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/doctor.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/fs.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/harnesses.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/help.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/i18n.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/locales/__init__.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/locales/zh_tw_cli.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/locales/zh_tw_common.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/locales/zh_tw_config.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/locales/zh_tw_picker.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/mechanisms.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/ops.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/output.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/profiles.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/settings.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/shims/claude.md.tmpl +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/shims/generic.md.tmpl +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/spinner.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/store.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/toml_check.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/ui/__init__.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/ui/config_menu.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/ui/logo.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/ui/menu.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/ui/model.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/ui/picker.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/ui/theme.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle/undo.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle.egg-info/SOURCES.txt +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle.egg-info/dependency_links.txt +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle.egg-info/entry_points.txt +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/agent_toggle.egg-info/top_level.txt +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/setup.cfg +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_alias.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_all_type.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_backends.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_cli_surface.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_color.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_companions.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_completions.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_config.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_config_menu.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_conformance.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_containment.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_cost.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_crash.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_doctor.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_encoding.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_fs.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_harnesses.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_help.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_i18n.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_install.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_logo.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_mechanisms.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_menu.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_mods.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_picker.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_platform.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_profiles.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_project.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_safety.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_same_name.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_secret_audit.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_settings.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_store.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_theme.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_undo.py +0 -0
- {agent_toggle-0.16.0 → agent_toggle-0.16.2}/tests/test_update_prompt.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-toggle
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.2
|
|
4
4
|
Summary: Temporarily disable / re-enable AI-agent skills, agents, commands, rules, plugins and MCP servers across harnesses (Claude Code, Codex, Grok, OpenCode, OpenClaw, Copilot, Vibe, Devin, Antigravity)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/weskao/agent-toggle
|
|
@@ -25,7 +25,7 @@ Requires-Python: >=3.10
|
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
27
|
Requires-Dist: telegram-kit<0.3,>=0.2.2
|
|
28
|
-
Requires-Dist: update-kit<0.3,>=0.2.
|
|
28
|
+
Requires-Dist: update-kit<0.3,>=0.2.2
|
|
29
29
|
Provides-Extra: windows
|
|
30
30
|
Requires-Dist: windows-curses; sys_platform == "win32" and extra == "windows"
|
|
31
31
|
Provides-Extra: telegram
|
|
@@ -498,7 +498,7 @@ invalid value in either is ignored with one warning on stderr.
|
|
|
498
498
|
|
|
499
499
|
## Update check
|
|
500
500
|
|
|
501
|
-
On every invocation (unless disabled) agent-toggle checks
|
|
501
|
+
On every invocation (unless disabled) agent-toggle checks the repository's release tags for a newer release. It starts
|
|
502
502
|
before the command and is offered after it, on every exit path; it overlaps the command and waits at most 0.8 s afterwards.
|
|
503
503
|
`update` and `completion` skip it.
|
|
504
504
|
|
|
@@ -513,9 +513,9 @@ before the command and is offered after it, on every exit path; it overlaps the
|
|
|
513
513
|
then ` agent-toggle update`.
|
|
514
514
|
- It never changes the exit code or stdout, so `--json` output stays one document.
|
|
515
515
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
516
|
+
The lookup is update-kit's: one `git ls-remote --tags --refs` of the repository on a background
|
|
517
|
+
thread, cached for 10 minutes in `~/.update-kit/state.json`. Only the version you chose to skip is
|
|
518
|
+
kept in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a normal git request. See `SECURITY.md`
|
|
519
519
|
for the hardening. To turn it off, set `AGENT_TOGGLE_UPDATE_CHECK=0` or the `update_check` setting
|
|
520
520
|
to off (the settings menu's "Check for updates").
|
|
521
521
|
|
|
@@ -931,7 +931,7 @@ bookkeeping.
|
|
|
931
931
|
| `parked/<sha8>/` | items parked by `--project` (`<sha8>` = first 8 hex of the SHA-1 of the resolved project dir) |
|
|
932
932
|
| `profiles/` | `<name>.json` profiles (dir `0700`, files `0600`) |
|
|
933
933
|
| `config.json` | [settings](#settings--config-menu) and the Telegram chat id (mode `0600`, atomic write, unknown keys kept) |
|
|
934
|
-
| `update-check.json` | the [update check](#update-check)'s
|
|
934
|
+
| `update-check.json` | the [update check](#update-check)'s skipped version (mode `0600`, atomic write) |
|
|
935
935
|
|
|
936
936
|
A killed run loses at most the one item in flight. If that was a flag write or a
|
|
937
937
|
dir move (with its companions), its `pending` record (the full entry, a flag's
|
|
@@ -464,7 +464,7 @@ invalid value in either is ignored with one warning on stderr.
|
|
|
464
464
|
|
|
465
465
|
## Update check
|
|
466
466
|
|
|
467
|
-
On every invocation (unless disabled) agent-toggle checks
|
|
467
|
+
On every invocation (unless disabled) agent-toggle checks the repository's release tags for a newer release. It starts
|
|
468
468
|
before the command and is offered after it, on every exit path; it overlaps the command and waits at most 0.8 s afterwards.
|
|
469
469
|
`update` and `completion` skip it.
|
|
470
470
|
|
|
@@ -479,9 +479,9 @@ before the command and is offered after it, on every exit path; it overlaps the
|
|
|
479
479
|
then ` agent-toggle update`.
|
|
480
480
|
- It never changes the exit code or stdout, so `--json` output stays one document.
|
|
481
481
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
482
|
+
The lookup is update-kit's: one `git ls-remote --tags --refs` of the repository on a background
|
|
483
|
+
thread, cached for 10 minutes in `~/.update-kit/state.json`. Only the version you chose to skip is
|
|
484
|
+
kept in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a normal git request. See `SECURITY.md`
|
|
485
485
|
for the hardening. To turn it off, set `AGENT_TOGGLE_UPDATE_CHECK=0` or the `update_check` setting
|
|
486
486
|
to off (the settings menu's "Check for updates").
|
|
487
487
|
|
|
@@ -897,7 +897,7 @@ bookkeeping.
|
|
|
897
897
|
| `parked/<sha8>/` | items parked by `--project` (`<sha8>` = first 8 hex of the SHA-1 of the resolved project dir) |
|
|
898
898
|
| `profiles/` | `<name>.json` profiles (dir `0700`, files `0600`) |
|
|
899
899
|
| `config.json` | [settings](#settings--config-menu) and the Telegram chat id (mode `0600`, atomic write, unknown keys kept) |
|
|
900
|
-
| `update-check.json` | the [update check](#update-check)'s
|
|
900
|
+
| `update-check.json` | the [update check](#update-check)'s skipped version (mode `0600`, atomic write) |
|
|
901
901
|
|
|
902
902
|
A killed run loses at most the one item in flight. If that was a flag write or a
|
|
903
903
|
dir move (with its companions), its `pending` record (the full entry, a flag's
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Update check on top of update-kit, plus the "skip this version" memory.
|
|
2
|
+
|
|
3
|
+
from agent_toggle.update_check import check
|
|
4
|
+
found = check("1.0.0", cache_path=Path.home() / ".agent-toggle" / "update-check.json")
|
|
5
|
+
if found:
|
|
6
|
+
print(f"{found.latest} is available (you have {found.current})")
|
|
7
|
+
|
|
8
|
+
The lookup itself -- ``git ls-remote`` of the release tags, the 10-minute cache in
|
|
9
|
+
``~/.update-kit/state.json``, the timeout -- is :func:`update_kit.check`; ``latest`` is
|
|
10
|
+
the tag as it returns it (``v1.2.0``). Only the skipped version lives in *cache_path*.
|
|
11
|
+
|
|
12
|
+
To overlap the lookup with the command's own work instead of paying for
|
|
13
|
+
it at exit, start it early and collect the result at the end:
|
|
14
|
+
|
|
15
|
+
started = start("1.0.0", cache_path=...)
|
|
16
|
+
... # the command's own work
|
|
17
|
+
found = collect(started)
|
|
18
|
+
|
|
19
|
+
To ask the user what to do instead of only hinting, hand :func:`offer` the
|
|
20
|
+
UI as a callback -- it owns what each answer does, the UI only picks one:
|
|
21
|
+
|
|
22
|
+
offer(started, ask, cache_path=..., upgrade=["uv", "tool", "upgrade", "agent-toggle"])
|
|
23
|
+
|
|
24
|
+
where ``ask(found)`` returns ``UPDATE_NOW``, ``SKIP`` or ``SKIP_VERSION``.
|
|
25
|
+
When the upgrade depends on the version or takes several steps, pass a
|
|
26
|
+
callable instead: ``upgrade=lambda found: [[...], [...]]`` runs each command
|
|
27
|
+
in order and stops at the first one that fails.
|
|
28
|
+
``SKIP_VERSION`` is remembered in *cache_path*; a newer release asks again.
|
|
29
|
+
|
|
30
|
+
Nothing here is allowed to raise to the caller: a missing cache, a bad
|
|
31
|
+
payload, or a dead network all become ``None``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import re
|
|
39
|
+
import subprocess
|
|
40
|
+
import threading
|
|
41
|
+
from collections.abc import Callable, Sequence
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"DEFAULT_TIMEOUT",
|
|
47
|
+
"REPO",
|
|
48
|
+
"SKIP",
|
|
49
|
+
"SKIP_VERSION",
|
|
50
|
+
"UPDATE_NOW",
|
|
51
|
+
"UPGRADE_FAILED",
|
|
52
|
+
"Started",
|
|
53
|
+
"UpdateAvailable",
|
|
54
|
+
"check",
|
|
55
|
+
"collect",
|
|
56
|
+
"offer",
|
|
57
|
+
"skip",
|
|
58
|
+
"start",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
REPO = "weskao/agent-toggle"
|
|
62
|
+
#: How long :func:`collect` waits for the background lookup before giving up on it.
|
|
63
|
+
DEFAULT_TIMEOUT = 0.8
|
|
64
|
+
|
|
65
|
+
#: Answers an ``ask`` callback returns to :func:`offer`.
|
|
66
|
+
UPDATE_NOW = "update-now"
|
|
67
|
+
SKIP = "skip"
|
|
68
|
+
SKIP_VERSION = "skip-version"
|
|
69
|
+
#: What :func:`offer` returns when UPDATE_NOW's command could not run or failed.
|
|
70
|
+
UPGRADE_FAILED = "upgrade-failed"
|
|
71
|
+
|
|
72
|
+
#: What a skipped version may look like before it is trusted: it is read from a file.
|
|
73
|
+
_VERSION_RE = re.compile(r"v?\d{1,6}(\.\d{1,6}){1,3}([a-zA-Z0-9.+-]{0,20})?")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class UpdateAvailable:
|
|
78
|
+
current: str
|
|
79
|
+
latest: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check(current: str, *, cache_path: Path) -> UpdateAvailable | None:
|
|
83
|
+
"""Return an update when the latest release is newer than *current* and not the
|
|
84
|
+
version skipped in *cache_path*, else None. Never raises."""
|
|
85
|
+
try:
|
|
86
|
+
import update_kit # lazy: a checkout run or a stale env lacks it
|
|
87
|
+
|
|
88
|
+
latest = update_kit.check(repo=REPO, current=current)
|
|
89
|
+
if not isinstance(latest, str) or _bare(latest) == _bare(_load_cache(cache_path).get("skipped")):
|
|
90
|
+
return None
|
|
91
|
+
return UpdateAvailable(current=current, latest=latest)
|
|
92
|
+
except Exception: # noqa: BLE001 - a hint must never fail the command
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class Started:
|
|
98
|
+
"""A background check in flight; pass to :func:`collect` to get its result."""
|
|
99
|
+
|
|
100
|
+
thread: threading.Thread
|
|
101
|
+
_result: list = field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def start(current: str, *, cache_path: Path) -> Started:
|
|
105
|
+
"""Run :func:`check` on a daemon thread so it overlaps the caller's own work."""
|
|
106
|
+
result: list = []
|
|
107
|
+
|
|
108
|
+
def run() -> None:
|
|
109
|
+
result.append(check(current, cache_path=cache_path))
|
|
110
|
+
|
|
111
|
+
thread = threading.Thread(target=run, name="agent-toggle-update-check", daemon=True)
|
|
112
|
+
thread.start()
|
|
113
|
+
return Started(thread=thread, _result=result)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def collect(started: Started | None, *, timeout: float = DEFAULT_TIMEOUT) -> UpdateAvailable | None:
|
|
117
|
+
"""Wait for a :func:`start`-ed check and return its result. Never raises."""
|
|
118
|
+
if started is None:
|
|
119
|
+
return None
|
|
120
|
+
try:
|
|
121
|
+
# Usually already done: it ran alongside the caller's own work.
|
|
122
|
+
started.thread.join(timeout)
|
|
123
|
+
return started._result[0] if started._result else None
|
|
124
|
+
except Exception: # noqa: BLE001 - a hint must never fail the command
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def skip(cache_path: Path, version: str) -> None:
|
|
129
|
+
"""Stop reporting *version*; a newer release is reported again. Never raises."""
|
|
130
|
+
data = _load_cache(cache_path)
|
|
131
|
+
data["skipped"] = version
|
|
132
|
+
_write_cache(cache_path, data)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def offer(
|
|
136
|
+
started: Started | None,
|
|
137
|
+
ask: Callable[[UpdateAvailable], str],
|
|
138
|
+
*,
|
|
139
|
+
cache_path: Path,
|
|
140
|
+
upgrade: Sequence[str] | Callable[[UpdateAvailable], Sequence[Sequence[str]]],
|
|
141
|
+
run: Callable[..., object] = subprocess.run,
|
|
142
|
+
) -> str | None:
|
|
143
|
+
"""Collect *started*, let *ask* pick an answer, then act on it.
|
|
144
|
+
|
|
145
|
+
*upgrade* is one command, or a callable building the commands from the
|
|
146
|
+
found release, run in order. Returns the answer acted on
|
|
147
|
+
(``UPGRADE_FAILED`` when a command is missing or exits nonzero — the
|
|
148
|
+
rest are then skipped), or None when there was nothing to offer. Ctrl+C
|
|
149
|
+
inside *ask* is ``SKIP``. Never raises.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
found = collect(started)
|
|
153
|
+
if found is None:
|
|
154
|
+
return None
|
|
155
|
+
try:
|
|
156
|
+
answer = ask(found)
|
|
157
|
+
except KeyboardInterrupt:
|
|
158
|
+
return SKIP
|
|
159
|
+
if answer == SKIP_VERSION:
|
|
160
|
+
skip(cache_path, found.latest)
|
|
161
|
+
elif answer == UPDATE_NOW:
|
|
162
|
+
steps = upgrade(found) if callable(upgrade) else [upgrade]
|
|
163
|
+
for step in steps:
|
|
164
|
+
try:
|
|
165
|
+
done = run(list(step), check=False)
|
|
166
|
+
except OSError:
|
|
167
|
+
return UPGRADE_FAILED
|
|
168
|
+
if getattr(done, "returncode", 1) != 0:
|
|
169
|
+
return UPGRADE_FAILED
|
|
170
|
+
return answer
|
|
171
|
+
except Exception: # noqa: BLE001 - an update offer must never fail the command
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _bare(version: object) -> str | None:
|
|
176
|
+
"""*version* without a leading ``v`` (an old cache holds PyPI's ``0.10.0``, now tags)."""
|
|
177
|
+
return version.removeprefix("v") if isinstance(version, str) else None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _load_cache(path: Path) -> dict:
|
|
181
|
+
try:
|
|
182
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
183
|
+
except (OSError, ValueError, RecursionError): # RecursionError: a deeply nested file
|
|
184
|
+
return {}
|
|
185
|
+
if not isinstance(data, dict):
|
|
186
|
+
return {}
|
|
187
|
+
# Only a valid skipped version survives: a hostile file can't smuggle anything
|
|
188
|
+
# (e.g. a 100k-deep dict that json.dumps can't write back), and old
|
|
189
|
+
# checked_at/latest keys are dropped on the next write.
|
|
190
|
+
skipped = data.get("skipped")
|
|
191
|
+
return {"skipped": skipped} if isinstance(skipped, str) and _VERSION_RE.fullmatch(skipped) else {}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _write_cache(path: Path, data: dict) -> None:
|
|
195
|
+
# tmp + os.replace never follows a symlink at *path*; 0600 from creation.
|
|
196
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
197
|
+
try:
|
|
198
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
199
|
+
tmp.unlink(missing_ok=True)
|
|
200
|
+
fd = os.open(tmp, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
201
|
+
try:
|
|
202
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
203
|
+
fh.write(json.dumps(data) + "\n")
|
|
204
|
+
os.replace(tmp, path)
|
|
205
|
+
except BaseException:
|
|
206
|
+
tmp.unlink(missing_ok=True)
|
|
207
|
+
raise
|
|
208
|
+
except (OSError, RecursionError, ValueError):
|
|
209
|
+
return
|
|
@@ -28,7 +28,7 @@ from .output import use_color
|
|
|
28
28
|
from .ui import theme
|
|
29
29
|
|
|
30
30
|
DIST = "agent-toggle"
|
|
31
|
-
REPO =
|
|
31
|
+
REPO = update_check.REPO
|
|
32
32
|
PLUGIN = "agent-toggle@agent-toggle"
|
|
33
33
|
UPDATE = [DIST, "update"]
|
|
34
34
|
RELEASE_NOTES = "https://github.com/weskao/agent-toggle/releases/tag/v%s"
|
|
@@ -93,12 +93,12 @@ def cache_path() -> Path:
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def start() -> update_check.Started | None:
|
|
96
|
-
"""Kick off the
|
|
97
|
-
(env AGENT_TOGGLE_UPDATE_CHECK wins) is off. Never raises."""
|
|
96
|
+
"""Kick off the release check (update_kit) on a daemon thread, unless the `update_check`
|
|
97
|
+
setting (env AGENT_TOGGLE_UPDATE_CHECK wins) is off. Never raises."""
|
|
98
98
|
try:
|
|
99
99
|
if not settings.get("update_check"):
|
|
100
100
|
return None
|
|
101
|
-
return update_check.start(
|
|
101
|
+
return update_check.start(__version__, cache_path=cache_path())
|
|
102
102
|
except Exception: # noqa: BLE001 - a hint must never fail the command
|
|
103
103
|
return None
|
|
104
104
|
|
|
@@ -112,7 +112,7 @@ def is_interactive() -> bool:
|
|
|
112
112
|
|
|
113
113
|
def _available(found: update_check.UpdateAvailable) -> str:
|
|
114
114
|
return t("update.available", "agent-toggle %s is available (you have %s)",
|
|
115
|
-
_clean(found.latest), _clean(found.current))
|
|
115
|
+
_clean(found.latest).removeprefix("v"), _clean(found.current))
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def hint(found: update_check.UpdateAvailable) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-toggle
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.2
|
|
4
4
|
Summary: Temporarily disable / re-enable AI-agent skills, agents, commands, rules, plugins and MCP servers across harnesses (Claude Code, Codex, Grok, OpenCode, OpenClaw, Copilot, Vibe, Devin, Antigravity)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/weskao/agent-toggle
|
|
@@ -25,7 +25,7 @@ Requires-Python: >=3.10
|
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
27
|
Requires-Dist: telegram-kit<0.3,>=0.2.2
|
|
28
|
-
Requires-Dist: update-kit<0.3,>=0.2.
|
|
28
|
+
Requires-Dist: update-kit<0.3,>=0.2.2
|
|
29
29
|
Provides-Extra: windows
|
|
30
30
|
Requires-Dist: windows-curses; sys_platform == "win32" and extra == "windows"
|
|
31
31
|
Provides-Extra: telegram
|
|
@@ -498,7 +498,7 @@ invalid value in either is ignored with one warning on stderr.
|
|
|
498
498
|
|
|
499
499
|
## Update check
|
|
500
500
|
|
|
501
|
-
On every invocation (unless disabled) agent-toggle checks
|
|
501
|
+
On every invocation (unless disabled) agent-toggle checks the repository's release tags for a newer release. It starts
|
|
502
502
|
before the command and is offered after it, on every exit path; it overlaps the command and waits at most 0.8 s afterwards.
|
|
503
503
|
`update` and `completion` skip it.
|
|
504
504
|
|
|
@@ -513,9 +513,9 @@ before the command and is offered after it, on every exit path; it overlaps the
|
|
|
513
513
|
then ` agent-toggle update`.
|
|
514
514
|
- It never changes the exit code or stdout, so `--json` output stays one document.
|
|
515
515
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
516
|
+
The lookup is update-kit's: one `git ls-remote --tags --refs` of the repository on a background
|
|
517
|
+
thread, cached for 10 minutes in `~/.update-kit/state.json`. Only the version you chose to skip is
|
|
518
|
+
kept in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a normal git request. See `SECURITY.md`
|
|
519
519
|
for the hardening. To turn it off, set `AGENT_TOGGLE_UPDATE_CHECK=0` or the `update_check` setting
|
|
520
520
|
to off (the settings menu's "Check for updates").
|
|
521
521
|
|
|
@@ -931,7 +931,7 @@ bookkeeping.
|
|
|
931
931
|
| `parked/<sha8>/` | items parked by `--project` (`<sha8>` = first 8 hex of the SHA-1 of the resolved project dir) |
|
|
932
932
|
| `profiles/` | `<name>.json` profiles (dir `0700`, files `0600`) |
|
|
933
933
|
| `config.json` | [settings](#settings--config-menu) and the Telegram chat id (mode `0600`, atomic write, unknown keys kept) |
|
|
934
|
-
| `update-check.json` | the [update check](#update-check)'s
|
|
934
|
+
| `update-check.json` | the [update check](#update-check)'s skipped version (mode `0600`, atomic write) |
|
|
935
935
|
|
|
936
936
|
A killed run loses at most the one item in flight. If that was a flag write or a
|
|
937
937
|
dir move (with its companions), its `pending` record (the full entry, a flag's
|
|
@@ -27,7 +27,7 @@ classifiers = [
|
|
|
27
27
|
]
|
|
28
28
|
requires-python = ">=3.10"
|
|
29
29
|
# telegram-kit is core (stdlib-only) so a plain reinstall can't drop the Telegram settings
|
|
30
|
-
dependencies = ["telegram-kit>=0.2.2,<0.3", "update-kit>=0.2.
|
|
30
|
+
dependencies = ["telegram-kit>=0.2.2,<0.3", "update-kit>=0.2.2,<0.3"]
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
33
33
|
Homepage = "https://github.com/weskao/agent-toggle"
|
|
@@ -159,13 +159,13 @@ class SandboxInvariantTest(SandboxCase):
|
|
|
159
159
|
self.assertEqual(Path(agent_toggle.__file__).resolve().parent, REPO / "agent_toggle")
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
TAG_99 = "v99.0.0" # update_kit.check returns the release tag
|
|
163
163
|
HINT = f"agent-toggle 99.0.0 is available (you have {__version__})"
|
|
164
164
|
|
|
165
165
|
|
|
166
166
|
class UpdateCheckTest(CliCase):
|
|
167
167
|
"""The update check: started before dispatch, offered in main()'s finally; and `update` /
|
|
168
|
-
`--update`. No network:
|
|
168
|
+
`--update`. No network: update_kit.check and update_kit.update are patched;
|
|
169
169
|
AGENT_TOGGLE_UPDATE_CHECK=0 (tests/base.py) is lifted here only."""
|
|
170
170
|
|
|
171
171
|
def setUp(self) -> None:
|
|
@@ -174,9 +174,9 @@ class UpdateCheckTest(CliCase):
|
|
|
174
174
|
env.start()
|
|
175
175
|
self.addCleanup(env.stop)
|
|
176
176
|
os.environ.pop("AGENT_TOGGLE_UPDATE_CHECK", None)
|
|
177
|
-
for target, kw in ((
|
|
178
|
-
(
|
|
179
|
-
|
|
177
|
+
for target, kw in ((update_prompt, {"is_interactive": mock.Mock(return_value=False)}),
|
|
178
|
+
(update_kit, {"check": mock.Mock(return_value=TAG_99),
|
|
179
|
+
"update": mock.Mock(return_value=0)}), # never install
|
|
180
180
|
(fs, {"WIN": False})): # the panel/upgrade flow; Windows has its own test
|
|
181
181
|
patch = mock.patch.multiple(target, **kw)
|
|
182
182
|
patch.start()
|
|
@@ -193,7 +193,7 @@ class UpdateCheckTest(CliCase):
|
|
|
193
193
|
self.assertNotIn("99.0.0", self.run_cli("status")[2])
|
|
194
194
|
settings.set("update_check", False)
|
|
195
195
|
self.assertNotIn("99.0.0", self.run_cli("status")[2])
|
|
196
|
-
|
|
196
|
+
update_kit.check.assert_not_called()
|
|
197
197
|
|
|
198
198
|
def test_json_stdout_is_one_document_and_never_prompts(self) -> None:
|
|
199
199
|
update_prompt.is_interactive.return_value = True
|
|
@@ -291,7 +291,7 @@ class UpdateCheckTest(CliCase):
|
|
|
291
291
|
self.assertNotIn("99.0.0", self.run_cli("update")[2])
|
|
292
292
|
self.assertNotIn("99.0.0", self.run_cli("completion", "bash")[2])
|
|
293
293
|
ask.assert_not_called()
|
|
294
|
-
|
|
294
|
+
update_kit.check.assert_not_called()
|
|
295
295
|
|
|
296
296
|
def test_update_and_dash_dash_update_exit_with_update_kits_code(self) -> None:
|
|
297
297
|
for argv in (("update",), ("--update",), ("--json", "--update")):
|