@ictechgy/context-guard 0.4.6 → 0.4.8

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.
@@ -39,6 +39,6 @@ Avoid language like:
39
39
 
40
40
  The `.example.json` fixtures intentionally use full `context-guard-bench-report-v1` shapes so tests can catch schema drift and overclaim wording.
41
41
 
42
- The self-hosted metrics example is a JSONL run-evidence sidecar, not a full report shape. Its fields are additive ledger evidence only: `latency_ms`, `peak_memory_mb`, and normalized `quality_score` describe local/model-server behavior and leave hosted API report calculations unchanged.
42
+ The self-hosted metrics example is a JSONL run-evidence sidecar, not a full report shape. Its fields are additive ledger evidence only: `latency_ms`, `peak_memory_mb`, and normalized `quality_score` describe local/model-server behavior and leave hosted API report calculations unchanged. Use `context-guard experiments plan self-hosted-metrics-ledger --json ...` only as a dry-run ledger-preview checker for explicit metrics; it does not write the benchmark ledger.
43
43
 
44
44
  For task/variant starter fixtures rather than full report-shape examples, see [`experimental-benchmark-fixtures.md`](experimental-benchmark-fixtures.md). Those files are fixture-only and synthetic dry-run-only starters until users replace the placeholder prompts and success checks; they are not shipped OCR, visual-token, learned-compression, or output-transform benchmark results, and real claims still require provider-measured matched successful tasks plus failure-rate, correction, and shifted-cost guardrails.
@@ -32,7 +32,7 @@ The visual/OCR fixtures describe sanitized textual visual evidence only and now
32
32
 
33
33
  ## Learned-compression fixture notes
34
34
 
35
- The learned-compression fixtures describe already-sanitized context-pack or artifact-digest comparisons and now demonstrate `variant_prompt_files` for baseline context-pack evidence versus a fixture-only compressed digest candidate. They do not invoke LLMLingua-style, gist-token, latent-context, embedding, or reranking implementations. Future experiments must follow a sanitized evidence only rule, keep protected evidence exact or receipt-retrievable, forbid semantic rewrites of identifiers, numeric constants, hashes, paths, quoted strings, stack frames, JSON keys, code fences, and diff zones, and record bytes before/after, primary provider tokens, cost, success, corrections, compressor latency, and external cost.
35
+ The learned-compression fixtures describe already-sanitized context-pack or artifact-digest comparisons and now demonstrate `variant_prompt_files` for baseline context-pack evidence versus a fixture-only compressed digest candidate. ContextGuard ships `context-guard experiments plan learned-compression` only as a deny-by-default dry-run checker for sanitized trusted prose plus exact fallback handles. It does not invoke or ship LLMLingua-style, gist-token, latent-context, embedding, reranking, model-call, or replacement-generation implementations. Future experiments must follow a sanitized evidence only rule, keep protected evidence exact or receipt-retrievable, forbid semantic rewrites of identifiers, numeric constants, hashes, paths, quoted strings, stack frames, JSON keys, code fences, and diff zones, and record bytes before/after, primary provider tokens, cost, success, corrections, compressor latency, and external cost.
36
36
 
37
37
  ## Reversible output-transform fixture notes
38
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ictechgy/context-guard",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "ContextGuard CLI helpers for keeping AI coding agent context focused and local-first.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/ictechgy/context-guard#readme",
@@ -16,6 +16,7 @@
16
16
  "context-guard": "plugins/context-guard/bin/context-guard",
17
17
  "context-guard-setup": "plugins/context-guard/bin/context-guard-setup",
18
18
  "context-guard-diet": "plugins/context-guard/bin/context-guard-diet",
19
+ "context-guard-experiments": "plugins/context-guard/bin/context-guard-experiments",
19
20
  "context-guard-audit": "plugins/context-guard/bin/context-guard-audit",
20
21
  "context-guard-trim-output": "plugins/context-guard/bin/context-guard-trim-output",
21
22
  "context-guard-sanitize-output": "plugins/context-guard/bin/context-guard-sanitize-output",
@@ -5,7 +5,7 @@ class ContextGuard < Formula
5
5
 
6
6
  desc "Local-first context guardrails for AI coding agents"
7
7
  homepage "https://github.com/ictechgy/context-guard"
8
- url "https://github.com/ictechgy/context-guard/archive/refs/tags/v0.4.6.tar.gz"
8
+ url "https://github.com/ictechgy/context-guard/archive/refs/tags/v0.4.8.tar.gz"
9
9
  sha256 "REPLACE_WITH_RELEASE_TARBALL_SHA256"
10
10
  license "Apache-2.0"
11
11
 
@@ -37,5 +37,5 @@
37
37
  "gated-experiments",
38
38
  "future-roadmap"
39
39
  ],
40
- "version": "0.4.6"
40
+ "version": "0.4.8"
41
41
  }
@@ -113,7 +113,7 @@ brief 모드는 코딩 에이전트가 군더더기를 줄이도록 요청하되
113
113
 
114
114
  ContextGuard는 모델 토큰을 줄이기 위해 작업을 외부 AI 서비스로 전송하지 않습니다. 모든 헬퍼 명령은 로컬에서 동작합니다. 로컬 RAM/디스크 보관본은 다음에 보낼 컨텍스트를 줄이는 데 도움될 수 있지만 provider prompt cache를 대체하지 않습니다. Anthropic 배포나 청구 설명 전에는 공식 prompt caching/pricing 문서를 다시 확인하세요: https://docs.anthropic.com/en/build-with-claude/prompt-caching 및 https://platform.claude.com/docs/en/about-claude/pricing.
115
115
 
116
- 미래 learned, multimodal, self-hosted 최적화 아이디어는 [`research/experimental-token-reduction-radar.md`](https://github.com/ictechgy/context-guard/blob/main/research/experimental-token-reduction-radar.md)에 gated experiment로만 기록하며, fixture-only 시작 예시는 [`docs/experimental-benchmark-fixtures.md`](https://github.com/ictechgy/context-guard/blob/main/docs/experimental-benchmark-fixtures.md)에 둡니다. radar와 fixture현재 제공되는 runtime 기능이 아니며, provider가 측정한 matched-task 근거 없이 hosted API 절감을 주장하지 않습니다. Radar의 later-roadmap gate는 neural/semantic compression, trust-tiered injection-aware compression, context-diff compaction, local proxy constraint도 별도 미래 PR이 gate를 통과하기 전까지 experimental/non-shipped로 묶습니다.
116
+ 미래 learned, self-hosted 최적화 아이디어는 [`research/experimental-token-reduction-radar.md`](https://github.com/ictechgy/context-guard/blob/main/research/experimental-token-reduction-radar.md)에 gated experiment 기록하며, fixture-only 시작 예시는 [`docs/experimental-benchmark-fixtures.md`](https://github.com/ictechgy/context-guard/blob/main/docs/experimental-benchmark-fixtures.md)에 둡니다. learned compression은 `context-guard experiments plan learned-compression` dry-run checker만 shipped 상태이고, self-hosted metrics ledger`context-guard experiments plan self-hosted-metrics-ledger` dry-run preview만, local proxy는 `context-guard experiments plan local-proxy` localhost-only dry-run advisory plan만 shipped 상태이며 no listener/no traffic forwarding/no API-key persistence/read-only ledger boundary를 유지합니다. learned/synthetic compressor runtime·embedding·reranker·model call·replacement 생성, self-hosted KV/latent runtime 최적화, 실제 proxy forwarding runtime은 shipped가 아닙니다. multimodal OCR/crop은 `context-guard experiments plan visual-crop-ocr` dry-run planner만 shipped 상태이고 실제 OCR/crop runtime·스크린샷 캡처·이미지 파싱·외부 OCR/이미지 서비스 호출은 shipped가 아닙니다. 이 radar와 fixture는 provider가 측정한 matched-task 근거 없이 hosted API 절감을 주장하지 않습니다. Radar의 later-roadmap gate는 neural/semantic compression, trust-tiered injection-aware compression, context-diff compaction, local proxy constraint도 별도 미래 PR이 gate를 통과하기 전까지 experimental/non-shipped로 묶습니다.
117
117
 
118
118
  교차 에이전트 규칙 스니펫은 권고 사항입니다. 대상 에이전트가 반드시 따른다고 보장할 수 없으므로, 절감 주장이 필요하면 실제 전후 동작을 직접 측정하세요.
119
119
 
@@ -119,7 +119,25 @@ These helpers reduce common sources of context bloat, but they do not guarantee
119
119
 
120
120
  ContextGuard also does not send work to external AI providers to save model tokens. All helper commands run locally. Local RAM/disk receipts can reduce what you choose to send, but they do not replace a provider prompt cache. Before release or billing claims for Anthropic, recheck the official prompt-caching and pricing docs: https://docs.anthropic.com/en/build-with-claude/prompt-caching and https://platform.claude.com/docs/en/about-claude/pricing.
121
121
 
122
- Future learned, multimodal, and self-hosted optimization ideas are tracked only in [`research/experimental-token-reduction-radar.md`](https://github.com/ictechgy/context-guard/blob/main/research/experimental-token-reduction-radar.md), with fixture-only starters in [`docs/experimental-benchmark-fixtures.md`](https://github.com/ictechgy/context-guard/blob/main/docs/experimental-benchmark-fixtures.md). That radar and the fixtures are not shipped runtime features and do not claim hosted API savings without provider-measured matched-task evidence. The radar's later-roadmap gates also keep neural/semantic compression, trust-tiered injection-aware compression, context-diff compaction, and local proxy constraints experimental/non-shipped until a separate future PR satisfies those gates.
122
+ Future learned, multimodal, and self-hosted optimization ideas are tracked in [`research/experimental-token-reduction-radar.md`](https://github.com/ictechgy/context-guard/blob/main/research/experimental-token-reduction-radar.md), with fixture-only starters in [`docs/experimental-benchmark-fixtures.md`](https://github.com/ictechgy/context-guard/blob/main/docs/experimental-benchmark-fixtures.md). ContextGuard ships dry-run planners/checkers for learned compression, self-hosted metrics ledger previews, and local-proxy advisory plans only; learned/synthetic compressor runtime, embeddings, rerankers, model calls, replacement generation, self-hosted KV/latent runtime optimization, and actual proxy forwarding runtime are not shipped. That radar and the fixtures do not claim hosted API savings without provider-measured matched-task evidence. The radar's later-roadmap gates also keep neural/semantic compression, trust-tiered injection-aware compression, context-diff compaction, and local proxy constraints experimental/non-shipped until a separate future PR satisfies those gates.
123
+
124
+ ## Experimental opt-ins
125
+
126
+ Experimental lanes are default off. The registry is project-local metadata only; enabling an experiment records intent in `.context-guard/experiments.json` and does not activate stable runtime behavior by itself.
127
+
128
+ ```bash
129
+ context-guard experiments list
130
+ context-guard experiments status --json
131
+ context-guard experiments plan context-diff-compaction --json < change.diff
132
+ context-guard experiments plan visual-crop-ocr --json --full-evidence-receipt <id> --crop-label <label> --crop-bounds 0,0,100,100 --image-size 800,600 --missed-context-note "outside crop omitted"
133
+ context-guard experiments plan learned-compression --json --sanitized --trusted-source --exact-fallback-receipt <id> --reexpand-command "context-guard-artifact get <id> --full" < sanitized-prose.txt
134
+ context-guard experiments plan self-hosted-metrics-ledger --json --latency-ms 123.5 --peak-memory-mb 2048 --quality-score 0.98
135
+ context-guard experiments plan local-proxy --json --bind-host 127.0.0.1 --target-host 127.0.0.1 --runtime-gate-ack
136
+ context-guard experiments enable output-receipt-trim --root .
137
+ context-guard experiments disable output-receipt-trim --root .
138
+ ```
139
+
140
+ Use `--config <path>` only for an explicit project-local override. Registry entries include risk, gate requirements, explicit command/flag surfaces, and claim boundaries; hosted API token/cost savings still require provider-measured matched-task evidence. The registry can discover existing explicit-flag experiments such as `context-guard-trim-output --digest ... --artifact-receipt` and `context-guard-compress --protected-policy`, and it can run dry-run advisory planners such as `context-guard experiments plan context-diff-compaction`, `context-guard experiments plan visual-crop-ocr`, `context-guard experiments plan learned-compression`, `context-guard experiments plan self-hosted-metrics-ledger`, and `context-guard experiments plan local-proxy`. The visual planner is shipped only as a metadata/fixture review surface: OCR/crop runtime, screenshot capture, image parsing, and external OCR/image services are not shipped. The learned-compression checker is likewise a deny-by-default dry-run policy check: learned/synthetic compressor runtime, embeddings, rerankers, model calls, and replacement generation are not shipped. The self-hosted metrics planner emits a read-only ledger-compatible preview for explicit local/model-server latency, memory, quality, energy, throughput, and local-cost metrics; it does not write a ledger or permit hosted API token/cost savings claims. The local-proxy planner emits localhost-only advisory metadata only: it starts no listener, forwards no traffic, persists no API keys, writes no ledger, blocks non-local targets, and requires a separate future runtime gate before any forwarding implementation. `experiments enable` records intent only; it does not run those helpers, remove the need for their explicit flags, or permit replacing content without exact receipt/re-expand evidence.
123
141
 
124
142
  Cross-agent rule snippets are advisory: the target agent may ignore them, so measure actual before/after behavior when you need a savings claim.
125
143
 
@@ -11,17 +11,24 @@ import json
11
11
  import os
12
12
  from pathlib import Path
13
13
  import subprocess
14
+ import stat
14
15
  import sys
15
16
  from typing import NoReturn
16
17
 
17
18
  COMMAND_NAME = "context-guard"
18
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
+ }
19
25
 
20
26
  HELPER_SUBCOMMANDS: dict[str, tuple[str, ...]] = {
21
27
  "setup": ("context-guard-setup",),
22
28
  "doctor": ("context-guard-setup", "--verify"),
23
29
  "audit": ("context-guard-audit",),
24
30
  "diet": ("context-guard-diet",),
31
+ "experiments": ("context-guard-experiments",),
25
32
  "scan": ("context-guard-diet", "scan"),
26
33
  "trim-output": ("context-guard-trim-output",),
27
34
  "trim": ("context-guard-trim-output",),
@@ -49,16 +56,182 @@ def _script_dir() -> Path:
49
56
 
50
57
  def _candidate_roots() -> list[Path]:
51
58
  script_dir = _script_dir()
52
- roots = [script_dir.parent, script_dir.parent.parent, Path.cwd()]
59
+ roots = [script_dir.parent, script_dir.parent.parent]
53
60
  # When run from context-guard-kit in a checkout, the repo root is one level up.
54
61
  if script_dir.name == "context-guard-kit":
55
62
  roots.insert(0, script_dir.parent)
56
63
  return list(dict.fromkeys(roots))
57
64
 
58
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
+
59
229
  def _load_json(path: Path) -> dict[str, object] | None:
230
+ text = _read_metadata_text(path)
231
+ if text is None:
232
+ return None
60
233
  try:
61
- data = json.loads(path.read_text(encoding="utf-8"))
234
+ data = json.loads(text)
62
235
  except (OSError, json.JSONDecodeError):
63
236
  return None
64
237
  return data if isinstance(data, dict) else None
@@ -766,10 +766,20 @@ def cap_text(text: str, max_chars: int) -> tuple[str, bool]:
766
766
  return text[:keep].rstrip() + marker, True
767
767
 
768
768
 
769
- def query_content(content: str, *, line_range: tuple[int, int] | None, pattern: str | None, max_lines: int) -> tuple[str, dict[str, object]]:
769
+ def query_content(
770
+ content: str,
771
+ *,
772
+ line_range: tuple[int, int] | None,
773
+ pattern: str | None,
774
+ max_lines: int,
775
+ full: bool = False,
776
+ ) -> tuple[str, dict[str, object]]:
770
777
  lines = content.splitlines(True)
771
778
  selected: list[tuple[int, str]] = []
772
- if line_range is not None:
779
+ if full:
780
+ selected = list(enumerate(lines, start=1))
781
+ selector = {"type": "full"}
782
+ elif line_range is not None:
773
783
  start, end = line_range
774
784
  selected = list(enumerate(lines[start - 1 : end], start=start))
775
785
  selector = {"type": "lines", "start": start, "end": end}
@@ -780,15 +790,18 @@ def query_content(content: str, *, line_range: tuple[int, int] | None, pattern:
780
790
  selected = list(enumerate(lines[:max_lines], start=1))
781
791
  selector = {"type": "head", "max_lines": max_lines}
782
792
  total_matches = len(selected)
783
- selected = selected[:max_lines]
793
+ if not full:
794
+ selected = selected[:max_lines]
784
795
  text = "".join(line for _idx, line in selected)
785
796
  return text, {"selector": selector, "returned_lines": len(selected), "matched_lines": total_matches, "total_lines": len(lines)}
786
797
 
787
798
 
788
799
  def get_command(args: argparse.Namespace) -> int:
789
800
  artifact_id = args.artifact_id
790
- max_chars = bounded_int(args.max_chars, DEFAULT_MAX_CHARS, 1, 1_000_000)
801
+ full = bool(getattr(args, "full", False))
791
802
  try:
803
+ if full and (args.lines or args.pattern or args.max_lines is not None):
804
+ raise ValueError("--full cannot be combined with --lines, --pattern, or --max-lines")
792
805
  last_missing: FileNotFoundError | None = None
793
806
  for directory in artifact_read_directories(args.dir):
794
807
  try:
@@ -815,12 +828,14 @@ def get_command(args: argparse.Namespace) -> int:
815
828
  actual_sha = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
816
829
  if actual_sha != expected_sha:
817
830
  raise ValueError(f"artifact content checksum mismatch: {artifact_id}")
831
+ default_max_chars = max(DEFAULT_MAX_CHARS, expected_bytes) if full else DEFAULT_MAX_CHARS
832
+ max_chars = bounded_int(args.max_chars, default_max_chars, 1, MAX_MAX_BYTES)
818
833
  line_range = parse_line_range(args.lines)
819
834
  if line_range is not None and args.max_lines is None:
820
835
  max_lines = min(line_range[1] - line_range[0] + 1, MAX_QUERY_LINES)
821
836
  else:
822
837
  max_lines = bounded_int(args.max_lines, DEFAULT_MAX_LINES, 1, MAX_QUERY_LINES)
823
- selected, query = query_content(content, line_range=line_range, pattern=args.pattern, max_lines=max_lines)
838
+ selected, query = query_content(content, line_range=line_range, pattern=args.pattern, max_lines=max_lines, full=full)
824
839
  selected, capped = cap_text(selected, max_chars)
825
840
  except (FileNotFoundError, ValueError, OSError, json.JSONDecodeError) as exc:
826
841
  print(f"context-guard-artifact: {exc}", file=sys.stderr)
@@ -895,7 +910,8 @@ def build_parser() -> argparse.ArgumentParser:
895
910
  get.add_argument("--lines", help="1-based inclusive line range, e.g. 10:40")
896
911
  get.add_argument("--pattern", help="literal substring filter")
897
912
  get.add_argument("--max-lines", type=int, default=None)
898
- get.add_argument("--max-chars", type=int, default=DEFAULT_MAX_CHARS)
913
+ get.add_argument("--full", action="store_true", help="return full stored artifact content; cannot be combined with selectors")
914
+ get.add_argument("--max-chars", type=int, default=None)
899
915
  get.add_argument("--json", action="store_true", help="emit query JSON with content")
900
916
  get.set_defaults(func=get_command)
901
917