@andespindola/crow-skills 0.9.0 → 0.10.1
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/README.md +3 -3
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/broadcast.py +987 -0
- package/crow_skills/cli.py +170 -0
- package/crow_skills/events.py +46 -4
- package/crow_skills/hook.py +256 -13
- package/crow_skills/integrations.py +277 -26
- package/crow_skills/resources.py +326 -102
- package/docs/cli.md +43 -17
- package/package.json +1 -1
- package/release.json +26 -16
- package/skills/crow-behaviour/SKILL.md +3 -1
- package/skills/crow-behaviour/references/broadcast.md +45 -0
- package/skills/crow-behaviour/references/orchestration.md +2 -0
- package/skills/crow-behaviour/references/progress-ledger.md +1 -1
- package/skills/crow-behaviour/references/resources.md +25 -5
- package/skills/crow-behaviour/scripts/crow_ledger.py +7 -1
package/crow_skills/hook.py
CHANGED
|
@@ -18,6 +18,15 @@ On a prompt event the hook also appends one memory line to the context, only
|
|
|
18
18
|
when the machine is under pressure or the recommended parallelism is not
|
|
19
19
|
"parallel" (crow_resources.py, copied beside this file; cached for a few
|
|
20
20
|
seconds, so most prompts cost one small file read).
|
|
21
|
+
|
|
22
|
+
crow-broadcast (crow_broadcast.py, copied beside this file): every event also
|
|
23
|
+
keeps the session's presence (git repository, CLI pid, PATHS of files its edit
|
|
24
|
+
tools target) in the snapshot; a prompt event appends a short digest of other
|
|
25
|
+
live sessions in the same repository, only when there are any; ``--check``
|
|
26
|
+
(a synchronous PreToolUse entry for edit and shell tools) warns about another
|
|
27
|
+
live session on the same file or clone without writing anything.
|
|
28
|
+
|
|
29
|
+
crow_hook.py hook --host HOST --event PreToolUse --check [--home DIR]
|
|
21
30
|
"""
|
|
22
31
|
from datetime import datetime, timezone
|
|
23
32
|
import hashlib
|
|
@@ -193,6 +202,34 @@ def resources_module():
|
|
|
193
202
|
return _RESOURCES or None
|
|
194
203
|
|
|
195
204
|
|
|
205
|
+
_BROADCAST = None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def broadcast_module():
|
|
209
|
+
"""crow-broadcast: the package module, or crow_broadcast.py beside this file; None when missing."""
|
|
210
|
+
global _BROADCAST
|
|
211
|
+
if _BROADCAST is None:
|
|
212
|
+
_BROADCAST = False
|
|
213
|
+
try:
|
|
214
|
+
if __package__:
|
|
215
|
+
from . import broadcast as module # type: ignore[attr-defined]
|
|
216
|
+
_BROADCAST = module
|
|
217
|
+
else:
|
|
218
|
+
import importlib.util
|
|
219
|
+
here = Path(os.path.abspath(__file__)).parent
|
|
220
|
+
for name in ("crow_broadcast.py", "broadcast.py"):
|
|
221
|
+
path = here / name
|
|
222
|
+
if path.is_file() and not path.is_symlink():
|
|
223
|
+
spec = importlib.util.spec_from_file_location("crow_broadcast", path)
|
|
224
|
+
module = importlib.util.module_from_spec(spec)
|
|
225
|
+
spec.loader.exec_module(module)
|
|
226
|
+
_BROADCAST = module
|
|
227
|
+
break
|
|
228
|
+
except Exception: # noqa: BLE001 - a broken module never breaks the hook
|
|
229
|
+
_BROADCAST = False
|
|
230
|
+
return _BROADCAST or None
|
|
231
|
+
|
|
232
|
+
|
|
196
233
|
def resource_hint(home):
|
|
197
234
|
"""The one-line memory hint, or None (no pressure, no probe, or any failure)."""
|
|
198
235
|
module = resources_module()
|
|
@@ -277,8 +314,11 @@ def git_branch(cwd):
|
|
|
277
314
|
return None
|
|
278
315
|
|
|
279
316
|
|
|
280
|
-
def update_session(home, event, extra=None, fallback_cwd=None):
|
|
281
|
-
"""Fold one event into the per-session snapshot (state, counters, last action).
|
|
317
|
+
def update_session(home, event, extra=None, fallback_cwd=None, presence=None):
|
|
318
|
+
"""Fold one event into the per-session snapshot (state, counters, last action).
|
|
319
|
+
|
|
320
|
+
presence: (broadcast module, touched paths, parent pid) to fold the crow-broadcast
|
|
321
|
+
presence in the same locked write; a failure there never loses the event."""
|
|
282
322
|
directory = no_follow_dir(events_dir(home) / "sessions")
|
|
283
323
|
key = session_key(event.get("host"), event.get("session"), event.get("cwd"))
|
|
284
324
|
path = directory / (key + ".json")
|
|
@@ -332,6 +372,12 @@ def update_session(home, event, extra=None, fallback_cwd=None):
|
|
|
332
372
|
snap["updatedAt"] = event.get("ts") or now_iso()
|
|
333
373
|
if extra:
|
|
334
374
|
snap.update(extra)
|
|
375
|
+
if presence:
|
|
376
|
+
module, touched, ppid = presence
|
|
377
|
+
try:
|
|
378
|
+
module.fold(snap, event, touched, ppid=ppid)
|
|
379
|
+
except Exception: # noqa: BLE001 - presence is best effort
|
|
380
|
+
pass
|
|
335
381
|
write_json(path, snap)
|
|
336
382
|
return snap
|
|
337
383
|
finally:
|
|
@@ -352,13 +398,13 @@ def prune_sessions(home, now=None):
|
|
|
352
398
|
pass
|
|
353
399
|
|
|
354
400
|
|
|
355
|
-
def record(home, fields, snapshot=False, fallback_cwd=None):
|
|
401
|
+
def record(home, fields, snapshot=False, fallback_cwd=None, presence=None):
|
|
356
402
|
"""Stamp and append an event; optionally fold it into the session snapshot."""
|
|
357
403
|
event = {"v": 1, "schema": EVENT_SCHEMA, "ts": now_iso()}
|
|
358
404
|
event.update({k: v for k, v in fields.items() if v is not None})
|
|
359
405
|
append_event(home, event)
|
|
360
406
|
if snapshot:
|
|
361
|
-
return event, update_session(home, event, fallback_cwd=fallback_cwd)
|
|
407
|
+
return event, update_session(home, event, fallback_cwd=fallback_cwd, presence=presence)
|
|
362
408
|
return event, None
|
|
363
409
|
|
|
364
410
|
|
|
@@ -380,7 +426,7 @@ def sessions(home, live_only=True, now=None):
|
|
|
380
426
|
continue
|
|
381
427
|
snap["ageSeconds"] = int(age)
|
|
382
428
|
result.append(snap)
|
|
383
|
-
return sorted(result, key=lambda s: s.get("updatedAt") or "", reverse=True)
|
|
429
|
+
return sorted(result, key=lambda s: str(s.get("updatedAt") or ""), reverse=True)
|
|
384
430
|
|
|
385
431
|
|
|
386
432
|
def tail(home, limit=50):
|
|
@@ -436,14 +482,26 @@ def foreign(host, payload, env, home):
|
|
|
436
482
|
return False
|
|
437
483
|
|
|
438
484
|
|
|
485
|
+
def tool_call(payload):
|
|
486
|
+
"""Antigravity sends the tool as {"toolCall": {"name", "args"}}."""
|
|
487
|
+
call = payload.get("toolCall")
|
|
488
|
+
return call if isinstance(call, dict) else {}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def workspace(payload):
|
|
492
|
+
"""Antigravity runs hooks from the hooks.json directory and sends the workspace as workspacePaths."""
|
|
493
|
+
paths = payload.get("workspacePaths")
|
|
494
|
+
return paths[0] if isinstance(paths, list) and paths and isinstance(paths[0], str) else None
|
|
495
|
+
|
|
496
|
+
|
|
439
497
|
def normalise(host, raw, payload):
|
|
440
498
|
kind = EVENTS.get(raw, "other")
|
|
441
|
-
tool = token(first(payload, "tool_name", "toolName", "tool"), 64)
|
|
499
|
+
tool = token(first(payload, "tool_name", "toolName", "tool") or tool_call(payload).get("name"), 64)
|
|
442
500
|
tool_input = payload.get("tool_input") if isinstance(payload.get("tool_input"), dict) else {}
|
|
443
501
|
event = {
|
|
444
502
|
"host": host, "event": kind, "raw": token(raw, 40),
|
|
445
503
|
"session": token(first(payload, "session_id", "sessionId", "conversation_id", "conversationId", "thread_id"), 128),
|
|
446
|
-
"cwd": plain_path(first(payload, "cwd", "workspace_root", "workspaceRoot")),
|
|
504
|
+
"cwd": plain_path(first(payload, "cwd", "workspace_root", "workspaceRoot") or workspace(payload)),
|
|
447
505
|
"model": token(first(payload, "model", "model_name", "modelName"), 80) if not isinstance(payload.get("model"), dict)
|
|
448
506
|
else token(payload["model"].get("id") or payload["model"].get("display_name"), 80),
|
|
449
507
|
"permissionMode": token(first(payload, "permission_mode", "permissionMode", "approval_policy", "approvalMode"), 40),
|
|
@@ -495,11 +553,107 @@ def output_for(host, raw, context=None, system=None, block=None):
|
|
|
495
553
|
return out or None
|
|
496
554
|
|
|
497
555
|
|
|
498
|
-
|
|
556
|
+
TOOL_START_EVENTS = ("PreToolUse", "BeforeTool")
|
|
557
|
+
SHELL_NAMES = frozenset(("Bash", "shell", "exec_command", "local_shell", "run_shell_command", "run_command", "exec", "bash"))
|
|
558
|
+
# Edit and shell tool names of every host (crow_broadcast.EDIT_TOOLS and SHELL_TOOLS; a test keeps
|
|
559
|
+
# them equal): only these tool events load the broadcast module.
|
|
560
|
+
BROADCAST_TOOLS = frozenset((
|
|
561
|
+
"Edit", "Write", "MultiEdit", "NotebookEdit", "apply_patch", "write_file", "replace", "code_action", "file_change",
|
|
562
|
+
"propose_code", "edit_notebook", "write_blob", "write_to_file", "replace_file_content", "multi_replace_file_content",
|
|
563
|
+
"edit", "write", "notebook_edit",
|
|
564
|
+
"Bash", "shell", "exec_command", "local_shell", "run_shell_command", "run_command", "exec", "bash"))
|
|
565
|
+
# Hosts whose tool-start hook is synchronous in our install (its output is read): the warning goes
|
|
566
|
+
# out from the same call. Claude Code and Codex record asynchronously and get a separate --check entry.
|
|
567
|
+
SYNC_TOOL_HOSTS = ("gemini", "agy", "devin")
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def tool_input_of(payload):
|
|
571
|
+
args = tool_call(payload).get("args")
|
|
572
|
+
if isinstance(args, dict):
|
|
573
|
+
return args
|
|
574
|
+
for key in ("tool_input", "toolInput", "input", "tool_args", "arguments", "args"):
|
|
575
|
+
value = payload.get(key)
|
|
576
|
+
if isinstance(value, dict):
|
|
577
|
+
return value
|
|
578
|
+
if isinstance(value, str) and key in ("arguments", "tool_input") and value[:1] == "{" and len(value) < MAX_STDIN:
|
|
579
|
+
try:
|
|
580
|
+
parsed = json.loads(value)
|
|
581
|
+
except ValueError:
|
|
582
|
+
continue
|
|
583
|
+
if isinstance(parsed, dict):
|
|
584
|
+
return parsed
|
|
585
|
+
return {}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# Where a warning can reach the model before/after a tool, per host (checked on the installed
|
|
589
|
+
# versions, see docs/broadcast.md): Claude Code and Codex read additionalContext on PreToolUse;
|
|
590
|
+
# Gemini CLI and Devin only on AfterTool/PostToolUse (their pre-tool hook can only block);
|
|
591
|
+
# Antigravity's PreToolUse reads only a decision (the next PreInvocation digest carries the warning).
|
|
592
|
+
PRE_CONTEXT_HOSTS = ("claude", "codex")
|
|
593
|
+
POST_CONTEXT_HOSTS = ("gemini", "devin")
|
|
594
|
+
POST_TOOL_EVENTS = ("PostToolUse", "AfterTool")
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def tool_output(host, raw, text, block=False):
|
|
598
|
+
"""Tool-hook warning (model-visible where the host reads it) or, with block, a refusal."""
|
|
599
|
+
if block:
|
|
600
|
+
if host == "claude":
|
|
601
|
+
return {"hookSpecificOutput": {"hookEventName": raw, "permissionDecision": "deny", "permissionDecisionReason": text,
|
|
602
|
+
"additionalContext": text}}
|
|
603
|
+
if host in ("agy", "gemini"):
|
|
604
|
+
return {"decision": "deny", "reason": text}
|
|
605
|
+
return {"decision": "block", "reason": text}
|
|
606
|
+
if raw in TOOL_START_EVENTS and host not in PRE_CONTEXT_HOSTS:
|
|
607
|
+
return None
|
|
608
|
+
return {"hookSpecificOutput": {"hookEventName": raw, "additionalContext": text}, "systemMessage": text.split("\n", 1)[0]}
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def broadcast_check(host, raw, home, module, me, touched, tree):
|
|
612
|
+
"""Run the conflict check; returns the host output or None. Never raises."""
|
|
613
|
+
try:
|
|
614
|
+
cfg = module.config(home)
|
|
615
|
+
pre = raw in TOOL_START_EVENTS
|
|
616
|
+
block = pre and bool(cfg["block_on_conflict"])
|
|
617
|
+
if pre and not block and host not in PRE_CONTEXT_HOSTS:
|
|
618
|
+
return None # nothing this host would show before the tool; the post-tool or prompt hook warns
|
|
619
|
+
text, conflict = module.check(home, (me or {}).get("key"), me, touched, tree, cfg=cfg)
|
|
620
|
+
if not text:
|
|
621
|
+
return None
|
|
622
|
+
return tool_output(host, raw, text, block=block and conflict)
|
|
623
|
+
except Exception: # noqa: BLE001
|
|
624
|
+
return None
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def check_only(host, raw, payload, home):
|
|
628
|
+
"""--check: read-only conflict check before an edit/shell tool (no event, no snapshot write)."""
|
|
629
|
+
tool = first(payload, "tool_name", "toolName", "tool") or tool_call(payload).get("name")
|
|
630
|
+
shell_input = tool_input_of(payload)
|
|
631
|
+
command = shell_input.get("command") or shell_input.get("cmd") or shell_input.get("CommandLine") or shell_input.get("commandLine")
|
|
632
|
+
if tool in SHELL_NAMES and "git" not in json.dumps(command or ""):
|
|
633
|
+
return None # a shell command without git cannot switch the clone's branch: nothing to load
|
|
634
|
+
module = broadcast_module()
|
|
635
|
+
if module is None or raw not in TOOL_START_EVENTS:
|
|
636
|
+
return None
|
|
637
|
+
event = normalise(host, raw, payload)
|
|
638
|
+
cwd = event.get("cwd") or process_cwd()
|
|
639
|
+
tool_input = tool_input_of(payload)
|
|
640
|
+
touched = module.edit_paths(host, event.get("tool"), tool_input, cwd)
|
|
641
|
+
tree = module.tree_command(host, event.get("tool"), tool_input, cwd)
|
|
642
|
+
if not touched and not tree:
|
|
643
|
+
return None
|
|
644
|
+
key = session_key(host, event.get("session"), event.get("cwd"))
|
|
645
|
+
me = read_json(events_dir(home) / "sessions" / (key + ".json")) or {}
|
|
646
|
+
me = dict(me, key=key, cwd=me.get("cwd") or cwd)
|
|
647
|
+
return broadcast_check(host, raw, home, module, me, touched, tree)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def handle(host, raw, payload, home, env=None, check=False):
|
|
499
651
|
"""Process one hook call; returns the JSON to print (or None). Never raises to the caller."""
|
|
500
652
|
env = os.environ if env is None else env
|
|
501
653
|
if host not in HOSTS or not isinstance(payload, dict) or foreign(host, payload, env, home):
|
|
502
654
|
return None
|
|
655
|
+
if check:
|
|
656
|
+
return check_only(host, raw, payload, home)
|
|
503
657
|
event = normalise(host, raw, payload)
|
|
504
658
|
current = read_mode(home)
|
|
505
659
|
if raw in PROMPT_EVENTS:
|
|
@@ -512,7 +666,49 @@ def handle(host, raw, payload, home, env=None):
|
|
|
512
666
|
current = write_mode(home, wanted, host)
|
|
513
667
|
record(home, dict(event, event="mode_command", mode=current["mode"]), snapshot=True, fallback_cwd=process_cwd())
|
|
514
668
|
return output_for(host, raw, block=indicator(current["mode"]) + " (crow-skills; vale para todos os CLIs a partir do próximo prompt)")
|
|
515
|
-
|
|
669
|
+
# The broadcast module is loaded only where it has work: prompts and session start (repo,
|
|
670
|
+
# pid, digest) and tool events of edit/shell tools (paths, checks). Other events keep the
|
|
671
|
+
# 0.9.0 cost; the snapshot's updatedAt is the heartbeat.
|
|
672
|
+
wanted = raw in PROMPT_EVENTS or raw == "SessionStart" or \
|
|
673
|
+
(event["event"] in ("tool_start", "tool_end") and event.get("tool") in BROADCAST_TOOLS)
|
|
674
|
+
module, presence, touched, tree, checked = broadcast_module() if wanted else None, None, [], [], []
|
|
675
|
+
if module is not None:
|
|
676
|
+
try:
|
|
677
|
+
cfg = module.config(home)
|
|
678
|
+
if cfg["enabled"]:
|
|
679
|
+
if event["event"] == "tool_start":
|
|
680
|
+
tool_input = tool_input_of(payload)
|
|
681
|
+
cwd = event.get("cwd") or process_cwd()
|
|
682
|
+
touched = module.edit_paths(host, event.get("tool"), tool_input, cwd)
|
|
683
|
+
checked = list(touched)
|
|
684
|
+
tree = module.tree_command(host, event.get("tool"), tool_input, cwd)
|
|
685
|
+
if touched and cfg["block_on_conflict"]:
|
|
686
|
+
# An edit the conflict check is about to refuse is not published as this
|
|
687
|
+
# session's: otherwise the session that edited first would be locked out too.
|
|
688
|
+
key = session_key(host, event.get("session"), event.get("cwd"))
|
|
689
|
+
mine = read_json(events_dir(home) / "sessions" / (key + ".json")) or {}
|
|
690
|
+
mine = dict(mine, key=key, cwd=mine.get("cwd") or cwd)
|
|
691
|
+
if module.check(home, key, mine, touched, (), cfg=cfg)[1]:
|
|
692
|
+
touched = []
|
|
693
|
+
presence = (module, touched, os.getppid())
|
|
694
|
+
else:
|
|
695
|
+
module = None
|
|
696
|
+
except Exception: # noqa: BLE001 - the broadcast never breaks the event
|
|
697
|
+
module, presence = None, None
|
|
698
|
+
stamped, snap = record(home, dict(event, mode=current["mode"]), snapshot=True, fallback_cwd=process_cwd(), presence=presence)
|
|
699
|
+
if module is not None and host in SYNC_TOOL_HOSTS:
|
|
700
|
+
if raw in TOOL_START_EVENTS and (touched or tree or checked):
|
|
701
|
+
return broadcast_check(host, raw, home, module, snap, checked or touched, tree)
|
|
702
|
+
if raw in POST_TOOL_EVENTS and host in POST_CONTEXT_HOSTS:
|
|
703
|
+
try:
|
|
704
|
+
tool_input = tool_input_of(payload)
|
|
705
|
+
cwd = event.get("cwd") or process_cwd()
|
|
706
|
+
after = module.edit_paths(host, event.get("tool"), tool_input, cwd)
|
|
707
|
+
tree_after = module.tree_command(host, event.get("tool"), tool_input, cwd)
|
|
708
|
+
except Exception: # noqa: BLE001
|
|
709
|
+
after, tree_after = [], []
|
|
710
|
+
if after or tree_after:
|
|
711
|
+
return broadcast_check(host, raw, home, module, snap, after, tree_after)
|
|
516
712
|
if raw not in PROMPT_EVENTS and raw != "SessionStart":
|
|
517
713
|
return None
|
|
518
714
|
system = None
|
|
@@ -528,6 +724,13 @@ def handle(host, raw, payload, home, env=None):
|
|
|
528
724
|
memory = resource_hint(home)
|
|
529
725
|
if memory:
|
|
530
726
|
context += "\n" + memory
|
|
727
|
+
if module is not None and snap is not None:
|
|
728
|
+
try:
|
|
729
|
+
others = module.digest(home, snap.get("key"), me=snap)
|
|
730
|
+
except Exception: # noqa: BLE001
|
|
731
|
+
others = None
|
|
732
|
+
if others:
|
|
733
|
+
context += "\n" + others
|
|
531
734
|
return output_for(host, raw, context=context, system=system)
|
|
532
735
|
|
|
533
736
|
|
|
@@ -571,7 +774,36 @@ def update_metrics(home, host, payload):
|
|
|
571
774
|
|
|
572
775
|
# --- entry point --------------------------------------------------------------------------
|
|
573
776
|
|
|
777
|
+
def fast_hook_args(argv):
|
|
778
|
+
"""The hook's own argv parsed without argparse (~5 ms per call); None when it is anything else."""
|
|
779
|
+
if not argv or argv[0] != "hook":
|
|
780
|
+
return None
|
|
781
|
+
values, flags, i = {}, set(), 1
|
|
782
|
+
while i < len(argv):
|
|
783
|
+
name = argv[i]
|
|
784
|
+
if name in ("--host", "--event", "--home") and i + 1 < len(argv):
|
|
785
|
+
values[name[2:]] = argv[i + 1]
|
|
786
|
+
i += 2
|
|
787
|
+
elif name == "--check":
|
|
788
|
+
flags.add("check")
|
|
789
|
+
i += 1
|
|
790
|
+
else:
|
|
791
|
+
return None
|
|
792
|
+
if "host" not in values or "event" not in values:
|
|
793
|
+
return None
|
|
794
|
+
import types
|
|
795
|
+
return types.SimpleNamespace(command="hook", host=values["host"], event=values["event"], home=values.get("home"),
|
|
796
|
+
check="check" in flags)
|
|
797
|
+
|
|
798
|
+
|
|
574
799
|
def main(argv=None):
|
|
800
|
+
args = fast_hook_args(list(sys.argv[1:] if argv is None else argv))
|
|
801
|
+
if args is None:
|
|
802
|
+
args = parse_args(argv)
|
|
803
|
+
return run_main(args)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def parse_args(argv):
|
|
575
807
|
import argparse
|
|
576
808
|
p = argparse.ArgumentParser(prog="crow_hook", description="Hook e modo do crow-skills.")
|
|
577
809
|
sub = p.add_subparsers(dest="command", required=True)
|
|
@@ -579,12 +811,16 @@ def main(argv=None):
|
|
|
579
811
|
hook.add_argument("--host", required=True)
|
|
580
812
|
hook.add_argument("--event", required=True)
|
|
581
813
|
hook.add_argument("--home")
|
|
814
|
+
hook.add_argument("--check", action="store_true")
|
|
582
815
|
mode = sub.add_parser("mode")
|
|
583
816
|
mode.add_argument("action", nargs="?", default="status", choices=("on", "off", "status", "toggle"))
|
|
584
817
|
mode.add_argument("--home")
|
|
585
818
|
mode.add_argument("--by")
|
|
586
819
|
mode.add_argument("--json", action="store_true")
|
|
587
|
-
|
|
820
|
+
return p.parse_args(argv)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def run_main(args):
|
|
588
824
|
home = Path(args.home).expanduser() if args.home else Path.home()
|
|
589
825
|
if args.command == "mode":
|
|
590
826
|
current = read_mode(home)
|
|
@@ -602,11 +838,18 @@ def main(argv=None):
|
|
|
602
838
|
try:
|
|
603
839
|
raw = sys.stdin.buffer.read(MAX_STDIN + 1) if not sys.stdin.isatty() else b""
|
|
604
840
|
payload = json.loads(raw.decode("utf-8")) if raw.strip() and len(raw) <= MAX_STDIN else {}
|
|
605
|
-
result = handle(args.host, args.event, payload, home)
|
|
606
|
-
if args.event == "SessionStart":
|
|
607
|
-
prune_sessions(home)
|
|
841
|
+
result = handle(args.host, args.event, payload, home, check=args.check)
|
|
608
842
|
if result:
|
|
609
843
|
sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n")
|
|
844
|
+
sys.stdout.flush()
|
|
845
|
+
if args.event == "SessionStart" and not args.check:
|
|
846
|
+
prune_sessions(home)
|
|
847
|
+
module = broadcast_module()
|
|
848
|
+
if module is not None:
|
|
849
|
+
try:
|
|
850
|
+
module.prune_claims(home)
|
|
851
|
+
except Exception: # noqa: BLE001 - pruning is housekeeping; the output already went out
|
|
852
|
+
pass
|
|
610
853
|
except Exception: # noqa: BLE001 - a hook must never break the agent
|
|
611
854
|
return 0
|
|
612
855
|
return 0
|