git-aware-coding-agent 1.0.0__py3-none-any.whl

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 (62) hide show
  1. avos_cli/__init__.py +3 -0
  2. avos_cli/agents/avos_ask_agent.md +47 -0
  3. avos_cli/agents/avos_ask_agent_JSON_converter.md +78 -0
  4. avos_cli/agents/avos_hisotry_agent_JSON_converter.md +92 -0
  5. avos_cli/agents/avos_history_agent.md +58 -0
  6. avos_cli/agents/git_diff_agent.md +63 -0
  7. avos_cli/artifacts/__init__.py +17 -0
  8. avos_cli/artifacts/base.py +47 -0
  9. avos_cli/artifacts/commit_builder.py +35 -0
  10. avos_cli/artifacts/doc_builder.py +30 -0
  11. avos_cli/artifacts/issue_builder.py +37 -0
  12. avos_cli/artifacts/pr_builder.py +50 -0
  13. avos_cli/cli/__init__.py +1 -0
  14. avos_cli/cli/main.py +504 -0
  15. avos_cli/commands/__init__.py +1 -0
  16. avos_cli/commands/ask.py +541 -0
  17. avos_cli/commands/connect.py +363 -0
  18. avos_cli/commands/history.py +549 -0
  19. avos_cli/commands/hook_install.py +260 -0
  20. avos_cli/commands/hook_sync.py +231 -0
  21. avos_cli/commands/ingest.py +506 -0
  22. avos_cli/commands/ingest_pr.py +239 -0
  23. avos_cli/config/__init__.py +1 -0
  24. avos_cli/config/hash_store.py +93 -0
  25. avos_cli/config/lock.py +122 -0
  26. avos_cli/config/manager.py +180 -0
  27. avos_cli/config/state.py +90 -0
  28. avos_cli/exceptions.py +272 -0
  29. avos_cli/models/__init__.py +58 -0
  30. avos_cli/models/api.py +75 -0
  31. avos_cli/models/artifacts.py +99 -0
  32. avos_cli/models/config.py +56 -0
  33. avos_cli/models/diff.py +117 -0
  34. avos_cli/models/query.py +234 -0
  35. avos_cli/parsers/__init__.py +21 -0
  36. avos_cli/parsers/artifact_ref_extractor.py +173 -0
  37. avos_cli/parsers/reference_parser.py +117 -0
  38. avos_cli/services/__init__.py +1 -0
  39. avos_cli/services/chronology_service.py +68 -0
  40. avos_cli/services/citation_validator.py +134 -0
  41. avos_cli/services/context_budget_service.py +104 -0
  42. avos_cli/services/diff_resolver.py +398 -0
  43. avos_cli/services/diff_summary_service.py +141 -0
  44. avos_cli/services/git_client.py +351 -0
  45. avos_cli/services/github_client.py +443 -0
  46. avos_cli/services/llm_client.py +312 -0
  47. avos_cli/services/memory_client.py +323 -0
  48. avos_cli/services/query_fallback_formatter.py +108 -0
  49. avos_cli/services/reply_output_service.py +341 -0
  50. avos_cli/services/sanitization_service.py +218 -0
  51. avos_cli/utils/__init__.py +1 -0
  52. avos_cli/utils/dotenv_load.py +50 -0
  53. avos_cli/utils/hashing.py +22 -0
  54. avos_cli/utils/logger.py +77 -0
  55. avos_cli/utils/output.py +232 -0
  56. avos_cli/utils/sanitization_diagnostics.py +81 -0
  57. avos_cli/utils/time_helpers.py +56 -0
  58. git_aware_coding_agent-1.0.0.dist-info/METADATA +390 -0
  59. git_aware_coding_agent-1.0.0.dist-info/RECORD +62 -0
  60. git_aware_coding_agent-1.0.0.dist-info/WHEEL +4 -0
  61. git_aware_coding_agent-1.0.0.dist-info/entry_points.txt +2 -0
  62. git_aware_coding_agent-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,50 @@
1
+ """Layered ``.env`` loading shared by the CLI and HTTP clients.
2
+
3
+ Order:
4
+ 1. Current working directory (no override of existing process env).
5
+ 2. Repository/package root ``.env`` beside ``avos_cli`` (override) so a
6
+ project-level token wins over cwd.
7
+ 3. ``~/.avos/.env`` if present (no override of keys already set).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ from dotenv import load_dotenv
15
+
16
+ _layers_loaded = False
17
+
18
+
19
+ def repository_root_env_path() -> Path:
20
+ """Return the path to the ``.env`` file next to the ``avos_cli`` package tree.
21
+
22
+ For editable installs this is the project root; for site-packages installs
23
+ it is the directory above the installed ``avos_cli`` package.
24
+
25
+ Returns:
26
+ Absolute path whose basename is always ``.env``.
27
+ """
28
+ pkg_root = Path(__file__).resolve().parent.parent.parent
29
+ return pkg_root / ".env"
30
+
31
+
32
+ def load_layers() -> None:
33
+ """Load layered dotenv files once per process."""
34
+ global _layers_loaded
35
+ if _layers_loaded:
36
+ return
37
+
38
+ load_dotenv()
39
+ try:
40
+ root_env = repository_root_env_path()
41
+ if root_env.exists():
42
+ load_dotenv(root_env, override=True)
43
+ except OSError:
44
+ pass
45
+
46
+ avos_user_env = Path.home() / ".avos" / ".env"
47
+ if avos_user_env.exists():
48
+ load_dotenv(avos_user_env, override=False)
49
+
50
+ _layers_loaded = True
@@ -0,0 +1,22 @@
1
+ """Content hashing for artifact idempotency.
2
+
3
+ Provides deterministic SHA-256 hashing of string content.
4
+ Used by artifact builders to generate content_hash() values
5
+ that enable duplicate detection during ingestion.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+
12
+
13
+ def content_hash(data: str) -> str:
14
+ """Compute a deterministic SHA-256 hex digest of the given string.
15
+
16
+ Args:
17
+ data: The string content to hash.
18
+
19
+ Returns:
20
+ 64-character lowercase hex string of the SHA-256 digest.
21
+ """
22
+ return hashlib.sha256(data.encode("utf-8")).hexdigest()
@@ -0,0 +1,77 @@
1
+ """Structured logging with secret redaction for AVOS CLI.
2
+
3
+ Provides a configured logger factory and a RedactionFilter that
4
+ masks API keys, tokens, and other sensitive patterns in log output.
5
+ Default level is INFO; DEBUG requires explicit --verbose opt-in.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import re
12
+
13
+ _SECRET_PATTERNS: list[re.Pattern[str]] = [
14
+ re.compile(r"sk_[a-zA-Z0-9_]{10,}"),
15
+ re.compile(r"ghp_[a-zA-Z0-9]{10,}"),
16
+ re.compile(r"gho_[a-zA-Z0-9]{10,}"),
17
+ re.compile(r"ghs_[a-zA-Z0-9]{10,}"),
18
+ re.compile(r"ghu_[a-zA-Z0-9]{10,}"),
19
+ re.compile(r"github_pat_[a-zA-Z0-9_]{10,}"),
20
+ ]
21
+
22
+ _REDACTED = "***REDACTED***"
23
+
24
+
25
+ class RedactionFilter(logging.Filter):
26
+ """Logging filter that redacts known secret patterns from log messages.
27
+
28
+ Covers Avos API keys (sk_*), GitHub tokens (ghp_*, gho_*, ghs_*, ghu_*),
29
+ and GitHub fine-grained PATs (github_pat_*).
30
+ """
31
+
32
+ def filter(self, record: logging.LogRecord) -> bool:
33
+ """Apply redaction to the log record message.
34
+
35
+ Args:
36
+ record: The log record to filter.
37
+
38
+ Returns:
39
+ Always True (record is never suppressed, only sanitized).
40
+ """
41
+ msg = record.getMessage()
42
+ redacted = msg
43
+ for pattern in _SECRET_PATTERNS:
44
+ redacted = pattern.sub(_REDACTED, redacted)
45
+ if redacted != msg:
46
+ record.msg = redacted
47
+ record.args = ()
48
+ return True
49
+
50
+
51
+ def get_logger(component: str, level: int = logging.INFO) -> logging.Logger:
52
+ """Create a configured logger for an AVOS CLI component.
53
+
54
+ Args:
55
+ component: Component name (e.g. 'memory_client', 'git_client').
56
+ level: Logging level (default INFO).
57
+
58
+ Returns:
59
+ A Logger instance with redaction filter and structured formatter.
60
+ """
61
+ logger = logging.getLogger(f"avos.{component}")
62
+ logger.setLevel(level)
63
+
64
+ if not logger.handlers:
65
+ handler = logging.StreamHandler()
66
+ handler.setLevel(level)
67
+ formatter = logging.Formatter(
68
+ fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
69
+ datefmt="%Y-%m-%dT%H:%M:%S",
70
+ )
71
+ handler.setFormatter(formatter)
72
+ logger.addHandler(handler)
73
+
74
+ if not any(isinstance(f, RedactionFilter) for f in logger.filters):
75
+ logger.addFilter(RedactionFilter())
76
+
77
+ return logger
@@ -0,0 +1,232 @@
1
+ """Terminal output formatting for AVOS CLI.
2
+
3
+ Uses Rich for interactive terminals (tables, panels, trees, progress bars,
4
+ colored status), with plain text fallback for piped/non-TTY output.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import sys
11
+
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.progress import Progress, SpinnerColumn, TextColumn
15
+ from rich.table import Table
16
+ from rich.theme import Theme
17
+ from rich.tree import Tree
18
+
19
+ _theme = Theme(
20
+ {
21
+ "success": "bold green",
22
+ "error": "bold red",
23
+ "warning": "bold yellow",
24
+ "info": "bold blue",
25
+ "dim": "dim",
26
+ "high": "bold red",
27
+ "medium": "bold yellow",
28
+ "low": "bold cyan",
29
+ }
30
+ )
31
+
32
+ console = Console(theme=_theme, stderr=True)
33
+
34
+
35
+ class _NullProgress:
36
+ """No-op progress context manager for JSON mode (suppress progress bars)."""
37
+
38
+ def __enter__(self) -> _NullProgress:
39
+ return self
40
+
41
+ def __exit__(self, *args: object) -> None:
42
+ pass
43
+
44
+
45
+ def is_interactive() -> bool:
46
+ """Check if stdout is connected to an interactive terminal."""
47
+ return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
48
+
49
+
50
+ def print_success(message: str) -> None:
51
+ """Print a success message in green."""
52
+ if is_interactive():
53
+ console.print("[success]\u2713[/success] ", end="")
54
+ console.print(message, markup=False)
55
+ else:
56
+ print(f"OK: {message}")
57
+
58
+
59
+ def print_error(message: str) -> None:
60
+ """Print an error message in red."""
61
+ if is_interactive():
62
+ console.print("[error]\u2717[/error] ", end="")
63
+ console.print(message, markup=False)
64
+ else:
65
+ print(f"ERROR: {message}", file=sys.stderr)
66
+
67
+
68
+ def print_warning(message: str) -> None:
69
+ """Print a warning message in yellow."""
70
+ if is_interactive():
71
+ console.print("[warning]\u26a0[/warning] ", end="")
72
+ console.print(message, markup=False)
73
+ else:
74
+ print(f"WARN: {message}", file=sys.stderr)
75
+
76
+
77
+ def print_info(message: str) -> None:
78
+ """Print an informational message."""
79
+ if is_interactive():
80
+ console.print("[info]\u2139[/info] ", end="")
81
+ console.print(message, markup=False)
82
+ else:
83
+ print(message)
84
+
85
+
86
+ def print_json(
87
+ success: bool,
88
+ data: dict[str, object] | None = None,
89
+ error: dict[str, object] | None = None,
90
+ ) -> None:
91
+ """Emit strict JSON envelope for machine-readable output.
92
+
93
+ Per Q13: envelope is {"success": bool, "data": {...}, "error": {...}}.
94
+ When success=True, error is None; when success=False, data is None.
95
+
96
+ Args:
97
+ success: Whether the operation succeeded.
98
+ data: Command-specific payload (when success).
99
+ error: Error payload with code, message, hint, retryable (when not success).
100
+ """
101
+ envelope: dict[str, object] = {
102
+ "success": success,
103
+ "data": data,
104
+ "error": error,
105
+ }
106
+ print(json.dumps(envelope))
107
+
108
+
109
+ def print_verbose(label: str, message: str, verbose: bool = False) -> None:
110
+ """Emit debug-level verbose line. Suppressed unless verbose is True.
111
+
112
+ Args:
113
+ label: Short label (e.g. "HTTP", "Config").
114
+ message: Debug message.
115
+ verbose: If False, does nothing.
116
+ """
117
+ if not verbose:
118
+ return
119
+ if is_interactive():
120
+ console.print(f"[dim][{label}] {message}[/dim]")
121
+ else:
122
+ print(f"[{label}] {message}", file=sys.stderr)
123
+
124
+
125
+ def create_progress(description: str = "Processing...", suppress: bool = False) -> Progress | _NullProgress:
126
+ """Create a Rich progress bar for long-running operations.
127
+
128
+ When suppress=True (e.g. --json mode), returns a no-op progress that does nothing.
129
+
130
+ Args:
131
+ description: Text label for the progress bar.
132
+ suppress: If True, return no-op progress (for JSON output mode).
133
+
134
+ Returns:
135
+ A Rich Progress context manager (or no-op when suppress).
136
+ """
137
+ if suppress:
138
+ return _NullProgress()
139
+ return Progress(
140
+ SpinnerColumn(),
141
+ TextColumn("[progress.description]{task.description}"),
142
+ console=console,
143
+ )
144
+
145
+
146
+ def render_table(
147
+ title: str,
148
+ columns: list[tuple[str, str]],
149
+ rows: list[list[str]],
150
+ ) -> None:
151
+ """Render a Rich table or plain-text equivalent.
152
+
153
+ Args:
154
+ title: Table title displayed above the table.
155
+ columns: List of (header_text, style) tuples.
156
+ rows: List of row data (each row is a list of strings matching columns).
157
+ """
158
+ if is_interactive():
159
+ table = Table(title=title, show_lines=False, pad_edge=True)
160
+ for header, style in columns:
161
+ table.add_column(header, style=style)
162
+ for row in rows:
163
+ table.add_row(*row)
164
+ console.print(table)
165
+ else:
166
+ print(f"\n{title}")
167
+ headers = [h for h, _ in columns]
168
+ print(" " + " | ".join(headers))
169
+ print(" " + "-+-".join("-" * len(h) for h in headers))
170
+ for row in rows:
171
+ print(" " + " | ".join(row))
172
+
173
+
174
+ def render_panel(title: str, content: str, style: str = "info") -> None:
175
+ """Render a Rich panel or plain-text equivalent.
176
+
177
+ Args:
178
+ title: Panel title.
179
+ content: Panel body text.
180
+ style: Border color style name.
181
+ """
182
+ if is_interactive():
183
+ console.print(Panel(content, title=title, border_style=style, expand=False))
184
+ else:
185
+ print(f"\n--- {title} ---")
186
+ print(content)
187
+ print("---")
188
+
189
+
190
+ def render_tree(label: str, children: list[tuple[str, list[str]]]) -> None:
191
+ """Render a Rich tree or plain-text indented equivalent.
192
+
193
+ Args:
194
+ label: Root label for the tree.
195
+ children: List of (branch_label, [leaf_labels]) tuples.
196
+ """
197
+ if is_interactive():
198
+ tree = Tree(f"[bold]{label}[/bold]")
199
+ for branch_label, leaves in children:
200
+ branch = tree.add(f"[info]{branch_label}[/info]")
201
+ for leaf in leaves:
202
+ branch.add(leaf)
203
+ console.print(tree)
204
+ else:
205
+ print(f"\n{label}")
206
+ for branch_label, leaves in children:
207
+ print(f" {branch_label}")
208
+ for leaf in leaves:
209
+ print(f" {leaf}")
210
+
211
+
212
+ def render_kv_panel(title: str, pairs: list[tuple[str, str]], style: str = "info") -> None:
213
+ """Render a key-value panel (Rich table inside a panel, or plain text).
214
+
215
+ Args:
216
+ title: Panel title.
217
+ pairs: List of (key, value) tuples.
218
+ style: Border color style name.
219
+ """
220
+ if is_interactive():
221
+ table = Table(show_header=False, show_edge=False, pad_edge=False, box=None)
222
+ table.add_column("Key", style="bold")
223
+ table.add_column("Value")
224
+ for k, v in pairs:
225
+ table.add_row(k, v)
226
+ console.print(Panel(table, title=title, border_style=style, expand=False))
227
+ else:
228
+ print(f"\n--- {title} ---")
229
+ for k, v in pairs:
230
+ print(f" {k}: {v}")
231
+
232
+
@@ -0,0 +1,81 @@
1
+ """Human-readable and JSON diagnostics when sanitization blocks LLM synthesis.
2
+
3
+ Used when ``SanitizationResult.confidence_score`` is below the ask/history
4
+ threshold so users understand why synthesis was skipped and how the score
5
+ was affected.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from avos_cli.models.query import SanitizationResult
13
+
14
+ _REDACTION_LABELS: dict[str, str] = {
15
+ "api_key": "API key-like strings (e.g. sk_…, AWS AKIA…)",
16
+ "token": "Bearer tokens or GitHub PAT-style tokens",
17
+ "credential": "password=/secret= style credential fields",
18
+ "private_key": "PEM private key blocks",
19
+ "pii": "email-shaped personal identifiers",
20
+ "injection": "prompt-injection-like phrases (redacted as [REDACTED_INJECTION])",
21
+ }
22
+
23
+
24
+ def explain_sanitization_gate(
25
+ result: SanitizationResult,
26
+ threshold: int,
27
+ ) -> tuple[str, list[str], dict[str, Any]]:
28
+ """Build headline, explanatory lines, and a JSON fragment for tooling.
29
+
30
+ Args:
31
+ result: Aggregate sanitization outcome for the retrieved artifacts.
32
+ threshold: Minimum confidence required to proceed to LLM synthesis.
33
+
34
+ Returns:
35
+ (headline for print_warning, detail lines for print_info,
36
+ dict to merge into JSON ``data``, e.g. ``{"sanitization": {...}}``).
37
+ """
38
+ score = result.confidence_score
39
+ types = list(result.redaction_types)
40
+
41
+ headline = (
42
+ f"Sanitization confidence is {score}/100 "
43
+ f"(minimum {threshold} required for LLM synthesis). "
44
+ "Showing sanitized evidence only, not an LLM summary."
45
+ )
46
+
47
+ lines: list[str] = [
48
+ "Why: Retrieved memory matched patterns we treat as sensitive or unsafe to "
49
+ "summarize (API keys, tokens, credentials, PEM private keys, email-shaped PII, "
50
+ "or prompt-injection-like text). Matching spans were replaced with "
51
+ "[REDACTED_*] placeholders. The confidence score reflects estimated remaining "
52
+ "risk after redaction; when it is below the threshold we skip synthesis.",
53
+ ]
54
+
55
+ if types:
56
+ described = [_REDACTION_LABELS.get(t, t.replace("_", " ")) for t in types]
57
+ lines.append("Categories flagged across retrieved snippets: " + "; ".join(described) + ".")
58
+ else:
59
+ lines.append(
60
+ "No aggregate redaction category list was recorded (unusual); "
61
+ "the low score may still reflect injection-pattern penalties."
62
+ )
63
+
64
+ if result.redaction_applied:
65
+ lines.append(
66
+ "At least one snippet had content redacted before display; "
67
+ "look for [REDACTED_API_KEY], [REDACTED_TOKEN], etc. in the evidence."
68
+ )
69
+
70
+ json_fragment: dict[str, Any] = {
71
+ "sanitization": {
72
+ "blocked_synthesis": True,
73
+ "confidence_score": score,
74
+ "required_minimum_confidence": threshold,
75
+ "redaction_applied": result.redaction_applied,
76
+ "redaction_types": types,
77
+ "fallback_reason": "safety_block",
78
+ }
79
+ }
80
+
81
+ return headline, lines, json_fragment
@@ -0,0 +1,56 @@
1
+ """Time utilities for date calculations and TTL checks.
2
+
3
+ Provides helpers for computing date windows (e.g. "180 days ago"),
4
+ checking TTL expiry, and parsing ISO 8601 timestamps.
5
+ All datetime operations use UTC.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime, timedelta, timezone
11
+
12
+
13
+ def days_ago(n: int) -> datetime:
14
+ """Return a UTC datetime representing N days before now.
15
+
16
+ Args:
17
+ n: Number of days to subtract from the current time.
18
+
19
+ Returns:
20
+ UTC-aware datetime N days in the past.
21
+ """
22
+ return datetime.now(tz=timezone.utc) - timedelta(days=n)
23
+
24
+
25
+ def is_within_ttl(timestamp_iso: str, hours: int) -> bool:
26
+ """Check whether an ISO 8601 timestamp is within the last N hours.
27
+
28
+ Args:
29
+ timestamp_iso: ISO 8601 formatted timestamp string.
30
+ hours: TTL window in hours.
31
+
32
+ Returns:
33
+ True if the timestamp is within the last `hours` hours.
34
+ """
35
+ dt = parse_iso8601(timestamp_iso)
36
+ cutoff = datetime.now(tz=timezone.utc) - timedelta(hours=hours)
37
+ return dt >= cutoff
38
+
39
+
40
+ def parse_iso8601(timestamp: str) -> datetime:
41
+ """Parse an ISO 8601 timestamp string into a UTC-aware datetime.
42
+
43
+ Handles 'Z' suffix, timezone offsets, and naive timestamps
44
+ (which are assumed to be UTC).
45
+
46
+ Args:
47
+ timestamp: ISO 8601 formatted string.
48
+
49
+ Returns:
50
+ Timezone-aware datetime in UTC.
51
+ """
52
+ cleaned = timestamp.replace("Z", "+00:00")
53
+ dt = datetime.fromisoformat(cleaned)
54
+ if dt.tzinfo is None:
55
+ dt = dt.replace(tzinfo=timezone.utc)
56
+ return dt