@ictechgy/context-guard 0.4.9 → 0.4.11
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 +28 -0
- package/README.ko.md +59 -31
- package/README.md +85 -36
- 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.evidence.example.jsonl +24 -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/benchmark-workflow-examples.md +3 -0
- package/docs/benchmark-workflows/context-pack-byte-proxy.example.json +278 -137
- package/docs/benchmark-workflows/measured-token-workflow.example.json +279 -138
- package/docs/benchmark-workflows/provider-cache-telemetry.example.json +279 -138
- package/docs/distribution.md +10 -7
- package/docs/experimental-benchmark-fixtures.md +30 -6
- package/package.json +4 -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 +20 -14
- package/plugins/context-guard/README.md +26 -17
- package/plugins/context-guard/bin/context-guard +147 -25
- package/plugins/context-guard/bin/context-guard-artifact +884 -79
- package/plugins/context-guard/bin/context-guard-audit +33 -2
- package/plugins/context-guard/bin/context-guard-bench +1542 -31
- package/plugins/context-guard/bin/context-guard-cache-score +665 -0
- package/plugins/context-guard/bin/context-guard-compress +146 -1
- package/plugins/context-guard/bin/context-guard-cost +790 -6
- package/plugins/context-guard/bin/context-guard-experiments +463 -26
- package/plugins/context-guard/bin/context-guard-failed-nudge +9 -2
- 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 +892 -49
- package/plugins/context-guard/bin/context-guard-rewrite-bash +3 -0
- package/plugins/context-guard/bin/context-guard-sanitize-output +76 -12
- 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 +480 -53
- package/plugins/context-guard/bin/context-guard-trim-output +288 -41
- package/plugins/context-guard/brief/README.md +5 -5
- package/plugins/context-guard/lib/context_guard_commands.py +230 -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
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Canonical ContextGuard command dispatcher.
|
|
3
|
-
|
|
4
|
-
The npm/Homebrew-friendly ``context-guard`` command is intentionally passive:
|
|
5
|
-
installation only exposes commands on PATH. Any project or user configuration is
|
|
6
|
-
performed later through explicit subcommands such as ``context-guard setup``.
|
|
7
|
-
"""
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
import subprocess
|
|
14
|
-
import stat
|
|
15
|
-
import sys
|
|
16
|
-
from typing import NoReturn
|
|
17
|
-
|
|
18
|
-
COMMAND_NAME = "context-guard"
|
|
19
|
-
PACKAGE_NAME = "@ictechgy/context-guard"
|
|
20
|
-
MAX_VERSION_METADATA_BYTES = 64 * 1024
|
|
21
|
-
ALLOWED_FIRST_ABSOLUTE_SYMLINKS = {
|
|
22
|
-
"tmp": Path("/private/tmp"),
|
|
23
|
-
"var": Path("/private/var"),
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
HELPER_SUBCOMMANDS: dict[str, tuple[str, ...]] = {
|
|
27
|
-
"setup": ("context-guard-setup",),
|
|
28
|
-
"doctor": ("context-guard-setup", "--verify"),
|
|
29
|
-
"audit": ("context-guard-audit",),
|
|
30
|
-
"diet": ("context-guard-diet",),
|
|
31
|
-
"experiments": ("context-guard-experiments",),
|
|
32
|
-
"scan": ("context-guard-diet", "scan"),
|
|
33
|
-
"trim-output": ("context-guard-trim-output",),
|
|
34
|
-
"trim": ("context-guard-trim-output",),
|
|
35
|
-
"sanitize-output": ("context-guard-sanitize-output",),
|
|
36
|
-
"sanitize": ("context-guard-sanitize-output",),
|
|
37
|
-
"filter": ("context-guard-filter",),
|
|
38
|
-
"artifact": ("context-guard-artifact",),
|
|
39
|
-
"pack": ("context-guard-pack",),
|
|
40
|
-
"tool-prune": ("context-guard-tool-prune",),
|
|
41
|
-
"compress": ("context-guard-compress",),
|
|
42
|
-
"cost": ("context-guard-cost",),
|
|
43
|
-
"bench": ("context-guard-bench",),
|
|
44
|
-
"read-symbol": ("context-guard-read-symbol",),
|
|
45
|
-
"rewrite-bash": ("context-guard-rewrite-bash",),
|
|
46
|
-
"guard-read": ("context-guard-guard-read",),
|
|
47
|
-
"failed-nudge": ("context-guard-failed-nudge",),
|
|
48
|
-
"statusline": ("context-guard-statusline",),
|
|
49
|
-
"statusline-merged": ("context-guard-statusline-merged",),
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _script_dir() -> Path:
|
|
54
|
-
return Path(__file__).resolve().parent
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _candidate_roots() -> list[Path]:
|
|
58
|
-
script_dir = _script_dir()
|
|
59
|
-
roots = [script_dir.parent, script_dir.parent.parent]
|
|
60
|
-
# When run from context-guard-kit in a checkout, the repo root is one level up.
|
|
61
|
-
if script_dir.name == "context-guard-kit":
|
|
62
|
-
roots.insert(0, script_dir.parent)
|
|
63
|
-
return list(dict.fromkeys(roots))
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _normalized_link_target(anchor: Path, raw_target: str) -> Path:
|
|
67
|
-
target = Path(raw_target)
|
|
68
|
-
if target.is_absolute():
|
|
69
|
-
return Path(os.path.normpath(str(target)))
|
|
70
|
-
return Path(os.path.normpath(str(anchor / target)))
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _normalize_allowed_first_absolute_symlink(path: Path) -> Path:
|
|
74
|
-
if not path.is_absolute():
|
|
75
|
-
return path
|
|
76
|
-
parts = path.parts
|
|
77
|
-
if len(parts) < 2:
|
|
78
|
-
return path
|
|
79
|
-
expected = ALLOWED_FIRST_ABSOLUTE_SYMLINKS.get(parts[1])
|
|
80
|
-
if expected is None:
|
|
81
|
-
return path
|
|
82
|
-
first = Path(path.anchor) / parts[1]
|
|
83
|
-
try:
|
|
84
|
-
if first.is_symlink() and _normalized_link_target(Path(path.anchor), os.readlink(first)) == expected:
|
|
85
|
-
return expected.joinpath(*parts[2:])
|
|
86
|
-
except OSError:
|
|
87
|
-
return path
|
|
88
|
-
return path
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _metadata_no_follow_supported() -> bool:
|
|
92
|
-
return (
|
|
93
|
-
hasattr(os, "O_NOFOLLOW")
|
|
94
|
-
and os.open in getattr(os, "supports_dir_fd", set())
|
|
95
|
-
and os.stat in getattr(os, "supports_dir_fd", set())
|
|
96
|
-
and os.stat in getattr(os, "supports_follow_symlinks", set())
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _directory_open_flags(*, follow_final: bool = False) -> int:
|
|
101
|
-
flags = os.O_RDONLY
|
|
102
|
-
if hasattr(os, "O_CLOEXEC"):
|
|
103
|
-
flags |= os.O_CLOEXEC
|
|
104
|
-
if hasattr(os, "O_DIRECTORY"):
|
|
105
|
-
flags |= os.O_DIRECTORY
|
|
106
|
-
if not follow_final:
|
|
107
|
-
flags |= os.O_NOFOLLOW
|
|
108
|
-
return flags
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def _metadata_file_open_flags() -> int:
|
|
112
|
-
flags = os.O_RDONLY | os.O_NOFOLLOW
|
|
113
|
-
if hasattr(os, "O_CLOEXEC"):
|
|
114
|
-
flags |= os.O_CLOEXEC
|
|
115
|
-
if hasattr(os, "O_NONBLOCK"):
|
|
116
|
-
flags |= os.O_NONBLOCK
|
|
117
|
-
if hasattr(os, "O_NOCTTY"):
|
|
118
|
-
flags |= os.O_NOCTTY
|
|
119
|
-
return flags
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def _leaf_name(path: Path) -> str | None:
|
|
123
|
-
name = path.name
|
|
124
|
-
if name in {"", ".", ".."}:
|
|
125
|
-
return None
|
|
126
|
-
return name
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _open_metadata_parent_no_follow(path: Path) -> int | None:
|
|
130
|
-
if not _metadata_no_follow_supported():
|
|
131
|
-
return None
|
|
132
|
-
path = _normalize_allowed_first_absolute_symlink(path)
|
|
133
|
-
try:
|
|
134
|
-
if path.is_absolute():
|
|
135
|
-
current_fd = os.open(path.anchor or os.sep, _directory_open_flags(follow_final=True))
|
|
136
|
-
parts = path.parts[1:-1]
|
|
137
|
-
else:
|
|
138
|
-
current_fd = os.open(".", _directory_open_flags(follow_final=True))
|
|
139
|
-
parts = path.parts[:-1]
|
|
140
|
-
except OSError:
|
|
141
|
-
return None
|
|
142
|
-
try:
|
|
143
|
-
for part in parts:
|
|
144
|
-
if part in {"", "."}:
|
|
145
|
-
continue
|
|
146
|
-
if part == "..":
|
|
147
|
-
return None
|
|
148
|
-
next_fd = -1
|
|
149
|
-
try:
|
|
150
|
-
next_fd = os.open(part, _directory_open_flags(), dir_fd=current_fd)
|
|
151
|
-
if not stat.S_ISDIR(os.fstat(next_fd).st_mode):
|
|
152
|
-
try:
|
|
153
|
-
os.close(next_fd)
|
|
154
|
-
except OSError:
|
|
155
|
-
pass
|
|
156
|
-
next_fd = -1
|
|
157
|
-
return None
|
|
158
|
-
except OSError:
|
|
159
|
-
if next_fd >= 0:
|
|
160
|
-
try:
|
|
161
|
-
os.close(next_fd)
|
|
162
|
-
except OSError:
|
|
163
|
-
pass
|
|
164
|
-
try:
|
|
165
|
-
os.close(current_fd)
|
|
166
|
-
except OSError:
|
|
167
|
-
pass
|
|
168
|
-
current_fd = -1
|
|
169
|
-
return None
|
|
170
|
-
try:
|
|
171
|
-
os.close(current_fd)
|
|
172
|
-
except OSError:
|
|
173
|
-
pass
|
|
174
|
-
current_fd = next_fd
|
|
175
|
-
owned_fd = current_fd
|
|
176
|
-
current_fd = -1
|
|
177
|
-
return owned_fd
|
|
178
|
-
finally:
|
|
179
|
-
if current_fd >= 0:
|
|
180
|
-
try:
|
|
181
|
-
os.close(current_fd)
|
|
182
|
-
except OSError:
|
|
183
|
-
pass
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def _read_metadata_text(path: Path) -> str | None:
|
|
187
|
-
path = _normalize_allowed_first_absolute_symlink(path)
|
|
188
|
-
parent_fd = _open_metadata_parent_no_follow(path)
|
|
189
|
-
if parent_fd is None:
|
|
190
|
-
return None
|
|
191
|
-
fd = -1
|
|
192
|
-
data = b""
|
|
193
|
-
try:
|
|
194
|
-
leaf = _leaf_name(path)
|
|
195
|
-
if leaf is None:
|
|
196
|
-
return None
|
|
197
|
-
pre_open = os.stat(leaf, dir_fd=parent_fd, follow_symlinks=False)
|
|
198
|
-
if not stat.S_ISREG(pre_open.st_mode):
|
|
199
|
-
return None
|
|
200
|
-
if pre_open.st_size > MAX_VERSION_METADATA_BYTES:
|
|
201
|
-
return None
|
|
202
|
-
fd = os.open(leaf, _metadata_file_open_flags(), dir_fd=parent_fd)
|
|
203
|
-
opened = os.fstat(fd)
|
|
204
|
-
if not stat.S_ISREG(opened.st_mode):
|
|
205
|
-
return None
|
|
206
|
-
if opened.st_size > MAX_VERSION_METADATA_BYTES:
|
|
207
|
-
return None
|
|
208
|
-
data = os.read(fd, MAX_VERSION_METADATA_BYTES + 1)
|
|
209
|
-
except OSError:
|
|
210
|
-
return None
|
|
211
|
-
finally:
|
|
212
|
-
if fd >= 0:
|
|
213
|
-
try:
|
|
214
|
-
os.close(fd)
|
|
215
|
-
except OSError:
|
|
216
|
-
pass
|
|
217
|
-
try:
|
|
218
|
-
os.close(parent_fd)
|
|
219
|
-
except OSError:
|
|
220
|
-
pass
|
|
221
|
-
if len(data) > MAX_VERSION_METADATA_BYTES:
|
|
222
|
-
return None
|
|
223
|
-
try:
|
|
224
|
-
return data.decode("utf-8")
|
|
225
|
-
except UnicodeDecodeError:
|
|
226
|
-
return None
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def _load_json(path: Path) -> dict[str, object] | None:
|
|
230
|
-
text = _read_metadata_text(path)
|
|
231
|
-
if text is None:
|
|
232
|
-
return None
|
|
233
|
-
try:
|
|
234
|
-
data = json.loads(text)
|
|
235
|
-
except (OSError, json.JSONDecodeError):
|
|
236
|
-
return None
|
|
237
|
-
return data if isinstance(data, dict) else None
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def project_version() -> str:
|
|
241
|
-
candidates: list[Path] = []
|
|
242
|
-
for root in _candidate_roots():
|
|
243
|
-
candidates.extend(
|
|
244
|
-
[
|
|
245
|
-
root / ".claude-plugin" / "plugin.json",
|
|
246
|
-
root / "plugins" / "context-guard" / ".claude-plugin" / "plugin.json",
|
|
247
|
-
root / "package.json",
|
|
248
|
-
]
|
|
249
|
-
)
|
|
250
|
-
for candidate in candidates:
|
|
251
|
-
data = _load_json(candidate)
|
|
252
|
-
version = data.get("version") if data else None
|
|
253
|
-
if isinstance(version, str) and version.strip():
|
|
254
|
-
return version.strip()
|
|
255
|
-
return "0.0.0+unknown"
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def print_help() -> None:
|
|
259
|
-
version = project_version()
|
|
260
|
-
commands = "\n".join(f" {name}" for name in sorted(HELPER_SUBCOMMANDS))
|
|
261
|
-
sys.stdout.write(
|
|
262
|
-
f"ContextGuard {version}\n"
|
|
263
|
-
f"\n"
|
|
264
|
-
f"Usage:\n"
|
|
265
|
-
f" {COMMAND_NAME} --version\n"
|
|
266
|
-
f" {COMMAND_NAME} <subcommand> [args...]\n"
|
|
267
|
-
f"\n"
|
|
268
|
-
f"Install examples:\n"
|
|
269
|
-
f" npm install -g {PACKAGE_NAME}\n"
|
|
270
|
-
f" npx {PACKAGE_NAME} setup --agent codex --scope project --plan\n"
|
|
271
|
-
f"\n"
|
|
272
|
-
f"Common subcommands:\n"
|
|
273
|
-
f"{commands}\n"
|
|
274
|
-
f"\n"
|
|
275
|
-
f"Run '{COMMAND_NAME} <subcommand> --help' for helper-specific options.\n"
|
|
276
|
-
f"Installing ContextGuard never writes configuration; use 'setup' explicitly.\n"
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def helper_path(name: str) -> Path | None:
|
|
281
|
-
script_dir = _script_dir()
|
|
282
|
-
candidates = [
|
|
283
|
-
script_dir / name,
|
|
284
|
-
script_dir.parent / "plugins" / "context-guard" / "bin" / name,
|
|
285
|
-
script_dir.parent.parent / "plugins" / "context-guard" / "bin" / name,
|
|
286
|
-
]
|
|
287
|
-
for candidate in candidates:
|
|
288
|
-
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
289
|
-
return candidate
|
|
290
|
-
return None
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def fail(message: str, code: int = 2) -> NoReturn:
|
|
294
|
-
sys.stderr.write(f"{COMMAND_NAME}: {message}\n")
|
|
295
|
-
raise SystemExit(code)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def run_helper(command: str, argv: list[str]) -> int:
|
|
299
|
-
mapping = HELPER_SUBCOMMANDS[command]
|
|
300
|
-
helper = helper_path(mapping[0])
|
|
301
|
-
if helper is None:
|
|
302
|
-
fail(
|
|
303
|
-
f"could not find helper {mapping[0]!r}; reinstall {PACKAGE_NAME} "
|
|
304
|
-
"or run from a complete ContextGuard checkout."
|
|
305
|
-
)
|
|
306
|
-
proc = subprocess.run([str(helper), *mapping[1:], *argv])
|
|
307
|
-
return int(proc.returncode)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def main(argv: list[str] | None = None) -> int:
|
|
311
|
-
args = list(sys.argv[1:] if argv is None else argv)
|
|
312
|
-
if not args or args[0] in {"-h", "--help", "help"}:
|
|
313
|
-
print_help()
|
|
314
|
-
return 0
|
|
315
|
-
if args[0] in {"-V", "--version", "version"}:
|
|
316
|
-
print(project_version())
|
|
317
|
-
return 0
|
|
318
|
-
command = args.pop(0).strip().lower()
|
|
319
|
-
if command not in HELPER_SUBCOMMANDS:
|
|
320
|
-
fail(f"unknown subcommand {command!r}. Run '{COMMAND_NAME} --help'.")
|
|
321
|
-
return run_helper(command, args)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if __name__ == "__main__":
|
|
325
|
-
raise SystemExit(main())
|