@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.ko.md +59 -31
  3. package/README.md +85 -36
  4. package/docs/benchmark-fixtures/token-savings-12task-baseline.prompt.example.md +7 -0
  5. package/docs/benchmark-fixtures/token-savings-12task-contextguard.prompt.example.md +7 -0
  6. package/docs/benchmark-fixtures/token-savings-12task.evidence.example.jsonl +24 -0
  7. package/docs/benchmark-fixtures/token-savings-12task.tasks.example.json +182 -0
  8. package/docs/benchmark-fixtures/token-savings-12task.variants.example.json +10 -0
  9. package/docs/benchmark-workflow-examples.md +3 -0
  10. package/docs/benchmark-workflows/context-pack-byte-proxy.example.json +278 -137
  11. package/docs/benchmark-workflows/measured-token-workflow.example.json +279 -138
  12. package/docs/benchmark-workflows/provider-cache-telemetry.example.json +279 -138
  13. package/docs/distribution.md +10 -7
  14. package/docs/experimental-benchmark-fixtures.md +30 -6
  15. package/package.json +4 -6
  16. package/packaging/homebrew/context-guard.rb.template +1 -1
  17. package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
  18. package/plugins/context-guard/README.ko.md +20 -14
  19. package/plugins/context-guard/README.md +26 -17
  20. package/plugins/context-guard/bin/context-guard +147 -25
  21. package/plugins/context-guard/bin/context-guard-artifact +884 -79
  22. package/plugins/context-guard/bin/context-guard-audit +33 -2
  23. package/plugins/context-guard/bin/context-guard-bench +1542 -31
  24. package/plugins/context-guard/bin/context-guard-cache-score +665 -0
  25. package/plugins/context-guard/bin/context-guard-compress +146 -1
  26. package/plugins/context-guard/bin/context-guard-cost +790 -6
  27. package/plugins/context-guard/bin/context-guard-experiments +463 -26
  28. package/plugins/context-guard/bin/context-guard-failed-nudge +9 -2
  29. package/plugins/context-guard/bin/context-guard-filter +163 -7
  30. package/plugins/context-guard/bin/context-guard-guard-read +3 -0
  31. package/plugins/context-guard/bin/context-guard-pack +892 -49
  32. package/plugins/context-guard/bin/context-guard-rewrite-bash +3 -0
  33. package/plugins/context-guard/bin/context-guard-sanitize-output +76 -12
  34. package/plugins/context-guard/bin/context-guard-setup +165 -31
  35. package/plugins/context-guard/bin/context-guard-statusline +490 -283
  36. package/plugins/context-guard/bin/context-guard-statusline-merged +5 -0
  37. package/plugins/context-guard/bin/context-guard-tool-prune +480 -53
  38. package/plugins/context-guard/bin/context-guard-trim-output +288 -41
  39. package/plugins/context-guard/brief/README.md +5 -5
  40. package/plugins/context-guard/lib/context_guard_commands.py +230 -0
  41. package/plugins/context-guard/skills/setup/SKILL.md +1 -0
  42. package/context-guard-kit/README.md +0 -91
  43. package/context-guard-kit/benchmark_runner.py +0 -2401
  44. package/context-guard-kit/claude_transcript_cost_audit.py +0 -2346
  45. package/context-guard-kit/context_compress.py +0 -695
  46. package/context-guard-kit/context_escrow.py +0 -935
  47. package/context-guard-kit/context_filter.py +0 -637
  48. package/context-guard-kit/context_guard_cli.py +0 -325
  49. package/context-guard-kit/context_guard_diet.py +0 -1711
  50. package/context-guard-kit/context_pack.py +0 -2713
  51. package/context-guard-kit/cost_guard.py +0 -2349
  52. package/context-guard-kit/experimental_registry.py +0 -4348
  53. package/context-guard-kit/failed_attempt_nudge.py +0 -567
  54. package/context-guard-kit/guard_large_read.py +0 -690
  55. package/context-guard-kit/hook_secret_patterns.py +0 -43
  56. package/context-guard-kit/read_symbol.py +0 -483
  57. package/context-guard-kit/rewrite_bash_for_token_budget.py +0 -501
  58. package/context-guard-kit/sanitize_output.py +0 -725
  59. package/context-guard-kit/settings.example.json +0 -67
  60. package/context-guard-kit/setup_wizard.py +0 -2515
  61. package/context-guard-kit/statusline.sh +0 -362
  62. package/context-guard-kit/statusline_merged.sh +0 -157
  63. package/context-guard-kit/tool_schema_pruner.py +0 -837
  64. 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())