leanwarp 0.2.3__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.
- leanwarp-0.2.3.dist-info/METADATA +210 -0
- leanwarp-0.2.3.dist-info/RECORD +24 -0
- leanwarp-0.2.3.dist-info/WHEEL +4 -0
- leanwarp-0.2.3.dist-info/entry_points.txt +2 -0
- leanwarp-0.2.3.dist-info/licenses/LICENSE +21 -0
- leanwarp_cloud/__init__.py +25 -0
- leanwarp_cloud/_version.py +8 -0
- leanwarp_cloud/agents.py +173 -0
- leanwarp_cloud/amounts.py +20 -0
- leanwarp_cloud/cli.py +700 -0
- leanwarp_cloud/client.py +781 -0
- leanwarp_cloud/config.py +82 -0
- leanwarp_cloud/edits.py +72 -0
- leanwarp_cloud/endpoints.py +39 -0
- leanwarp_cloud/mcp_server.py +568 -0
- leanwarp_cloud/outcome.py +112 -0
- leanwarp_cloud/project.py +613 -0
- leanwarp_cloud/py.typed +0 -0
- leanwarp_cloud/resources.py +50 -0
- leanwarp_cloud/session.py +1334 -0
- leanwarp_cloud/skills/leanwarp/SKILL.md +215 -0
- leanwarp_cloud/skills/leanwarp/references/usage.md +460 -0
- leanwarp_cloud/transport.py +221 -0
- leanwarp_cloud/updates.py +144 -0
leanwarp_cloud/cli.py
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
"""Agent-friendly JSON CLI; stdin/stdout credentials never pass through the model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import getpass
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from importlib.resources import files
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, NoReturn, get_args
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .agents import AGENTS, setup
|
|
17
|
+
from .amounts import usd_spending_cap
|
|
18
|
+
from .client import (
|
|
19
|
+
FeedbackKind,
|
|
20
|
+
FeedbackSeverity,
|
|
21
|
+
LeanWarpCloud,
|
|
22
|
+
LeanWarpCloudError,
|
|
23
|
+
OperationTimeout,
|
|
24
|
+
)
|
|
25
|
+
from .config import credentials_path, load_client, save_credentials, signed_in
|
|
26
|
+
from .endpoints import ConfigurationError
|
|
27
|
+
from .outcome import OperationOutcome, reported
|
|
28
|
+
from .project import ProjectError, plan_upload
|
|
29
|
+
from .resources import published_worker_resources
|
|
30
|
+
from .session import (
|
|
31
|
+
MAX_INLINE_WAIT_SECONDS,
|
|
32
|
+
ProjectSession,
|
|
33
|
+
SessionError,
|
|
34
|
+
UsageError,
|
|
35
|
+
check_project,
|
|
36
|
+
)
|
|
37
|
+
from .updates import noted, update
|
|
38
|
+
|
|
39
|
+
DEFAULT_WAIT_SECONDS = 30.0
|
|
40
|
+
#: Where an account creates API keys.
|
|
41
|
+
KEYS_URL = "https://isagoge.in/dashboard?section=keys"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _spending_cap(value: str) -> int:
|
|
45
|
+
try:
|
|
46
|
+
return usd_spending_cap(value)
|
|
47
|
+
except ValueError as error:
|
|
48
|
+
raise argparse.ArgumentTypeError(str(error)) from error
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_DESCRIPTION = """\
|
|
52
|
+
Check and verify Lean proofs on LeanWarp's hosted workers.
|
|
53
|
+
Every command prints one JSON object."""
|
|
54
|
+
|
|
55
|
+
_EPILOG = f"""\
|
|
56
|
+
a typical session:
|
|
57
|
+
leanwarp doctor show which environment this project runs on
|
|
58
|
+
leanwarp files list the Lean files that would upload
|
|
59
|
+
leanwarp connect create the project's workspace (no compute yet)
|
|
60
|
+
leanwarp check Main.lean compile a file and report Lean errors
|
|
61
|
+
leanwarp verify Main.lean --declaration NAME --target 'STATEMENT'
|
|
62
|
+
leanwarp stop stop the worker when you are done
|
|
63
|
+
|
|
64
|
+
results:
|
|
65
|
+
Operations wait up to {DEFAULT_WAIT_SECONDS:g} seconds for their result. "success" is true
|
|
66
|
+
when the operation did its job (a check passed, a proof was verified, a
|
|
67
|
+
suggestion closed its hole, a trial or listing ran), false when it did not,
|
|
68
|
+
and null while the work is still running. A trial or listing that ran is not
|
|
69
|
+
a proof: run a strict check before calling a file done.
|
|
70
|
+
|
|
71
|
+
exit codes:
|
|
72
|
+
0 success
|
|
73
|
+
1 the check or proof did not pass, or the project has no environment
|
|
74
|
+
2 the command could not run (see "message")
|
|
75
|
+
3 still running: run `leanwarp wait`, never submit it again
|
|
76
|
+
|
|
77
|
+
AI agents: run `leanwarp skill` for the full workflow."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _wait_seconds(value: str) -> float:
|
|
81
|
+
try:
|
|
82
|
+
seconds = float(value)
|
|
83
|
+
except ValueError:
|
|
84
|
+
raise argparse.ArgumentTypeError("must be a number of seconds") from None
|
|
85
|
+
if not 0 <= seconds <= MAX_INLINE_WAIT_SECONDS:
|
|
86
|
+
raise argparse.ArgumentTypeError(
|
|
87
|
+
f"must be between 0 and {MAX_INLINE_WAIT_SECONDS} seconds; "
|
|
88
|
+
"run `leanwarp wait` to wait longer"
|
|
89
|
+
)
|
|
90
|
+
return seconds
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _Parser(argparse.ArgumentParser):
|
|
94
|
+
"""Report argument mistakes as JSON on stdout, like every other result."""
|
|
95
|
+
|
|
96
|
+
def error(self, message: str) -> NoReturn:
|
|
97
|
+
_emit({"error": "invalid_arguments", "message": message, "usage": self.format_usage()})
|
|
98
|
+
raise SystemExit(2)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parser() -> argparse.ArgumentParser:
|
|
102
|
+
root = _Parser(
|
|
103
|
+
prog="leanwarp",
|
|
104
|
+
description=_DESCRIPTION,
|
|
105
|
+
epilog=_EPILOG,
|
|
106
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
107
|
+
)
|
|
108
|
+
root.add_argument("--version", action="version", version=f"leanwarp {__version__}")
|
|
109
|
+
root.add_argument(
|
|
110
|
+
"--project",
|
|
111
|
+
default=".",
|
|
112
|
+
metavar="DIR",
|
|
113
|
+
help="the Lean project directory (default: the current directory)",
|
|
114
|
+
)
|
|
115
|
+
commands = root.add_subparsers(dest="command", required=True, metavar="COMMAND")
|
|
116
|
+
|
|
117
|
+
auth = commands.add_parser("auth", help="sign in with an API key, or sign out")
|
|
118
|
+
auth_commands = auth.add_subparsers(dest="auth_command", required=True, metavar="ACTION")
|
|
119
|
+
login = auth_commands.add_parser(
|
|
120
|
+
"login",
|
|
121
|
+
help="open the dashboard's API keys, then paste a key at a hidden prompt "
|
|
122
|
+
"(in automation, set LEANWARP_API_KEY instead)",
|
|
123
|
+
)
|
|
124
|
+
login.add_argument(
|
|
125
|
+
"--no-browser", action="store_true", help="print the dashboard link without opening it"
|
|
126
|
+
)
|
|
127
|
+
auth_commands.add_parser(
|
|
128
|
+
"logout", help="forget the saved key on this machine (revoke it in the dashboard)"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
environment_help = (
|
|
132
|
+
"an environment from `leanwarp environments`; use it even if the project pins "
|
|
133
|
+
"another toolchain or lockfile"
|
|
134
|
+
)
|
|
135
|
+
doctor = commands.add_parser(
|
|
136
|
+
"doctor", help="show which environment this project runs on (no compute)"
|
|
137
|
+
)
|
|
138
|
+
doctor.add_argument("--environment", metavar="NAME", help=environment_help)
|
|
139
|
+
commands.add_parser(
|
|
140
|
+
"files",
|
|
141
|
+
help=(
|
|
142
|
+
"list the Lean files that would upload, with sizes and problems; .gitignore and "
|
|
143
|
+
".leanwarpignore exclude files (no key needed)"
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
connect = commands.add_parser(
|
|
148
|
+
"connect", help="create this project's workspace (no compute until the first operation)"
|
|
149
|
+
)
|
|
150
|
+
selection = connect.add_mutually_exclusive_group()
|
|
151
|
+
selection.add_argument("--environment", metavar="NAME", help=environment_help)
|
|
152
|
+
selection.add_argument("--bundle", help=argparse.SUPPRESS)
|
|
153
|
+
connect.add_argument(
|
|
154
|
+
"--workspace",
|
|
155
|
+
metavar="ID",
|
|
156
|
+
help="connect to an existing workspace from `leanwarp workspaces` instead of creating one",
|
|
157
|
+
)
|
|
158
|
+
# Deprecated wire compatibility for existing automation and saved requests.
|
|
159
|
+
connect.add_argument("--profile", metavar="SIZE", help=argparse.SUPPRESS)
|
|
160
|
+
connect.add_argument(
|
|
161
|
+
"--max-profile",
|
|
162
|
+
metavar="SIZE",
|
|
163
|
+
help=argparse.SUPPRESS,
|
|
164
|
+
)
|
|
165
|
+
connect.add_argument(
|
|
166
|
+
"--max-spend",
|
|
167
|
+
metavar="USD",
|
|
168
|
+
type=_spending_cap,
|
|
169
|
+
help="cap total workspace spending in USD (up to six decimals); no cap by default",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
check = commands.add_parser("check", help="compile a file and report Lean errors and warnings")
|
|
173
|
+
check.add_argument("file", metavar="FILE", help="a .lean file in the project")
|
|
174
|
+
check.add_argument(
|
|
175
|
+
"--draft",
|
|
176
|
+
action="store_true",
|
|
177
|
+
help=(
|
|
178
|
+
"a faster check while you edit: Lean reuses its work on the unchanged part of "
|
|
179
|
+
"the file, and `sorry` is only a warning"
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
inspect = commands.add_parser(
|
|
184
|
+
"inspect", help="show the goals and local context at a position in a proof"
|
|
185
|
+
)
|
|
186
|
+
inspect.add_argument("file", metavar="FILE", help="a .lean file in the project")
|
|
187
|
+
|
|
188
|
+
tactics = commands.add_parser(
|
|
189
|
+
"try-tactics", help="try tactics at a position without editing the file"
|
|
190
|
+
)
|
|
191
|
+
tactics.add_argument("file", metavar="FILE", help="a .lean file in the project")
|
|
192
|
+
|
|
193
|
+
for command in (inspect, tactics):
|
|
194
|
+
command.add_argument("--line", type=int, required=True, help="line number, from 1")
|
|
195
|
+
command.add_argument("--column", type=int, required=True, help="column number, from 1")
|
|
196
|
+
tactics.add_argument(
|
|
197
|
+
"--tactic",
|
|
198
|
+
action="append",
|
|
199
|
+
required=True,
|
|
200
|
+
metavar="TACTIC",
|
|
201
|
+
help="a tactic to try; repeat for several",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
verify = commands.add_parser(
|
|
205
|
+
"verify", help="check that a declaration proves exactly the statement you give"
|
|
206
|
+
)
|
|
207
|
+
verify.add_argument("file", metavar="FILE", help="the .lean file that contains the proof")
|
|
208
|
+
verify.add_argument(
|
|
209
|
+
"--declaration", required=True, metavar="NAME", help="the theorem's name in FILE"
|
|
210
|
+
)
|
|
211
|
+
verify.add_argument(
|
|
212
|
+
"--target",
|
|
213
|
+
required=True,
|
|
214
|
+
metavar="STATEMENT",
|
|
215
|
+
help="the statement the proof must establish, e.g. '∀ n : Nat, n + 0 = n'",
|
|
216
|
+
)
|
|
217
|
+
verify.add_argument(
|
|
218
|
+
"--context-file",
|
|
219
|
+
type=Path,
|
|
220
|
+
metavar="PATH",
|
|
221
|
+
help="Lean imports and definitions the statement needs, e.g. `import Mathlib`",
|
|
222
|
+
)
|
|
223
|
+
verify.add_argument(
|
|
224
|
+
"--fresh", action="store_true", help="verify on a separate worker that stops afterwards"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
suggest = commands.add_parser(
|
|
228
|
+
"suggest",
|
|
229
|
+
help="find a tactic that closes the hole at a position; `leanwarp apply` writes it",
|
|
230
|
+
)
|
|
231
|
+
suggest.add_argument("file", metavar="FILE", help="a .lean file in the project")
|
|
232
|
+
suggest.add_argument("--line", type=int, required=True, help="line number, from 1")
|
|
233
|
+
suggest.add_argument("--column", type=int, required=True, help="column number, from 1")
|
|
234
|
+
suggest.add_argument(
|
|
235
|
+
"--tactic",
|
|
236
|
+
action="append",
|
|
237
|
+
metavar="TACTIC",
|
|
238
|
+
help="a tactic to try instead of LeanWarp's ranked portfolio; repeat for several",
|
|
239
|
+
)
|
|
240
|
+
suggest.add_argument(
|
|
241
|
+
"--tactic-timeout",
|
|
242
|
+
type=int,
|
|
243
|
+
metavar="MS",
|
|
244
|
+
help="milliseconds per tactic; LeanWarp's learned budgets by default",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
holes = commands.add_parser(
|
|
248
|
+
"holes", help="list every sorry and ?_ in a file, with goals for the first 16"
|
|
249
|
+
)
|
|
250
|
+
holes.add_argument("file", metavar="FILE", help="a .lean file in the project")
|
|
251
|
+
holes.add_argument("--no-goals", action="store_true", help="list positions only (faster)")
|
|
252
|
+
|
|
253
|
+
declarations = commands.add_parser(
|
|
254
|
+
"declarations", help="list the declarations a file adds, with types and dependencies"
|
|
255
|
+
)
|
|
256
|
+
declarations.add_argument("file", metavar="FILE", help="a .lean file in the project")
|
|
257
|
+
|
|
258
|
+
obligations = commands.add_parser(
|
|
259
|
+
"sorry2lemma",
|
|
260
|
+
help="move each unfinished theorem's proof into a named lemma; "
|
|
261
|
+
"`leanwarp apply` writes the new file",
|
|
262
|
+
)
|
|
263
|
+
obligations.add_argument("file", metavar="FILE", help="a .lean file in the project")
|
|
264
|
+
obligations.add_argument(
|
|
265
|
+
"--max", type=int, default=64, metavar="N", help="at most this many, 1 to 256"
|
|
266
|
+
)
|
|
267
|
+
obligations.add_argument("--prefix", metavar="NAME", help="prefix for the generated lemmas")
|
|
268
|
+
obligations.add_argument(
|
|
269
|
+
"--strict-final-check",
|
|
270
|
+
action="store_true",
|
|
271
|
+
help="also report whether the result passes a strict check (it keeps the sorrys)",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
batch = commands.add_parser(
|
|
275
|
+
"check-files", help="check several files in one operation (up to 64)"
|
|
276
|
+
)
|
|
277
|
+
batch.add_argument("files", nargs="+", metavar="FILE", help=".lean files in the project")
|
|
278
|
+
batch.add_argument(
|
|
279
|
+
"--draft",
|
|
280
|
+
action="store_true",
|
|
281
|
+
help="accept files that compile, even with sorry; strict by default",
|
|
282
|
+
)
|
|
283
|
+
batch.add_argument(
|
|
284
|
+
"--file-timeout", type=int, metavar="SECONDS", help="Lean time per file, 1 to 300"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
commands.add_parser(
|
|
288
|
+
"apply",
|
|
289
|
+
help="write the latest suggest or sorry2lemma result into its file and upload it",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
for operation in (
|
|
293
|
+
check,
|
|
294
|
+
inspect,
|
|
295
|
+
tactics,
|
|
296
|
+
verify,
|
|
297
|
+
suggest,
|
|
298
|
+
holes,
|
|
299
|
+
declarations,
|
|
300
|
+
obligations,
|
|
301
|
+
batch,
|
|
302
|
+
):
|
|
303
|
+
operation.add_argument(
|
|
304
|
+
"--wait",
|
|
305
|
+
type=_wait_seconds,
|
|
306
|
+
default=DEFAULT_WAIT_SECONDS,
|
|
307
|
+
metavar="SECONDS",
|
|
308
|
+
help=(
|
|
309
|
+
f"wait up to this long for the result, 0 to {MAX_INLINE_WAIT_SECONDS} "
|
|
310
|
+
f"(default: {DEFAULT_WAIT_SECONDS:g}); then use `leanwarp wait`"
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
operation.add_argument(
|
|
314
|
+
"--execution-timeout",
|
|
315
|
+
type=int,
|
|
316
|
+
metavar="SECONDS",
|
|
317
|
+
help="stop the operation on the server after this long, 1 to 600",
|
|
318
|
+
)
|
|
319
|
+
operation.add_argument(
|
|
320
|
+
"--profile",
|
|
321
|
+
metavar="SIZE",
|
|
322
|
+
help=argparse.SUPPRESS,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
wait = commands.add_parser("wait", help="wait for the latest operation's result")
|
|
326
|
+
wait.add_argument(
|
|
327
|
+
"--timeout",
|
|
328
|
+
type=float,
|
|
329
|
+
default=300,
|
|
330
|
+
metavar="SECONDS",
|
|
331
|
+
help="give up waiting after this long (default: 300); the operation keeps running",
|
|
332
|
+
)
|
|
333
|
+
commands.add_parser("status", help="show the workspace and the latest operation")
|
|
334
|
+
commands.add_parser("sync", help="upload changed files (operations also sync automatically)")
|
|
335
|
+
commands.add_parser("cancel", help="cancel the latest operation")
|
|
336
|
+
commands.add_parser("stop", help="stop the worker; saved files are kept")
|
|
337
|
+
commands.add_parser("recover", help="resend a request whose response was lost")
|
|
338
|
+
commands.add_parser(
|
|
339
|
+
"reconcile",
|
|
340
|
+
help="after another client changed the workspace, adopt its revision as the "
|
|
341
|
+
"base; the next sync then uploads this project's files over it",
|
|
342
|
+
)
|
|
343
|
+
commands.add_parser(
|
|
344
|
+
"disconnect",
|
|
345
|
+
help="forget this project's stopped workspace so it can connect to another "
|
|
346
|
+
"environment (run stop first)",
|
|
347
|
+
)
|
|
348
|
+
commands.add_parser(
|
|
349
|
+
"workspaces", help="list your account's workspaces and which one this project uses"
|
|
350
|
+
)
|
|
351
|
+
export = commands.add_parser(
|
|
352
|
+
"export", help="save a workspace's stored operations and results as JSON Lines"
|
|
353
|
+
)
|
|
354
|
+
export.add_argument(
|
|
355
|
+
"--output", required=True, type=Path, metavar="FILE", help="a new file to write"
|
|
356
|
+
)
|
|
357
|
+
export.add_argument(
|
|
358
|
+
"--workspace", metavar="ID", help="another workspace (default: this project's)"
|
|
359
|
+
)
|
|
360
|
+
delete = commands.add_parser(
|
|
361
|
+
"delete",
|
|
362
|
+
help="delete a workspace with its files and results; credit is kept (cannot be undone)",
|
|
363
|
+
)
|
|
364
|
+
delete.add_argument(
|
|
365
|
+
"--confirm",
|
|
366
|
+
required=True,
|
|
367
|
+
metavar="ID",
|
|
368
|
+
help="the ID of the workspace to delete, from `leanwarp status` or `leanwarp workspaces`",
|
|
369
|
+
)
|
|
370
|
+
commands.add_parser("account", help="show your credit balance")
|
|
371
|
+
commands.add_parser(
|
|
372
|
+
"environments", aliases=["versions"], help="list the Lean environments LeanWarp serves"
|
|
373
|
+
)
|
|
374
|
+
commands.add_parser("resources", help="show worker resources and pricing")
|
|
375
|
+
report = commands.add_parser(
|
|
376
|
+
"feedback",
|
|
377
|
+
help="report a confirmed problem with LeanWarp itself to the LeanWarp team "
|
|
378
|
+
"(free; names this project's latest operation)",
|
|
379
|
+
)
|
|
380
|
+
report.add_argument("--kind", required=True, choices=get_args(FeedbackKind))
|
|
381
|
+
report.add_argument(
|
|
382
|
+
"--summary", required=True, metavar="TEXT", help="one line, 8 to 200 characters"
|
|
383
|
+
)
|
|
384
|
+
report.add_argument(
|
|
385
|
+
"--details",
|
|
386
|
+
default="",
|
|
387
|
+
metavar="TEXT",
|
|
388
|
+
help="what you did, expected and got, up to 4000 characters; never the user's source",
|
|
389
|
+
)
|
|
390
|
+
report.add_argument(
|
|
391
|
+
"--severity", choices=get_args(FeedbackSeverity), default="minor", help="default: minor"
|
|
392
|
+
)
|
|
393
|
+
report.add_argument(
|
|
394
|
+
"--operation", metavar="ID", help="the operation it is about (default: the latest)"
|
|
395
|
+
)
|
|
396
|
+
install = commands.add_parser(
|
|
397
|
+
"setup",
|
|
398
|
+
help="install the LeanWarp skill and MCP server for Claude Code, Codex, Cursor and "
|
|
399
|
+
"~/.agents (no key needed)",
|
|
400
|
+
)
|
|
401
|
+
install.add_argument(
|
|
402
|
+
"--agent",
|
|
403
|
+
action="append",
|
|
404
|
+
choices=AGENTS,
|
|
405
|
+
default=[],
|
|
406
|
+
help="set up only this agent, even if it is not found (repeatable)",
|
|
407
|
+
)
|
|
408
|
+
install.add_argument("--no-mcp", action="store_true", help="install the skill only")
|
|
409
|
+
install.add_argument(
|
|
410
|
+
"--refresh",
|
|
411
|
+
action="store_true",
|
|
412
|
+
help="only replace skills setup already installed; leave MCP configuration alone",
|
|
413
|
+
)
|
|
414
|
+
commands.add_parser(
|
|
415
|
+
"update", help="upgrade leanwarp to the latest release and refresh its skills"
|
|
416
|
+
)
|
|
417
|
+
skill = commands.add_parser("skill", help="print the instructions for AI agents")
|
|
418
|
+
skill.add_argument("--reference", action="store_true", help="print the full reference instead")
|
|
419
|
+
commands.add_parser("mcp", help="run the MCP server for this project over stdio")
|
|
420
|
+
return root
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _open_keys_page(*, browser: bool) -> None:
|
|
424
|
+
"""Point the user at the page that creates keys. The prompt and messages go to
|
|
425
|
+
the terminal's stderr, keeping stdout for the one JSON result."""
|
|
426
|
+
opened = False
|
|
427
|
+
if browser:
|
|
428
|
+
import webbrowser
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
opened = webbrowser.open(KEYS_URL, new=2)
|
|
432
|
+
except webbrowser.Error:
|
|
433
|
+
opened = False
|
|
434
|
+
action = "Opened" if opened else "Open"
|
|
435
|
+
sys.stderr.write(f"{action} {KEYS_URL} to create an API key, then paste it below.\n")
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _emit(value: object) -> None:
|
|
439
|
+
# Every result, refusals included, says when the service names a newer SDK.
|
|
440
|
+
if isinstance(value, dict):
|
|
441
|
+
value = noted(value)
|
|
442
|
+
sys.stdout.write(json.dumps(value, ensure_ascii=False, allow_nan=False) + "\n")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _error(code: str, message: str, **details: object) -> int:
|
|
446
|
+
_emit({"error": code, "message": message, **details})
|
|
447
|
+
return 2
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _exit_code(operation: dict[str, Any]) -> int:
|
|
451
|
+
outcome = OperationOutcome(operation)
|
|
452
|
+
if not outcome.terminal:
|
|
453
|
+
return 3
|
|
454
|
+
return 0 if outcome.successful else 1
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _doctor(cloud: LeanWarpCloud, root: str, environment: str | None) -> int:
|
|
458
|
+
report = ProjectSession(cloud, root).doctor(environment=environment)
|
|
459
|
+
_emit(report)
|
|
460
|
+
return 0 if report["compatible"] else 1
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _check_before_sign_in(root: str, environment: str | None) -> int:
|
|
464
|
+
"""Check a project against the public catalog, so it is known to run before signing up."""
|
|
465
|
+
with LeanWarpCloud(None, interface="cli") as cloud:
|
|
466
|
+
catalog = cloud.versions()["versions"]
|
|
467
|
+
report = check_project(root, catalog, environment)
|
|
468
|
+
_emit(
|
|
469
|
+
{
|
|
470
|
+
**report,
|
|
471
|
+
"signed_in": False,
|
|
472
|
+
"next": "run `leanwarp auth login` in your terminal (it opens the page that "
|
|
473
|
+
"creates an API key), then `leanwarp connect`",
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
return 0 if report["compatible"] else 1
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _operation(project: ProjectSession, args: argparse.Namespace) -> dict[str, Any]:
|
|
480
|
+
kind = {"declarations": "extract_decls"}.get(args.command, args.command.replace("-", "_"))
|
|
481
|
+
payload: dict[str, Any] = {} if kind == "check_files" else {"file": args.file}
|
|
482
|
+
if kind == "suggest":
|
|
483
|
+
payload.update(line=args.line, column=args.column)
|
|
484
|
+
if args.tactic:
|
|
485
|
+
payload["tactics"] = args.tactic
|
|
486
|
+
if args.tactic_timeout is not None:
|
|
487
|
+
payload["timeout_ms"] = args.tactic_timeout
|
|
488
|
+
elif kind == "holes":
|
|
489
|
+
payload["goals"] = not args.no_goals
|
|
490
|
+
elif kind == "extract_decls":
|
|
491
|
+
pass
|
|
492
|
+
elif kind == "sorry2lemma":
|
|
493
|
+
payload.update(max_extractions=args.max, strict_final_check=args.strict_final_check)
|
|
494
|
+
if args.prefix is not None:
|
|
495
|
+
payload["name_prefix"] = args.prefix
|
|
496
|
+
elif kind == "check_files":
|
|
497
|
+
payload.update(files=args.files, strict=not args.draft)
|
|
498
|
+
if args.file_timeout is not None:
|
|
499
|
+
payload["timeout"] = args.file_timeout
|
|
500
|
+
elif kind == "check":
|
|
501
|
+
payload["strict"] = not args.draft
|
|
502
|
+
elif kind in {"inspect", "try_tactics"}:
|
|
503
|
+
payload.update(line=args.line, column=args.column)
|
|
504
|
+
if kind == "try_tactics":
|
|
505
|
+
payload["tactics"] = args.tactic
|
|
506
|
+
else:
|
|
507
|
+
kind = "verify_target"
|
|
508
|
+
context = ""
|
|
509
|
+
if args.context_file:
|
|
510
|
+
with args.context_file.open("rb") as stream:
|
|
511
|
+
raw = stream.read(128 * 1024 + 1)
|
|
512
|
+
if len(raw) > 128 * 1024:
|
|
513
|
+
raise UsageError(f"{args.context_file} is larger than 128 KiB")
|
|
514
|
+
context = raw.decode("utf-8")
|
|
515
|
+
payload.update(
|
|
516
|
+
candidate_declaration=args.declaration,
|
|
517
|
+
target_statement=args.target,
|
|
518
|
+
target_context=context,
|
|
519
|
+
execution_mode="fresh" if args.fresh else "reusable",
|
|
520
|
+
)
|
|
521
|
+
return project.submit_and_wait(
|
|
522
|
+
kind,
|
|
523
|
+
payload,
|
|
524
|
+
wait_seconds=args.wait,
|
|
525
|
+
resource_profile=args.profile,
|
|
526
|
+
timeout_seconds=args.execution_timeout,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _export(project: ProjectSession, output: Path, workspace_id: str | None) -> dict[str, Any]:
|
|
531
|
+
"""Write one operation per line, newest first; never replaces an existing file."""
|
|
532
|
+
count = 0
|
|
533
|
+
with output.open("x", encoding="utf-8") as stream:
|
|
534
|
+
try:
|
|
535
|
+
for operation in project.history(workspace_id):
|
|
536
|
+
stream.write(json.dumps(operation, ensure_ascii=False, allow_nan=False) + "\n")
|
|
537
|
+
count += 1
|
|
538
|
+
except BaseException:
|
|
539
|
+
# A partial export must not pass for a complete one.
|
|
540
|
+
stream.close()
|
|
541
|
+
output.unlink(missing_ok=True)
|
|
542
|
+
raise
|
|
543
|
+
return {"status": "exported", "operations": count, "output": str(output)}
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def main(argv: list[str] | None = None) -> int:
|
|
547
|
+
args = parser().parse_args(argv)
|
|
548
|
+
try:
|
|
549
|
+
if args.command == "skill":
|
|
550
|
+
document = "references/usage.md" if args.reference else "SKILL.md"
|
|
551
|
+
sys.stdout.write(
|
|
552
|
+
files("leanwarp_cloud")
|
|
553
|
+
.joinpath(f"skills/leanwarp/{document}")
|
|
554
|
+
.read_text(encoding="utf-8")
|
|
555
|
+
)
|
|
556
|
+
return 0
|
|
557
|
+
if args.command == "files":
|
|
558
|
+
plan = plan_upload(args.project)
|
|
559
|
+
_emit(plan.public())
|
|
560
|
+
return 1 if plan.problems else 0
|
|
561
|
+
if args.command == "setup":
|
|
562
|
+
report = setup(agents=args.agent, mcp=not args.no_mcp, refresh=args.refresh)
|
|
563
|
+
if not args.refresh:
|
|
564
|
+
report["next"] = (
|
|
565
|
+
"restart your agent so it loads LeanWarp"
|
|
566
|
+
if signed_in()
|
|
567
|
+
else "run `leanwarp auth login` in your terminal, then restart your agent"
|
|
568
|
+
)
|
|
569
|
+
_emit(report)
|
|
570
|
+
unfinished = {"failed", "unreadable_config"}
|
|
571
|
+
return 2 if any(entry.get("mcp") in unfinished for entry in report["agents"]) else 0
|
|
572
|
+
if args.command == "update":
|
|
573
|
+
report = update()
|
|
574
|
+
_emit(report)
|
|
575
|
+
return 0 if report["status"] in {"updated", "current"} else 2
|
|
576
|
+
if args.command == "mcp":
|
|
577
|
+
from .mcp_server import serve
|
|
578
|
+
|
|
579
|
+
serve(args.project)
|
|
580
|
+
return 0
|
|
581
|
+
if args.command == "auth":
|
|
582
|
+
if args.auth_command == "logout":
|
|
583
|
+
credentials_path().unlink(missing_ok=True)
|
|
584
|
+
_emit({"status": "local_credentials_removed", "key_revoked": False})
|
|
585
|
+
else:
|
|
586
|
+
if not sys.stdin.isatty():
|
|
587
|
+
raise SessionError(
|
|
588
|
+
"auth login needs an interactive terminal; in automation, set "
|
|
589
|
+
"LEANWARP_API_KEY instead"
|
|
590
|
+
)
|
|
591
|
+
_open_keys_page(browser=not args.no_browser)
|
|
592
|
+
save_credentials(getpass.getpass("LeanWarp API key (hidden): "))
|
|
593
|
+
_emit({"status": "authenticated"})
|
|
594
|
+
return 0
|
|
595
|
+
if args.command == "doctor" and not signed_in():
|
|
596
|
+
return _check_before_sign_in(args.project, args.environment)
|
|
597
|
+
with load_client(interface="cli") as cloud:
|
|
598
|
+
if args.command == "doctor":
|
|
599
|
+
return _doctor(cloud, args.project, args.environment)
|
|
600
|
+
if args.command in {"environments", "versions"}:
|
|
601
|
+
catalog = cloud.versions()
|
|
602
|
+
_emit(
|
|
603
|
+
catalog if args.command == "versions" else {"environments": catalog["versions"]}
|
|
604
|
+
)
|
|
605
|
+
return 0
|
|
606
|
+
if args.command in {"account", "resources"}:
|
|
607
|
+
_emit(
|
|
608
|
+
published_worker_resources(cloud.resources())
|
|
609
|
+
if args.command == "resources"
|
|
610
|
+
else cloud.account()
|
|
611
|
+
)
|
|
612
|
+
return 0
|
|
613
|
+
project = ProjectSession(cloud, args.project)
|
|
614
|
+
if args.command == "connect":
|
|
615
|
+
_emit(
|
|
616
|
+
project.connect(
|
|
617
|
+
resource_profile=args.profile,
|
|
618
|
+
max_resource_profile=args.max_profile,
|
|
619
|
+
max_spend_microusd=args.max_spend,
|
|
620
|
+
environment=args.environment,
|
|
621
|
+
bundle_id=args.bundle,
|
|
622
|
+
workspace_id=args.workspace,
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
return 0
|
|
626
|
+
if args.command == "workspaces":
|
|
627
|
+
_emit(project.workspaces())
|
|
628
|
+
return 0
|
|
629
|
+
if args.command == "delete":
|
|
630
|
+
_emit(project.delete(args.confirm))
|
|
631
|
+
return 0
|
|
632
|
+
if args.command == "feedback":
|
|
633
|
+
_emit(
|
|
634
|
+
project.feedback(
|
|
635
|
+
args.kind,
|
|
636
|
+
args.summary,
|
|
637
|
+
details=args.details,
|
|
638
|
+
severity=args.severity,
|
|
639
|
+
operation_id=args.operation,
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
return 0
|
|
643
|
+
if args.command == "export":
|
|
644
|
+
_emit(_export(project, args.output, args.workspace))
|
|
645
|
+
return 0
|
|
646
|
+
if args.command in {
|
|
647
|
+
"status",
|
|
648
|
+
"recover",
|
|
649
|
+
"reconcile",
|
|
650
|
+
"stop",
|
|
651
|
+
"cancel",
|
|
652
|
+
"disconnect",
|
|
653
|
+
"sync",
|
|
654
|
+
"apply",
|
|
655
|
+
}:
|
|
656
|
+
result = getattr(project, args.command)()
|
|
657
|
+
if args.command in {"recover", "cancel"} and "operation_id" in result:
|
|
658
|
+
_emit(reported(result))
|
|
659
|
+
return _exit_code(result)
|
|
660
|
+
_emit(result)
|
|
661
|
+
return 0
|
|
662
|
+
result = (
|
|
663
|
+
project.wait(timeout=args.timeout)
|
|
664
|
+
if args.command == "wait"
|
|
665
|
+
else _operation(project, args)
|
|
666
|
+
)
|
|
667
|
+
_emit(reported(result))
|
|
668
|
+
return _exit_code(result)
|
|
669
|
+
except OperationTimeout as error:
|
|
670
|
+
_emit(
|
|
671
|
+
{
|
|
672
|
+
"error": "poll_timeout",
|
|
673
|
+
"message": "the operation is still running; run `leanwarp wait` again",
|
|
674
|
+
"operation_id": error.operation_id,
|
|
675
|
+
"running": True,
|
|
676
|
+
}
|
|
677
|
+
)
|
|
678
|
+
return 3
|
|
679
|
+
except LeanWarpCloudError as error:
|
|
680
|
+
return _error(error.code, str(error), http_status=error.status_code)
|
|
681
|
+
except httpx.TransportError:
|
|
682
|
+
return _error(
|
|
683
|
+
"transport_error",
|
|
684
|
+
"could not reach LeanWarp; check your connection, then run `leanwarp recover` "
|
|
685
|
+
"before resubmitting anything",
|
|
686
|
+
action="recover any pending request; do not resubmit it",
|
|
687
|
+
)
|
|
688
|
+
except (SessionError, ConfigurationError, ProjectError, UsageError) as error:
|
|
689
|
+
return _error("local_error", str(error))
|
|
690
|
+
except UnicodeDecodeError:
|
|
691
|
+
return _error("local_error", "the file is not UTF-8 text")
|
|
692
|
+
except OSError as error:
|
|
693
|
+
# The path and the system's reason only; never file contents or credentials.
|
|
694
|
+
return _error("local_error", f"{error.strerror or 'cannot read'}: {error.filename}")
|
|
695
|
+
except ValueError:
|
|
696
|
+
return _error("local_error", "the request could not be prepared")
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
if __name__ == "__main__":
|
|
700
|
+
raise SystemExit(main())
|