@caddis/cli 0.4.61 → 0.4.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundles/antigravity-plugin/caddis_statusline.py +78 -6
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/repo_freshness.py +13 -1
- package/bundles/antigravity-plugin/scripts/caddis_hidden_desktop.py +232 -0
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +2 -1
- package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +62 -5
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +306 -54
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +3 -0
- package/bundles/antigravity-plugin/scripts/caddis_spend.py +25 -3
- package/bundles/antigravity-plugin/scripts/caddis_statusline.py +78 -6
- package/bundles/antigravity-plugin/scripts/caddis_todo.py +4 -1
- package/bundles/antigravity-plugin/scripts/caddis_workstreams.py +3 -1
- package/bundles/manifest.json +2 -2
- package/package.json +1 -1
|
@@ -71,8 +71,9 @@ DEFAULT_CONFIG = {
|
|
|
71
71
|
"bar_width": 8,
|
|
72
72
|
"token_counts": True, # ◉ 412k/1M — off falls back to a bare ctx:41%
|
|
73
73
|
"cached_tokens": True, # ⚡38k cached — rendered LOUD, see render_claude()
|
|
74
|
-
"ram":
|
|
75
|
-
"
|
|
74
|
+
"ram": False, # 💻 69% (44 GB) - off by default (owner, 20 Oct 2026)
|
|
75
|
+
"thinking": False, # Claude's "thinking" flag, agy's "thinking" state - off by default (20 Oct 2026)
|
|
76
|
+
"clock": False, # ◷ 03:41 - off by default (owner, 20 Oct 2026)
|
|
76
77
|
"relay": True, # ⏺ relay (a parked resume pointer exists)
|
|
77
78
|
"rate_limits": True, # 5h:12% 7d:40%
|
|
78
79
|
"session_id": False, # #a1b2c3d4
|
|
@@ -468,7 +469,7 @@ def cached_segment(c: C, cfg: dict, cached: int) -> str:
|
|
|
468
469
|
|
|
469
470
|
|
|
470
471
|
def ram_segment(c: C, cfg: dict) -> str:
|
|
471
|
-
if not cfg.get("ram",
|
|
472
|
+
if not cfg.get("ram", False):
|
|
472
473
|
return ""
|
|
473
474
|
min_w = as_int(cfg.get("ram_min_width"), 0)
|
|
474
475
|
if min_w and term_width() < min_w:
|
|
@@ -482,7 +483,7 @@ def ram_segment(c: C, cfg: dict) -> str:
|
|
|
482
483
|
|
|
483
484
|
|
|
484
485
|
def clock_segment(c: C, cfg: dict) -> str:
|
|
485
|
-
if not cfg.get("clock",
|
|
486
|
+
if not cfg.get("clock", False):
|
|
486
487
|
return ""
|
|
487
488
|
return c.dim + icon(cfg, "clock") + time.strftime("%H:%M") + c.reset
|
|
488
489
|
|
|
@@ -549,7 +550,7 @@ def render_claude(data: dict, cfg: dict) -> str:
|
|
|
549
550
|
if style and str(style).lower() != "default":
|
|
550
551
|
parts.append(c.blue + "style:" + str(style) + c.reset)
|
|
551
552
|
|
|
552
|
-
if dig(data, "thinking", "enabled") is True:
|
|
553
|
+
if cfg.get("thinking", False) and dig(data, "thinking", "enabled") is True:
|
|
553
554
|
parts.append(c.magenta + "thinking" + c.reset)
|
|
554
555
|
|
|
555
556
|
if cfg.get("rate_limits", True):
|
|
@@ -581,7 +582,7 @@ def render_claude(data: dict, cfg: dict) -> str:
|
|
|
581
582
|
|
|
582
583
|
session_name = dig(data, "session_name")
|
|
583
584
|
if session_name:
|
|
584
|
-
parts.append(c.
|
|
585
|
+
parts.append(c.bold + c.magenta + str(session_name) + c.reset) # bright, so the session is easy to spot
|
|
585
586
|
|
|
586
587
|
if cfg.get("session_id", False):
|
|
587
588
|
sid = str(dig(data, "session_id") or "")
|
|
@@ -616,6 +617,8 @@ def render_agy(data: dict, cfg: dict) -> str:
|
|
|
616
617
|
parts.append(c.white + c.bold + "agy" + c.reset)
|
|
617
618
|
|
|
618
619
|
state = str(dig(data, "agent_state") or dig(data, "execution_mode") or "").lower()
|
|
620
|
+
if state == "thinking" and not cfg.get("thinking", False):
|
|
621
|
+
state = "" # working and idle still show
|
|
619
622
|
if state:
|
|
620
623
|
label, col = {
|
|
621
624
|
"working": (icon(cfg, "bolt") + "working", c.green),
|
|
@@ -704,6 +707,61 @@ HOST_SETTINGS = {
|
|
|
704
707
|
}
|
|
705
708
|
|
|
706
709
|
|
|
710
|
+
VERSION_STAMP = INSTALL_DIR / "statusline.version"
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def plugin_version(here: Path | None = None) -> str:
|
|
714
|
+
"""The caddis version of the plugin this file ships in: `.claude-plugin/plugin.json` or a root `plugin.json`
|
|
715
|
+
one folder above `scripts/`. "" in a source checkout or when unreadable."""
|
|
716
|
+
base = Path(here or Path(__file__).resolve().parent).parent
|
|
717
|
+
for manifest in (base / ".claude-plugin" / "plugin.json", base / "plugin.json"):
|
|
718
|
+
try:
|
|
719
|
+
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
720
|
+
except Exception:
|
|
721
|
+
continue
|
|
722
|
+
if isinstance(data, dict) and str(data.get("name", "")).startswith("caddis") and data.get("version"):
|
|
723
|
+
return str(data["version"])
|
|
724
|
+
return ""
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _version_key(text: str):
|
|
728
|
+
parts = str(text or "").strip().split(".")
|
|
729
|
+
return tuple(int(x) for x in parts) if parts and all(x.isdigit() for x in parts) else None
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def refresh_installed(src: Path | None = None, version: str | None = None) -> bool:
|
|
733
|
+
"""Bring the installed renderer (`~/.caddis/statusline.py`) up to this plugin's copy.
|
|
734
|
+
|
|
735
|
+
Why (20 Oct 2026): only `--install` copied the renderer, so a caddis update never reached it. This machine
|
|
736
|
+
kept a copy whose `git` calls opened a console window each time agy drew its status line.
|
|
737
|
+
Rules: nothing happens unless the status line was installed; the copy happens only when this plugin's version
|
|
738
|
+
is newer than the one stamped at the last copy (an older session never downgrades it); a source checkout
|
|
739
|
+
(no plugin version) never copies. Returns True when it copied."""
|
|
740
|
+
src = Path(src or Path(__file__).resolve())
|
|
741
|
+
version = plugin_version() if version is None else version
|
|
742
|
+
if not INSTALLED_SCRIPT.is_file() or _version_key(version) is None:
|
|
743
|
+
return False
|
|
744
|
+
try:
|
|
745
|
+
stamped = VERSION_STAMP.read_text(encoding="utf-8").strip()
|
|
746
|
+
except OSError:
|
|
747
|
+
stamped = ""
|
|
748
|
+
old = _version_key(stamped)
|
|
749
|
+
if old is not None and old >= _version_key(version):
|
|
750
|
+
return False
|
|
751
|
+
try:
|
|
752
|
+
if src.resolve() != INSTALLED_SCRIPT.resolve() and src.read_bytes() != INSTALLED_SCRIPT.read_bytes():
|
|
753
|
+
tmp = INSTALLED_SCRIPT.with_name("%s.%d.tmp" % (INSTALLED_SCRIPT.name, os.getpid()))
|
|
754
|
+
shutil.copyfile(str(src), str(tmp))
|
|
755
|
+
os.replace(str(tmp), str(INSTALLED_SCRIPT))
|
|
756
|
+
copied = True
|
|
757
|
+
else:
|
|
758
|
+
copied = False
|
|
759
|
+
VERSION_STAMP.write_text(version + "\n", encoding="utf-8")
|
|
760
|
+
return copied
|
|
761
|
+
except OSError:
|
|
762
|
+
return False # a session start must never fail over the status line
|
|
763
|
+
|
|
764
|
+
|
|
707
765
|
def python_command() -> str:
|
|
708
766
|
"""The interpreter NAME to write into settings — not an absolute path.
|
|
709
767
|
|
|
@@ -726,6 +784,14 @@ def install(hosts, dry_run: bool = False) -> int:
|
|
|
726
784
|
# because the version is in the path.
|
|
727
785
|
if not dry_run and src != INSTALLED_SCRIPT.resolve():
|
|
728
786
|
shutil.copyfile(str(src), str(INSTALLED_SCRIPT))
|
|
787
|
+
if not dry_run:
|
|
788
|
+
# Stamp the version, so a later --refresh from an OLDER plugin session cannot replace this copy.
|
|
789
|
+
version = plugin_version()
|
|
790
|
+
if _version_key(version) is not None:
|
|
791
|
+
try:
|
|
792
|
+
VERSION_STAMP.write_text(version + "\n", encoding="utf-8")
|
|
793
|
+
except OSError:
|
|
794
|
+
pass
|
|
729
795
|
print("renderer -> %s" % INSTALLED_SCRIPT)
|
|
730
796
|
|
|
731
797
|
if not CONFIG_PATH.exists():
|
|
@@ -903,8 +969,14 @@ def main(argv=None) -> int:
|
|
|
903
969
|
parser.add_argument("--dry-run", action="store_true", help="with --install: print, write nothing")
|
|
904
970
|
parser.add_argument("--no-color", action="store_true", help="render without ANSI colour")
|
|
905
971
|
parser.add_argument("--title", action="store_true", help="render terminal title (agy)")
|
|
972
|
+
parser.add_argument("--refresh", action="store_true",
|
|
973
|
+
help="copy this plugin's renderer over an older installed one (session-start hook)")
|
|
906
974
|
args = parser.parse_args(argv)
|
|
907
975
|
|
|
976
|
+
if args.refresh:
|
|
977
|
+
refresh_installed()
|
|
978
|
+
return 0 # silent and never failing: it runs at every session start
|
|
979
|
+
|
|
908
980
|
# The line is Unicode. On Windows the default cp1252 stdout raises on ◆/⎇/█.
|
|
909
981
|
try:
|
|
910
982
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
@@ -43,7 +43,10 @@ def _path(root: Path, workstream: str | None) -> tuple[Path, Path]:
|
|
|
43
43
|
artifact = _artifact(root)
|
|
44
44
|
name = _name(workstream) if workstream is not None else "inbox"
|
|
45
45
|
folder = artifact / "todo-list"
|
|
46
|
-
|
|
46
|
+
# Only a folder that EXISTS can point elsewhere. Resolving one that does not exist yet raced a concurrent
|
|
47
|
+
# writer creating it: on Windows the two paths could resolve differently (a short 8.3 name on one side), and a
|
|
48
|
+
# safe add failed with this error (main CI, 20 Oct 2026, test_concurrent_inbox_add_loses_no_item).
|
|
49
|
+
if folder.is_symlink() or (folder.exists() and not folder.resolve().is_relative_to(artifact.resolve())):
|
|
47
50
|
raise ValueError("todo-list path escapes artifact root")
|
|
48
51
|
path = folder / f"{name}.md"
|
|
49
52
|
# Reads are unlocked, so refuse here as well as in locked_path. On Windows a writer replacing the file
|
|
@@ -58,7 +58,9 @@ def _plain_name(value: str) -> str:
|
|
|
58
58
|
def _artifact(root: Path) -> Path:
|
|
59
59
|
project = Path(root).resolve()
|
|
60
60
|
artifact = Path(root) / ".caddis"
|
|
61
|
-
|
|
61
|
+
# Only an existing .caddis can point elsewhere; resolving one that does not exist yet raced a concurrent writer
|
|
62
|
+
# creating it (see caddis_todo._path, 20 Oct 2026).
|
|
63
|
+
if artifact.is_symlink() or (artifact.exists() and not artifact.resolve().is_relative_to(project)):
|
|
62
64
|
raise ValueError("artifact root escapes project")
|
|
63
65
|
return artifact
|
|
64
66
|
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED