forgecommandline 0.8.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 (113) hide show
  1. forgecli/__init__.py +2 -0
  2. forgecli/ads.py +322 -0
  3. forgecli/agent.py +1093 -0
  4. forgecli/art.py +574 -0
  5. forgecli/attach.py +246 -0
  6. forgecli/brief.py +215 -0
  7. forgecli/browsercheck.py +99 -0
  8. forgecli/checkpoints.py +81 -0
  9. forgecli/city.py +833 -0
  10. forgecli/cli.py +1668 -0
  11. forgecli/config.py +194 -0
  12. forgecli/courier.py +177 -0
  13. forgecli/courier_data.py +164 -0
  14. forgecli/courier_hd.py +431 -0
  15. forgecli/courier_hd_data.bin +0 -0
  16. forgecli/courier_hd_data.py +19 -0
  17. forgecli/ghlogin.py +76 -0
  18. forgecli/github.py +54 -0
  19. forgecli/guards.py +403 -0
  20. forgecli/hooks.py +50 -0
  21. forgecli/httpapi.py +434 -0
  22. forgecli/images.py +110 -0
  23. forgecli/keys.py +152 -0
  24. forgecli/keyvault.py +320 -0
  25. forgecli/mcp.py +674 -0
  26. forgecli/memory.py +42 -0
  27. forgecli/openapi.py +331 -0
  28. forgecli/parallel.py +308 -0
  29. forgecli/prices.py +72 -0
  30. forgecli/procs.py +144 -0
  31. forgecli/providers.py +758 -0
  32. forgecli/quality.py +192 -0
  33. forgecli/repomap.py +78 -0
  34. forgecli/roster.py +227 -0
  35. forgecli/safety.py +93 -0
  36. forgecli/sandbox.py +83 -0
  37. forgecli/sessions.py +97 -0
  38. forgecli/skills.py +446 -0
  39. forgecli/tools.py +1184 -0
  40. forgecli/ui.py +860 -0
  41. forgecli/worktrees.py +79 -0
  42. forgecommandline-0.8.0.data/data/skills/commit/SKILL.md +14 -0
  43. forgecommandline-0.8.0.data/data/skills/debug/SKILL.md +17 -0
  44. forgecommandline-0.8.0.data/data/skills/explain/SKILL.md +14 -0
  45. forgecommandline-0.8.0.data/data/skills/init/SKILL.md +19 -0
  46. forgecommandline-0.8.0.data/data/skills/library/LICENSES/agent-skills-MIT.txt +21 -0
  47. forgecommandline-0.8.0.data/data/skills/library/LICENSES/wondelai-MIT.txt +23 -0
  48. forgecommandline-0.8.0.data/data/skills/library/README.md +9 -0
  49. forgecommandline-0.8.0.data/data/skills/library/_references/accessibility-checklist.md +160 -0
  50. forgecommandline-0.8.0.data/data/skills/library/_references/definition-of-done.md +67 -0
  51. forgecommandline-0.8.0.data/data/skills/library/_references/observability-checklist.md +92 -0
  52. forgecommandline-0.8.0.data/data/skills/library/_references/orchestration-patterns.md +370 -0
  53. forgecommandline-0.8.0.data/data/skills/library/_references/performance-checklist.md +238 -0
  54. forgecommandline-0.8.0.data/data/skills/library/_references/security-checklist.md +248 -0
  55. forgecommandline-0.8.0.data/data/skills/library/_references/testing-patterns.md +235 -0
  56. forgecommandline-0.8.0.data/data/skills/library/api-and-interface-design/SKILL.md +367 -0
  57. forgecommandline-0.8.0.data/data/skills/library/architecture-optimization/SKILL.md +279 -0
  58. forgecommandline-0.8.0.data/data/skills/library/architecture-optimization/references/artifact-templates.md +157 -0
  59. forgecommandline-0.8.0.data/data/skills/library/architecture-optimization/references/methods.md +87 -0
  60. forgecommandline-0.8.0.data/data/skills/library/awwwards-tier-web-application/SKILL.md +877 -0
  61. forgecommandline-0.8.0.data/data/skills/library/browser-testing-with-devtools/SKILL.md +317 -0
  62. forgecommandline-0.8.0.data/data/skills/library/ci-cd-and-automation/SKILL.md +390 -0
  63. forgecommandline-0.8.0.data/data/skills/library/code-review-and-quality/SKILL.md +398 -0
  64. forgecommandline-0.8.0.data/data/skills/library/code-simplification/SKILL.md +331 -0
  65. forgecommandline-0.8.0.data/data/skills/library/constraint-driven-development/SKILL.md +311 -0
  66. forgecommandline-0.8.0.data/data/skills/library/constraint-driven-development/references/floor-guard.md +182 -0
  67. forgecommandline-0.8.0.data/data/skills/library/context-engineering/SKILL.md +353 -0
  68. forgecommandline-0.8.0.data/data/skills/library/debugging-and-error-recovery/SKILL.md +300 -0
  69. forgecommandline-0.8.0.data/data/skills/library/deprecation-and-migration/SKILL.md +247 -0
  70. forgecommandline-0.8.0.data/data/skills/library/documentation-and-adrs/SKILL.md +297 -0
  71. forgecommandline-0.8.0.data/data/skills/library/doubt-driven-development/SKILL.md +243 -0
  72. forgecommandline-0.8.0.data/data/skills/library/frontend-ui-engineering/SKILL.md +340 -0
  73. forgecommandline-0.8.0.data/data/skills/library/git-workflow-and-versioning/SKILL.md +355 -0
  74. forgecommandline-0.8.0.data/data/skills/library/idea-refine/SKILL.md +193 -0
  75. forgecommandline-0.8.0.data/data/skills/library/idea-refine/examples.md +238 -0
  76. forgecommandline-0.8.0.data/data/skills/library/idea-refine/frameworks.md +99 -0
  77. forgecommandline-0.8.0.data/data/skills/library/idea-refine/refinement-criteria.md +113 -0
  78. forgecommandline-0.8.0.data/data/skills/library/idea-refine/scripts/idea-refine.sh +15 -0
  79. forgecommandline-0.8.0.data/data/skills/library/improve-code-quality/SKILL.md +275 -0
  80. forgecommandline-0.8.0.data/data/skills/library/improve-code-quality/references/artifact-templates.md +101 -0
  81. forgecommandline-0.8.0.data/data/skills/library/incremental-implementation/SKILL.md +249 -0
  82. forgecommandline-0.8.0.data/data/skills/library/interview-me/SKILL.md +235 -0
  83. forgecommandline-0.8.0.data/data/skills/library/observability-and-instrumentation/SKILL.md +238 -0
  84. forgecommandline-0.8.0.data/data/skills/library/performance-optimization/SKILL.md +267 -0
  85. forgecommandline-0.8.0.data/data/skills/library/performance-optimization/references/optimization-patterns.md +250 -0
  86. forgecommandline-0.8.0.data/data/skills/library/planning-and-task-breakdown/SKILL.md +257 -0
  87. forgecommandline-0.8.0.data/data/skills/library/remove-technical-debt/SKILL.md +203 -0
  88. forgecommandline-0.8.0.data/data/skills/library/remove-technical-debt/references/artifact-templates.md +79 -0
  89. forgecommandline-0.8.0.data/data/skills/library/security-and-hardening/SKILL.md +216 -0
  90. forgecommandline-0.8.0.data/data/skills/library/security-and-hardening/references/hardening-patterns.md +324 -0
  91. forgecommandline-0.8.0.data/data/skills/library/shipping-and-launch/SKILL.md +336 -0
  92. forgecommandline-0.8.0.data/data/skills/library/source-driven-development/SKILL.md +216 -0
  93. forgecommandline-0.8.0.data/data/skills/library/spec-driven-development/SKILL.md +259 -0
  94. forgecommandline-0.8.0.data/data/skills/library/system-design/SKILL.md +207 -0
  95. forgecommandline-0.8.0.data/data/skills/library/system-design/references/building-blocks.md +272 -0
  96. forgecommandline-0.8.0.data/data/skills/library/system-design/references/common-designs.md +257 -0
  97. forgecommandline-0.8.0.data/data/skills/library/system-design/references/database-scaling.md +295 -0
  98. forgecommandline-0.8.0.data/data/skills/library/system-design/references/estimation-numbers.md +250 -0
  99. forgecommandline-0.8.0.data/data/skills/library/system-design/references/four-step-process.md +286 -0
  100. forgecommandline-0.8.0.data/data/skills/library/system-design/references/reliability-operations.md +373 -0
  101. forgecommandline-0.8.0.data/data/skills/library/test-driven-development/SKILL.md +398 -0
  102. forgecommandline-0.8.0.data/data/skills/library/using-agent-skills/SKILL.md +192 -0
  103. forgecommandline-0.8.0.data/data/skills/review/SKILL.md +16 -0
  104. forgecommandline-0.8.0.data/data/skills/run/SKILL.md +17 -0
  105. forgecommandline-0.8.0.data/data/skills/security-review/SKILL.md +20 -0
  106. forgecommandline-0.8.0.data/data/skills/simplify/SKILL.md +18 -0
  107. forgecommandline-0.8.0.data/data/skills/test/SKILL.md +16 -0
  108. forgecommandline-0.8.0.data/data/skills/web-build/SKILL.md +25 -0
  109. forgecommandline-0.8.0.dist-info/METADATA +357 -0
  110. forgecommandline-0.8.0.dist-info/RECORD +113 -0
  111. forgecommandline-0.8.0.dist-info/WHEEL +5 -0
  112. forgecommandline-0.8.0.dist-info/entry_points.txt +2 -0
  113. forgecommandline-0.8.0.dist-info/top_level.txt +1 -0
forgecli/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """Forge: a terminal coding agent with API key rotation and provider failover."""
2
+ __version__ = "0.8.0"
forgecli/ads.py ADDED
@@ -0,0 +1,322 @@
1
+ """One sponsor line, shown on the goodbye screen, under the rules in HANDOFF.md (NEXT 8).
2
+
3
+ - Off unless an ad server is configured (FORGE_ADS_URL, or AD_SERVER below once Harry runs one) and
4
+ its public key is known (FORGE_ADS_KEY / AD_PUBLIC_KEY). Users turn it off with FORGE_ADS=0 or
5
+ `/ads off`. Never in pipes, scripts, CI or `forge -p` runs, and not in the first week after install.
6
+ - The request carries only Forge's version and a random install id (`/ads reset` makes a new one):
7
+ no prompts, code, paths, keys or project details.
8
+ - Fetched in the background with a 0.5 s timeout; slow or down server = no ad, never a slower reply.
9
+ - Every ad is signed with Harry's Ed25519 key and rejected otherwise, so whoever takes over the
10
+ server's address can't show their own lines. The text is reduced to plain printable words and one
11
+ https link, so an ad can't clear the screen, move the cursor, fake a Forge message or hide its link.
12
+ - An ad is display text only: it never reaches the model, a tool or the shell.
13
+
14
+ `python -m forgecli.ads keygen` makes the key pair for the server (see server/ads-worker/README.md).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ import json
20
+ import os
21
+ import pathlib
22
+ import re
23
+ import secrets
24
+ import sys
25
+ import threading
26
+ import time
27
+ import unicodedata
28
+ import urllib.parse
29
+ import urllib.request
30
+
31
+ AD_SERVER = "" # e.g. "https://ads.forge-cli.dev" once deployed
32
+ AD_PUBLIC_KEY = "" # hex Ed25519 public key printed by `python -m forgecli.ads keygen`
33
+ TIMEOUT = 0.5
34
+ QUIET_DAYS = 7
35
+ MAX_TEXT, MAX_URL = 90, 100
36
+
37
+
38
+ def _home() -> pathlib.Path:
39
+ return pathlib.Path(os.environ.get("FORGE_HOME", pathlib.Path.home() / ".forge"))
40
+
41
+
42
+ def _state_path() -> pathlib.Path:
43
+ return _home() / "ads.json"
44
+
45
+
46
+ def load_state() -> dict:
47
+ try:
48
+ st = json.loads(_state_path().read_text(encoding="utf-8"))
49
+ if isinstance(st, dict):
50
+ return st
51
+ except (OSError, ValueError):
52
+ pass
53
+ return {}
54
+
55
+
56
+ def save_state(st: dict) -> None:
57
+ try:
58
+ _state_path().parent.mkdir(parents=True, exist_ok=True)
59
+ _state_path().write_text(json.dumps(st, indent=1), encoding="utf-8")
60
+ except OSError:
61
+ pass
62
+
63
+
64
+ def install_id(st: dict) -> str:
65
+ if not st.get("id"):
66
+ st["id"] = secrets.token_hex(12)
67
+ st.setdefault("since", time.time())
68
+ save_state(st)
69
+ return st["id"]
70
+
71
+
72
+ def server() -> str:
73
+ return (os.environ.get("FORGE_ADS_URL") or AD_SERVER).strip().rstrip("/")
74
+
75
+
76
+ def public_key() -> str:
77
+ return (os.environ.get("FORGE_ADS_KEY") or AD_PUBLIC_KEY).strip().lower()
78
+
79
+
80
+ def user_off(st: dict | None = None) -> bool:
81
+ env = os.environ.get("FORGE_ADS", "").strip().lower()
82
+ if env in ("0", "off", "false", "no"):
83
+ return True
84
+ return (st if st is not None else load_state()).get("off") is True
85
+
86
+
87
+ def allowed(st: dict | None = None, interactive: bool | None = None) -> tuple[bool, str]:
88
+ """(may we show an ad, why not)."""
89
+ st = st if st is not None else load_state()
90
+ if user_off(st):
91
+ return False, "turned off"
92
+ if not server() or not public_key():
93
+ return False, "no ad server configured"
94
+ if interactive is None:
95
+ interactive = sys.stdin.isatty() and sys.stdout.isatty()
96
+ if not interactive or os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS"):
97
+ return False, "not an interactive terminal"
98
+ since = st.get("since") or time.time()
99
+ if time.time() - since < QUIET_DAYS * 86400:
100
+ return False, f"first {QUIET_DAYS} days after install"
101
+ return True, ""
102
+
103
+
104
+ # --- cleaning --------------------------------------------------------------------------------------
105
+
106
+ _BIDI = set(range(0x202A, 0x202F)) | set(range(0x2066, 0x206A)) | {0x200E, 0x200F, 0x061C}
107
+
108
+
109
+ def clean_text(text: str) -> str:
110
+ """Printable words only: no escape codes, control or format characters, bidi tricks or newlines."""
111
+ out = []
112
+ raw = str(text or "")
113
+ raw = re.sub(r"\x1b\][^\x07\x1b]*(\x07|\x1b\\)?", " ", raw) # OSC (titles, hyperlinks)
114
+ raw = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", " ", raw) # CSI (cursor, colour, clear)
115
+ raw = re.sub(r"\x1b[PX^_][^\x1b]*(\x1b\\)?", " ", raw) # DCS/SOS/PM/APC (sixel etc.)
116
+ raw = re.sub(r"\x1b.?", " ", raw)
117
+ raw = re.sub(r"\x9b[0-?]*[ -/]*[@-~]", " ", raw) # 8-bit CSI
118
+ for ch in unicodedata.normalize("NFKC", raw):
119
+ cat = unicodedata.category(ch)
120
+ if ord(ch) in _BIDI or cat in ("Cc", "Cf", "Cs", "Co", "Cn", "Zl", "Zp"):
121
+ out.append(" ")
122
+ else:
123
+ out.append(ch)
124
+ s = re.sub(r"\s+", " ", "".join(out)).strip()
125
+ return s if len(s) <= MAX_TEXT else s[:MAX_TEXT - 1].rstrip() + "…"
126
+
127
+
128
+ def clean_url(url: str) -> str:
129
+ """One plain https link a person can read; anything else is dropped."""
130
+ u = str(url or "").strip()
131
+ if len(u) > MAX_URL or not re.fullmatch(r"https://[A-Za-z0-9.\-]+(:\d+)?(/[A-Za-z0-9._~/%?=&+\-#]*)?", u):
132
+ return ""
133
+ host = urllib.parse.urlparse(u).hostname or ""
134
+ if "xn--" in host or "." not in host:
135
+ return ""
136
+ return u
137
+
138
+
139
+ # --- signatures (pure-Python Ed25519, RFC 8032) -----------------------------------------------------
140
+
141
+ _P = 2 ** 255 - 19
142
+ _L = 2 ** 252 + 27742317777372353535851937790883648493
143
+ _D = -121665 * pow(121666, _P - 2, _P) % _P
144
+ _I = pow(2, (_P - 1) // 4, _P)
145
+
146
+
147
+ def _inv(x):
148
+ return pow(x, _P - 2, _P)
149
+
150
+
151
+ def _xrecover(y):
152
+ xx = (y * y - 1) * _inv(_D * y * y + 1)
153
+ x = pow(xx, (_P + 3) // 8, _P)
154
+ if (x * x - xx) % _P:
155
+ x = x * _I % _P
156
+ if (x * x - xx) % _P:
157
+ raise ValueError("not on curve")
158
+ return _P - x if x & 1 else x
159
+
160
+
161
+ _BY = 4 * _inv(5) % _P
162
+ _BX = _xrecover(_BY)
163
+ _B = (_BX, _BY, 1, _BX * _BY % _P)
164
+
165
+
166
+ def _add(p, q):
167
+ a = (p[1] - p[0]) * (q[1] - q[0]) % _P
168
+ b = (p[1] + p[0]) * (q[1] + q[0]) % _P
169
+ c = 2 * p[3] * q[3] * _D % _P
170
+ d = 2 * p[2] * q[2] % _P
171
+ e, f, g, h = b - a, d - c, d + c, b + a
172
+ return (e * f % _P, g * h % _P, f * g % _P, e * h % _P)
173
+
174
+
175
+ def _mul(s, p):
176
+ q = (0, 1, 1, 0)
177
+ while s:
178
+ if s & 1:
179
+ q = _add(q, p)
180
+ p = _add(p, p)
181
+ s >>= 1
182
+ return q
183
+
184
+
185
+ def _enc(p):
186
+ zi = _inv(p[2])
187
+ x, y = p[0] * zi % _P, p[1] * zi % _P
188
+ return (y | ((x & 1) << 255)).to_bytes(32, "little")
189
+
190
+
191
+ def _dec(b):
192
+ y = int.from_bytes(b, "little")
193
+ sign, y = y >> 255, y & ((1 << 255) - 1)
194
+ if y >= _P:
195
+ raise ValueError("bad point")
196
+ x = _xrecover(y)
197
+ if (x & 1) != sign:
198
+ if x == 0:
199
+ raise ValueError("bad point")
200
+ x = _P - x
201
+ return (x, y, 1, x * y % _P)
202
+
203
+
204
+ def _h(m):
205
+ return hashlib.sha512(m).digest()
206
+
207
+
208
+ def _secret(seed: bytes):
209
+ h = _h(seed)
210
+ a = int.from_bytes(h[:32], "little")
211
+ a = (a & ((1 << 254) - 8)) | (1 << 254)
212
+ return a, h[32:]
213
+
214
+
215
+ def public_from_seed(seed: bytes) -> bytes:
216
+ return _enc(_mul(_secret(seed)[0], _B))
217
+
218
+
219
+ def sign(seed: bytes, msg: bytes) -> bytes:
220
+ a, prefix = _secret(seed)
221
+ pub = _enc(_mul(a, _B))
222
+ r = int.from_bytes(_h(prefix + msg), "little") % _L
223
+ rb = _enc(_mul(r, _B))
224
+ k = int.from_bytes(_h(rb + pub + msg), "little") % _L
225
+ return rb + ((r + k * a) % _L).to_bytes(32, "little")
226
+
227
+
228
+ def verify(pub: bytes, msg: bytes, sig: bytes) -> bool:
229
+ if len(pub) != 32 or len(sig) != 64:
230
+ return False
231
+ try:
232
+ a, r = _dec(pub), _dec(sig[:32])
233
+ except ValueError:
234
+ return False
235
+ s = int.from_bytes(sig[32:], "little")
236
+ if s >= _L:
237
+ return False
238
+ k = int.from_bytes(_h(sig[:32] + pub + msg), "little") % _L
239
+ return _enc(_mul(s, _B)) == _enc(_add(r, _mul(k, a)))
240
+
241
+
242
+ def signed_message(ad: dict) -> bytes:
243
+ """Exactly what the server signs (server/ads-worker/worker.js builds the same string)."""
244
+ return "\n".join(str(ad.get(k, "")) for k in ("id", "text", "url", "exp")).encode("utf-8")
245
+
246
+
247
+ def check(ad: dict, key_hex: str | None = None, now: float | None = None) -> dict | None:
248
+ """A signed, unexpired, cleaned ad, or None."""
249
+ try:
250
+ key = bytes.fromhex(key_hex if key_hex is not None else public_key())
251
+ sig = bytes.fromhex(str(ad.get("sig", "")))
252
+ exp = int(ad.get("exp", 0))
253
+ except (ValueError, TypeError, AttributeError):
254
+ return None
255
+ if exp < (now or time.time()) or not verify(key, signed_message(ad), sig):
256
+ return None
257
+ text, url = clean_text(ad.get("text", "")), clean_url(ad.get("url", ""))
258
+ if not text:
259
+ return None
260
+ return {"id": str(ad.get("id", ""))[:40], "text": text, "url": url, "exp": exp}
261
+
262
+
263
+ # --- fetching and showing ---------------------------------------------------------------------------
264
+
265
+ class Ads:
266
+ """Starts one background fetch per session; `line()` gives the goodbye line, if any."""
267
+
268
+ def __init__(self, version: str, interactive: bool | None = None):
269
+ self.version = version
270
+ self.st = load_state()
271
+ install_id(self.st)
272
+ self.ok, self.why = allowed(self.st, interactive)
273
+ self.ad: dict | None = None
274
+ self._thread: threading.Thread | None = None
275
+ if self.ok:
276
+ self._thread = threading.Thread(target=self._fetch, daemon=True)
277
+ self._thread.start()
278
+
279
+ def _fetch(self) -> None:
280
+ q = urllib.parse.urlencode({"v": self.version, "id": self.st["id"]})
281
+ try:
282
+ req = urllib.request.Request(f"{server()}/v1/ad?{q}", headers={"User-Agent": "forge-cli"})
283
+ with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
284
+ data = json.loads(r.read(4096).decode("utf-8", "replace"))
285
+ ad = check(data) if isinstance(data, dict) and data.get("id") else None
286
+ if ad:
287
+ self.st["cached"] = ad
288
+ save_state(self.st)
289
+ self.ad = ad
290
+ except Exception:
291
+ self.ad = None # offline or slow: fall back to the cached ad below
292
+
293
+ def line(self) -> str | None:
294
+ if not self.ok:
295
+ return None
296
+ if self._thread is not None:
297
+ self._thread.join(timeout=0.05) # never hold up quitting
298
+ ad = self.ad
299
+ if ad is None:
300
+ cached = self.st.get("cached")
301
+ ad = check(cached) if isinstance(cached, dict) and cached.get("sig") else None
302
+ if ad is None:
303
+ return None
304
+ return f"sponsor · {ad['text']}" + (f" {ad['url']}" if ad["url"] else "")
305
+
306
+
307
+ def keygen() -> None:
308
+ import base64
309
+ seed = secrets.token_bytes(32)
310
+ pub = public_from_seed(seed)
311
+ pkcs8 = bytes.fromhex("302e020100300506032b657004220420") + seed
312
+ print("PUBLIC key (put in forgecli/ads.py AD_PUBLIC_KEY, or FORGE_ADS_KEY):")
313
+ print(" " + pub.hex())
314
+ print("PRIVATE key for the Worker (wrangler secret put AD_SIGNING_KEY) - keep it secret, never commit:")
315
+ print(" " + base64.b64encode(pkcs8).decode())
316
+
317
+
318
+ if __name__ == "__main__":
319
+ if sys.argv[1:2] == ["keygen"]:
320
+ keygen()
321
+ else:
322
+ print("usage: python -m forgecli.ads keygen")