offset-terminal 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 (102) hide show
  1. offset/__init__.py +8 -0
  2. offset/__main__.py +133 -0
  3. offset/auth.py +291 -0
  4. offset/core/__init__.py +1 -0
  5. offset/core/agent.py +431 -0
  6. offset/core/audit.py +880 -0
  7. offset/core/branches.py +198 -0
  8. offset/core/bridge.py +1336 -0
  9. offset/core/collab.py +1766 -0
  10. offset/core/compaction.py +403 -0
  11. offset/core/context.py +214 -0
  12. offset/core/daemon.py +209 -0
  13. offset/core/decompose.py +915 -0
  14. offset/core/entries.py +144 -0
  15. offset/core/forge.py +886 -0
  16. offset/core/fsnapshot.py +682 -0
  17. offset/core/index.py +544 -0
  18. offset/core/jobs.py +851 -0
  19. offset/core/loopback.py +1067 -0
  20. offset/core/multimodel.py +402 -0
  21. offset/core/permissions.py +159 -0
  22. offset/core/scoring.py +656 -0
  23. offset/core/session.py +417 -0
  24. offset/core/settings.py +552 -0
  25. offset/core/snapshots.py +481 -0
  26. offset/core/speculate.py +591 -0
  27. offset/core/symbols.py +717 -0
  28. offset/core/tasks.py +579 -0
  29. offset/core/update.py +1164 -0
  30. offset/core/vcs.py +580 -0
  31. offset/core/workflow.py +489 -0
  32. offset/eggs/__init__.py +14 -0
  33. offset/eggs/catalogue.py +285 -0
  34. offset/eggs/engine.py +269 -0
  35. offset/providers/__init__.py +43 -0
  36. offset/providers/anthropic.py +163 -0
  37. offset/providers/auth.py +437 -0
  38. offset/providers/base.py +266 -0
  39. offset/providers/catalogue.py +525 -0
  40. offset/providers/google.py +334 -0
  41. offset/providers/mock.py +84 -0
  42. offset/providers/oauth.py +793 -0
  43. offset/providers/ollama.py +108 -0
  44. offset/providers/openai.py +244 -0
  45. offset/providers/opencode.py +228 -0
  46. offset/providers/registry.py +400 -0
  47. offset/providers/schema.py +305 -0
  48. offset/providers/sse.py +62 -0
  49. offset/providers/transport.py +120 -0
  50. offset/shell/__init__.py +5 -0
  51. offset/shell/app.py +979 -0
  52. offset/shell/commands.py +1256 -0
  53. offset/shell/consent.py +262 -0
  54. offset/shell/render.py +387 -0
  55. offset/tools/__init__.py +23 -0
  56. offset/tools/agents.py +516 -0
  57. offset/tools/ask.py +232 -0
  58. offset/tools/base.py +239 -0
  59. offset/tools/builtin.py +421 -0
  60. offset/tools/custom.py +278 -0
  61. offset/tools/debug/__init__.py +121 -0
  62. offset/tools/debug/adapters.py +376 -0
  63. offset/tools/debug/client.py +1188 -0
  64. offset/tools/debug/protocol.py +876 -0
  65. offset/tools/debug/tool.py +581 -0
  66. offset/tools/documents.py +780 -0
  67. offset/tools/github.py +438 -0
  68. offset/tools/lsp/__init__.py +83 -0
  69. offset/tools/lsp/client.py +1051 -0
  70. offset/tools/lsp/protocol.py +594 -0
  71. offset/tools/lsp/servers.py +601 -0
  72. offset/tools/lsp/tool.py +440 -0
  73. offset/tools/mcp/__init__.py +76 -0
  74. offset/tools/mcp/client.py +514 -0
  75. offset/tools/mcp/manager.py +903 -0
  76. offset/tools/mcp/marketplace.py +1046 -0
  77. offset/tools/mcp/transport.py +478 -0
  78. offset/tools/patch.py +861 -0
  79. offset/tools/plugins.py +905 -0
  80. offset/tools/retrieve.py +444 -0
  81. offset/tools/runtime.py +204 -0
  82. offset/tools/system.py +571 -0
  83. offset/tools/todo.py +406 -0
  84. offset/tools/walk.py +242 -0
  85. offset/tools/web/__init__.py +67 -0
  86. offset/tools/web/browser.py +373 -0
  87. offset/tools/web/cdp.py +1277 -0
  88. offset/tools/web/wsclient.py +618 -0
  89. offset/tools/websearch.py +270 -0
  90. offset/ui/__init__.py +11 -0
  91. offset/ui/anim.py +198 -0
  92. offset/ui/brutal.py +419 -0
  93. offset/ui/canvas.py +186 -0
  94. offset/ui/demo.py +251 -0
  95. offset/ui/ghost.py +601 -0
  96. offset/ui/theme.py +330 -0
  97. offset/ui/tokens.py +367 -0
  98. offset_terminal-0.8.0.dist-info/METADATA +497 -0
  99. offset_terminal-0.8.0.dist-info/RECORD +102 -0
  100. offset_terminal-0.8.0.dist-info/WHEEL +4 -0
  101. offset_terminal-0.8.0.dist-info/entry_points.txt +2 -0
  102. offset_terminal-0.8.0.dist-info/licenses/LICENSE +647 -0
offset/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """offset — a terminal coding agent that builds several answers at once.
2
+
3
+ Design rule for the whole package: the aesthetic is not a theme, it is the
4
+ construction method. Hard edges, flat fills, zero-blur shadows, no rounded
5
+ corners anywhere. See `offset.ui.tokens` for the single source of truth.
6
+ """
7
+
8
+ __version__ = "0.8.0"
offset/__main__.py ADDED
@@ -0,0 +1,133 @@
1
+ """Command line entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+
9
+ def _autoupdate() -> None:
10
+ """Install a waiting update, then become the new version.
11
+
12
+ Deliberately quiet: it prints only when it actually does something, so the
13
+ overwhelmingly common case of "already current" costs the user no output
14
+ and no delay. It reads the cache the previous run's background check left
15
+ behind rather than the network, so an offline start is not paid for here.
16
+
17
+ Every failure is swallowed. A program that refuses to start because it
18
+ could not upgrade itself is worse than one running last week's build.
19
+ """
20
+ try:
21
+ from offset.core.update import autoupdate, reexec
22
+ except Exception:
23
+ return
24
+ try:
25
+ outcome = autoupdate(echo=lambda line: print(line, flush=True))
26
+ except Exception:
27
+ return
28
+ for line in outcome.report():
29
+ print(line, flush=True)
30
+ if outcome.acted:
31
+ reexec() # returns only if exec failed, in which case carry on
32
+
33
+
34
+ def main(argv: list[str] | None = None) -> int:
35
+ parser = argparse.ArgumentParser(prog="offset", description="terminal coding agent")
36
+ sub = parser.add_subparsers(dest="cmd")
37
+
38
+ chat = sub.add_parser("chat", help="start an interactive session")
39
+ chat.add_argument("--model", default=None,
40
+ help="model id; defaults to model.default from config, else the scripted mock")
41
+ chat.add_argument("--workspace", default=".", help="directory the tools may touch")
42
+ chat.add_argument("--approve", default=None,
43
+ choices=("safe", "auto-edit", "yolo", "full"),
44
+ help="approval mode; defaults to the stored permission grant")
45
+ chat.add_argument("--resume", nargs="?", const="", default=None, metavar="ID",
46
+ help="carry on an earlier session; bare --resume takes the most recent")
47
+ chat.add_argument("--continue", dest="continue_", action="store_true",
48
+ help="carry on the most recent session")
49
+
50
+ sub.add_parser("login", help="sign in with your Google or GitHub account")
51
+ sub.add_parser("sync", help="sync Offset Plus subscription status from your account")
52
+
53
+ upgrade = sub.add_parser("upgrade", help="redeem a Gumroad licence key to unlock Offset Plus")
54
+ upgrade.add_argument("key", help="the licence key from your Gumroad receipt")
55
+
56
+ upd = sub.add_parser("update", help="check for a newer offset and install it")
57
+ upd.add_argument("--check", action="store_true",
58
+ help="only report whether an update exists")
59
+
60
+ dmn = sub.add_parser("daemon", help="run headless, for an editor or a remote client")
61
+ dmn.add_argument("--workspace", default=".", help="directory the tools may touch")
62
+ dmn.add_argument("--model", default=None, help="model id; defaults to the configured one")
63
+ dmn.add_argument("--listen", default="", metavar="ADDR",
64
+ help="bind TCP instead of a unix socket: 'tcp', 'host:port' or ':port'. "
65
+ "Anything other than loopback exposes the agent to that network")
66
+ dmn.add_argument("--idle", type=float, default=0.0, metavar="SECONDS",
67
+ help="exit after this long with no client connected; 0 never exits")
68
+ dmn.add_argument("--quiet", action="store_true", help="do not print the descriptor")
69
+
70
+ demo = sub.add_parser("demo", help="render the design system")
71
+ demo.add_argument("--once", action="store_true", help="print one frame and exit")
72
+ demo.add_argument("--time", type=float, default=2.4, help="timestamp of the frame to print")
73
+ demo.add_argument("--fps", type=float, default=24.0)
74
+ demo.add_argument("--width", type=int, default=None)
75
+ demo.add_argument("--height", type=int, default=None)
76
+
77
+ args = parser.parse_args(argv)
78
+
79
+ if args.cmd == "login":
80
+ from offset.auth import prompt_account_login
81
+ prompt_account_login()
82
+ return 0
83
+
84
+ if args.cmd == "sync":
85
+ from offset.auth import sync_command
86
+ return sync_command()
87
+
88
+ if args.cmd == "upgrade":
89
+ from offset.auth import verify_direct_license_key
90
+ return verify_direct_license_key(args.key)
91
+
92
+ if args.cmd == "update":
93
+ from offset.core.update import update_command
94
+ return update_command(check_only=args.check)
95
+
96
+ if args.cmd == "daemon":
97
+ from offset.core.daemon import command as daemon_command
98
+ return daemon_command(args)
99
+
100
+ if args.cmd == "demo":
101
+ from offset.ui import demo as demo_mod
102
+
103
+ if args.once:
104
+ print(demo_mod.frame(args.time, w=args.width, h=args.height))
105
+ return 0
106
+ return demo_mod.run(fps=args.fps)
107
+
108
+ if args.cmd in (None, "chat"):
109
+ # Before the shell, never during it: the modules a running session has
110
+ # already imported cannot be swapped underneath it, so an update
111
+ # applied mid-turn would leave half the program on the old version.
112
+ # A successful update re-executes, so the user gets the new build in
113
+ # the same invocation rather than being told to start again.
114
+ _autoupdate()
115
+
116
+ from offset.shell.app import main as chat_main
117
+
118
+ resume = getattr(args, "resume", None)
119
+ if getattr(args, "continue_", False) and resume is None:
120
+ resume = "" # bare --continue means "the most recent"
121
+ return chat_main(
122
+ workspace=getattr(args, "workspace", "."),
123
+ model=getattr(args, "model", None),
124
+ approval=getattr(args, "approve", None),
125
+ resume=resume,
126
+ )
127
+
128
+ parser.error(f"unknown command {args.cmd!r}")
129
+ return 2
130
+
131
+
132
+ if __name__ == "__main__":
133
+ sys.exit(main())
offset/auth.py ADDED
@@ -0,0 +1,291 @@
1
+ """Account linking and Offset Plus entitlement.
2
+
3
+ The entitlement itself lives on the server; this module only caches the answer.
4
+ `auth.json` holds a bearer token, so it is written the same way credentials are:
5
+ parent directory created first, atomically replaced, owner-only permissions.
6
+
7
+ The cache is deliberately allowed to outlive a network outage. Downgrading a
8
+ paying subscriber to Lite because their wifi dropped is worse than trusting a
9
+ token we already verified, so `sync_account_tier` keeps the last known tier when
10
+ it cannot reach the server and says so instead of silently re-tiering.
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import sys
16
+ import time
17
+ import urllib.error
18
+ import urllib.request
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from offset.core import settings
23
+
24
+ # Production auth server URL on Render
25
+ AUTH_SERVER_URL = os.environ.get("OFFSET_AUTH_SERVER", "https://offset-backend.onrender.com").rstrip("/")
26
+
27
+ #: Test-only shortcuts (`test-plus-key`, emails containing "plus") are a
28
+ #: developer convenience and a paywall bypass in production - `me+plus@gmail.com`
29
+ #: is a perfectly ordinary address. They stay off unless explicitly enabled.
30
+ def _dev_mode() -> bool:
31
+ return bool(os.environ.get("OFFSET_DEV_MODE")) or "PYTEST_CURRENT_TEST" in os.environ
32
+
33
+
34
+ def _auth_file() -> Path:
35
+ return settings.home() / "auth.json"
36
+
37
+
38
+ def _read_auth() -> dict[str, Any]:
39
+ """Cached entitlement, or an empty dict if absent/unreadable."""
40
+ path = _auth_file()
41
+ if not path.exists():
42
+ return {}
43
+ try:
44
+ data = json.loads(path.read_text(encoding="utf-8"))
45
+ except (OSError, ValueError):
46
+ return {}
47
+ return data if isinstance(data, dict) else {}
48
+
49
+
50
+ def _write_auth(data: dict[str, Any]) -> Path:
51
+ """Persist the entitlement cache.
52
+
53
+ Creating the parent first is the whole point: on a fresh install `~/.offset`
54
+ does not exist yet, and `write_text` reported that as `FileNotFoundError`
55
+ from inside the login prompt (issue #1). Written via a temp file so an
56
+ interrupted write cannot leave a half-parsed token behind, and chmod 0600
57
+ because `token` is a bearer credential.
58
+ """
59
+ path = _auth_file()
60
+ path.parent.mkdir(parents=True, exist_ok=True)
61
+ tmp = path.with_suffix(".tmp")
62
+ tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
63
+ os.chmod(tmp, 0o600)
64
+ os.replace(tmp, path)
65
+ os.chmod(path, 0o600)
66
+ return path
67
+
68
+
69
+ def check_login():
70
+ """Verify login status on CLI startup."""
71
+ if not sys.stdin.isatty() or "PYTEST_CURRENT_TEST" in os.environ:
72
+ return
73
+
74
+ data = _read_auth()
75
+ if data.get("logged_in") and data.get("account"):
76
+ return
77
+
78
+ prompt_account_login()
79
+
80
+
81
+ def prompt_account_login():
82
+ """Prompt user to sign in with their Google or GitHub account."""
83
+ print("\033[1;36mWelcome to Offset!\033[0m")
84
+ print("Sign in with your Google or GitHub account to activate your workspace:")
85
+ print(" \033[1m1.\033[0m GitHub Account")
86
+ print(" \033[1m2.\033[0m Google Account")
87
+ print(" \033[1m3.\033[0m Enter Gumroad License Key Directly")
88
+
89
+ try:
90
+ choice = input("Select option [1/2/3]: ").strip()
91
+ except (EOFError, KeyboardInterrupt):
92
+ print()
93
+ return
94
+
95
+ if choice == "3":
96
+ try:
97
+ key = input("Enter your Gumroad license key: ").strip()
98
+ except (EOFError, KeyboardInterrupt):
99
+ print()
100
+ return
101
+ verify_direct_license_key(key)
102
+ return
103
+
104
+ provider = "github" if choice == "1" else "google"
105
+ try:
106
+ account_email = input(f"Enter your {provider.capitalize()} account email: ").strip()
107
+ except (EOFError, KeyboardInterrupt):
108
+ print()
109
+ return
110
+
111
+ if not account_email:
112
+ print("\033[1;31m✗ An email address is required to link your account.\033[0m")
113
+ return
114
+
115
+ print(f"\nAuthenticating with {provider.capitalize()}...")
116
+ time.sleep(1.0)
117
+ print(f"\033[1;32m✓ Signed in as {account_email}\033[0m")
118
+
119
+ try:
120
+ tier, reachable = sync_account_tier(account_email, provider)
121
+ except OSError as exc:
122
+ # A write we could not complete: say so rather than claiming the
123
+ # account is unlicensed.
124
+ print(f"\033[1;31m✗ Could not save your login: {exc}\033[0m")
125
+ return
126
+
127
+ if tier == "plus":
128
+ print("\033[1;32m★ Offset Plus is active.\033[0m Thank you for funding this.")
129
+ return
130
+
131
+ if not reachable:
132
+ # Not a degraded state any more: nothing is withheld either way, so
133
+ # this is a note about sync, not an apology for a missing feature.
134
+ print("\033[1;33m! Could not reach the licence server; every feature is available anyway.\033[0m")
135
+ return
136
+
137
+ print("\033[1;32m✓ Signed in.\033[0m Every feature is available - offset does not gate on a licence.")
138
+ print(f"\n\033[1;33mIf it is useful, you can fund it:\033[0m https://debarghya47.gumroad.com/l/qzqnxk")
139
+ print(f" Subscribing with {account_email} adds hosted inference; it unlocks nothing locally.\n")
140
+
141
+
142
+ def _query_tier(account_email: str, provider: str) -> tuple[str, str | None, bool]:
143
+ """Ask the server for an entitlement: `(tier, token, reachable)`.
144
+
145
+ `reachable` separates "we asked and you are not a subscriber" from "we never
146
+ got an answer", which are the same value of `tier` but very different things
147
+ to tell a customer who has just paid.
148
+ """
149
+ req = urllib.request.Request(
150
+ f"{AUTH_SERVER_URL}/auth/verify_account",
151
+ data=json.dumps({"email": account_email, "provider": provider}).encode(),
152
+ headers={"Content-Type": "application/json"},
153
+ )
154
+ try:
155
+ with urllib.request.urlopen(req, timeout=8) as response:
156
+ result = json.loads(response.read().decode())
157
+ except (urllib.error.URLError, OSError, ValueError, TimeoutError):
158
+ return "lite", None, False
159
+
160
+ if result.get("tier") == "plus":
161
+ return "plus", result.get("access_token"), True
162
+ return "lite", None, True
163
+
164
+
165
+ def sync_account_tier(account_email: str, provider: str = "google") -> tuple[str, bool]:
166
+ """Refresh and persist the entitlement for `account_email`.
167
+
168
+ Returns `(tier, reachable)`. When the server cannot be reached the tier
169
+ already on disk is kept: a subscriber who opens a laptop on a plane keeps
170
+ the Plus they paid for, and a non-subscriber gains nothing.
171
+ """
172
+ tier, token, reachable = _query_tier(account_email, provider)
173
+ data = _read_auth()
174
+
175
+ if not reachable:
176
+ if _dev_mode() and "plus" in account_email.lower():
177
+ tier = "plus"
178
+ elif data.get("account") == account_email and data.get("tier") == "plus":
179
+ tier = "plus"
180
+ token = data.get("token")
181
+
182
+ data["logged_in"] = True
183
+ data["account"] = account_email
184
+ data["provider"] = provider
185
+ data["tier"] = tier
186
+ if token:
187
+ data["token"] = token
188
+ elif tier != "plus":
189
+ data.pop("token", None)
190
+
191
+ _write_auth(data)
192
+ return tier, reachable
193
+
194
+
195
+ def verify_direct_license_key(key: str) -> int:
196
+ """Verify a Gumroad licence key and store the entitlement it grants."""
197
+ if not key:
198
+ print("\033[1;31m✗ No licence key entered.\033[0m")
199
+ return 1
200
+
201
+ print(f"Verifying license key '{key}' with server...")
202
+
203
+ result: dict[str, Any] | None = None
204
+ req = urllib.request.Request(
205
+ f"{AUTH_SERVER_URL}/auth/verify_license",
206
+ data=json.dumps({"license_key": key}).encode(),
207
+ headers={"Content-Type": "application/json"},
208
+ )
209
+ try:
210
+ with urllib.request.urlopen(req, timeout=8) as response:
211
+ result = json.loads(response.read().decode())
212
+ except urllib.error.HTTPError as exc:
213
+ # 402 is the server's "this key is not valid" - a definite answer.
214
+ if exc.code != 402:
215
+ print(f"\033[1;31m✗ Licence server error ({exc.code}).\033[0m")
216
+ return 1
217
+ except (urllib.error.URLError, OSError, ValueError, TimeoutError):
218
+ if _dev_mode() and key == "test-plus-key":
219
+ result = {"tier": "plus", "email": "test@subscriber.com"}
220
+ else:
221
+ print("\033[1;31m✗ Could not reach the licence server. Check your connection.\033[0m")
222
+ return 1
223
+
224
+ if not result or result.get("tier") != "plus":
225
+ print("\033[1;31m✗ Invalid or expired licence key.\033[0m")
226
+ return 1
227
+
228
+ # Verification succeeded. A failure to persist it is a disk problem, not an
229
+ # invalid key, and must never be reported as one - previously the write sat
230
+ # inside the request's `try`, so a fresh install turned a real subscriber's
231
+ # valid key into "invalid licence key".
232
+ data = _read_auth()
233
+ data["logged_in"] = True
234
+ data["account"] = result.get("email") or "licensed_user@offset.dev"
235
+ data["tier"] = "plus"
236
+ if result.get("plan"):
237
+ data["plan"] = result["plan"]
238
+ if result.get("access_token"):
239
+ data["token"] = result["access_token"]
240
+
241
+ try:
242
+ _write_auth(data)
243
+ except OSError as exc:
244
+ print(f"\033[1;31m✗ Licence is valid but could not be saved: {exc}\033[0m")
245
+ return 1
246
+
247
+ print("\033[1;32m★ OFFSET PLUS ACTIVATED!\033[0m All features unlocked.")
248
+ return 0
249
+
250
+
251
+ def sync_command() -> int:
252
+ """CLI handler for 'offset sync'."""
253
+ data = _read_auth()
254
+ account = data.get("account")
255
+ if not account:
256
+ print("No linked account found. Please sign in first.")
257
+ prompt_account_login()
258
+ return 0
259
+
260
+ provider = data.get("provider", "google")
261
+ print(f"Syncing subscription status for \033[1m{account}\033[0m ({provider})...")
262
+
263
+ try:
264
+ tier, reachable = sync_account_tier(account, provider)
265
+ except OSError as exc:
266
+ print(f"\033[1;31m✗ Could not save your subscription status: {exc}\033[0m")
267
+ return 1
268
+
269
+ if not reachable:
270
+ print("\033[1;31m✗ Could not reach the licence server. Check your connection.\033[0m")
271
+ return 1
272
+
273
+ if tier == "plus":
274
+ print("\033[1;32m★ Offset Plus is active.\033[0m Thank you for funding this.")
275
+ else:
276
+ print(f"\033[1;33mSigned in as {account}.\033[0m Every feature is available.")
277
+ return 0
278
+
279
+
280
+ def current_tier() -> str:
281
+ """`plus` or `lite`, for display only.
282
+
283
+ Deliberately not a gate any more. Every workflow offset performs runs on
284
+ this machine against the user's own API keys, so charging for the ability
285
+ to *invoke* local code was a barrier without a cost behind it - and one
286
+ that a user could lift by editing a single function, which made it
287
+ theatre rather than a boundary. A subscription now buys the hosted
288
+ services that genuinely cost money to run, and nothing in this repository
289
+ checks it before doing work.
290
+ """
291
+ return "plus" if _read_auth().get("tier") == "plus" else "lite"
@@ -0,0 +1 @@
1
+ """Session state, providers, tools — everything that is not pixels."""