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.
- avos_cli/__init__.py +3 -0
- avos_cli/agents/avos_ask_agent.md +47 -0
- avos_cli/agents/avos_ask_agent_JSON_converter.md +78 -0
- avos_cli/agents/avos_hisotry_agent_JSON_converter.md +92 -0
- avos_cli/agents/avos_history_agent.md +58 -0
- avos_cli/agents/git_diff_agent.md +63 -0
- avos_cli/artifacts/__init__.py +17 -0
- avos_cli/artifacts/base.py +47 -0
- avos_cli/artifacts/commit_builder.py +35 -0
- avos_cli/artifacts/doc_builder.py +30 -0
- avos_cli/artifacts/issue_builder.py +37 -0
- avos_cli/artifacts/pr_builder.py +50 -0
- avos_cli/cli/__init__.py +1 -0
- avos_cli/cli/main.py +504 -0
- avos_cli/commands/__init__.py +1 -0
- avos_cli/commands/ask.py +541 -0
- avos_cli/commands/connect.py +363 -0
- avos_cli/commands/history.py +549 -0
- avos_cli/commands/hook_install.py +260 -0
- avos_cli/commands/hook_sync.py +231 -0
- avos_cli/commands/ingest.py +506 -0
- avos_cli/commands/ingest_pr.py +239 -0
- avos_cli/config/__init__.py +1 -0
- avos_cli/config/hash_store.py +93 -0
- avos_cli/config/lock.py +122 -0
- avos_cli/config/manager.py +180 -0
- avos_cli/config/state.py +90 -0
- avos_cli/exceptions.py +272 -0
- avos_cli/models/__init__.py +58 -0
- avos_cli/models/api.py +75 -0
- avos_cli/models/artifacts.py +99 -0
- avos_cli/models/config.py +56 -0
- avos_cli/models/diff.py +117 -0
- avos_cli/models/query.py +234 -0
- avos_cli/parsers/__init__.py +21 -0
- avos_cli/parsers/artifact_ref_extractor.py +173 -0
- avos_cli/parsers/reference_parser.py +117 -0
- avos_cli/services/__init__.py +1 -0
- avos_cli/services/chronology_service.py +68 -0
- avos_cli/services/citation_validator.py +134 -0
- avos_cli/services/context_budget_service.py +104 -0
- avos_cli/services/diff_resolver.py +398 -0
- avos_cli/services/diff_summary_service.py +141 -0
- avos_cli/services/git_client.py +351 -0
- avos_cli/services/github_client.py +443 -0
- avos_cli/services/llm_client.py +312 -0
- avos_cli/services/memory_client.py +323 -0
- avos_cli/services/query_fallback_formatter.py +108 -0
- avos_cli/services/reply_output_service.py +341 -0
- avos_cli/services/sanitization_service.py +218 -0
- avos_cli/utils/__init__.py +1 -0
- avos_cli/utils/dotenv_load.py +50 -0
- avos_cli/utils/hashing.py +22 -0
- avos_cli/utils/logger.py +77 -0
- avos_cli/utils/output.py +232 -0
- avos_cli/utils/sanitization_diagnostics.py +81 -0
- avos_cli/utils/time_helpers.py +56 -0
- git_aware_coding_agent-1.0.0.dist-info/METADATA +390 -0
- git_aware_coding_agent-1.0.0.dist-info/RECORD +62 -0
- git_aware_coding_agent-1.0.0.dist-info/WHEEL +4 -0
- git_aware_coding_agent-1.0.0.dist-info/entry_points.txt +2 -0
- git_aware_coding_agent-1.0.0.dist-info/licenses/LICENSE +201 -0
avos_cli/cli/main.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"""CLI entry point for the avos command.
|
|
2
|
+
|
|
3
|
+
Thin layer: parses arguments, resolves credentials from the environment,
|
|
4
|
+
connected repo config (with env overlay), and delegates to orchestrators.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from avos_cli import __version__
|
|
16
|
+
from avos_cli.exceptions import AuthError
|
|
17
|
+
from avos_cli.utils.dotenv_load import load_layers
|
|
18
|
+
from avos_cli.utils.output import print_error
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from avos_cli.services.diff_summary_service import DiffSummaryService
|
|
22
|
+
from avos_cli.services.github_client import GitHubClient
|
|
23
|
+
from avos_cli.services.reply_output_service import ReplyOutputService
|
|
24
|
+
|
|
25
|
+
# Load .env: cwd, then repository root (beside avos_cli) with override so
|
|
26
|
+
# GITHUB_TOKEN in project root wins, then ~/.avos/.env without overriding.
|
|
27
|
+
load_layers()
|
|
28
|
+
|
|
29
|
+
app = typer.Typer(
|
|
30
|
+
name="avos",
|
|
31
|
+
help="Developer memory CLI for repositories.",
|
|
32
|
+
no_args_is_help=False,
|
|
33
|
+
add_completion=False,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _version_callback(value: bool) -> None:
|
|
38
|
+
"""Print version and exit."""
|
|
39
|
+
if value:
|
|
40
|
+
typer.echo(f"avos {__version__}")
|
|
41
|
+
raise typer.Exit()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _first_env(*keys: str) -> str:
|
|
45
|
+
"""Return the first non-empty env value for any of the given keys."""
|
|
46
|
+
for k in keys:
|
|
47
|
+
v = os.environ.get(k, "")
|
|
48
|
+
if v and isinstance(v, str):
|
|
49
|
+
return v.strip()
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _github_client_or_exit(repo_root: Path) -> GitHubClient:
|
|
54
|
+
"""Resolve GitHub client from connected config (overlay) and env; exit if missing."""
|
|
55
|
+
from avos_cli.services.github_client import github_client_for_repo
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
return github_client_for_repo(repo_root)
|
|
59
|
+
except AuthError:
|
|
60
|
+
print_error(
|
|
61
|
+
"[AUTH_ERROR] GitHub token is required. Set GITHUB_TOKEN or use a layered .env; "
|
|
62
|
+
"it is not written to .avos/config.json."
|
|
63
|
+
)
|
|
64
|
+
raise typer.Exit(1) from None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_reply_model_config() -> tuple[str, str, str]:
|
|
68
|
+
"""Get REPLY_MODEL configuration from environment.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Tuple of (model, url, api_key). Empty strings if not configured.
|
|
72
|
+
"""
|
|
73
|
+
model = _first_env("REPLY_MODEL", "reply_model")
|
|
74
|
+
url = _first_env("REPLY_MODEL_URL", "reply_model_URL", "reply_model_url")
|
|
75
|
+
api_key = _first_env("REPLY_MODEL_API_KEY", "reply_model_API_KEY", "reply_model_api_key")
|
|
76
|
+
return model, url, api_key
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _make_reply_service() -> ReplyOutputService | None:
|
|
80
|
+
"""Build ReplyOutputService from env if REPLY_MODEL, REPLY_MODEL_URL, REPLY_MODEL_API_KEY are set."""
|
|
81
|
+
model, url, api_key = _get_reply_model_config()
|
|
82
|
+
if model and url and api_key:
|
|
83
|
+
from avos_cli.services.reply_output_service import ReplyOutputService
|
|
84
|
+
return ReplyOutputService(api_key=api_key, api_url=url, model=model)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _make_diff_summary_service() -> DiffSummaryService | None:
|
|
89
|
+
"""Build DiffSummaryService from env if REPLY_MODEL config is available."""
|
|
90
|
+
model, url, api_key = _get_reply_model_config()
|
|
91
|
+
if model and url and api_key:
|
|
92
|
+
from avos_cli.services.diff_summary_service import DiffSummaryService
|
|
93
|
+
|
|
94
|
+
return DiffSummaryService(api_key=api_key, api_url=url, model=model)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _github_client_optional(repo_root: Path) -> GitHubClient | None:
|
|
99
|
+
"""Resolve GitHub client from connected config (overlay) and env; return None if missing."""
|
|
100
|
+
from avos_cli.services.github_client import github_client_for_repo
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
return github_client_for_repo(repo_root)
|
|
104
|
+
except AuthError:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.callback(invoke_without_command=True)
|
|
109
|
+
def main(
|
|
110
|
+
ctx: typer.Context,
|
|
111
|
+
version: bool | None = typer.Option(
|
|
112
|
+
None,
|
|
113
|
+
"--version",
|
|
114
|
+
"-v",
|
|
115
|
+
help="Show version and exit.",
|
|
116
|
+
callback=_version_callback,
|
|
117
|
+
is_eager=True,
|
|
118
|
+
),
|
|
119
|
+
verbose: bool = typer.Option(
|
|
120
|
+
False,
|
|
121
|
+
"--verbose",
|
|
122
|
+
help="Enable verbose debug output.",
|
|
123
|
+
),
|
|
124
|
+
json_output: bool = typer.Option(
|
|
125
|
+
False,
|
|
126
|
+
"--json",
|
|
127
|
+
help="Emit machine-readable JSON output.",
|
|
128
|
+
),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""AVOS CLI - Developer memory for repositories."""
|
|
131
|
+
if ctx.invoked_subcommand is None:
|
|
132
|
+
help_text = ctx.get_help()
|
|
133
|
+
if "--version" not in help_text:
|
|
134
|
+
help_text = f"{help_text.rstrip()}\n\n--version -v Show version and exit.\n"
|
|
135
|
+
typer.echo(help_text)
|
|
136
|
+
raise typer.Exit(0)
|
|
137
|
+
|
|
138
|
+
ctx.ensure_object(dict)
|
|
139
|
+
ctx.obj["verbose"] = verbose
|
|
140
|
+
ctx.obj["json"] = json_output
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.command()
|
|
144
|
+
def connect(
|
|
145
|
+
ctx: typer.Context,
|
|
146
|
+
repo: str | None = typer.Argument(
|
|
147
|
+
None,
|
|
148
|
+
help=(
|
|
149
|
+
"Repository slug in 'org/repo' format. "
|
|
150
|
+
"Omit to detect from git remote origin (GitHub HTTPS or SSH URL)."
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Connect a repository to Avos Memory."""
|
|
155
|
+
from avos_cli.commands.connect import ConnectOrchestrator
|
|
156
|
+
from avos_cli.config.manager import find_repo_root
|
|
157
|
+
from avos_cli.exceptions import RepositoryContextError
|
|
158
|
+
from avos_cli.services.git_client import GitClient
|
|
159
|
+
from avos_cli.services.memory_client import AvosMemoryClient
|
|
160
|
+
|
|
161
|
+
json_output = ctx.obj.get("json", False)
|
|
162
|
+
|
|
163
|
+
api_key = os.environ.get("AVOS_API_KEY", "")
|
|
164
|
+
api_url = os.environ.get("AVOS_API_URL", "https://api.avos.ai")
|
|
165
|
+
|
|
166
|
+
if not api_key:
|
|
167
|
+
print_error("[AUTH_ERROR] AVOS_API_KEY environment variable is required.")
|
|
168
|
+
raise typer.Exit(1)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
repo_root = find_repo_root(Path.cwd())
|
|
172
|
+
except RepositoryContextError as e:
|
|
173
|
+
print_error(f"[REPOSITORY_CONTEXT_ERROR] {e}")
|
|
174
|
+
raise typer.Exit(1) from e
|
|
175
|
+
|
|
176
|
+
gh_client = _github_client_or_exit(repo_root)
|
|
177
|
+
|
|
178
|
+
orchestrator = ConnectOrchestrator(
|
|
179
|
+
git_client=GitClient(),
|
|
180
|
+
github_client=gh_client,
|
|
181
|
+
memory_client=AvosMemoryClient(api_key=api_key, api_url=api_url),
|
|
182
|
+
repo_root=repo_root,
|
|
183
|
+
)
|
|
184
|
+
code = orchestrator.run(repo, json_output=json_output)
|
|
185
|
+
raise typer.Exit(code)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parse_since_days(value: str) -> int:
|
|
189
|
+
"""Parse a '--since Nd' value like '90d' into integer days."""
|
|
190
|
+
cleaned = value.strip().lower()
|
|
191
|
+
if cleaned.endswith("d"):
|
|
192
|
+
cleaned = cleaned[:-1]
|
|
193
|
+
try:
|
|
194
|
+
days = int(cleaned)
|
|
195
|
+
if days <= 0:
|
|
196
|
+
raise typer.BadParameter("--since must be a positive number of days.")
|
|
197
|
+
return days
|
|
198
|
+
except ValueError as e:
|
|
199
|
+
raise typer.BadParameter(f"Invalid --since value: '{value}'. Expected format: '90d' or '90'.") from e
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@app.command()
|
|
203
|
+
def ingest(
|
|
204
|
+
ctx: typer.Context,
|
|
205
|
+
repo: str = typer.Argument(..., help="Repository slug in 'org/repo' format."),
|
|
206
|
+
since: str = typer.Option("90d", "--since", help="Time window, e.g. '90d' for 90 days."),
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Ingest repository history into Avos Memory."""
|
|
209
|
+
from avos_cli.commands.ingest import IngestOrchestrator
|
|
210
|
+
from avos_cli.config.hash_store import IngestHashStore
|
|
211
|
+
from avos_cli.config.lock import IngestLockManager
|
|
212
|
+
from avos_cli.config.manager import find_repo_root
|
|
213
|
+
from avos_cli.exceptions import RepositoryContextError
|
|
214
|
+
from avos_cli.services.git_client import GitClient
|
|
215
|
+
from avos_cli.services.memory_client import AvosMemoryClient
|
|
216
|
+
|
|
217
|
+
json_output = ctx.obj.get("json", False)
|
|
218
|
+
since_days = _parse_since_days(since)
|
|
219
|
+
|
|
220
|
+
api_key = os.environ.get("AVOS_API_KEY", "")
|
|
221
|
+
api_url = os.environ.get("AVOS_API_URL", "https://api.avos.ai")
|
|
222
|
+
|
|
223
|
+
if not api_key:
|
|
224
|
+
print_error("[AUTH_ERROR] AVOS_API_KEY environment variable is required.")
|
|
225
|
+
raise typer.Exit(1)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
repo_root = find_repo_root(Path.cwd())
|
|
229
|
+
except RepositoryContextError as e:
|
|
230
|
+
print_error(f"[REPOSITORY_CONTEXT_ERROR] {e}")
|
|
231
|
+
raise typer.Exit(1) from e
|
|
232
|
+
|
|
233
|
+
gh_client = _github_client_or_exit(repo_root)
|
|
234
|
+
|
|
235
|
+
avos_dir = repo_root / ".avos"
|
|
236
|
+
hash_store = IngestHashStore(avos_dir)
|
|
237
|
+
hash_store.load()
|
|
238
|
+
|
|
239
|
+
orchestrator = IngestOrchestrator(
|
|
240
|
+
memory_client=AvosMemoryClient(api_key=api_key, api_url=api_url),
|
|
241
|
+
github_client=gh_client,
|
|
242
|
+
git_client=GitClient(),
|
|
243
|
+
hash_store=hash_store,
|
|
244
|
+
lock_manager=IngestLockManager(avos_dir),
|
|
245
|
+
repo_root=repo_root,
|
|
246
|
+
)
|
|
247
|
+
code = orchestrator.run(repo, since_days=since_days, json_output=json_output)
|
|
248
|
+
raise typer.Exit(code)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@app.command(name="ingest-pr")
|
|
252
|
+
def ingest_pr(
|
|
253
|
+
ctx: typer.Context,
|
|
254
|
+
repo: str = typer.Argument(..., help="Repository slug in 'org/repo' format."),
|
|
255
|
+
pr_number: int = typer.Argument(..., help="PR number to ingest."),
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Ingest a single PR into Avos Memory."""
|
|
258
|
+
from avos_cli.commands.ingest_pr import IngestPROrchestrator
|
|
259
|
+
from avos_cli.config.hash_store import IngestHashStore
|
|
260
|
+
from avos_cli.config.manager import find_repo_root
|
|
261
|
+
from avos_cli.exceptions import RepositoryContextError
|
|
262
|
+
from avos_cli.services.memory_client import AvosMemoryClient
|
|
263
|
+
|
|
264
|
+
json_output = ctx.obj.get("json", False)
|
|
265
|
+
|
|
266
|
+
api_key = os.environ.get("AVOS_API_KEY", "")
|
|
267
|
+
api_url = os.environ.get("AVOS_API_URL", "https://api.avos.ai")
|
|
268
|
+
|
|
269
|
+
if not api_key:
|
|
270
|
+
print_error("[AUTH_ERROR] AVOS_API_KEY environment variable is required.")
|
|
271
|
+
raise typer.Exit(1)
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
repo_root = find_repo_root(Path.cwd())
|
|
275
|
+
except RepositoryContextError as e:
|
|
276
|
+
print_error(f"[REPOSITORY_CONTEXT_ERROR] {e}")
|
|
277
|
+
raise typer.Exit(1) from e
|
|
278
|
+
|
|
279
|
+
gh_client = _github_client_or_exit(repo_root)
|
|
280
|
+
|
|
281
|
+
avos_dir = repo_root / ".avos"
|
|
282
|
+
hash_store = IngestHashStore(avos_dir)
|
|
283
|
+
hash_store.load()
|
|
284
|
+
|
|
285
|
+
orchestrator = IngestPROrchestrator(
|
|
286
|
+
memory_client=AvosMemoryClient(api_key=api_key, api_url=api_url),
|
|
287
|
+
github_client=gh_client,
|
|
288
|
+
hash_store=hash_store,
|
|
289
|
+
repo_root=repo_root,
|
|
290
|
+
)
|
|
291
|
+
code = orchestrator.run(repo, pr_number, json_output=json_output)
|
|
292
|
+
raise typer.Exit(code)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@app.command()
|
|
296
|
+
def ask(
|
|
297
|
+
ctx: typer.Context,
|
|
298
|
+
question: str = typer.Argument(..., help="Natural language question about the repository."),
|
|
299
|
+
) -> None:
|
|
300
|
+
"""Ask a question about the repository and get an evidence-backed answer."""
|
|
301
|
+
from avos_cli.commands.ask import AskOrchestrator
|
|
302
|
+
from avos_cli.config.manager import find_repo_root, load_config
|
|
303
|
+
from avos_cli.exceptions import (
|
|
304
|
+
ConfigurationNotInitializedError,
|
|
305
|
+
RepositoryContextError,
|
|
306
|
+
)
|
|
307
|
+
from avos_cli.services.llm_client import LLMClient
|
|
308
|
+
from avos_cli.services.memory_client import AvosMemoryClient
|
|
309
|
+
|
|
310
|
+
json_output = ctx.obj.get("json", False)
|
|
311
|
+
|
|
312
|
+
api_key = os.environ.get("AVOS_API_KEY", "")
|
|
313
|
+
api_url = os.environ.get("AVOS_API_URL", "https://api.avos.ai")
|
|
314
|
+
|
|
315
|
+
if not api_key:
|
|
316
|
+
print_error("[AUTH_ERROR] AVOS_API_KEY environment variable is required.")
|
|
317
|
+
raise typer.Exit(1)
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
repo_root = find_repo_root(Path.cwd())
|
|
321
|
+
except RepositoryContextError as e:
|
|
322
|
+
print_error(f"[REPOSITORY_CONTEXT_ERROR] {e}")
|
|
323
|
+
raise typer.Exit(1) from e
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
config = load_config(repo_root)
|
|
327
|
+
except ConfigurationNotInitializedError as e:
|
|
328
|
+
print_error("[AUTH_ERROR] Repository not connected. Run 'avos connect org/repo' first.")
|
|
329
|
+
raise typer.Exit(1) from e
|
|
330
|
+
|
|
331
|
+
provider = config.llm.provider.lower()
|
|
332
|
+
if provider == "openai":
|
|
333
|
+
llm_api_key = os.environ.get("OPENAI_API_KEY", "")
|
|
334
|
+
if not llm_api_key:
|
|
335
|
+
print_error("[AUTH_ERROR] OPENAI_API_KEY environment variable is required for OpenAI.")
|
|
336
|
+
raise typer.Exit(1)
|
|
337
|
+
else:
|
|
338
|
+
llm_api_key = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
339
|
+
if not llm_api_key:
|
|
340
|
+
print_error("[AUTH_ERROR] ANTHROPIC_API_KEY environment variable is required for LLM synthesis.")
|
|
341
|
+
raise typer.Exit(1)
|
|
342
|
+
|
|
343
|
+
reply_service = _make_reply_service()
|
|
344
|
+
github_client = _github_client_optional(repo_root)
|
|
345
|
+
diff_summary_service = _make_diff_summary_service()
|
|
346
|
+
orchestrator = AskOrchestrator(
|
|
347
|
+
memory_client=AvosMemoryClient(api_key=api_key, api_url=api_url),
|
|
348
|
+
llm_client=LLMClient(api_key=llm_api_key, provider=provider),
|
|
349
|
+
repo_root=repo_root,
|
|
350
|
+
reply_service=reply_service,
|
|
351
|
+
github_client=github_client,
|
|
352
|
+
diff_summary_service=diff_summary_service,
|
|
353
|
+
)
|
|
354
|
+
code = orchestrator.run("_/_", question, json_output=json_output)
|
|
355
|
+
raise typer.Exit(code)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@app.command()
|
|
359
|
+
def history(
|
|
360
|
+
ctx: typer.Context,
|
|
361
|
+
subject: str = typer.Argument(..., help="Subject or topic for chronological history."),
|
|
362
|
+
) -> None:
|
|
363
|
+
"""Get a chronological history of a subject in the repository."""
|
|
364
|
+
from avos_cli.commands.history import HistoryOrchestrator
|
|
365
|
+
from avos_cli.config.manager import find_repo_root, load_config
|
|
366
|
+
from avos_cli.exceptions import (
|
|
367
|
+
ConfigurationNotInitializedError,
|
|
368
|
+
RepositoryContextError,
|
|
369
|
+
)
|
|
370
|
+
from avos_cli.services.llm_client import LLMClient
|
|
371
|
+
from avos_cli.services.memory_client import AvosMemoryClient
|
|
372
|
+
|
|
373
|
+
json_output = ctx.obj.get("json", False)
|
|
374
|
+
|
|
375
|
+
api_key = os.environ.get("AVOS_API_KEY", "")
|
|
376
|
+
api_url = os.environ.get("AVOS_API_URL", "https://api.avos.ai")
|
|
377
|
+
|
|
378
|
+
if not api_key:
|
|
379
|
+
print_error("[AUTH_ERROR] AVOS_API_KEY environment variable is required.")
|
|
380
|
+
raise typer.Exit(1)
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
repo_root = find_repo_root(Path.cwd())
|
|
384
|
+
except RepositoryContextError as e:
|
|
385
|
+
print_error(f"[REPOSITORY_CONTEXT_ERROR] {e}")
|
|
386
|
+
raise typer.Exit(1) from e
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
config = load_config(repo_root)
|
|
390
|
+
except ConfigurationNotInitializedError as e:
|
|
391
|
+
print_error("[AUTH_ERROR] Repository not connected. Run 'avos connect org/repo' first.")
|
|
392
|
+
raise typer.Exit(1) from e
|
|
393
|
+
|
|
394
|
+
provider = config.llm.provider.lower()
|
|
395
|
+
if provider == "openai":
|
|
396
|
+
llm_api_key = os.environ.get("OPENAI_API_KEY", "")
|
|
397
|
+
if not llm_api_key:
|
|
398
|
+
print_error("[AUTH_ERROR] OPENAI_API_KEY environment variable is required for OpenAI.")
|
|
399
|
+
raise typer.Exit(1)
|
|
400
|
+
else:
|
|
401
|
+
llm_api_key = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
402
|
+
if not llm_api_key:
|
|
403
|
+
print_error("[AUTH_ERROR] ANTHROPIC_API_KEY environment variable is required for LLM synthesis.")
|
|
404
|
+
raise typer.Exit(1)
|
|
405
|
+
|
|
406
|
+
reply_service = _make_reply_service()
|
|
407
|
+
github_client = _github_client_optional(repo_root)
|
|
408
|
+
diff_summary_service = _make_diff_summary_service()
|
|
409
|
+
orchestrator = HistoryOrchestrator(
|
|
410
|
+
memory_client=AvosMemoryClient(api_key=api_key, api_url=api_url),
|
|
411
|
+
llm_client=LLMClient(api_key=llm_api_key, provider=provider),
|
|
412
|
+
repo_root=repo_root,
|
|
413
|
+
reply_service=reply_service,
|
|
414
|
+
github_client=github_client,
|
|
415
|
+
diff_summary_service=diff_summary_service,
|
|
416
|
+
)
|
|
417
|
+
code = orchestrator.run("_/_", subject, json_output=json_output)
|
|
418
|
+
raise typer.Exit(code)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@app.command(name="hook-install")
|
|
422
|
+
def hook_install(
|
|
423
|
+
force: bool = typer.Option(
|
|
424
|
+
False, "--force", "-f", help="Overwrite existing pre-push hook."
|
|
425
|
+
),
|
|
426
|
+
) -> None:
|
|
427
|
+
"""Install git hook for automatic commit sync on push."""
|
|
428
|
+
from avos_cli.commands.hook_install import HookInstallOrchestrator
|
|
429
|
+
from avos_cli.config.manager import find_repo_root
|
|
430
|
+
from avos_cli.exceptions import RepositoryContextError
|
|
431
|
+
from avos_cli.services.git_client import GitClient
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
repo_root = find_repo_root(Path.cwd())
|
|
435
|
+
except RepositoryContextError as e:
|
|
436
|
+
print_error(f"[REPOSITORY_CONTEXT_ERROR] {e}")
|
|
437
|
+
raise typer.Exit(1) from e
|
|
438
|
+
|
|
439
|
+
orchestrator = HookInstallOrchestrator(
|
|
440
|
+
git_client=GitClient(),
|
|
441
|
+
repo_root=repo_root,
|
|
442
|
+
)
|
|
443
|
+
code = orchestrator.run(force=force)
|
|
444
|
+
raise typer.Exit(code)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@app.command(name="hook-uninstall")
|
|
448
|
+
def hook_uninstall() -> None:
|
|
449
|
+
"""Remove the avos pre-push git hook."""
|
|
450
|
+
from avos_cli.commands.hook_install import HookUninstallOrchestrator
|
|
451
|
+
from avos_cli.config.manager import find_repo_root
|
|
452
|
+
from avos_cli.exceptions import RepositoryContextError
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
repo_root = find_repo_root(Path.cwd())
|
|
456
|
+
except RepositoryContextError as e:
|
|
457
|
+
print_error(f"[REPOSITORY_CONTEXT_ERROR] {e}")
|
|
458
|
+
raise typer.Exit(1) from e
|
|
459
|
+
|
|
460
|
+
orchestrator = HookUninstallOrchestrator(repo_root=repo_root)
|
|
461
|
+
code = orchestrator.run()
|
|
462
|
+
raise typer.Exit(code)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@app.command(name="hook-sync", hidden=True)
|
|
466
|
+
def hook_sync(
|
|
467
|
+
old_sha: str = typer.Argument(..., help="Base commit SHA (remote has this)."),
|
|
468
|
+
new_sha: str = typer.Argument(..., help="Target commit SHA (pushing this)."),
|
|
469
|
+
) -> None:
|
|
470
|
+
"""Sync commits to Avos Memory (called by pre-push hook)."""
|
|
471
|
+
from avos_cli.commands.hook_sync import HookSyncOrchestrator
|
|
472
|
+
from avos_cli.config.hash_store import IngestHashStore
|
|
473
|
+
from avos_cli.config.manager import find_repo_root
|
|
474
|
+
from avos_cli.exceptions import RepositoryContextError
|
|
475
|
+
from avos_cli.services.git_client import GitClient
|
|
476
|
+
from avos_cli.services.memory_client import AvosMemoryClient
|
|
477
|
+
|
|
478
|
+
api_key = os.environ.get("AVOS_API_KEY", "")
|
|
479
|
+
api_url = os.environ.get("AVOS_API_URL", "https://api.avos.ai")
|
|
480
|
+
|
|
481
|
+
if not api_key:
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
repo_root = find_repo_root(Path.cwd())
|
|
486
|
+
except RepositoryContextError:
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
avos_dir = repo_root / ".avos"
|
|
490
|
+
hash_store = IngestHashStore(avos_dir)
|
|
491
|
+
hash_store.load()
|
|
492
|
+
|
|
493
|
+
orchestrator = HookSyncOrchestrator(
|
|
494
|
+
memory_client=AvosMemoryClient(api_key=api_key, api_url=api_url),
|
|
495
|
+
git_client=GitClient(),
|
|
496
|
+
hash_store=hash_store,
|
|
497
|
+
repo_root=repo_root,
|
|
498
|
+
)
|
|
499
|
+
code = orchestrator.run(old_sha, new_sha)
|
|
500
|
+
raise typer.Exit(code)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
if __name__ == "__main__":
|
|
504
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|