ags-cli 0.1.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.
- ags_cli-0.1.0.dist-info/METADATA +332 -0
- ags_cli-0.1.0.dist-info/RECORD +156 -0
- ags_cli-0.1.0.dist-info/WHEEL +5 -0
- ags_cli-0.1.0.dist-info/entry_points.txt +2 -0
- ags_cli-0.1.0.dist-info/top_level.txt +2 -0
- cli/__init__.py +0 -0
- cli/__main__.py +4 -0
- cli/client.py +145 -0
- cli/commands/__init__.py +0 -0
- cli/commands/adapter.py +33 -0
- cli/commands/artifact.py +21 -0
- cli/commands/ask.py +212 -0
- cli/commands/chat.py +649 -0
- cli/commands/diff.py +49 -0
- cli/commands/doctor.py +93 -0
- cli/commands/gui.py +45 -0
- cli/commands/init.py +20 -0
- cli/commands/mcp.py +50 -0
- cli/commands/memory.py +65 -0
- cli/commands/model.py +73 -0
- cli/commands/policy.py +52 -0
- cli/commands/project.py +122 -0
- cli/commands/quota.py +107 -0
- cli/commands/run.py +84 -0
- cli/commands/serve.py +171 -0
- cli/commands/service.py +236 -0
- cli/commands/session.py +219 -0
- cli/commands/stats.py +96 -0
- cli/commands/status.py +36 -0
- cli/commands/task.py +37 -0
- cli/commands/usage.py +86 -0
- cli/local.py +84 -0
- cli/main.py +149 -0
- harness/__init__.py +4 -0
- harness/adapters/__init__.py +26 -0
- harness/adapters/antigravity.py +301 -0
- harness/adapters/claude_code.py +522 -0
- harness/adapters/local_openai.py +534 -0
- harness/adapters/mock.py +112 -0
- harness/adapters/probe.py +65 -0
- harness/adapters/registry.py +56 -0
- harness/adapters/warm_pool.py +255 -0
- harness/api/__init__.py +0 -0
- harness/api/router.py +38 -0
- harness/api/v1/__init__.py +0 -0
- harness/api/v1/adapters.py +105 -0
- harness/api/v1/approvals.py +173 -0
- harness/api/v1/artifacts.py +44 -0
- harness/api/v1/attachments.py +48 -0
- harness/api/v1/doctor.py +22 -0
- harness/api/v1/health.py +37 -0
- harness/api/v1/mcp.py +131 -0
- harness/api/v1/memory.py +80 -0
- harness/api/v1/policies.py +49 -0
- harness/api/v1/project.py +302 -0
- harness/api/v1/runs.py +98 -0
- harness/api/v1/sessions.py +248 -0
- harness/api/v1/stream.py +110 -0
- harness/api/v1/tasks.py +30 -0
- harness/api/v1/usage.py +58 -0
- harness/bootstrap.py +216 -0
- harness/db.py +164 -0
- harness/deps.py +58 -0
- harness/eventbus/__init__.py +20 -0
- harness/eventbus/inprocess_bus.py +85 -0
- harness/main.py +144 -0
- harness/mcp/__init__.py +22 -0
- harness/mcp/client.py +139 -0
- harness/mcp/config_gen.py +77 -0
- harness/mcp/registry.py +156 -0
- harness/mcp/tools.py +81 -0
- harness/memory/__init__.py +13 -0
- harness/memory/context_budget.py +116 -0
- harness/memory/embed.py +217 -0
- harness/memory/extract.py +74 -0
- harness/memory/ingest.py +106 -0
- harness/memory/namespaces.py +57 -0
- harness/memory/ranking.py +31 -0
- harness/memory/redact.py +37 -0
- harness/memory/service.py +320 -0
- harness/memory/sqlite_vec_backend.py +266 -0
- harness/memory/summarize.py +68 -0
- harness/models/__init__.py +35 -0
- harness/models/adapter_health.py +27 -0
- harness/models/approval.py +37 -0
- harness/models/artifact.py +35 -0
- harness/models/audit_log.py +33 -0
- harness/models/comparison.py +33 -0
- harness/models/enums.py +146 -0
- harness/models/mcp_server_config.py +49 -0
- harness/models/memory.py +76 -0
- harness/models/policy.py +29 -0
- harness/models/provider_config.py +35 -0
- harness/models/run.py +46 -0
- harness/models/run_event.py +35 -0
- harness/models/session.py +50 -0
- harness/models/task.py +30 -0
- harness/models/types.py +63 -0
- harness/models/workspace.py +27 -0
- harness/observability/__init__.py +4 -0
- harness/observability/audit.py +88 -0
- harness/observability/logging.py +47 -0
- harness/observability/metrics.py +43 -0
- harness/observability/tracing.py +38 -0
- harness/orchestrator/__init__.py +19 -0
- harness/orchestrator/engine.py +821 -0
- harness/orchestrator/handoff.py +33 -0
- harness/orchestrator/lifecycle.py +69 -0
- harness/orchestrator/native_resume.py +93 -0
- harness/orchestrator/policies.py +101 -0
- harness/orchestrator/reconcile.py +39 -0
- harness/orchestrator/run_graph.py +62 -0
- harness/orchestrator/snapshot.py +49 -0
- harness/schemas/__init__.py +1 -0
- harness/schemas/adapter.py +26 -0
- harness/schemas/approval.py +25 -0
- harness/schemas/artifact.py +17 -0
- harness/schemas/common.py +20 -0
- harness/schemas/memory.py +50 -0
- harness/schemas/policy.py +39 -0
- harness/schemas/run.py +116 -0
- harness/schemas/session.py +93 -0
- harness/schemas/task.py +24 -0
- harness/security/__init__.py +5 -0
- harness/security/approval_policy.py +107 -0
- harness/security/auth.py +106 -0
- harness/security/permissions.py +59 -0
- harness/security/risk.py +59 -0
- harness/security/secrets.py +49 -0
- harness/services/__init__.py +1 -0
- harness/services/adapters_health.py +212 -0
- harness/services/approvals.py +84 -0
- harness/services/artifacts.py +78 -0
- harness/services/attachments.py +228 -0
- harness/services/comparison.py +345 -0
- harness/services/doctor.py +156 -0
- harness/services/files.py +287 -0
- harness/services/mcp_servers.py +179 -0
- harness/services/project_git.py +101 -0
- harness/services/retention.py +97 -0
- harness/services/runs.py +155 -0
- harness/services/runs_diff.py +201 -0
- harness/services/sessions.py +242 -0
- harness/services/ssh.py +300 -0
- harness/services/stats.py +132 -0
- harness/services/tasks.py +41 -0
- harness/services/workspaces.py +184 -0
- harness/services/worktrees.py +439 -0
- harness/settings.py +186 -0
- harness/skills/__init__.py +11 -0
- harness/skills/manager.py +141 -0
- harness/tools/__init__.py +1 -0
- harness/tools/fs_tools.py +295 -0
- harness/workers/__init__.py +0 -0
- harness/workers/dispatch.py +26 -0
- harness/workers/inprocess.py +135 -0
cli/commands/adapter.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from cli.client import HarnessClient
|
|
8
|
+
from cli.commands.ask import _canon
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="List and test adapters.")
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("list")
|
|
15
|
+
def list_adapters() -> None:
|
|
16
|
+
"""List configured adapters."""
|
|
17
|
+
client = HarnessClient()
|
|
18
|
+
rows = client.get("/adapters")
|
|
19
|
+
table = Table("name", "kind", "model", "enabled")
|
|
20
|
+
for r in rows:
|
|
21
|
+
table.add_row(r["name"], r["kind"], r.get("model") or "-", str(r["enabled"]))
|
|
22
|
+
console.print(table)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("test")
|
|
26
|
+
def test(name: str = typer.Argument(..., help="Provider config name.")) -> None:
|
|
27
|
+
"""Run an adapter health check."""
|
|
28
|
+
client = HarnessClient()
|
|
29
|
+
name = _canon(name) # accept friendly aliases (agy -> antigravity) like `ask`/`model`
|
|
30
|
+
data = client.post(f"/adapters/{name}/test")
|
|
31
|
+
color = "green" if data["ok"] else "red"
|
|
32
|
+
console.print(f"[{color}]{name}[/{color}] ok={data['ok']} latency={data['latency_ms']:.1f}ms")
|
|
33
|
+
console.print_json(data=data["detail"])
|
cli/commands/artifact.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from cli.client import HarnessClient
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(help="Browse and open artifacts.")
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("list")
|
|
13
|
+
def list_artifacts(session_id: str = typer.Option(None, "--session")) -> None:
|
|
14
|
+
"""List artifacts (optionally for a session)."""
|
|
15
|
+
client = HarnessClient()
|
|
16
|
+
# Artifacts are surfaced via session detail; a standalone /artifacts API is planned.
|
|
17
|
+
if session_id:
|
|
18
|
+
data = client.get(f"/sessions/{session_id}")
|
|
19
|
+
console.print_json(data=data)
|
|
20
|
+
else:
|
|
21
|
+
console.print("[yellow]Specify --session to list a session's artifacts.[/yellow]")
|
cli/commands/ask.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""``ags ask`` — the one-liner way to work a project with multiple providers.
|
|
2
|
+
|
|
3
|
+
It removes all session bookkeeping: the *first* ask in a workspace starts a session;
|
|
4
|
+
every later ask **continues that same session**, so memory is shared automatically.
|
|
5
|
+
|
|
6
|
+
Which provider runs is a knob — you don't have to name it every time. Resolution
|
|
7
|
+
order (first hit wins):
|
|
8
|
+
|
|
9
|
+
1. an explicit name on the command: ``ags ask claude "..."`` / ``-P agy``
|
|
10
|
+
2. the ``HARNESS_PROVIDER`` env var: ``export HARNESS_PROVIDER=agy`` (per terminal)
|
|
11
|
+
3. a sticky default for the workspace: ``ags use claude`` (survives new terminals)
|
|
12
|
+
4. fallback: ``claude``
|
|
13
|
+
|
|
14
|
+
So a typical flow becomes:
|
|
15
|
+
|
|
16
|
+
ags use claude
|
|
17
|
+
ags ask "audit the cache layer and propose a fix"
|
|
18
|
+
ags ask "implement it"
|
|
19
|
+
ags use agy # flip the knob
|
|
20
|
+
ags ask "review and optimize what claude wrote"
|
|
21
|
+
|
|
22
|
+
The session id and the sticky provider are remembered per workspace under
|
|
23
|
+
``~/.ags/<workspace>.{session,provider}``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import os
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
import typer
|
|
32
|
+
from rich.console import Console
|
|
33
|
+
|
|
34
|
+
from cli.client import APIError, HarnessClient
|
|
35
|
+
|
|
36
|
+
console = Console()
|
|
37
|
+
|
|
38
|
+
# Friendly aliases -> the provider name configured in config.yaml.
|
|
39
|
+
_ALIASES = {
|
|
40
|
+
"agy": "antigravity",
|
|
41
|
+
"gemini": "antigravity",
|
|
42
|
+
"google": "antigravity",
|
|
43
|
+
"anthropic": "claude",
|
|
44
|
+
}
|
|
45
|
+
_DEFAULT_PROVIDER = "claude"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _canon(name: str) -> str:
|
|
49
|
+
return _ALIASES.get(name.lower(), name.lower())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _state_dir() -> Path:
|
|
53
|
+
d = Path.home() / ".ags"
|
|
54
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
return d
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _session_file(workspace: str) -> Path:
|
|
59
|
+
return _state_dir() / f"{workspace}.session"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _provider_file(workspace: str) -> Path:
|
|
63
|
+
return _state_dir() / f"{workspace}.provider"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_valid_providers(client: HarnessClient) -> list[str]:
|
|
67
|
+
"""Fetch the list of enabled providers from the API."""
|
|
68
|
+
try:
|
|
69
|
+
adapters = client.get("/adapters")
|
|
70
|
+
return [a["name"] for a in adapters if a.get("enabled", True)]
|
|
71
|
+
except Exception:
|
|
72
|
+
# If the API is unreachable, return a sensible default fallback list
|
|
73
|
+
# so offline commands don't completely break before the server is running.
|
|
74
|
+
return ["claude", "antigravity", "local", "mock"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _resolve_provider(explicit: str | None, workspace: str) -> str:
|
|
78
|
+
"""Knob resolution: explicit > HARNESS_PROVIDER env > sticky `use` > default."""
|
|
79
|
+
if explicit:
|
|
80
|
+
return _canon(explicit)
|
|
81
|
+
if env := os.environ.get("HARNESS_PROVIDER"):
|
|
82
|
+
return _canon(env)
|
|
83
|
+
pf = _provider_file(workspace)
|
|
84
|
+
if pf.exists() and (v := pf.read_text().strip()):
|
|
85
|
+
return _canon(v)
|
|
86
|
+
return _DEFAULT_PROVIDER
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def use(
|
|
90
|
+
provider: str | None = typer.Argument(
|
|
91
|
+
None, help="Provider all asks default to: claude | agy | local | mock. Omit to show."
|
|
92
|
+
),
|
|
93
|
+
workspace: str = typer.Option("default", "--workspace", "-w", envvar="HARNESS_WORKSPACE"),
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Set (or show) the default provider for a workspace — the knob for `ask`."""
|
|
96
|
+
pf = _provider_file(workspace)
|
|
97
|
+
if provider is None:
|
|
98
|
+
current = pf.read_text().strip() if pf.exists() else f"{_DEFAULT_PROVIDER} (default)"
|
|
99
|
+
console.print(f"default provider for '[bold]{workspace}[/bold]': {current}")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
canon = _canon(provider)
|
|
103
|
+
client = HarnessClient()
|
|
104
|
+
valid_providers = get_valid_providers(client)
|
|
105
|
+
|
|
106
|
+
if canon not in valid_providers:
|
|
107
|
+
console.print(
|
|
108
|
+
f"[yellow]Unknown or disabled provider '{provider}'.\n"
|
|
109
|
+
f"Valid providers are: {', '.join(valid_providers)}[/yellow]"
|
|
110
|
+
)
|
|
111
|
+
raise typer.Exit(1)
|
|
112
|
+
|
|
113
|
+
pf.write_text(canon)
|
|
114
|
+
console.print(f"✓ '[bold]{workspace}[/bold]' now defaults to [bold]{canon}[/bold] "
|
|
115
|
+
f"— `ags ask \"...\"` will use it until you change it.")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def ask(
|
|
119
|
+
arg1: str = typer.Argument(
|
|
120
|
+
..., help="The prompt — OR a provider name when a second arg (the prompt) follows."
|
|
121
|
+
),
|
|
122
|
+
arg2: str | None = typer.Argument(None, help="The prompt, if arg1 named a provider."),
|
|
123
|
+
provider: str | None = typer.Option(
|
|
124
|
+
None, "--provider", "-P", help="Override the provider knob for just this turn."
|
|
125
|
+
),
|
|
126
|
+
model: str | None = typer.Option(
|
|
127
|
+
None, "--model", "-m", help="One-off model override for just this turn."
|
|
128
|
+
),
|
|
129
|
+
workspace: str = typer.Option("default", "--workspace", "-w", envvar="HARNESS_WORKSPACE"),
|
|
130
|
+
policy: str = typer.Option("single", "--policy", "-p"),
|
|
131
|
+
new: bool = typer.Option(False, "--new", help="Start a fresh session for this workspace."),
|
|
132
|
+
isolated: bool | None = typer.Option(
|
|
133
|
+
None, "--isolated/--no-isolated",
|
|
134
|
+
help="Run a NEW session in a private git worktree (own branch) instead of "
|
|
135
|
+
"directly in the project folder. Unset: the workspace's default.",
|
|
136
|
+
),
|
|
137
|
+
) -> None:
|
|
138
|
+
# Support both `ask "<prompt>"` (knob picks provider) and the explicit
|
|
139
|
+
# `ask <provider> "<prompt>"` form.
|
|
140
|
+
if arg2 is None:
|
|
141
|
+
positional_provider, prompt = None, arg1
|
|
142
|
+
else:
|
|
143
|
+
positional_provider, prompt = arg1, arg2
|
|
144
|
+
provider = _resolve_provider(provider or positional_provider, workspace)
|
|
145
|
+
|
|
146
|
+
client = HarnessClient()
|
|
147
|
+
state = _session_file(workspace)
|
|
148
|
+
session_id = None if new else (state.read_text().strip() if state.exists() else None)
|
|
149
|
+
|
|
150
|
+
if session_id:
|
|
151
|
+
console.print(f"[dim]continuing session {session_id} on [bold]{provider}[/bold][/dim]")
|
|
152
|
+
data = client.post(
|
|
153
|
+
f"/sessions/{session_id}/resume",
|
|
154
|
+
json={
|
|
155
|
+
"policy": policy,
|
|
156
|
+
"providers": [provider],
|
|
157
|
+
"additional_objective": prompt,
|
|
158
|
+
"model": model,
|
|
159
|
+
},
|
|
160
|
+
inline=True,
|
|
161
|
+
)
|
|
162
|
+
session_id = data["session_id"]
|
|
163
|
+
runs = client.get(f"/sessions/{session_id}/runs")
|
|
164
|
+
else:
|
|
165
|
+
console.print(
|
|
166
|
+
f"[dim]starting new session in '{workspace}' on [bold]{provider}[/bold][/dim]"
|
|
167
|
+
)
|
|
168
|
+
data = client.post(
|
|
169
|
+
"/runs/start",
|
|
170
|
+
json={
|
|
171
|
+
"objective": prompt,
|
|
172
|
+
"workspace": workspace,
|
|
173
|
+
"policy": policy,
|
|
174
|
+
"providers": [provider],
|
|
175
|
+
"model": model,
|
|
176
|
+
# None → omit: the server applies the workspace default.
|
|
177
|
+
"workspace_mode": (
|
|
178
|
+
None if isolated is None else ("worktree" if isolated else "main")
|
|
179
|
+
),
|
|
180
|
+
},
|
|
181
|
+
inline=True,
|
|
182
|
+
)
|
|
183
|
+
session_id = data["session_id"]
|
|
184
|
+
runs = data["runs"]
|
|
185
|
+
|
|
186
|
+
state.write_text(session_id)
|
|
187
|
+
|
|
188
|
+
# Show this turn's answer: the most recently finished run for this provider.
|
|
189
|
+
mine = [r for r in runs if (r.get("graph_node_id") or "").startswith(provider)]
|
|
190
|
+
mine.sort(key=lambda r: r.get("finished_at") or "", reverse=True)
|
|
191
|
+
if not mine:
|
|
192
|
+
console.print("[yellow]no run produced for this provider[/yellow]")
|
|
193
|
+
raise typer.Exit(1)
|
|
194
|
+
run = mine[0]
|
|
195
|
+
status = run["status"]
|
|
196
|
+
color = "green" if status == "succeeded" else "red"
|
|
197
|
+
console.print(f"\n[{color}]{provider} [{status}][/{color}] "
|
|
198
|
+
f"[dim]session {session_id}[/dim]")
|
|
199
|
+
console.print(run.get("final_output") or run.get("error") or "[dim](no output)[/dim]")
|
|
200
|
+
# Isolated sessions edit a private worktree, not the project folder — say so,
|
|
201
|
+
# or a user checking the project after `ask` sees no change and is confused.
|
|
202
|
+
try:
|
|
203
|
+
wt = client.get(f"/sessions/{session_id}/workspace") or {}
|
|
204
|
+
if wt.get("workspace_mode") == "worktree" and wt.get("changed_files"):
|
|
205
|
+
n = len(wt["changed_files"])
|
|
206
|
+
console.print(
|
|
207
|
+
f"[dim]{n} changed file{'s' if n != 1 else ''} in a private workspace "
|
|
208
|
+
f"(branch {wt.get('branch')}). Apply with: "
|
|
209
|
+
f"ags session merge {session_id}[/dim]"
|
|
210
|
+
)
|
|
211
|
+
except APIError:
|
|
212
|
+
pass
|