codigest 0.3.5__tar.gz → 0.3.6__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.6}/PKG-INFO +42 -7
  2. {codigest-0.3.5 → codigest-0.3.6}/README.md +41 -6
  3. {codigest-0.3.5 → codigest-0.3.6}/pyproject.toml +1 -1
  4. codigest-0.3.6/src/codigest/commands/clean.py +45 -0
  5. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/diff.py +19 -10
  6. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/digest.py +43 -22
  7. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/scan.py +55 -33
  8. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/semdiff.py +21 -12
  9. codigest-0.3.6/src/codigest/commands/status.py +151 -0
  10. codigest-0.3.6/src/codigest/core/cache.py +78 -0
  11. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/shadow.py +35 -18
  12. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/main.py +3 -1
  13. {codigest-0.3.5 → codigest-0.3.6}/uv.lock +1 -5
  14. {codigest-0.3.5 → codigest-0.3.6}/.github/workflows/publish.yml +0 -0
  15. {codigest-0.3.5 → codigest-0.3.6}/.gitignore +0 -0
  16. {codigest-0.3.5 → codigest-0.3.6}/.python-version +0 -0
  17. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/__init__.py +0 -0
  18. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/__main__.py +0 -0
  19. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/init.py +0 -0
  20. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/tree.py +0 -0
  21. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/config.py +0 -0
  22. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/common.py +0 -0
  23. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/git_ops.py +0 -0
  24. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/processor.py +0 -0
  25. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/prompts.py +0 -0
  26. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/resolver.py +0 -0
  27. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/scanner.py +0 -0
  28. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/semdiff.py +0 -0
  29. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/structure.py +0 -0
  30. {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/tags.py +0 -0
  31. {codigest-0.3.5 → codigest-0.3.6}/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.6
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.6"
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,35 @@ 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
+ blob_hash = cache.get_file_blob_hash(file_path)
59
+ summary = None
60
+ if blob_hash:
61
+ summary = content_cache.get_summary(rel_path, blob_hash)
56
62
 
57
- block = tags.file(rel_path, summary)
58
- summary_blocks.append(block)
59
- except Exception:
60
- continue
63
+ if summary is None:
64
+ try:
65
+ content = file_path.read_text(encoding="utf-8")
66
+ summary = semdiff.summarize(content)
67
+ if summary and blob_hash:
68
+ content_cache.set_summary(rel_path, blob_hash, summary)
69
+ except Exception:
70
+ continue
71
+
72
+ if summary:
73
+ block = tags.file(rel_path, summary)
74
+ summary_blocks.append(block)
75
+
76
+ content_cache.save()
61
77
 
62
78
  summary_blob = "\n".join(summary_blocks)
63
79
 
@@ -70,20 +86,25 @@ def handle(
70
86
  instruction=message
71
87
  )
72
88
  except Exception as e:
73
- console.print(f"[red]❌ Rendering Failed:[/red] {e}")
89
+ log_console.print(f"[red]❌ Rendering Failed:[/red] {e}")
74
90
  raise typer.Exit(1)
75
91
 
76
92
  progress.update(task, completed=100)
77
93
 
78
94
  token_count = tokenizer.estimate_tokens(digest_content)
79
- console.print(f"[bold green]Digest Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
95
+ log_console.print(f"[bold green]Digest Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
80
96
 
81
97
  if copy:
82
- pyperclip.copy(digest_content)
83
- console.print("[dim]📋 Copied to clipboard[/dim]")
98
+ try:
99
+ pyperclip.copy(digest_content)
100
+ log_console.print("[dim]📋 Copied to clipboard[/dim]")
101
+ except Exception:
102
+ log_console.print("[dim]⚠️ Clipboard unavailable (skipped copy)[/dim]")
84
103
 
85
- if save:
86
- out_path = root_path / ".codigest" / "digest.xml"
104
+ if stdout:
105
+ print(digest_content)
106
+ elif save:
107
+ out_path = root_path / ".codigest" / "digest.txt"
87
108
  out_path.parent.mkdir(exist_ok=True)
88
109
  out_path.write_text(digest_content, encoding="utf-8")
89
- console.print(f"[dim]💾 Saved to {out_path}[/dim]")
110
+ 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,33 @@ 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
+ blob_hash = cache.get_file_blob_hash(file_path)
127
+ cached_block = None
128
+ if blob_hash:
129
+ cached_block = content_cache.get_file_block(rel_path, blob_hash, line_numbers)
130
+
131
+ if cached_block is not None:
132
+ file_blocks.append(cached_block)
133
+ else:
134
+ try:
135
+ content = processor.read_file_content(file_path, add_line_numbers=line_numbers)
136
+ block = tags.file(rel_path, content)
137
+ file_blocks.append(block)
138
+ if blob_hash:
139
+ content_cache.set_file_block(rel_path, blob_hash, line_numbers, block)
140
+ except Exception:
141
+ continue
142
+
143
+ content_cache.save()
125
144
 
126
145
  source_code_blob = "\n\n".join(file_blocks)
127
146
 
@@ -134,27 +153,30 @@ def handle(
134
153
  instruction=message
135
154
  )
136
155
  except Exception as e:
137
- console.print(f"[red][Error] Template Rendering Failed:[/red] {e}")
156
+ log_console.print(f"[red][Error] Template Rendering Failed:[/red] {e}")
138
157
  raise typer.Exit(1)
139
158
 
140
159
  try:
141
160
  anchor.update(files)
142
161
  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)
162
+ log_console.print(f"[yellow][Warning] Failed to update context anchor: {e}[/yellow]")
148
163
 
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]")
164
+ if stdout:
165
+ print(snapshot_content)
166
+ else:
167
+ try:
168
+ output_path.write_text(snapshot_content, encoding="utf-8")
169
+ final_token_count = tokenizer.estimate_tokens(snapshot_content)
170
+
171
+ log_console.print("[bold green]Snapshot Saved![/bold green]")
172
+ log_console.print(f" Path: [underline]{output_path}[/underline]")
173
+ log_console.print(f" Final Tokens: [bold cyan]~{final_token_count:,}[/bold cyan]")
174
+
175
+ if anchor.has_history():
176
+ pre_diff_path = artifact_dir / "previous_changes.diff"
177
+ if pre_diff_path.exists() and pre_diff_path.stat().st_size > 0:
178
+ log_console.print(f" [dim]Changes before this scan saved to: {pre_diff_path.name}[/dim]")
157
179
 
158
- except Exception as e:
159
- console.print(f"[bold red][Error] Save Failed:[/bold red] {e}")
160
- raise typer.Exit(1)
180
+ except Exception as e:
181
+ log_console.print(f"[bold red][Error] Save Failed:[/bold red] {e}")
182
+ raise typer.Exit(1)
@@ -8,12 +8,14 @@ from ..core import prompts, shadow, semdiff, tags, tokenizer, 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(
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/semdiff.xml"),
17
+ save: bool = typer.Option(True, help="Save to .codigest/semdiff.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
  # [추가] resolve 옵션
19
21
  resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
@@ -22,6 +24,8 @@ def handle(
22
24
  [Advanced] Generates a Semantic Diff (AST-based) report.
23
25
  Shows ADDED/REMOVED/MODIFIED functions & classes instead of raw text lines.
24
26
  """
27
+ log_console = err_console if stdout else console
28
+
25
29
  # [1] Context Setup
26
30
  ctx = common.get_context(target)
27
31
  root_path = ctx.root_path
@@ -30,10 +34,10 @@ def handle(
30
34
 
31
35
  last_update = anchor.get_last_update_time()
32
36
  if last_update == "Never":
33
- console.print("[yellow]⚠️ No scan history found. Run [bold]cdg scan[/bold] first.[/yellow]")
37
+ log_console.print("[yellow]⚠️ No scan history found. Run [bold]cdg scan[/bold] first.[/yellow]")
34
38
  raise typer.Exit(1)
35
39
 
36
- console.print(f"[dim]Analyzing structural changes since ({last_update})...[/dim]")
40
+ log_console.print(f"[dim]Analyzing structural changes since ({last_update})...[/dim]")
37
41
 
38
42
  reports = []
39
43
 
@@ -41,7 +45,7 @@ def handle(
41
45
  SpinnerColumn(),
42
46
  TextColumn("[bold blue]Parsing AST...[/bold blue]"),
43
47
  transient=True,
44
- console=console
48
+ console=log_console
45
49
  ) as progress:
46
50
  task = progress.add_task("semdiff", total=None)
47
51
 
@@ -108,7 +112,7 @@ def handle(
108
112
  progress.update(task, completed=100)
109
113
 
110
114
  if not reports:
111
- console.print("[green]No structural (AST) changes detected.[/green]")
115
+ log_console.print("[green]No structural (AST) changes detected.[/green]")
112
116
  return
113
117
 
114
118
  # [4] Render
@@ -124,18 +128,23 @@ def handle(
124
128
  instruction=message
125
129
  )
126
130
  except Exception as e:
127
- console.print(f"[red]Template Error:[/red] {e}")
131
+ log_console.print(f"[red]Template Error:[/red] {e}")
128
132
  raise typer.Exit(1)
129
133
 
130
134
  token_count = tokenizer.estimate_tokens(final_output)
131
- console.print(f"[bold green]SemDiff Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
135
+ log_console.print(f"[bold green]SemDiff Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
132
136
 
133
137
  if copy:
134
- pyperclip.copy(final_output)
135
- console.print("[dim]Clipboard copied[/dim]")
138
+ try:
139
+ pyperclip.copy(final_output)
140
+ log_console.print("[dim]Clipboard copied[/dim]")
141
+ except Exception:
142
+ log_console.print("[dim]⚠️ Clipboard unavailable (skipped copy)[/dim]")
136
143
 
137
- if save:
138
- out_path = root_path / ".codigest" / "semdiff.xml"
144
+ if stdout:
145
+ print(final_output)
146
+ elif save:
147
+ out_path = root_path / ".codigest" / "semdiff.txt"
139
148
  out_path.parent.mkdir(exist_ok=True)
140
149
  out_path.write_text(final_output, encoding="utf-8")
141
- console.print(f"[dim]Saved to {out_path}[/dim]")
150
+ log_console.print(f"[dim]Saved to {out_path}[/dim]")
@@ -0,0 +1,151 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+ from rich.table import Table
6
+ from rich.filesize import decimal
7
+
8
+ from ..core import common, shadow, tokenizer
9
+
10
+ app = typer.Typer()
11
+ console = Console()
12
+
13
+ @app.callback(invoke_without_command=True)
14
+ def handle(
15
+ target: Path = typer.Argument(
16
+ Path.cwd(),
17
+ help="Target directory to check status",
18
+ exists=True,
19
+ file_okay=False,
20
+ dir_okay=True,
21
+ resolve_path=True
22
+ ),
23
+ resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports when checking context files"),
24
+ ):
25
+ """
26
+ [Environment Status] Displays information about the .codigest setup, anchor baseline, changes, and artifacts.
27
+ """
28
+ ctx = common.get_context(target)
29
+ root_path = ctx.root_path
30
+ codigest_dir = root_path / ".codigest"
31
+
32
+ is_initialized = codigest_dir.exists() and codigest_dir.is_dir()
33
+
34
+ # Header Panel
35
+ init_status_str = "[bold green]Initialized[/bold green]" if is_initialized else "[bold yellow]Not Initialized[/bold yellow]"
36
+ header_content = (
37
+ f"🦁 [bold cyan]Codigest Environment Status[/bold cyan]\n"
38
+ f"📂 [bold]Root:[/bold] {root_path}\n"
39
+ f"⚙️ [bold]Status:[/bold] {init_status_str} "
40
+ f"({'[dim].codigest/ present[/dim]' if is_initialized else '[dim]run cdg init to set up[/dim]'})"
41
+ )
42
+ console.print(Panel(header_content, expand=False))
43
+
44
+ if not is_initialized:
45
+ console.print("\n[yellow]💡 Tip: Run [bold cyan]cdg init[/bold cyan] to initialize context tracking for this repository.[/yellow]")
46
+ return
47
+
48
+ anchor = shadow.ContextAnchor(root_path)
49
+ has_anchor = anchor.has_history()
50
+ last_update = anchor.get_last_update_time() if has_anchor else "Never"
51
+
52
+ # Fetch target files
53
+ current_files = ctx.get_target_files(resolve_deps=resolve)
54
+ total_files = len(current_files)
55
+
56
+ # 1. Baseline Section
57
+ console.print("\n[bold blue]📍 Context Anchor Baseline[/bold blue]")
58
+ console.print(f" • [bold]Last Snapshot:[/bold] {last_update}")
59
+ console.print(f" • [bold]Tracked Source Files:[/bold] {total_files} files")
60
+
61
+ # 2. Pending Working Tree Changes
62
+ console.print("\n[bold blue]📝 Pending Working Tree Changes[/bold blue] [dim](since last scan)[/dim]")
63
+ if not has_anchor:
64
+ console.print(" [yellow]⚠️ No baseline scan found. Run [bold cyan]cdg scan[/bold cyan] to create the initial snapshot.[/yellow]")
65
+ else:
66
+ diff_content = anchor.get_changes(current_files)
67
+ changed_files = anchor.get_changed_files(current_files)
68
+
69
+ if not diff_content.strip():
70
+ console.print(" [green]✔ Working tree clean (No pending changes detected)[/green]")
71
+ else:
72
+ diff_tokens = tokenizer.estimate_tokens(diff_content)
73
+ console.print(f" • [bold]Changed Files:[/bold] [yellow]{len(changed_files)} files[/yellow]")
74
+ console.print(f" • [bold]Est. Diff Size:[/bold] [cyan]~{diff_tokens:,} tokens[/cyan] ({len(diff_content):,} chars)")
75
+
76
+ console.print(" • [bold]Modified / New Files:[/bold]")
77
+ max_show = 10
78
+ for idx, p in enumerate(changed_files):
79
+ if idx >= max_show:
80
+ remaining = len(changed_files) - max_show
81
+ console.print(f" [dim]... and {remaining} more file(s)[/dim]")
82
+ break
83
+ try:
84
+ rel_p = p.relative_to(root_path).as_posix()
85
+ except ValueError:
86
+ rel_p = str(p)
87
+
88
+ # Check if new file or modified
89
+ try:
90
+ rel_path_obj = p.relative_to(root_path)
91
+ anchor_file_content = anchor.read_anchor_file(rel_path_obj)
92
+ except Exception:
93
+ anchor_file_content = ""
94
+
95
+ status_label = "[green][NEW] [/green]" if not anchor_file_content else "[yellow][MODIFIED][/yellow]"
96
+ console.print(f" {status_label} {rel_p}")
97
+
98
+ # 3. Artifacts Summary Table
99
+ console.print("\n[bold blue]📦 Context Artifacts[/bold blue] [dim](.codigest/)[/dim]")
100
+
101
+ artifacts = [
102
+ ("snapshot.txt", "Full Context Snapshot", "cdg scan"),
103
+ ("changes.diff", "Incremental Diff", "cdg diff"),
104
+ ("semdiff.txt", "Semantic AST Diff", "cdg semdiff"),
105
+ ("digest.txt", "Architecture Digest", "cdg digest"),
106
+ ]
107
+
108
+ table = Table(box=None, header_style="bold cyan", padding=(0, 2))
109
+ table.add_column("Artifact", style="bold white")
110
+ table.add_column("Description", style="dim")
111
+ table.add_column("Status")
112
+ table.add_column("Size", justify="right")
113
+ table.add_column("Tokens", justify="right", style="cyan")
114
+
115
+ for filename, desc, cmd_hint in artifacts:
116
+ art_path = codigest_dir / filename
117
+ if art_path.exists() and art_path.is_file():
118
+ stat = art_path.stat()
119
+ size_str = decimal(stat.st_size)
120
+ try:
121
+ text_content = art_path.read_text(encoding="utf-8", errors="replace")
122
+ tokens = tokenizer.estimate_tokens(text_content)
123
+ tokens_str = f"~{tokens:,}"
124
+ except Exception:
125
+ tokens_str = "N/A"
126
+ status_str = "[bold green]Ready[/bold green]"
127
+ else:
128
+ size_str = "-"
129
+ tokens_str = "-"
130
+ status_str = f"[dim]Missing ({cmd_hint})[/dim]"
131
+
132
+ table.add_row(filename, desc, status_str, size_str, tokens_str)
133
+
134
+ console.print(table)
135
+
136
+ # 4. Configuration Summary
137
+ console.print("\n[bold blue]⚙️ Configuration[/bold blue] [dim](.codigest/config.toml)[/dim]")
138
+ config_file = codigest_dir / "config.toml"
139
+ if config_file.exists():
140
+ console.print(" • [bold]Config File:[/bold] [green]Found[/green]")
141
+ else:
142
+ console.print(" • [bold]Config File:[/bold] [yellow]Default settings (no config.toml)[/yellow]")
143
+
144
+ if ctx.config_extensions:
145
+ exts_str = ", ".join(sorted(ctx.config_extensions))
146
+ console.print(f" • [bold]Filter Extensions:[/bold] [cyan]{exts_str}[/cyan]")
147
+ else:
148
+ console.print(" • [bold]Filter Extensions:[/bold] [dim]All non-ignored extensions[/dim]")
149
+
150
+ if ctx.config_ignores:
151
+ console.print(f" • [bold]Extra Ignores:[/bold] {len(ctx.config_ignores)} custom patterns")
@@ -0,0 +1,78 @@
1
+ """
2
+ Git Blob Hash-based Content & AST Cache.
3
+ Provides fast caching for scan snapshot blocks and digest AST summaries.
4
+ """
5
+ import json
6
+ import hashlib
7
+ from pathlib import Path
8
+ from typing import Optional, Dict, Any
9
+
10
+ def compute_git_blob_hash(data: bytes) -> str:
11
+ """Computes Git-compatible SHA-1 blob hash for raw bytes."""
12
+ header = f"blob {len(data)}\0".encode("utf-8")
13
+ return hashlib.sha1(header + data).hexdigest()
14
+
15
+ def get_file_blob_hash(file_path: Path) -> Optional[str]:
16
+ """Reads file and computes its Git blob hash."""
17
+ try:
18
+ data = file_path.read_bytes()
19
+ return compute_git_blob_hash(data)
20
+ except Exception:
21
+ return None
22
+
23
+ class ContentCache:
24
+ def __init__(self, root_path: Path):
25
+ self.root_path = root_path
26
+ self.cache_file = root_path / ".codigest" / "cache.json"
27
+ self._data: Dict[str, Any] = {}
28
+ self.hits = 0
29
+ self.misses = 0
30
+ self._load()
31
+
32
+ def _load(self):
33
+ if self.cache_file.exists():
34
+ try:
35
+ with open(self.cache_file, "r", encoding="utf-8") as f:
36
+ self._data = json.load(f)
37
+ except Exception:
38
+ self._data = {}
39
+ else:
40
+ self._data = {}
41
+
42
+ def save(self):
43
+ try:
44
+ self.cache_file.parent.mkdir(parents=True, exist_ok=True)
45
+ with open(self.cache_file, "w", encoding="utf-8") as f:
46
+ json.dump(self._data, f, ensure_ascii=False, indent=2)
47
+ except Exception:
48
+ pass
49
+
50
+ def get_file_block(self, rel_path: str, blob_hash: str, line_numbers: bool) -> Optional[str]:
51
+ cache_key = f"{rel_path}:{blob_hash}:lines={line_numbers}"
52
+ val = self._data.get("file_blocks", {}).get(cache_key)
53
+ if val is not None:
54
+ self.hits += 1
55
+ else:
56
+ self.misses += 1
57
+ return val
58
+
59
+ def set_file_block(self, rel_path: str, blob_hash: str, line_numbers: bool, block: str):
60
+ if "file_blocks" not in self._data:
61
+ self._data["file_blocks"] = {}
62
+ cache_key = f"{rel_path}:{blob_hash}:lines={line_numbers}"
63
+ self._data["file_blocks"][cache_key] = block
64
+
65
+ def get_summary(self, rel_path: str, blob_hash: str) -> Optional[str]:
66
+ cache_key = f"{rel_path}:{blob_hash}"
67
+ val = self._data.get("summaries", {}).get(cache_key)
68
+ if val is not None:
69
+ self.hits += 1
70
+ else:
71
+ self.misses += 1
72
+ return val
73
+
74
+ def set_summary(self, rel_path: str, blob_hash: str, summary: str):
75
+ if "summaries" not in self._data:
76
+ self._data["summaries"] = {}
77
+ cache_key = f"{rel_path}:{blob_hash}"
78
+ self._data["summaries"][cache_key] = summary
@@ -10,17 +10,16 @@ from loguru import logger
10
10
 
11
11
  class ContextAnchor:
12
12
  def __init__(self, root_path: Path):
13
- self.root = root_path
14
- self.anchor_dir = root_path / ".codigest" / "anchor"
15
-
16
- self.git_dir = self.anchor_dir / ".shadow_git"
13
+ self.root = root_path.resolve()
14
+ self.anchor_dir = (self.root / ".codigest" / "anchor").resolve()
15
+ self.git_dir = (self.anchor_dir / ".shadow_git").resolve()
17
16
 
18
17
  def has_history(self) -> bool:
19
18
  """Checks if a valid anchor (git repo with commits) exists."""
20
19
  return self.git_dir.exists() and (self.git_dir / "HEAD").exists()
21
20
 
22
21
  def _run_git(self, args: list[str], cwd: Path | None = None, check=True) -> str:
23
-
22
+ self.anchor_dir.mkdir(parents=True, exist_ok=True)
24
23
  base_cmd = [
25
24
  "git",
26
25
  "--git-dir", str(self.git_dir),
@@ -42,11 +41,16 @@ class ContextAnchor:
42
41
  return (result.stdout or "").strip()
43
42
 
44
43
  def update(self, source_files: list[Path]):
45
- if not self.git_dir.exists():
46
- self.anchor_dir.mkdir(parents=True, exist_ok=True)
47
- self.git_dir.mkdir(parents=True, exist_ok=True)
48
-
49
- self._run_git(["init"])
44
+ self.anchor_dir.mkdir(parents=True, exist_ok=True)
45
+ (self.anchor_dir / ".gitignore").write_text(".shadow_git\n", encoding="utf-8")
46
+ if not self.git_dir.exists() or not (self.git_dir / "HEAD").exists():
47
+ if self.git_dir.exists():
48
+ shutil.rmtree(self.git_dir)
49
+ subprocess.run(
50
+ ["git", "init", "--bare", str(self.git_dir)],
51
+ capture_output=True, check=False
52
+ )
53
+ self._run_git(["config", "core.bare", "false"])
50
54
  self._run_git(["config", "user.email", "codigest@ai"])
51
55
  self._run_git(["config", "user.name", "Context Manager"])
52
56
  self._run_git(["config", "core.autocrlf", "false"])
@@ -63,7 +67,7 @@ class ContextAnchor:
63
67
  source_rel_paths = set()
64
68
 
65
69
  for src in source_files:
66
- if ".git" in src.parts:
70
+ if ".git" in src.parts or ".codigest" in src.parts:
67
71
  continue
68
72
  try:
69
73
  rel = src.relative_to(self.root)
@@ -80,6 +84,7 @@ class ContextAnchor:
80
84
  if src_stat.st_size == dest_stat.st_size and dest_stat.st_mtime >= src_stat.st_mtime:
81
85
  should_copy = False
82
86
 
87
+ source_rel_paths.add(rel)
83
88
  if should_copy:
84
89
  shutil.copy2(src, dest)
85
90
  except Exception:
@@ -87,7 +92,7 @@ class ContextAnchor:
87
92
 
88
93
  for anchor_file in self.anchor_dir.rglob("*"):
89
94
  if anchor_file.is_file():
90
- if ".git" in anchor_file.parts:
95
+ if ".shadow_git" in anchor_file.parts:
91
96
  continue
92
97
 
93
98
  try:
@@ -99,7 +104,10 @@ class ContextAnchor:
99
104
  continue
100
105
 
101
106
  self._run_git(["add", "."])
102
- if self._run_git(["diff-index", "--quiet", "HEAD", "--"], check=False) != "":
107
+ if not self._run_git(["rev-parse", "--verify", "HEAD"], check=False):
108
+ self._run_git(["commit", "-m", f"Snapshot: {int(time.time())}"])
109
+ logger.info("Initial context anchor created.")
110
+ elif self._run_git(["diff-index", "--quiet", "HEAD", "--"], check=False) != "":
103
111
  self._run_git(["commit", "-m", f"Snapshot: {int(time.time())}"])
104
112
  logger.info("Context anchor updated.")
105
113
 
@@ -118,7 +126,7 @@ class ContextAnchor:
118
126
  try:
119
127
  current_rel_paths = set()
120
128
  for src in current_files:
121
- if ".git" in src.parts:
129
+ if ".git" in src.parts or ".codigest" in src.parts:
122
130
  continue
123
131
  try:
124
132
  rel = src.relative_to(self.root)
@@ -149,7 +157,15 @@ class ContextAnchor:
149
157
 
150
158
  diff_text = (result.stdout or "")
151
159
  diff_text = diff_text.replace("temp_diff_baseline/", "").replace("temp_diff_current/", "")
152
- return diff_text
160
+
161
+ clean_lines = []
162
+ skip = False
163
+ for line in diff_text.splitlines():
164
+ if line.startswith("diff --git"):
165
+ skip = any(x in line for x in [".shadow_git", ".codigest", ".git"])
166
+ if not skip:
167
+ clean_lines.append(line)
168
+ return "\n".join(clean_lines)
153
169
 
154
170
  finally:
155
171
  for d in [temp_current, temp_baseline]:
@@ -158,7 +174,7 @@ class ContextAnchor:
158
174
 
159
175
  def _prune_ignored_files(self, baseline_dir: Path, valid_rel_paths: set[Path]):
160
176
  for file_path in baseline_dir.rglob("*"):
161
- if file_path.is_file() and ".git" not in file_path.parts:
177
+ if file_path.is_file() and ".git" not in file_path.parts and ".codigest" not in file_path.parts and ".shadow_git" not in file_path.parts:
162
178
  try:
163
179
  rel_path = file_path.relative_to(baseline_dir)
164
180
  if rel_path in valid_rel_paths:
@@ -209,5 +225,6 @@ class ContextAnchor:
209
225
  if len(parts) >= 4:
210
226
  p = parts[-1]
211
227
  if p.startswith("b/") or p.startswith("a/"): p = p[2:]
212
- paths.add(self.root / p)
213
- return sorted(list(paths))
228
+ if not any(part in (".codigest", ".git", ".shadow_git") for part in Path(p).parts):
229
+ paths.add(self.root / p)
230
+ return sorted(list(paths))
@@ -6,7 +6,7 @@ import typer
6
6
  from rich.console import Console
7
7
  from importlib.metadata import version, PackageNotFoundError
8
8
 
9
- from .commands import init, scan, tree, digest, diff, semdiff
9
+ from .commands import init, scan, tree, digest, diff, semdiff, status, clean
10
10
 
11
11
  console = Console()
12
12
 
@@ -18,6 +18,8 @@ app = typer.Typer(
18
18
  )
19
19
 
20
20
  app.add_typer(init.app, name="init")
21
+ app.add_typer(status.app, name="status")
22
+ app.add_typer(clean.app, name="clean")
21
23
  app.add_typer(scan.app, name="scan")
22
24
  app.add_typer(tree.app, name="tree")
23
25
  app.add_typer(digest.app, name="digest")
@@ -16,11 +16,7 @@ wheels = [
16
16
 
17
17
  [[package]]
18
18
  name = "codigest"
19
- <<<<<<< HEAD
20
- version = "0.3.2"
21
- =======
22
- version = "0.3.5"
23
- >>>>>>> 03f5ec5087ff49559e917b3620b915c9ace496c2
19
+ version = "0.3.6"
24
20
  source = { editable = "." }
25
21
  dependencies = [
26
22
  { name = "loguru" },
File without changes
File without changes