@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.ko.md +41 -24
  3. package/README.md +66 -26
  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.tasks.example.json +182 -0
  7. package/docs/benchmark-fixtures/token-savings-12task.variants.example.json +10 -0
  8. package/docs/distribution.md +10 -7
  9. package/docs/experimental-benchmark-fixtures.md +8 -1
  10. package/package.json +3 -6
  11. package/packaging/homebrew/context-guard.rb.template +1 -1
  12. package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
  13. package/plugins/context-guard/README.ko.md +9 -6
  14. package/plugins/context-guard/README.md +21 -13
  15. package/plugins/context-guard/bin/context-guard +113 -26
  16. package/plugins/context-guard/bin/context-guard-artifact +542 -46
  17. package/plugins/context-guard/bin/context-guard-cache-score +380 -0
  18. package/plugins/context-guard/bin/context-guard-compress +146 -1
  19. package/plugins/context-guard/bin/context-guard-cost +783 -4
  20. package/plugins/context-guard/bin/context-guard-experiments +99 -18
  21. package/plugins/context-guard/bin/context-guard-failed-nudge +3 -0
  22. package/plugins/context-guard/bin/context-guard-filter +163 -7
  23. package/plugins/context-guard/bin/context-guard-guard-read +3 -0
  24. package/plugins/context-guard/bin/context-guard-pack +602 -43
  25. package/plugins/context-guard/bin/context-guard-rewrite-bash +3 -0
  26. package/plugins/context-guard/bin/context-guard-setup +165 -31
  27. package/plugins/context-guard/bin/context-guard-statusline +490 -283
  28. package/plugins/context-guard/bin/context-guard-statusline-merged +5 -0
  29. package/plugins/context-guard/bin/context-guard-tool-prune +241 -1
  30. package/plugins/context-guard/lib/context_guard_commands.py +206 -0
  31. package/plugins/context-guard/skills/setup/SKILL.md +1 -0
  32. package/context-guard-kit/README.md +0 -91
  33. package/context-guard-kit/benchmark_runner.py +0 -2401
  34. package/context-guard-kit/claude_transcript_cost_audit.py +0 -2346
  35. package/context-guard-kit/context_compress.py +0 -695
  36. package/context-guard-kit/context_escrow.py +0 -935
  37. package/context-guard-kit/context_filter.py +0 -637
  38. package/context-guard-kit/context_guard_cli.py +0 -325
  39. package/context-guard-kit/context_guard_diet.py +0 -1711
  40. package/context-guard-kit/context_pack.py +0 -2713
  41. package/context-guard-kit/cost_guard.py +0 -2349
  42. package/context-guard-kit/experimental_registry.py +0 -4348
  43. package/context-guard-kit/failed_attempt_nudge.py +0 -567
  44. package/context-guard-kit/guard_large_read.py +0 -690
  45. package/context-guard-kit/hook_secret_patterns.py +0 -43
  46. package/context-guard-kit/read_symbol.py +0 -483
  47. package/context-guard-kit/rewrite_bash_for_token_budget.py +0 -501
  48. package/context-guard-kit/sanitize_output.py +0 -725
  49. package/context-guard-kit/settings.example.json +0 -67
  50. package/context-guard-kit/setup_wizard.py +0 -2515
  51. package/context-guard-kit/statusline.sh +0 -362
  52. package/context-guard-kit/statusline_merged.sh +0 -157
  53. package/context-guard-kit/tool_schema_pruner.py +0 -837
  54. 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 typing import NoReturn
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
- 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
- }
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