codigest 0.3.5__tar.gz → 0.3.7__tar.gz

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 (31) hide show
  1. {codigest-0.3.5 → codigest-0.3.7}/PKG-INFO +42 -7
  2. {codigest-0.3.5 → codigest-0.3.7}/README.md +41 -6
  3. {codigest-0.3.5 → codigest-0.3.7}/pyproject.toml +1 -1
  4. codigest-0.3.7/src/codigest/commands/clean.py +45 -0
  5. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/commands/diff.py +19 -10
  6. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/commands/digest.py +41 -22
  7. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/commands/scan.py +62 -33
  8. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/commands/semdiff.py +21 -12
  9. codigest-0.3.7/src/codigest/commands/status.py +151 -0
  10. codigest-0.3.7/src/codigest/core/cache.py +170 -0
  11. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/shadow.py +76 -26
  12. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/main.py +3 -1
  13. {codigest-0.3.5 → codigest-0.3.7}/uv.lock +1 -5
  14. {codigest-0.3.5 → codigest-0.3.7}/.github/workflows/publish.yml +0 -0
  15. {codigest-0.3.5 → codigest-0.3.7}/.gitignore +0 -0
  16. {codigest-0.3.5 → codigest-0.3.7}/.python-version +0 -0
  17. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/__init__.py +0 -0
  18. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/__main__.py +0 -0
  19. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/commands/init.py +0 -0
  20. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/commands/tree.py +0 -0
  21. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/config.py +0 -0
  22. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/common.py +0 -0
  23. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/git_ops.py +0 -0
  24. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/processor.py +0 -0
  25. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/prompts.py +0 -0
  26. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/resolver.py +0 -0
  27. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/scanner.py +0 -0
  28. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/semdiff.py +0 -0
  29. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/structure.py +0 -0
  30. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/tags.py +0 -0
  31. {codigest-0.3.5 → codigest-0.3.7}/src/codigest/core/tokenizer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codigest
3
- Version: 0.3.5
3
+ Version: 0.3.7
4
4
  Summary: Human-Centered Codebase Context Manager for LLMs
5
5
  Project-URL: Homepage, https://github.com/SJB777/codigest
6
6
  Project-URL: Repository, https://github.com/SJB777/codigest
@@ -92,13 +92,12 @@ cdg init
92
92
 
93
93
  Scans the codebase and generates a structured XML snapshot. Includes a **Pre-flight Check** to prevent accidental token overflow.
94
94
 
95
- * **Output:** `.codigest/snapshot.xml`
95
+ * **Output:** `.codigest/snapshot.txt` (or stdout with `-s`)
96
96
  * **Features:**
97
97
  * **Smart Confirmation:** Automatically skips confirmation for small contexts, but warns you for large ones (>30k tokens).
98
98
  * **Dependency Resolution (`-r`):** Automatically finds and includes local files imported by your target files.
99
99
  * **Scope Control:** You can specify folders or files to scan.
100
-
101
-
100
+ * **Terminal Output (`-s` / `--stdout`):** Output directly to stdout instead of saving to file.
102
101
 
103
102
  ```bash
104
103
  # Basic scan (Interactive confirmation if large)
@@ -109,30 +108,39 @@ cdg scan src/main.py -r
109
108
 
110
109
  # Force execution without confirmation (Good for CI/CD)
111
110
  cdg scan -y --message "Automated snapshot"
111
+
112
+ # Output directly to stdout
113
+ cdg scan -s
112
114
  ```
113
115
 
114
116
  ### 3. Incremental Changes (`diff`)
115
117
 
116
118
  Tracks text-based changes between the last `scan` and the current working tree.
117
119
 
118
- * **Output:** `.codigest/changes.diff`
120
+ * **Output:** `.codigest/changes.diff` (or stdout with `-s`)
119
121
  * **Use Case:** "I modified 3 files. Here is exactly what changed since the last snapshot."
120
122
  * **Note:** Checks against the internal Shadow Git, enabling tracking without committing to the real Git.
121
123
 
122
124
  ```bash
123
125
  cdg diff
126
+
127
+ # Output directly to stdout
128
+ cdg diff -s
124
129
  ```
125
130
 
126
131
  ### 4. Semantic Analysis (`semdiff`)
127
132
 
128
133
  Analyzes **structural changes** (AST-based) rather than line-by-line text differences.
129
134
 
130
- * **Output:** `.codigest/semdiff.xml`
135
+ * **Output:** `.codigest/semdiff.txt` (or stdout with `-s`)
131
136
  * **Use Case:** "I refactored the API. Show me added/removed functions or signature changes."
132
137
  * **Benefit:** Reduces token usage by ignoring formatting/comment changes.
133
138
 
134
139
  ```bash
135
140
  cdg semdiff
141
+
142
+ # Output directly to stdout
143
+ cdg semdiff -s
136
144
  ```
137
145
 
138
146
  ### 5. Project Tree (`tree`)
@@ -150,11 +158,38 @@ cdg tree
150
158
 
151
159
  Generates a high-level outline of the project structure (Classes, Functions, Methods only).
152
160
 
153
- * **Output:** `.codigest/digest.xml`
161
+ * **Output:** `.codigest/digest.txt` (or stdout with `-s`)
154
162
  * **Use Case:** "Don't read the implementation details. Just understand the class hierarchy."
155
163
 
156
164
  ```bash
157
165
  cdg digest
166
+
167
+ # Output directly to stdout
168
+ cdg digest -s
169
+ ```
170
+
171
+ ### 7. Environment Status (`status`)
172
+
173
+ Displays the health of `.codigest`, anchor snapshot baseline, pending working tree changes, and generated artifacts.
174
+
175
+ * **Use Case:** "Check what files have changed since my last snapshot and inspect the state of generated context artifacts."
176
+
177
+ ```bash
178
+ cdg status
179
+ ```
180
+
181
+ ### 8. Environment Cleanup (`clean`)
182
+
183
+ Removes the `.codigest/` environment and resets all context anchors and artifacts.
184
+
185
+ * **Use Case:** "Clean up context files and resets Codigest tracking."
186
+
187
+ ```bash
188
+ # Interactive confirmation
189
+ cdg clean
190
+
191
+ # Force clean without prompt
192
+ cdg clean -y
158
193
  ```
159
194
 
160
195
  ---
@@ -67,13 +67,12 @@ cdg init
67
67
 
68
68
  Scans the codebase and generates a structured XML snapshot. Includes a **Pre-flight Check** to prevent accidental token overflow.
69
69
 
70
- * **Output:** `.codigest/snapshot.xml`
70
+ * **Output:** `.codigest/snapshot.txt` (or stdout with `-s`)
71
71
  * **Features:**
72
72
  * **Smart Confirmation:** Automatically skips confirmation for small contexts, but warns you for large ones (>30k tokens).
73
73
  * **Dependency Resolution (`-r`):** Automatically finds and includes local files imported by your target files.
74
74
  * **Scope Control:** You can specify folders or files to scan.
75
-
76
-
75
+ * **Terminal Output (`-s` / `--stdout`):** Output directly to stdout instead of saving to file.
77
76
 
78
77
  ```bash
79
78
  # Basic scan (Interactive confirmation if large)
@@ -84,30 +83,39 @@ cdg scan src/main.py -r
84
83
 
85
84
  # Force execution without confirmation (Good for CI/CD)
86
85
  cdg scan -y --message "Automated snapshot"
86
+
87
+ # Output directly to stdout
88
+ cdg scan -s
87
89
  ```
88
90
 
89
91
  ### 3. Incremental Changes (`diff`)
90
92
 
91
93
  Tracks text-based changes between the last `scan` and the current working tree.
92
94
 
93
- * **Output:** `.codigest/changes.diff`
95
+ * **Output:** `.codigest/changes.diff` (or stdout with `-s`)
94
96
  * **Use Case:** "I modified 3 files. Here is exactly what changed since the last snapshot."
95
97
  * **Note:** Checks against the internal Shadow Git, enabling tracking without committing to the real Git.
96
98
 
97
99
  ```bash
98
100
  cdg diff
101
+
102
+ # Output directly to stdout
103
+ cdg diff -s
99
104
  ```
100
105
 
101
106
  ### 4. Semantic Analysis (`semdiff`)
102
107
 
103
108
  Analyzes **structural changes** (AST-based) rather than line-by-line text differences.
104
109
 
105
- * **Output:** `.codigest/semdiff.xml`
110
+ * **Output:** `.codigest/semdiff.txt` (or stdout with `-s`)
106
111
  * **Use Case:** "I refactored the API. Show me added/removed functions or signature changes."
107
112
  * **Benefit:** Reduces token usage by ignoring formatting/comment changes.
108
113
 
109
114
  ```bash
110
115
  cdg semdiff
116
+
117
+ # Output directly to stdout
118
+ cdg semdiff -s
111
119
  ```
112
120
 
113
121
  ### 5. Project Tree (`tree`)
@@ -125,11 +133,38 @@ cdg tree
125
133
 
126
134
  Generates a high-level outline of the project structure (Classes, Functions, Methods only).
127
135
 
128
- * **Output:** `.codigest/digest.xml`
136
+ * **Output:** `.codigest/digest.txt` (or stdout with `-s`)
129
137
  * **Use Case:** "Don't read the implementation details. Just understand the class hierarchy."
130
138
 
131
139
  ```bash
132
140
  cdg digest
141
+
142
+ # Output directly to stdout
143
+ cdg digest -s
144
+ ```
145
+
146
+ ### 7. Environment Status (`status`)
147
+
148
+ Displays the health of `.codigest`, anchor snapshot baseline, pending working tree changes, and generated artifacts.
149
+
150
+ * **Use Case:** "Check what files have changed since my last snapshot and inspect the state of generated context artifacts."
151
+
152
+ ```bash
153
+ cdg status
154
+ ```
155
+
156
+ ### 8. Environment Cleanup (`clean`)
157
+
158
+ Removes the `.codigest/` environment and resets all context anchors and artifacts.
159
+
160
+ * **Use Case:** "Clean up context files and resets Codigest tracking."
161
+
162
+ ```bash
163
+ # Interactive confirmation
164
+ cdg clean
165
+
166
+ # Force clean without prompt
167
+ cdg clean -y
133
168
  ```
134
169
 
135
170
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codigest"
3
- version = "0.3.5"
3
+ version = "0.3.7"
4
4
  description = "Human-Centered Codebase Context Manager for LLMs"
5
5
  authors = [{name = "Codigest Team", email = "sngd024@google.com"}]
6
6
  readme = "README.md"
@@ -0,0 +1,45 @@
1
+ import typer
2
+ import shutil
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+
6
+ from ..core import common
7
+
8
+ app = typer.Typer()
9
+ console = Console()
10
+
11
+ @app.callback(invoke_without_command=True)
12
+ def handle(
13
+ target: Path = typer.Argument(
14
+ Path.cwd(),
15
+ help="Target directory to clean",
16
+ exists=True,
17
+ file_okay=False,
18
+ dir_okay=True,
19
+ resolve_path=True
20
+ ),
21
+ yes: bool = typer.Option(False, "-y", "--yes", "-f", "--force", help="Skip confirmation prompt"),
22
+ ):
23
+ """
24
+ [Cleanup] Removes the .codigest environment and clears all context anchors and artifacts.
25
+ """
26
+ ctx = common.get_context(target)
27
+ root_path = ctx.root_path
28
+ codigest_dir = root_path / ".codigest"
29
+
30
+ if not codigest_dir.exists():
31
+ console.print(f"[yellow]No .codigest directory found in {root_path}[/yellow]")
32
+ return
33
+
34
+ if not yes:
35
+ console.print(f"[yellow]Warning: This will remove [bold]{codigest_dir}[/bold] and all stored context/anchors.[/yellow]")
36
+ if not typer.confirm("Are you sure you want to remove .codigest?"):
37
+ console.print("[red]Aborted.[/red]")
38
+ raise typer.Exit()
39
+
40
+ try:
41
+ shutil.rmtree(codigest_dir)
42
+ console.print(f"[bold green]✔ Successfully removed .codigest environment from {root_path}[/bold green]")
43
+ except Exception as e:
44
+ console.print(f"[bold red]Error removing .codigest:[/bold red] {e}")
45
+ raise typer.Exit(1)
@@ -8,6 +8,7 @@ from ..core import prompts, shadow, tags, common
8
8
 
9
9
  app = typer.Typer()
10
10
  console = Console()
11
+ err_console = Console(stderr=True)
11
12
 
12
13
  @app.callback(invoke_without_command=True)
13
14
  def handle(
@@ -21,6 +22,7 @@ def handle(
21
22
  ),
22
23
  copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
23
24
  save: bool = typer.Option(True, help="Save to .codigest/changes.diff"),
25
+ stdout: bool = typer.Option(False, "-s", "--stdout", help="Print output to terminal (stdout) instead of file"),
24
26
  message: str = typer.Option("", "--message", "-m", help="Add specific instruction context"),
25
27
  # [추가] resolve 옵션
26
28
  resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
@@ -29,6 +31,8 @@ def handle(
29
31
  [Context Update] Shows changes since the last 'codigest scan'.
30
32
  Useful for updating LLM context without re-uploading everything.
31
33
  """
34
+ log_console = err_console if stdout else console
35
+
32
36
  # [1] Context Setup
33
37
  ctx = common.get_context(target)
34
38
  root_path = ctx.root_path
@@ -38,18 +42,18 @@ def handle(
38
42
  # Check Baseline
39
43
  last_update = anchor.get_last_update_time()
40
44
  if last_update == "Never":
41
- console.print("[yellow]⚠️ No scan history found.[/yellow]")
42
- console.print(" Run [bold cyan]cdg scan[/bold cyan] first to establish a baseline.")
45
+ log_console.print("[yellow]⚠️ No scan history found.[/yellow]")
46
+ log_console.print(" Run [bold cyan]cdg scan[/bold cyan] first to establish a baseline.")
43
47
  raise typer.Exit(1)
44
48
 
45
- console.print(f"[dim]Checking changes since last scan ({last_update})...[/dim]")
49
+ log_console.print(f"[dim]Checking changes since last scan ({last_update})...[/dim]")
46
50
 
47
51
  # [2] Calculate Diff via Context
48
52
  with Progress(
49
53
  SpinnerColumn(),
50
54
  TextColumn("[bold blue]Analyzing changes...[/bold blue]"),
51
55
  transient=True,
52
- console=console
56
+ console=log_console
53
57
  ) as progress:
54
58
  task = progress.add_task("diff", total=None)
55
59
 
@@ -62,7 +66,7 @@ def handle(
62
66
  progress.update(task, completed=100)
63
67
 
64
68
  if not diff_content.strip():
65
- console.print("[green]No changes detected since last scan.[/green]")
69
+ log_console.print("[green]No changes detected since last scan.[/green]")
66
70
  return
67
71
 
68
72
  # [3] Render
@@ -81,14 +85,19 @@ def handle(
81
85
  formatted_diff = f"<diff>\n{safe_content}\n</diff>"
82
86
 
83
87
  # [4] Output
84
- console.print(f"[bold green]Changes Detected![/bold green] ({len(formatted_diff)} chars)")
88
+ log_console.print(f"[bold green]Changes Detected![/bold green] ({len(formatted_diff)} chars)")
85
89
 
86
90
  if copy:
87
- pyperclip.copy(formatted_diff)
88
- console.print("[dim]Clipboard copied[/dim]")
91
+ try:
92
+ pyperclip.copy(formatted_diff)
93
+ log_console.print("[dim]Clipboard copied[/dim]")
94
+ except Exception:
95
+ log_console.print("[dim]⚠️ Clipboard unavailable (skipped copy)[/dim]")
89
96
 
90
- if save:
97
+ if stdout:
98
+ print(formatted_diff)
99
+ elif save:
91
100
  out_path = root_path / ".codigest" / "changes.diff"
92
101
  out_path.parent.mkdir(exist_ok=True)
93
102
  out_path.write_text(formatted_diff, encoding="utf-8")
94
- console.print(f"[dim]Saved to {out_path}[/dim]")
103
+ log_console.print(f"[dim]Saved to {out_path}[/dim]")
@@ -4,16 +4,18 @@ from pathlib import Path
4
4
  from rich.console import Console
5
5
  from rich.progress import Progress, SpinnerColumn, TextColumn
6
6
 
7
- from ..core import structure, prompts, semdiff, tags, tokenizer, common
7
+ from ..core import structure, prompts, semdiff, tags, tokenizer, common, cache
8
8
 
9
9
  app = typer.Typer()
10
10
  console = Console()
11
+ err_console = Console(stderr=True)
11
12
 
12
13
  @app.callback(invoke_without_command=True)
13
14
  def handle(
14
15
  target: Path = typer.Argument(Path.cwd(), help="Target directory"),
15
16
  copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
16
- save: bool = typer.Option(True, help="Save to .codigest/digest.xml"),
17
+ save: bool = typer.Option(True, help="Save to .codigest/digest.txt"),
18
+ stdout: bool = typer.Option(False, "-s", "--stdout", help="Print output to terminal (stdout) instead of file"),
17
19
  message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
18
20
  # [추가]
19
21
  resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
@@ -21,6 +23,8 @@ def handle(
21
23
  """
22
24
  [Architectural View] Summarizes the codebase structure (Classes/Functions only).
23
25
  """
26
+ log_console = err_console if stdout else console
27
+
24
28
  # [1] Context Setup
25
29
  ctx = common.get_context(target)
26
30
  root_path = ctx.root_path
@@ -31,7 +35,7 @@ def handle(
31
35
  SpinnerColumn(),
32
36
  TextColumn("[bold blue]Digesting Architecture...[/bold blue]"),
33
37
  transient=True,
34
- console=console
38
+ console=log_console
35
39
  ) as progress:
36
40
 
37
41
  task = progress.add_task("digest", total=None)
@@ -41,23 +45,33 @@ def handle(
41
45
 
42
46
  tree_str = structure.generate_ascii_tree(files, root_path)
43
47
 
48
+ content_cache = cache.ContentCache(root_path)
44
49
  summary_blocks = []
50
+
45
51
  for file_path in files:
46
52
  if file_path.suffix in (".py", ".pyi"):
47
53
  try:
48
- content = file_path.read_text(encoding="utf-8")
49
- summary = semdiff.summarize(content)
50
-
51
- if summary:
52
- try:
53
- rel_path = file_path.relative_to(root_path).as_posix()
54
- except ValueError:
55
- rel_path = f"[EXTERNAL]/{file_path.name}"
54
+ rel_path = file_path.relative_to(root_path).as_posix()
55
+ except ValueError:
56
+ rel_path = f"[EXTERNAL]/{file_path.name}"
57
+
58
+ summary = content_cache.get_summary(rel_path, blob_hash=None, file_path=file_path)
56
59
 
57
- block = tags.file(rel_path, summary)
58
- summary_blocks.append(block)
59
- except Exception:
60
- continue
60
+ if summary is None:
61
+ try:
62
+ blob_hash = cache.get_file_blob_hash(file_path)
63
+ content = file_path.read_text(encoding="utf-8")
64
+ summary = semdiff.summarize(content)
65
+ if summary and blob_hash:
66
+ content_cache.set_summary(rel_path, blob_hash, summary, file_path=file_path)
67
+ except Exception:
68
+ continue
69
+
70
+ if summary:
71
+ block = tags.file(rel_path, summary)
72
+ summary_blocks.append(block)
73
+
74
+ content_cache.save()
61
75
 
62
76
  summary_blob = "\n".join(summary_blocks)
63
77
 
@@ -70,20 +84,25 @@ def handle(
70
84
  instruction=message
71
85
  )
72
86
  except Exception as e:
73
- console.print(f"[red]❌ Rendering Failed:[/red] {e}")
87
+ log_console.print(f"[red]❌ Rendering Failed:[/red] {e}")
74
88
  raise typer.Exit(1)
75
89
 
76
90
  progress.update(task, completed=100)
77
91
 
78
92
  token_count = tokenizer.estimate_tokens(digest_content)
79
- console.print(f"[bold green]Digest Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
93
+ log_console.print(f"[bold green]Digest Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
80
94
 
81
95
  if copy:
82
- pyperclip.copy(digest_content)
83
- console.print("[dim]📋 Copied to clipboard[/dim]")
96
+ try:
97
+ pyperclip.copy(digest_content)
98
+ log_console.print("[dim]📋 Copied to clipboard[/dim]")
99
+ except Exception:
100
+ log_console.print("[dim]⚠️ Clipboard unavailable (skipped copy)[/dim]")
84
101
 
85
- if save:
86
- out_path = root_path / ".codigest" / "digest.xml"
102
+ if stdout:
103
+ print(digest_content)
104
+ elif save:
105
+ out_path = root_path / ".codigest" / "digest.txt"
87
106
  out_path.parent.mkdir(exist_ok=True)
88
107
  out_path.write_text(digest_content, encoding="utf-8")
89
- console.print(f"[dim]💾 Saved to {out_path}[/dim]")
108
+ log_console.print(f"[dim]💾 Saved to {out_path}[/dim]")
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  import typer
2
3
  from pathlib import Path
3
4
  from rich.console import Console
@@ -5,10 +6,11 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
5
6
  from rich.panel import Panel
6
7
  from rich.filesize import decimal
7
8
 
8
- from ..core import structure, tags, prompts, processor, shadow, tokenizer, common
9
+ from ..core import structure, tags, prompts, processor, shadow, tokenizer, common, cache
9
10
 
10
11
  app = typer.Typer()
11
12
  console = Console()
13
+ err_console = Console(stderr=True)
12
14
 
13
15
  @app.callback(invoke_without_command=True)
14
16
  def handle(
@@ -18,7 +20,8 @@ def handle(
18
20
  exists=True,
19
21
  resolve_path=True
20
22
  ),
21
- output: str = typer.Option("snapshot.xml", help="Output filename inside .codigest/"),
23
+ output: str = typer.Option("snapshot.txt", "-o", "--output", help="Output filename inside .codigest/"),
24
+ stdout: bool = typer.Option(False, "-s", "--stdout", help="Print output to terminal (stdout) instead of file"),
22
25
  all: bool = typer.Option(False, "--all", "-a", help="Ignore config filters"),
23
26
  message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
24
27
  line_numbers: bool = typer.Option(False, "--lines", "-l", help="Add line numbers to code blocks"),
@@ -29,6 +32,8 @@ def handle(
29
32
  Scans the codebase.
30
33
  If TARGETS provided, only scans those paths within the project.
31
34
  """
35
+ log_console = err_console if stdout else console
36
+
32
37
  # [1] Context Setup (Centralized)
33
38
  ctx = common.get_context(targets)
34
39
  root_path = ctx.root_path
@@ -36,11 +41,11 @@ def handle(
36
41
  # Init check
37
42
  artifact_dir = root_path / ".codigest"
38
43
  if not artifact_dir.exists():
39
- console.print(f"[yellow][Warning] .codigest directory missing in {root_path.name}. Running init...[/yellow]")
44
+ log_console.print(f"[yellow][Warning] .codigest directory missing in {root_path.name}. Running init...[/yellow]")
40
45
  try:
41
46
  artifact_dir.mkdir(exist_ok=True)
42
47
  except PermissionError:
43
- console.print(f"[red][Error] Cannot create .codigest at {root_path}[/red]")
48
+ log_console.print(f"[red][Error] Cannot create .codigest at {root_path}[/red]")
44
49
  raise typer.Exit(1)
45
50
 
46
51
  output_path = artifact_dir / output
@@ -52,7 +57,7 @@ def handle(
52
57
  SpinnerColumn(),
53
58
  TextColumn("[bold blue]Scanning...[/bold blue]"),
54
59
  transient=True,
55
- console=console
60
+ console=log_console
56
61
  ) as progress:
57
62
  task = progress.add_task("scanning", total=None)
58
63
 
@@ -70,7 +75,7 @@ def handle(
70
75
  total_size = sum(f.stat().st_size for f in files)
71
76
  est_tokens = int(total_size / 4)
72
77
 
73
- console.print(Panel(f"""[bold]Scan Plan[/bold]
78
+ log_console.print(Panel(f"""[bold]Scan Plan[/bold]
74
79
  Target: [cyan]{root_path}[/cyan]
75
80
  Scope: {total_files} files
76
81
  Est. Size: {decimal(total_size)}
@@ -83,19 +88,19 @@ def handle(
83
88
  if yes:
84
89
  pass
85
90
  elif is_large_context:
86
- console.print(f"[yellow][Warning] Large context detected (> {TOKEN_THRESHOLD:,} tokens or > {FILE_COUNT_THRESHOLD} files).[/yellow]")
91
+ log_console.print(f"[yellow][Warning] Large context detected (> {TOKEN_THRESHOLD:,} tokens or > {FILE_COUNT_THRESHOLD} files).[/yellow]")
87
92
  if not typer.confirm("Proceed with digestion?"):
88
- console.print("[red]Aborted.[/red]")
93
+ log_console.print("[red]Aborted.[/red]")
89
94
  raise typer.Exit()
90
95
  else:
91
- console.print("[dim]Small context detected. Automatically proceeding...[/dim]")
96
+ log_console.print("[dim]Small context detected. Automatically proceeding...[/dim]")
92
97
 
93
98
  # [4] Execution
94
99
  with Progress(
95
100
  SpinnerColumn(),
96
101
  TextColumn("[bold blue]Generating Snapshot...[/bold blue]"),
97
102
  transient=True,
98
- console=console
103
+ console=log_console
99
104
  ) as progress:
100
105
 
101
106
  if anchor.has_history():
@@ -109,19 +114,40 @@ def handle(
109
114
 
110
115
  tree_str = structure.generate_ascii_tree(files, root_path)
111
116
 
117
+ content_cache = cache.ContentCache(root_path)
112
118
  file_blocks = []
119
+
113
120
  for file_path in files:
114
121
  try:
115
122
  rel_path = file_path.relative_to(root_path).as_posix()
116
123
  except ValueError:
117
124
  rel_path = f"[EXTERNAL]/{file_path.name}"
118
125
 
119
- try:
120
- content = processor.read_file_content(file_path, add_line_numbers=line_numbers)
121
- block = tags.file(rel_path, content)
122
- file_blocks.append(block)
123
- except Exception:
124
- continue
126
+ cached_block = content_cache.get_file_block(
127
+ rel_path=rel_path,
128
+ line_numbers=line_numbers,
129
+ file_path=file_path
130
+ )
131
+
132
+ if cached_block is not None:
133
+ file_blocks.append(cached_block)
134
+ else:
135
+ try:
136
+ blob_hash = cache.get_file_blob_hash(file_path)
137
+ content = processor.read_file_content(file_path, add_line_numbers=line_numbers)
138
+ block = tags.file(rel_path, content)
139
+ file_blocks.append(block)
140
+ content_cache.set_file_block(
141
+ rel_path=rel_path,
142
+ blob_hash=blob_hash,
143
+ line_numbers=line_numbers,
144
+ block=block,
145
+ file_path=file_path
146
+ )
147
+ except Exception:
148
+ continue
149
+
150
+ content_cache.save()
125
151
 
126
152
  source_code_blob = "\n\n".join(file_blocks)
127
153
 
@@ -134,27 +160,30 @@ def handle(
134
160
  instruction=message
135
161
  )
136
162
  except Exception as e:
137
- console.print(f"[red][Error] Template Rendering Failed:[/red] {e}")
163
+ log_console.print(f"[red][Error] Template Rendering Failed:[/red] {e}")
138
164
  raise typer.Exit(1)
139
165
 
140
166
  try:
141
167
  anchor.update(files)
142
168
  except Exception as e:
143
- console.print(f"[yellow][Warning] Failed to update context anchor: {e}[/yellow]")
144
-
145
- try:
146
- output_path.write_text(snapshot_content, encoding="utf-8")
147
- final_token_count = tokenizer.estimate_tokens(snapshot_content)
169
+ log_console.print(f"[yellow][Warning] Failed to update context anchor: {e}[/yellow]")
148
170
 
149
- console.print("[bold green]Snapshot Saved![/bold green]")
150
- console.print(f" Path: [underline]{output_path}[/underline]")
151
- console.print(f" Final Tokens: [bold cyan]~{final_token_count:,}[/bold cyan]")
152
-
153
- if anchor.has_history():
154
- pre_diff_path = artifact_dir / "previous_changes.diff"
155
- if pre_diff_path.exists() and pre_diff_path.stat().st_size > 0:
156
- console.print(f" [dim]Changes before this scan saved to: {pre_diff_path.name}[/dim]")
171
+ if stdout:
172
+ print(snapshot_content)
173
+ else:
174
+ try:
175
+ output_path.write_text(snapshot_content, encoding="utf-8")
176
+ final_token_count = tokenizer.estimate_tokens(snapshot_content)
177
+
178
+ log_console.print("[bold green]Snapshot Saved![/bold green]")
179
+ log_console.print(f" Path: [underline]{output_path}[/underline]")
180
+ log_console.print(f" Final Tokens: [bold cyan]~{final_token_count:,}[/bold cyan]")
181
+
182
+ if anchor.has_history():
183
+ pre_diff_path = artifact_dir / "previous_changes.diff"
184
+ if pre_diff_path.exists() and pre_diff_path.stat().st_size > 0:
185
+ log_console.print(f" [dim]Changes before this scan saved to: {pre_diff_path.name}[/dim]")
157
186
 
158
- except Exception as e:
159
- console.print(f"[bold red][Error] Save Failed:[/bold red] {e}")
160
- raise typer.Exit(1)
187
+ except Exception as e:
188
+ log_console.print(f"[bold red][Error] Save Failed:[/bold red] {e}")
189
+ raise typer.Exit(1)