@ictechgy/context-guard 0.4.9 → 0.4.10
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/CHANGELOG.md +16 -0
- package/README.ko.md +41 -24
- package/README.md +66 -26
- package/docs/benchmark-fixtures/token-savings-12task-baseline.prompt.example.md +7 -0
- package/docs/benchmark-fixtures/token-savings-12task-contextguard.prompt.example.md +7 -0
- package/docs/benchmark-fixtures/token-savings-12task.tasks.example.json +182 -0
- package/docs/benchmark-fixtures/token-savings-12task.variants.example.json +10 -0
- package/docs/distribution.md +10 -7
- package/docs/experimental-benchmark-fixtures.md +8 -1
- package/package.json +3 -6
- package/packaging/homebrew/context-guard.rb.template +1 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +9 -6
- package/plugins/context-guard/README.md +21 -13
- package/plugins/context-guard/bin/context-guard +113 -26
- package/plugins/context-guard/bin/context-guard-artifact +542 -46
- package/plugins/context-guard/bin/context-guard-cache-score +380 -0
- package/plugins/context-guard/bin/context-guard-compress +146 -1
- package/plugins/context-guard/bin/context-guard-cost +783 -4
- package/plugins/context-guard/bin/context-guard-experiments +99 -18
- package/plugins/context-guard/bin/context-guard-failed-nudge +3 -0
- package/plugins/context-guard/bin/context-guard-filter +163 -7
- package/plugins/context-guard/bin/context-guard-guard-read +3 -0
- package/plugins/context-guard/bin/context-guard-pack +602 -43
- package/plugins/context-guard/bin/context-guard-rewrite-bash +3 -0
- package/plugins/context-guard/bin/context-guard-setup +165 -31
- package/plugins/context-guard/bin/context-guard-statusline +490 -283
- package/plugins/context-guard/bin/context-guard-statusline-merged +5 -0
- package/plugins/context-guard/bin/context-guard-tool-prune +241 -1
- package/plugins/context-guard/lib/context_guard_commands.py +206 -0
- package/plugins/context-guard/skills/setup/SKILL.md +1 -0
- package/context-guard-kit/README.md +0 -91
- package/context-guard-kit/benchmark_runner.py +0 -2401
- package/context-guard-kit/claude_transcript_cost_audit.py +0 -2346
- package/context-guard-kit/context_compress.py +0 -695
- package/context-guard-kit/context_escrow.py +0 -935
- package/context-guard-kit/context_filter.py +0 -637
- package/context-guard-kit/context_guard_cli.py +0 -325
- package/context-guard-kit/context_guard_diet.py +0 -1711
- package/context-guard-kit/context_pack.py +0 -2713
- package/context-guard-kit/cost_guard.py +0 -2349
- package/context-guard-kit/experimental_registry.py +0 -4348
- package/context-guard-kit/failed_attempt_nudge.py +0 -567
- package/context-guard-kit/guard_large_read.py +0 -690
- package/context-guard-kit/hook_secret_patterns.py +0 -43
- package/context-guard-kit/read_symbol.py +0 -483
- package/context-guard-kit/rewrite_bash_for_token_budget.py +0 -501
- package/context-guard-kit/sanitize_output.py +0 -725
- package/context-guard-kit/settings.example.json +0 -67
- package/context-guard-kit/setup_wizard.py +0 -2515
- package/context-guard-kit/statusline.sh +0 -362
- package/context-guard-kit/statusline_merged.sh +0 -157
- package/context-guard-kit/tool_schema_pruner.py +0 -837
- package/context-guard-kit/trim_command_output.py +0 -1449
|
@@ -13,41 +13,124 @@ from pathlib import Path
|
|
|
13
13
|
import subprocess
|
|
14
14
|
import stat
|
|
15
15
|
import sys
|
|
16
|
-
from
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
from typing import Any, NoReturn
|
|
17
18
|
|
|
18
19
|
COMMAND_NAME = "context-guard"
|
|
19
20
|
PACKAGE_NAME = "@ictechgy/context-guard"
|
|
20
21
|
MAX_VERSION_METADATA_BYTES = 64 * 1024
|
|
22
|
+
MAX_COMMAND_MANIFEST_BYTES = 128 * 1024
|
|
21
23
|
ALLOWED_FIRST_ABSOLUTE_SYMLINKS = {
|
|
22
24
|
"tmp": Path("/private/tmp"),
|
|
23
25
|
"var": Path("/private/var"),
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
28
|
+
MANIFEST_LOAD_ERROR: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _manifest_candidates(script_dir: Path) -> tuple[Path, ...]:
|
|
32
|
+
# Layout-aware and intentionally narrow: checkout scripts load only the
|
|
33
|
+
# colocated kit manifest; packaged plugin/npm bins load only plugin-local
|
|
34
|
+
# ../lib. A stray bin/context_guard_commands.py must not shadow the package
|
|
35
|
+
# manifest.
|
|
36
|
+
if script_dir.name == "context-guard-kit":
|
|
37
|
+
return (script_dir / "context_guard_commands.py",)
|
|
38
|
+
if script_dir.name == "bin":
|
|
39
|
+
return (script_dir.parent / "lib" / "context_guard_commands.py",)
|
|
40
|
+
return ()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _manifest_open_flags() -> int | None:
|
|
44
|
+
if not hasattr(os, "O_NOFOLLOW"):
|
|
45
|
+
return None
|
|
46
|
+
flags = os.O_RDONLY | os.O_NOFOLLOW
|
|
47
|
+
if hasattr(os, "O_CLOEXEC"):
|
|
48
|
+
flags |= os.O_CLOEXEC
|
|
49
|
+
if hasattr(os, "O_NONBLOCK"):
|
|
50
|
+
flags |= os.O_NONBLOCK
|
|
51
|
+
if hasattr(os, "O_NOCTTY"):
|
|
52
|
+
flags |= os.O_NOCTTY
|
|
53
|
+
return flags
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_manifest_source(path: Path) -> str | None:
|
|
57
|
+
flags = _manifest_open_flags()
|
|
58
|
+
if flags is None:
|
|
59
|
+
return None
|
|
60
|
+
fd = -1
|
|
61
|
+
try:
|
|
62
|
+
fd = os.open(path, flags)
|
|
63
|
+
st = os.fstat(fd)
|
|
64
|
+
if not stat.S_ISREG(st.st_mode) or st.st_size > MAX_COMMAND_MANIFEST_BYTES:
|
|
65
|
+
return None
|
|
66
|
+
chunks: list[bytes] = []
|
|
67
|
+
total = 0
|
|
68
|
+
while True:
|
|
69
|
+
chunk = os.read(fd, min(64 * 1024, MAX_COMMAND_MANIFEST_BYTES + 1 - total))
|
|
70
|
+
if not chunk:
|
|
71
|
+
break
|
|
72
|
+
chunks.append(chunk)
|
|
73
|
+
total += len(chunk)
|
|
74
|
+
if total > MAX_COMMAND_MANIFEST_BYTES:
|
|
75
|
+
return None
|
|
76
|
+
return b"".join(chunks).decode("utf-8")
|
|
77
|
+
except (OSError, UnicodeDecodeError):
|
|
78
|
+
return None
|
|
79
|
+
finally:
|
|
80
|
+
if fd >= 0:
|
|
81
|
+
try:
|
|
82
|
+
os.close(fd)
|
|
83
|
+
except OSError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _load_manifest_from_path(path: Path) -> ModuleType | None:
|
|
88
|
+
source = _read_manifest_source(path)
|
|
89
|
+
if source is None:
|
|
90
|
+
return None
|
|
91
|
+
module = ModuleType("_context_guard_commands_manifest")
|
|
92
|
+
try:
|
|
93
|
+
exec(compile(source, str(path), "exec"), module.__dict__)
|
|
94
|
+
except Exception:
|
|
95
|
+
return None
|
|
96
|
+
return module
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _coerce_helper_subcommands(value: Any) -> dict[str, tuple[str, ...]] | None:
|
|
100
|
+
if not isinstance(value, dict):
|
|
101
|
+
return None
|
|
102
|
+
coerced: dict[str, tuple[str, ...]] = {}
|
|
103
|
+
for key, command in value.items():
|
|
104
|
+
if not isinstance(key, str) or not key:
|
|
105
|
+
return None
|
|
106
|
+
if not isinstance(command, (tuple, list)) or not command:
|
|
107
|
+
return None
|
|
108
|
+
parts: list[str] = []
|
|
109
|
+
for part in command:
|
|
110
|
+
if not isinstance(part, str) or not part:
|
|
111
|
+
return None
|
|
112
|
+
parts.append(part)
|
|
113
|
+
coerced[key] = tuple(parts)
|
|
114
|
+
return coerced
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_helper_subcommands() -> dict[str, tuple[str, ...]]:
|
|
118
|
+
global MANIFEST_LOAD_ERROR
|
|
119
|
+
script_dir = Path(__file__).resolve().parent
|
|
120
|
+
candidates = _manifest_candidates(script_dir)
|
|
121
|
+
for candidate in candidates:
|
|
122
|
+
module = _load_manifest_from_path(candidate)
|
|
123
|
+
if module is None:
|
|
124
|
+
continue
|
|
125
|
+
loaded = _coerce_helper_subcommands(getattr(module, "DISPATCHER_SUBCOMMANDS", None))
|
|
126
|
+
if loaded is not None:
|
|
127
|
+
MANIFEST_LOAD_ERROR = None
|
|
128
|
+
return loaded
|
|
129
|
+
MANIFEST_LOAD_ERROR = "could not load trusted command manifest from " + ", ".join(str(path) for path in candidates)
|
|
130
|
+
return {}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
HELPER_SUBCOMMANDS: dict[str, tuple[str, ...]] = load_helper_subcommands()
|
|
51
134
|
|
|
52
135
|
|
|
53
136
|
def _script_dir() -> Path:
|
|
@@ -310,6 +393,8 @@ def run_helper(command: str, argv: list[str]) -> int:
|
|
|
310
393
|
def main(argv: list[str] | None = None) -> int:
|
|
311
394
|
args = list(sys.argv[1:] if argv is None else argv)
|
|
312
395
|
if not args or args[0] in {"-h", "--help", "help"}:
|
|
396
|
+
if MANIFEST_LOAD_ERROR:
|
|
397
|
+
fail(MANIFEST_LOAD_ERROR)
|
|
313
398
|
print_help()
|
|
314
399
|
return 0
|
|
315
400
|
if args[0] in {"-V", "--version", "version"}:
|
|
@@ -317,6 +402,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
317
402
|
return 0
|
|
318
403
|
command = args.pop(0).strip().lower()
|
|
319
404
|
if command not in HELPER_SUBCOMMANDS:
|
|
405
|
+
if MANIFEST_LOAD_ERROR:
|
|
406
|
+
fail(MANIFEST_LOAD_ERROR)
|
|
320
407
|
fail(f"unknown subcommand {command!r}. Run '{COMMAND_NAME} --help'.")
|
|
321
408
|
return run_helper(command, args)
|
|
322
409
|
|