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.
Files changed (156) hide show
  1. ags_cli-0.1.0.dist-info/METADATA +332 -0
  2. ags_cli-0.1.0.dist-info/RECORD +156 -0
  3. ags_cli-0.1.0.dist-info/WHEEL +5 -0
  4. ags_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. ags_cli-0.1.0.dist-info/top_level.txt +2 -0
  6. cli/__init__.py +0 -0
  7. cli/__main__.py +4 -0
  8. cli/client.py +145 -0
  9. cli/commands/__init__.py +0 -0
  10. cli/commands/adapter.py +33 -0
  11. cli/commands/artifact.py +21 -0
  12. cli/commands/ask.py +212 -0
  13. cli/commands/chat.py +649 -0
  14. cli/commands/diff.py +49 -0
  15. cli/commands/doctor.py +93 -0
  16. cli/commands/gui.py +45 -0
  17. cli/commands/init.py +20 -0
  18. cli/commands/mcp.py +50 -0
  19. cli/commands/memory.py +65 -0
  20. cli/commands/model.py +73 -0
  21. cli/commands/policy.py +52 -0
  22. cli/commands/project.py +122 -0
  23. cli/commands/quota.py +107 -0
  24. cli/commands/run.py +84 -0
  25. cli/commands/serve.py +171 -0
  26. cli/commands/service.py +236 -0
  27. cli/commands/session.py +219 -0
  28. cli/commands/stats.py +96 -0
  29. cli/commands/status.py +36 -0
  30. cli/commands/task.py +37 -0
  31. cli/commands/usage.py +86 -0
  32. cli/local.py +84 -0
  33. cli/main.py +149 -0
  34. harness/__init__.py +4 -0
  35. harness/adapters/__init__.py +26 -0
  36. harness/adapters/antigravity.py +301 -0
  37. harness/adapters/claude_code.py +522 -0
  38. harness/adapters/local_openai.py +534 -0
  39. harness/adapters/mock.py +112 -0
  40. harness/adapters/probe.py +65 -0
  41. harness/adapters/registry.py +56 -0
  42. harness/adapters/warm_pool.py +255 -0
  43. harness/api/__init__.py +0 -0
  44. harness/api/router.py +38 -0
  45. harness/api/v1/__init__.py +0 -0
  46. harness/api/v1/adapters.py +105 -0
  47. harness/api/v1/approvals.py +173 -0
  48. harness/api/v1/artifacts.py +44 -0
  49. harness/api/v1/attachments.py +48 -0
  50. harness/api/v1/doctor.py +22 -0
  51. harness/api/v1/health.py +37 -0
  52. harness/api/v1/mcp.py +131 -0
  53. harness/api/v1/memory.py +80 -0
  54. harness/api/v1/policies.py +49 -0
  55. harness/api/v1/project.py +302 -0
  56. harness/api/v1/runs.py +98 -0
  57. harness/api/v1/sessions.py +248 -0
  58. harness/api/v1/stream.py +110 -0
  59. harness/api/v1/tasks.py +30 -0
  60. harness/api/v1/usage.py +58 -0
  61. harness/bootstrap.py +216 -0
  62. harness/db.py +164 -0
  63. harness/deps.py +58 -0
  64. harness/eventbus/__init__.py +20 -0
  65. harness/eventbus/inprocess_bus.py +85 -0
  66. harness/main.py +144 -0
  67. harness/mcp/__init__.py +22 -0
  68. harness/mcp/client.py +139 -0
  69. harness/mcp/config_gen.py +77 -0
  70. harness/mcp/registry.py +156 -0
  71. harness/mcp/tools.py +81 -0
  72. harness/memory/__init__.py +13 -0
  73. harness/memory/context_budget.py +116 -0
  74. harness/memory/embed.py +217 -0
  75. harness/memory/extract.py +74 -0
  76. harness/memory/ingest.py +106 -0
  77. harness/memory/namespaces.py +57 -0
  78. harness/memory/ranking.py +31 -0
  79. harness/memory/redact.py +37 -0
  80. harness/memory/service.py +320 -0
  81. harness/memory/sqlite_vec_backend.py +266 -0
  82. harness/memory/summarize.py +68 -0
  83. harness/models/__init__.py +35 -0
  84. harness/models/adapter_health.py +27 -0
  85. harness/models/approval.py +37 -0
  86. harness/models/artifact.py +35 -0
  87. harness/models/audit_log.py +33 -0
  88. harness/models/comparison.py +33 -0
  89. harness/models/enums.py +146 -0
  90. harness/models/mcp_server_config.py +49 -0
  91. harness/models/memory.py +76 -0
  92. harness/models/policy.py +29 -0
  93. harness/models/provider_config.py +35 -0
  94. harness/models/run.py +46 -0
  95. harness/models/run_event.py +35 -0
  96. harness/models/session.py +50 -0
  97. harness/models/task.py +30 -0
  98. harness/models/types.py +63 -0
  99. harness/models/workspace.py +27 -0
  100. harness/observability/__init__.py +4 -0
  101. harness/observability/audit.py +88 -0
  102. harness/observability/logging.py +47 -0
  103. harness/observability/metrics.py +43 -0
  104. harness/observability/tracing.py +38 -0
  105. harness/orchestrator/__init__.py +19 -0
  106. harness/orchestrator/engine.py +821 -0
  107. harness/orchestrator/handoff.py +33 -0
  108. harness/orchestrator/lifecycle.py +69 -0
  109. harness/orchestrator/native_resume.py +93 -0
  110. harness/orchestrator/policies.py +101 -0
  111. harness/orchestrator/reconcile.py +39 -0
  112. harness/orchestrator/run_graph.py +62 -0
  113. harness/orchestrator/snapshot.py +49 -0
  114. harness/schemas/__init__.py +1 -0
  115. harness/schemas/adapter.py +26 -0
  116. harness/schemas/approval.py +25 -0
  117. harness/schemas/artifact.py +17 -0
  118. harness/schemas/common.py +20 -0
  119. harness/schemas/memory.py +50 -0
  120. harness/schemas/policy.py +39 -0
  121. harness/schemas/run.py +116 -0
  122. harness/schemas/session.py +93 -0
  123. harness/schemas/task.py +24 -0
  124. harness/security/__init__.py +5 -0
  125. harness/security/approval_policy.py +107 -0
  126. harness/security/auth.py +106 -0
  127. harness/security/permissions.py +59 -0
  128. harness/security/risk.py +59 -0
  129. harness/security/secrets.py +49 -0
  130. harness/services/__init__.py +1 -0
  131. harness/services/adapters_health.py +212 -0
  132. harness/services/approvals.py +84 -0
  133. harness/services/artifacts.py +78 -0
  134. harness/services/attachments.py +228 -0
  135. harness/services/comparison.py +345 -0
  136. harness/services/doctor.py +156 -0
  137. harness/services/files.py +287 -0
  138. harness/services/mcp_servers.py +179 -0
  139. harness/services/project_git.py +101 -0
  140. harness/services/retention.py +97 -0
  141. harness/services/runs.py +155 -0
  142. harness/services/runs_diff.py +201 -0
  143. harness/services/sessions.py +242 -0
  144. harness/services/ssh.py +300 -0
  145. harness/services/stats.py +132 -0
  146. harness/services/tasks.py +41 -0
  147. harness/services/workspaces.py +184 -0
  148. harness/services/worktrees.py +439 -0
  149. harness/settings.py +186 -0
  150. harness/skills/__init__.py +11 -0
  151. harness/skills/manager.py +141 -0
  152. harness/tools/__init__.py +1 -0
  153. harness/tools/fs_tools.py +295 -0
  154. harness/workers/__init__.py +0 -0
  155. harness/workers/dispatch.py +26 -0
  156. harness/workers/inprocess.py +135 -0
@@ -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"])
@@ -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