@ictechgy/context-guard 0.4.15 → 0.4.16

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 (29) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.ko.md +46 -1
  3. package/README.md +58 -2
  4. package/package.json +1 -1
  5. package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
  6. package/plugins/context-guard/README.ko.md +18 -0
  7. package/plugins/context-guard/README.md +18 -0
  8. package/plugins/context-guard/bin/context-guard-artifact +90 -9
  9. package/plugins/context-guard/bin/context-guard-audit +169 -66
  10. package/plugins/context-guard/bin/context-guard-bench +5765 -224
  11. package/plugins/context-guard/bin/context-guard-compress +90 -8
  12. package/plugins/context-guard/bin/context-guard-diet +1 -7
  13. package/plugins/context-guard/bin/context-guard-experiments +5 -1
  14. package/plugins/context-guard/bin/context-guard-failed-nudge +705 -83
  15. package/plugins/context-guard/bin/context-guard-guard-read +490 -55
  16. package/plugins/context-guard/bin/context-guard-pack +110 -11
  17. package/plugins/context-guard/bin/context-guard-read-symbol +7 -2
  18. package/plugins/context-guard/bin/context-guard-rewrite-bash +2204 -223
  19. package/plugins/context-guard/bin/context-guard-sanitize-output +560 -85
  20. package/plugins/context-guard/bin/context-guard-setup +1073 -147
  21. package/plugins/context-guard/bin/context-guard-statusline +131 -54
  22. package/plugins/context-guard/bin/context-guard-statusline-merged +7 -3
  23. package/plugins/context-guard/bin/context-guard-tool-prune +44 -11
  24. package/plugins/context-guard/bin/context-guard-trim-output +89 -13
  25. package/plugins/context-guard/brief/README.md +19 -0
  26. package/plugins/context-guard/brief/narration-mode.quiet.md +21 -0
  27. package/plugins/context-guard/lib/context_guard_commands.py +6 -2
  28. package/plugins/context-guard/lib/credential_policy.py +177 -0
  29. package/plugins/context-guard/lib/transcript_usage_reducer.py +378 -0
@@ -165,6 +165,7 @@ class SourceSpec:
165
165
  label: str | None = None
166
166
  input_index: int = 0
167
167
  origin: str = "cli"
168
+ sanitization_context: str = "source_code"
168
169
 
169
170
 
170
171
  @dataclass
@@ -205,9 +206,32 @@ class PackError(ValueError):
205
206
  pass
206
207
 
207
208
 
209
+ SANITIZATION_CONTEXTS = frozenset(
210
+ {
211
+ "unknown_text",
212
+ "command_search_diff",
213
+ "filesystem_listing",
214
+ "source_code",
215
+ }
216
+ )
217
+
218
+
219
+ def parse_sanitization_context(value: object) -> str:
220
+ context = str(value or "unknown_text")
221
+ if context not in SANITIZATION_CONTEXTS:
222
+ raise PackError(f"unsupported sanitization context: {context}")
223
+ return context
224
+
225
+
208
226
  class FallbackLineSanitizer:
209
- def __init__(self, *, show_paths: bool = False) -> None:
227
+ def __init__(
228
+ self,
229
+ *,
230
+ show_paths: bool = False,
231
+ context: str = "unknown_text",
232
+ ) -> None:
210
233
  self.show_paths = show_paths
234
+ self.context = context
211
235
  self.redactions = 0
212
236
 
213
237
  def sanitize(self, raw_line: str) -> tuple[str, bool]:
@@ -252,7 +276,12 @@ def load_line_sanitizer_factory() -> Any:
252
276
  if spec is None:
253
277
  raise RuntimeError("import spec unavailable")
254
278
  module = importlib.util.module_from_spec(spec)
255
- loader.exec_module(module)
279
+ sys.modules[loader.name] = module
280
+ try:
281
+ loader.exec_module(module)
282
+ except Exception:
283
+ sys.modules.pop(loader.name, None)
284
+ raise
256
285
  _LINE_SANITIZER_FACTORY_CACHE = module.LineSanitizer
257
286
  return _LINE_SANITIZER_FACTORY_CACHE
258
287
  except Exception as exc:
@@ -261,13 +290,53 @@ def load_line_sanitizer_factory() -> Any:
261
290
  return _LINE_SANITIZER_FACTORY_CACHE
262
291
 
263
292
 
264
- def load_line_sanitizer(show_paths: bool = False) -> object:
293
+ def instantiate_line_sanitizer(
294
+ factory: Any,
295
+ *,
296
+ show_paths: bool,
297
+ context: str,
298
+ private_roots: tuple[str, ...] = (),
299
+ ) -> object:
300
+ try:
301
+ return factory(
302
+ show_paths=show_paths,
303
+ context=context,
304
+ private_roots=private_roots,
305
+ )
306
+ except TypeError:
307
+ if context != "unknown_text" or private_roots:
308
+ raise RuntimeError(
309
+ "adjacent sanitizer does not support required explicit context"
310
+ )
311
+ return factory(show_paths=show_paths)
312
+
313
+
314
+ def load_line_sanitizer(
315
+ show_paths: bool = False,
316
+ context: str = "unknown_text",
317
+ private_roots: tuple[str, ...] = (),
318
+ ) -> object:
265
319
  sanitizer_factory = load_line_sanitizer_factory()
266
- return sanitizer_factory(show_paths=show_paths)
320
+ return instantiate_line_sanitizer(
321
+ sanitizer_factory,
322
+ show_paths=show_paths,
323
+ context=context,
324
+ private_roots=private_roots,
325
+ )
267
326
 
268
327
 
269
- def sanitize_text(text: str, *, show_paths: bool = False) -> tuple[str, int]:
270
- sanitizer = load_line_sanitizer(show_paths)
328
+ def sanitize_text(
329
+ text: str,
330
+ *,
331
+ show_paths: bool = False,
332
+ context: str = "unknown_text",
333
+ private_roots: tuple[str, ...] = (),
334
+ ) -> tuple[str, int]:
335
+ sanitizer = load_line_sanitizer(
336
+ show_paths,
337
+ context=context,
338
+ private_roots=private_roots,
339
+ )
271
340
  redacted = 0
272
341
  out: list[str] = []
273
342
  for line in text.splitlines(True):
@@ -278,7 +347,13 @@ def sanitize_text(text: str, *, show_paths: bool = False) -> tuple[str, int]:
278
347
  return "".join(out), redacted
279
348
 
280
349
 
281
- def sanitize_source_lines(handle: Any, requested: LineRange | None) -> tuple[list[str], int, int]:
350
+ def sanitize_source_lines(
351
+ handle: Any,
352
+ requested: LineRange | None,
353
+ *,
354
+ context: str = "source_code",
355
+ private_roots: tuple[str, ...] = (),
356
+ ) -> tuple[list[str], int, int]:
282
357
  """Sanitize a source stream while retaining only the requested line window.
283
358
 
284
359
  Explicit line-window retrieval still scans the complete file so global
@@ -286,7 +361,10 @@ def sanitize_source_lines(handle: Any, requested: LineRange | None) -> tuple[lis
286
361
  outputs, but it no longer materializes a sanitized all-lines list before
287
362
  slicing.
288
363
  """
289
- sanitizer = load_line_sanitizer()
364
+ sanitizer = load_line_sanitizer(
365
+ context=context,
366
+ private_roots=private_roots,
367
+ )
290
368
  selected: list[str] = []
291
369
  redacted = 0
292
370
  total_lines = 0
@@ -886,6 +964,9 @@ def read_manifest(path: Path) -> list[SourceSpec]:
886
964
  lines=lines,
887
965
  label=cap_label(item.get("label")),
888
966
  origin="manifest",
967
+ sanitization_context=parse_sanitization_context(
968
+ item.get("sanitization_context", item.get("context"))
969
+ ),
889
970
  ))
890
971
  return out
891
972
 
@@ -917,6 +998,9 @@ def parse_source_spec(raw: str) -> SourceSpec:
917
998
  lines=lines,
918
999
  label=cap_label(values.get("label")),
919
1000
  origin="cli",
1001
+ sanitization_context=parse_sanitization_context(
1002
+ values.get("sanitization_context", values.get("context"))
1003
+ ),
920
1004
  )
921
1005
 
922
1006
 
@@ -1094,7 +1178,16 @@ def resolve_source(root: Path, spec: SourceSpec) -> tuple[ResolvedSource | None,
1094
1178
  try:
1095
1179
  with handle:
1096
1180
  requested = spec.lines
1097
- selected, total_lines, redacted_lines = sanitize_source_lines(handle, requested)
1181
+ selected, total_lines, redacted_lines = sanitize_source_lines(
1182
+ handle,
1183
+ requested,
1184
+ context=spec.sanitization_context,
1185
+ private_roots=(
1186
+ (str(root),)
1187
+ if spec.sanitization_context == "filesystem_listing"
1188
+ else ()
1189
+ ),
1190
+ )
1098
1191
  except OSError:
1099
1192
  return None, omission(spec, "unsafe_path", path=display, redacted_path=redacted_path)
1100
1193
  if total_lines <= 0:
@@ -1977,10 +2070,16 @@ def run_git_diff(root: Path, diff_ref: str) -> str:
1977
2070
  except (OSError, UnicodeError, subprocess.TimeoutExpired) as exc:
1978
2071
  raise PackError(f"could not read diff: {exc.__class__.__name__}") from exc
1979
2072
  if proc.returncode != 0:
1980
- detail = sanitize_text(proc.stderr or proc.stdout or "git diff failed")[0].strip().splitlines()
2073
+ detail = sanitize_text(
2074
+ proc.stderr or proc.stdout or "git diff failed",
2075
+ context="command_search_diff",
2076
+ )[0].strip().splitlines()
1981
2077
  message = detail[0] if detail else "git diff failed"
1982
2078
  raise PackError(f"could not read diff: {cap_label(message, default='git diff failed', limit=160)}")
1983
- return sanitize_text(proc.stdout[:MAX_SUGGEST_INPUT_BYTES])[0]
2079
+ return sanitize_text(
2080
+ proc.stdout[:MAX_SUGGEST_INPUT_BYTES],
2081
+ context="command_search_diff",
2082
+ )[0]
1984
2083
 
1985
2084
 
1986
2085
  def collect_diff_candidates(root: Path, diff_ref: str, query_terms: set[str], context_lines: int) -> list[SuggestCandidate]:
@@ -51,7 +51,12 @@ def _load_sanitize_output():
51
51
  if spec is None:
52
52
  continue
53
53
  module = importlib.util.module_from_spec(spec)
54
- loader.exec_module(module)
54
+ sys.modules[loader.name] = module
55
+ try:
56
+ loader.exec_module(module)
57
+ except Exception:
58
+ sys.modules.pop(loader.name, None)
59
+ raise
55
60
  return module
56
61
  raise ImportError("sanitize_output helper not found in " + ", ".join(searched))
57
62
 
@@ -412,7 +417,7 @@ def strip_line_for_brace_count(line: str, in_block_comment: bool = False) -> tup
412
417
 
413
418
 
414
419
  def redact_symbol_content(content: str) -> str:
415
- sanitizer = LineSanitizer(show_paths=True)
420
+ sanitizer = LineSanitizer(show_paths=False, context="source_code")
416
421
  return "".join(sanitizer.sanitize(line)[0] for line in content.splitlines(keepends=True))
417
422
 
418
423