rockycode 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 (83) hide show
  1. rockycode/__init__.py +1 -0
  2. rockycode/banner.py +37 -0
  3. rockycode/cli.py +1386 -0
  4. rockycode/config.py +178 -0
  5. rockycode/dream/__init__.py +9 -0
  6. rockycode/dream/core.py +523 -0
  7. rockycode/dream/judge.py +134 -0
  8. rockycode/dream/mining.py +152 -0
  9. rockycode/dream/proposals.py +440 -0
  10. rockycode/engine/__init__.py +10 -0
  11. rockycode/engine/artifact.py +367 -0
  12. rockycode/engine/budget.py +90 -0
  13. rockycode/engine/checks.py +157 -0
  14. rockycode/engine/compaction.py +181 -0
  15. rockycode/engine/container.py +225 -0
  16. rockycode/engine/effort.py +46 -0
  17. rockycode/engine/events.py +101 -0
  18. rockycode/engine/explore.py +592 -0
  19. rockycode/engine/goal.py +541 -0
  20. rockycode/engine/goal_review.py +161 -0
  21. rockycode/engine/goal_session.py +259 -0
  22. rockycode/engine/headless.py +481 -0
  23. rockycode/engine/loop.py +711 -0
  24. rockycode/engine/lsp.py +473 -0
  25. rockycode/engine/mcp.py +364 -0
  26. rockycode/engine/modes.py +123 -0
  27. rockycode/engine/outcome.py +81 -0
  28. rockycode/engine/permission.py +198 -0
  29. rockycode/engine/planmode.py +249 -0
  30. rockycode/engine/providers.py +196 -0
  31. rockycode/engine/redact.py +83 -0
  32. rockycode/engine/safety.py +139 -0
  33. rockycode/engine/sandbox.py +219 -0
  34. rockycode/engine/server.py +431 -0
  35. rockycode/engine/skills.py +178 -0
  36. rockycode/engine/titler.py +46 -0
  37. rockycode/engine/tools.py +479 -0
  38. rockycode/engine/trajectory.py +131 -0
  39. rockycode/engine/web.py +431 -0
  40. rockycode/engine/worktree.py +128 -0
  41. rockycode/memory/__init__.py +7 -0
  42. rockycode/memory/index.py +260 -0
  43. rockycode/memory/store.py +331 -0
  44. rockycode/modes/learn/learn.md +46 -0
  45. rockycode/modes/research/deep-research.md +53 -0
  46. rockycode/modes/research/paper-reading.md +49 -0
  47. rockycode/modes/research/prove.md +60 -0
  48. rockycode/modes/research/whiteboard.md +64 -0
  49. rockycode/onboarding.py +332 -0
  50. rockycode/palette.py +15 -0
  51. rockycode/pricing.py +178 -0
  52. rockycode/prompts/__init__.py +0 -0
  53. rockycode/prompts/rocky.py +257 -0
  54. rockycode/routines.py +287 -0
  55. rockycode/runners/__init__.py +0 -0
  56. rockycode/runners/agent.py +273 -0
  57. rockycode/runners/data.py +61 -0
  58. rockycode/runners/raw.py +176 -0
  59. rockycode/score.py +114 -0
  60. rockycode/session.py +298 -0
  61. rockycode/skills/architecture-viz/SKILL.md +71 -0
  62. rockycode/skills/architecture-viz/template.html +87 -0
  63. rockycode/skills/lean-prover/SKILL.md +155 -0
  64. rockycode/skills/lean-prover/torchlean-api.md +85 -0
  65. rockycode/tui/__init__.py +1 -0
  66. rockycode/tui/app.py +2450 -0
  67. rockycode/tui/exitsheet.py +181 -0
  68. rockycode/tui/goal_screen.py +315 -0
  69. rockycode/tui/mdterm.py +232 -0
  70. rockycode/tui/mdview.py +99 -0
  71. rockycode/tui/modepicker.py +103 -0
  72. rockycode/tui/permission.py +154 -0
  73. rockycode/tui/plangate.py +110 -0
  74. rockycode/tui/prompt_history.py +77 -0
  75. rockycode/tui/proposalcard.py +126 -0
  76. rockycode/tui/resume.py +142 -0
  77. rockycode/tui/rocky_pet.py +96 -0
  78. rockycode/tui/routinecard.py +123 -0
  79. rockycode-0.1.0.dist-info/METADATA +488 -0
  80. rockycode-0.1.0.dist-info/RECORD +83 -0
  81. rockycode-0.1.0.dist-info/WHEEL +4 -0
  82. rockycode-0.1.0.dist-info/entry_points.txt +2 -0
  83. rockycode-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: paper-reading
3
+ description: go deep on one paper — PDF, TeX, notes, and the user's repo, as a peer
4
+ ---
5
+
6
+ For reading one paper closely, as a colleague: what it claims, how it is
7
+ organized, what it assumes, where the contribution actually sits — and how it
8
+ connects to the user's own work.
9
+
10
+ # How you hold this session
11
+
12
+ Treat the paper as an object to be read, not a pretext for performing
13
+ intelligence. Do not summarize early, do not flatten everything into generic
14
+ takeaways. First understand what the paper claims, what objects it introduces,
15
+ what assumptions it makes, and where its contribution sits.
16
+
17
+ ## Getting the text
18
+
19
+ Work from markdown or TeX, not raw PDF, whenever possible. Check, in order:
20
+ a converted `.md` of the paper may already exist near the PDF (look before
21
+ re-extracting); your skill list may have a paper-conversion pipeline — prefer
22
+ it; otherwise a short pymupdf script via bash gets the text out. Read the
23
+ result with `read_file` so sections can be quoted exactly.
24
+
25
+ ## Reading depth
26
+
27
+ The session decides the depth, not you. Sometimes it stays broad — mapping
28
+ sections, locating core claims, deciding where to look closer. Sometimes it
29
+ slows down around one definition, one experiment, one derivation. Follow the
30
+ user's pointer; restate the current point before pushing forward.
31
+
32
+ ## Boundaries
33
+
34
+ Distinguish what the text directly supports from your interpretation, and say
35
+ which is which. When a promising side-idea appears (good papers trigger them),
36
+ mark it — one line, "worth returning to" — and return to the reading. The
37
+ session is reading THIS paper; protect that.
38
+
39
+ ## Connecting to the user's work
40
+
41
+ When the user's repo is present, connections are welcome AS connections:
42
+ "their eq. 3 is what `loss.py` calls the balance term" — cite the file path so
43
+ it is clickable. Do not drift into redesigning the user's code mid-reading.
44
+
45
+ ## Tone
46
+
47
+ Attentive, restrained, non-performative. No speed-reading theatrics; no
48
+ treating every intuition as a thesis. Figures or reading maps worth keeping
49
+ can become an artifact (`create_artifact`).
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: prove
3
+ description: make a claim rigorous — formalize in Lean, machine-verify, render the green/amber/red result
4
+ ---
5
+
6
+ For turning an informal claim into a machine-checked one: a math property, a
7
+ neural-network guarantee, an attention identity. The output is a Lean file the
8
+ compiler certifies, rendered as a verdict artifact — not an argument, a proof.
9
+
10
+ # How you hold this session
11
+
12
+ The Lean compiler is ground truth, and this session runs on that discipline:
13
+ **never say "proved" without a green compile you actually ran.** You are a
14
+ proof assistant, not a persuader — a correctly formalized statement with honest
15
+ `sorry`s is real value; a confident claim with no compile is worthless here.
16
+
17
+ ## Use the lean-prover skill — it is the how-to
18
+
19
+ This mode sets the cadence; the mechanics live in the **lean-prover** skill.
20
+ Call it FIRST (via the `skill` tool) and follow it: the preflight (is Lean even
21
+ installed? — offer install / browser / formalize-only, never assume), the
22
+ formalize → compile → repair loop, the name-grounding rules, and the
23
+ green/amber/red verdict vocabulary. Do not reinvent that pipeline here.
24
+
25
+ ## Two layers, never blurred
26
+
27
+ - **Math layer (Mathlib)** — the mathematics itself, exact reals, all sizes at
28
+ once: "softmax lands in the probability simplex, for every n."
29
+ - **Model layer (TorchLean)** — a concrete network in Lean's typed tensor
30
+ framework: shape guarantees by compilation, robustness certificates over
31
+ float32.
32
+
33
+ A claim proved about real-number math is not a claim about a float32 kernel.
34
+ When a session spans both — the user's code, the math behind it, the model
35
+ property — keep each labelled with the layer it lives in.
36
+
37
+ ## Scope before formalizing
38
+
39
+ Restate the informal claim and confirm it before writing any Lean — a proof of
40
+ the wrong statement is worse than no proof. If the claim is ambiguous
41
+ ("attention is stable"), pin it to a formal shape the user agrees to first.
42
+ This is a scoping conversation, not a race to a theorem.
43
+
44
+ ## The artifact is the deliverable
45
+
46
+ Render the result with `create_artifact`, verdict badge up top
47
+ (green/amber/red — amber lists the remaining goals). When the work spans code +
48
+ math + model, render the **triptych**: the user's original code, the math-layer
49
+ theorem, and the model-layer result — three panels, one story. Formulas render
50
+ as real typeset math (KaTeX, so a reader can copy the TeX back out), the same
51
+ visual system a whiteboard draft uses — so draft → formalize → verified proof
52
+ flows without a visual seam.
53
+
54
+ ## Honesty
55
+
56
+ The verdict comes from the last compile you ran, nothing else. Quote compiler
57
+ output when it disagrees with your expectation. If the claim is false, say so —
58
+ a disproof or counterexample (`decide`, `norm_num`, an explicit witness) is a
59
+ fully valid green result. If Lean isn't installed and you couldn't compile, the
60
+ result is **unverified** — never green, never amber.
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: whiteboard
3
+ description: think together on an idea or draft — the user keeps the pen
4
+ ---
5
+
6
+ For co-thinking on an idea, a design, or a hand-crafted math/LaTeX draft.
7
+ Alignment before writing, one step at a time, and the user's authorship is
8
+ preserved throughout.
9
+
10
+ # How you hold this session
11
+
12
+ You are a co-author at the same desk — careful with evidence, never
13
+ improvising missing structure, never trying to win the narrative. The draft
14
+ is the user's craft: notation, rhythm, and local consistency matter, and
15
+ correctness is the floor, not the goal.
16
+
17
+ ## The cadence
18
+
19
+ When the user opens a micro-topic (one paragraph, one definition, one symbol
20
+ family), first align: what they are trying to achieve, what is already fixed
21
+ as fact or constraint, and what you think the NEXT step is — only the next
22
+ step. Then stop and wait for "yes, that's the thread" before writing new
23
+ definitions or proposing edits. This keeps the session on the whiteboard
24
+ instead of drifting into auto-completing a paper.
25
+
26
+ ## Evidence is the spine
27
+
28
+ Lean only on what is in the session: their snippets, their code excerpts,
29
+ their explicit decisions. Missing a needed fragment? Say exactly which one —
30
+ "I don't see the definition of A_v in what you pasted; paste that paragraph
31
+ and I can align the notation." Never patch a gap with "standard practice";
32
+ alternatives you offer are candidates the user can reject without friction.
33
+
34
+ ## Their notation is ground truth
35
+
36
+ The draft has its own internal aesthetic. Follow its logic rather than
37
+ overwriting it with your default conventions. A stated naming or style
38
+ preference is a design decision — preserve it until they change it. If a
39
+ local choice might affect later sections, flag it in one line and stop there.
40
+
41
+ ## Local consistency over global completion
42
+
43
+ Work the one knot being pointed at — a symbol collision, a map signature, a
44
+ paragraph's flow. Don't expand into future chapters unasked. A board worth
45
+ keeping (a diagram, the current symbol table, a draft fragment) can become an
46
+ artifact (`create_artifact`) so it survives the session.
47
+
48
+ ## Formulas render as math, not as source
49
+
50
+ When a board contains formulas, the artifact must show REAL rendered math,
51
+ never raw TeX text. Prefer KaTeX (with its copy-tex extension, so selecting
52
+ a rendered formula copies the TeX source back out — the user round-trips
53
+ their own draft); MathJax tex-svg is the fallback, but its SVG output is not
54
+ selectable. Keep their macros and symbol choices exactly as written.
55
+
56
+ ## When a draft is ready to be made rigorous
57
+
58
+ The board is where a claim takes shape; it is not where it gets proven. When a
59
+ drafted statement is firm enough to be checked — an attention identity, a
60
+ softmax property, a model guarantee — offer to hand it to **prove mode**
61
+ (`/research prove`, which drives the lean-prover skill): formalize it in Lean,
62
+ machine-verify green/amber/red, render the verdict in the same KaTeX visual
63
+ system this board uses. Don't switch unasked — offer it as the natural next
64
+ step, so draft → formalize → verified proof is one continuous flow.
@@ -0,0 +1,332 @@
1
+ """First-run setup — the paste-your-key flow — and the credential chain.
2
+
3
+ A brand-new user who runs `rockycode` with no API key shouldn't hit a stack
4
+ trace. If nothing is configured we walk them through pasting a key once and save
5
+ it to ~/.rockycode/.env (a 0600 file that every run loads), so from then on
6
+ `rockycode` just works in any folder — no shell exports to remember.
7
+
8
+ Rocky reads exactly ONE key name: ROCKYCODE_API_KEY (env or the opt-in OS
9
+ keychain). It never picks up an ambient OPENAI_API_KEY — that usually belongs
10
+ to a DIFFERENT provider, and silently sending it to a DeepSeek endpoint would
11
+ ship a real OpenAI credential to a third party. Installs that stored the key
12
+ under the old OPENAI_API_KEY name (in rocky's OWN .env / keychain entry, where
13
+ it is rocky's key by construction) are renamed in place by
14
+ bootstrap_credentials().
15
+
16
+ The key lands in a file, never asked to be exported into the shell env by hand;
17
+ rocky's SDK clients receive it explicitly (require_key()) instead of trawling
18
+ the process env, and rocky's redaction keeps it out of tool output /
19
+ trajectories.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import re
25
+ from pathlib import Path
26
+
27
+ from dotenv import load_dotenv
28
+
29
+ # The one key name rocky reads — rocky-owned, collides with nothing.
30
+ KEY_ENV = "ROCKYCODE_API_KEY"
31
+ # Never read: it usually belongs to a different provider. Mentioned in
32
+ # messages/warnings only.
33
+ _LEGACY_KEY_ENV = "OPENAI_API_KEY"
34
+ # The one endpoint name rocky reads. The SDK's ambient OPENAI_BASE_URL is
35
+ # never consulted — whoever controls the URL receives the Authorization
36
+ # header, so the endpoint gets the same protection as the key itself.
37
+ BASE_URL_ENV = "ROCKYCODE_BASE_URL"
38
+ DEFAULT_BASE_URL = "https://api.deepseek.com/v1"
39
+
40
+ # $ROCKYCODE_HOME (tests and users who relocate the store) — same convention
41
+ # as session.py / trajectory.py.
42
+ _HOME = Path(os.environ.get("ROCKYCODE_HOME") or Path.home() / ".rockycode")
43
+ GLOBAL_ENV = _HOME / ".env"
44
+ _PLACEHOLDERS = {"", "replace-me", "sk-replace-me", "your-api-key", "your_api_key", "changeme"}
45
+
46
+ # Optional OS keychain (the `rockycode[keyring]` extra) — encrypted at rest, the
47
+ # safest place for the secret. macOS Keychain / Windows Credential Manager /
48
+ # Linux Secret Service. When absent we fall back to the 0600 dotenv.
49
+ _KEYRING_SERVICE = "rockycode"
50
+ _KEYRING_USER = KEY_ENV
51
+
52
+
53
+ def _keyring():
54
+ """The keyring module IF the extra is installed AND has a real backend (not
55
+ the fail/null backend on headless Linux); else None."""
56
+ try:
57
+ import keyring
58
+ from keyring.backends.fail import Keyring as _FailBackend
59
+ if isinstance(keyring.get_keyring(), _FailBackend):
60
+ return None
61
+ return keyring
62
+ except Exception: # noqa: BLE001 — optional; any import/backend issue → unavailable
63
+ return None
64
+
65
+
66
+ def _keyring_key() -> str | None:
67
+ kr = _keyring()
68
+ if kr is None:
69
+ return None
70
+ try:
71
+ return (kr.get_password(_KEYRING_SERVICE, _KEYRING_USER) or "").strip() or None
72
+ except Exception: # noqa: BLE001
73
+ return None
74
+
75
+
76
+ def _env_key() -> str | None:
77
+ """ROCKYCODE_API_KEY from the env — placeholder-aware, so a template
78
+ line pasted without editing still counts as unset (and triggers setup)."""
79
+ v = (os.getenv(KEY_ENV) or "").strip()
80
+ if v and v.lower() not in _PLACEHOLDERS:
81
+ return v
82
+ return None
83
+
84
+
85
+ def current_key() -> str | None:
86
+ """The configured API key, or None."""
87
+ v = _env_key()
88
+ if v:
89
+ return v
90
+ v = _keyring_key() # opt-in OS keychain
91
+ if v and v.lower() not in _PLACEHOLDERS:
92
+ return v
93
+ return None
94
+
95
+
96
+ # Set once by bootstrap_credentials()/save_credentials(): "shell export",
97
+ # the global .env's display path, or "keychain". Recorded at fill time because
98
+ # afterwards everything looks like an env var (the keychain is copied into the
99
+ # process env) — post-hoc inspection can't tell the sources apart.
100
+ _KEY_SOURCE: str | None = None
101
+
102
+
103
+ def _global_env_display() -> str:
104
+ return str(GLOBAL_ENV).replace(str(Path.home()), "~")
105
+
106
+
107
+ def current_key_source() -> str | None:
108
+ """Where the key ACTUALLY came from — "shell export", the global .env's
109
+ path, or "keychain". Surfaced in auth-error diagnostics so "which key is
110
+ rocky using?" never needs guesswork. Falls back to coarse detection when
111
+ bootstrap hasn't run (direct API/library use)."""
112
+ if current_key() is None:
113
+ return None
114
+ if _KEY_SOURCE is not None:
115
+ return _KEY_SOURCE
116
+ if _env_key():
117
+ return KEY_ENV
118
+ return "keychain"
119
+
120
+
121
+ def is_configured() -> bool:
122
+ return current_key() is not None
123
+
124
+
125
+ def require_key() -> str:
126
+ """The configured key, or a RuntimeError that says how to fix it. SDK
127
+ clients call this instead of letting the SDK trawl env for OPENAI_API_KEY —
128
+ that fallback is exactly the misrouting this module exists to prevent."""
129
+ key = current_key()
130
+ if key:
131
+ return key
132
+ raise RuntimeError(
133
+ f"no API key configured — run `rockycode` once to set one up, or set {KEY_ENV}. "
134
+ f"(rocky reads only {KEY_ENV}; an ambient {_LEGACY_KEY_ENV} is never used — "
135
+ "it usually belongs to a different provider)"
136
+ )
137
+
138
+
139
+ def require_base_url() -> str:
140
+ """The endpoint rocky talks to: {BASE_URL_ENV} (shell export or rocky's
141
+ global .env) with the DeepSeek default. Clients pass this explicitly so
142
+ the SDK never falls back to an ambient OPENAI_BASE_URL — and a project
143
+ folder can never redirect the key (see project_env_warnings)."""
144
+ v = (os.getenv(BASE_URL_ENV) or "").strip()
145
+ return v or DEFAULT_BASE_URL
146
+
147
+
148
+ def provider_key(key_env: str) -> str:
149
+ """A named provider's key from its ROCKY-OWNED env var (loaded from
150
+ ~/.rockycode by bootstrap). NEVER an ambient provider key: a user's own
151
+ MINIMAX_API_KEY is theirs; rocky reads only ROCKYCODE_<PROVIDER>_API_KEY.
152
+ Raises with the exact var to set if it's missing."""
153
+ if key_env == KEY_ENV: # the default provider — full keychain chain
154
+ return require_key()
155
+ v = (os.getenv(key_env) or "").strip()
156
+ if v and v.lower() not in _PLACEHOLDERS:
157
+ return v
158
+ raise RuntimeError(
159
+ f"no key for this provider — set {key_env} in ~/.rockycode/.env "
160
+ f"(or export it). rocky reads only rocky-owned {key_env}, never an "
161
+ f"ambient provider key."
162
+ )
163
+
164
+
165
+ # Credential-shaped names in a PROJECT .env — matched by name only, values
166
+ # are never read out. \w*_BASE_URL catches OPENAI_/ROCKYCODE_/any proxy vars.
167
+ _CREDENTIAL_SHAPED = re.compile(
168
+ r"^\s*(?:export\s+)?((?:\w+_)?API_KEY|\w+_BASE_URL|\w+_AUTH_TOKEN)\s*=",
169
+ re.MULTILINE,
170
+ )
171
+
172
+
173
+ def project_env_warnings(workdir: Path) -> list[str]:
174
+ """One warning line per credential-shaped var NAME in the project's .env.
175
+
176
+ Rocky never loads project .env files (a repo must not be able to supply a
177
+ key or redirect the endpoint), but silence made that indistinguishable
178
+ from "it worked" — a stale key shadowing the real one cost an evening.
179
+ Names only; values never appear in the output."""
180
+ try:
181
+ content = (workdir / ".env").read_text()
182
+ except OSError:
183
+ return []
184
+ names = sorted(set(_CREDENTIAL_SHAPED.findall(content)))
185
+ return [
186
+ f"project .env sets {name} — ignored. rocky reads credentials and "
187
+ f"endpoint only from ~/.rockycode (or shell {KEY_ENV} / {BASE_URL_ENV})"
188
+ for name in names
189
+ ]
190
+
191
+
192
+ def load_credentials_into_env() -> None:
193
+ """Load a keychain-stored key into the process env so every entry point
194
+ resolves the same chain. No-op if a key is already set or the keyring extra
195
+ isn't installed."""
196
+ if _env_key():
197
+ return
198
+ v = _keyring_key()
199
+ if v:
200
+ os.environ[KEY_ENV] = v
201
+
202
+
203
+ def bootstrap_credentials() -> None:
204
+ """CLI startup: load rocky's global .env, then fill from the keychain.
205
+ Shell exports win (load_dotenv never overrides existing vars); project
206
+ folders are never read. Records where the key came from — attribution is
207
+ only knowable at fill time. Idempotent (first run wins the attribution)."""
208
+ global _KEY_SOURCE
209
+ first = _KEY_SOURCE is None
210
+ if first and _env_key():
211
+ _KEY_SOURCE = "shell export"
212
+ load_dotenv(GLOBAL_ENV)
213
+ if first and _KEY_SOURCE is None and _env_key():
214
+ _KEY_SOURCE = _global_env_display()
215
+ load_credentials_into_env()
216
+ if first and _KEY_SOURCE is None and _env_key():
217
+ _KEY_SOURCE = "keychain"
218
+
219
+
220
+ def save_credentials(key: str, base_url: str, model: str, path: Path = GLOBAL_ENV,
221
+ *, prefer_keyring: bool = False) -> Path:
222
+ """Persist creds and apply to the running process. With prefer_keyring (and
223
+ the [keyring] extra installed) the secret goes to the OS keychain and is kept
224
+ OUT of the file; otherwise it lands in a 0600 dotenv. Non-secret settings
225
+ always go to the .env. Dir is 0700, file 0600 — defense in depth."""
226
+ path.parent.mkdir(parents=True, exist_ok=True)
227
+ try:
228
+ path.parent.chmod(0o700) # ~/.rockycode: user-only
229
+ except OSError:
230
+ pass
231
+
232
+ in_keyring = False
233
+ if prefer_keyring:
234
+ kr = _keyring()
235
+ if kr is not None:
236
+ try:
237
+ kr.set_password(_KEYRING_SERVICE, _KEYRING_USER, key)
238
+ in_keyring = True
239
+ except Exception: # noqa: BLE001 — fall back to the file
240
+ in_keyring = False
241
+
242
+ lines = [f"{BASE_URL_ENV}={base_url}", f"ROCKYCODE_MODEL={model}"]
243
+ if not in_keyring:
244
+ lines.insert(0, f"{KEY_ENV}={key}")
245
+ content = ("\n".join(lines) + "\n").encode("utf-8")
246
+ # Create with 0600 from the start — a write-then-chmod leaves the secret
247
+ # briefly group/other-readable (umask 0644) on a shared host.
248
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
249
+ try:
250
+ os.write(fd, content)
251
+ finally:
252
+ os.close(fd)
253
+ try:
254
+ path.chmod(0o600) # tighten an existing file (O_CREAT keeps old perms)
255
+ except OSError:
256
+ pass
257
+
258
+ os.environ[KEY_ENV] = key
259
+ os.environ.setdefault(BASE_URL_ENV, base_url)
260
+ os.environ.setdefault("ROCKYCODE_MODEL", model)
261
+ global _KEY_SOURCE
262
+ _KEY_SOURCE = "keychain" if in_keyring else _global_env_display()
263
+ return path
264
+
265
+
266
+ def _key_rejected(key: str, base_url: str) -> bool:
267
+ """True only when the endpoint definitively rejects the key (HTTP 401),
268
+ via the cheapest authenticated call there is (GET /models). Unreachable
269
+ endpoint / any other failure → False: never block setup when we can't
270
+ verify (offline, odd proxy, provider without /models). Would have caught
271
+ a paste with three stray blanks the moment it was entered."""
272
+ from openai import AuthenticationError, OpenAI
273
+ try:
274
+ OpenAI(api_key=key, base_url=base_url, max_retries=0, timeout=10.0).models.list()
275
+ except AuthenticationError:
276
+ return True
277
+ except Exception: # noqa: BLE001 — can't verify ≠ invalid
278
+ return False
279
+ return False
280
+
281
+
282
+ def run_setup(console) -> None:
283
+ """Interactive first-run. No-op once a key is configured."""
284
+ if is_configured():
285
+ return
286
+ import typer
287
+
288
+ from rockycode.banner import info, show_banner
289
+
290
+ show_banner(console)
291
+ console.print()
292
+ info(console, "first run — let's set up your API key (saved to ~/.rockycode/.env, not your shell).")
293
+ console.print(
294
+ " get a DeepSeek key: [cyan]https://platform.deepseek.com/api_keys[/] "
295
+ "· any OpenAI-compatible key works too"
296
+ )
297
+ console.print()
298
+ key = typer.prompt(" paste your API key", hide_input=True).strip()
299
+ while not key:
300
+ key = typer.prompt(" a key is required — paste it", hide_input=True).strip()
301
+ base_url = typer.prompt(" API base URL", default="https://api.deepseek.com/v1").strip()
302
+ while _key_rejected(key, base_url):
303
+ info(console, "that key was rejected by the endpoint (401) — check for typos or stray spaces.")
304
+ key = typer.prompt(" paste your API key again", hide_input=True).strip()
305
+ model = typer.prompt(" default model (deepseek-v4-flash is the cheaper, lighter tier)",
306
+ default="deepseek-v4-pro").strip()
307
+ # Reply language — asked once so a 中文 user's first session already
308
+ # answers in Chinese instead of depending on model luck. auto mirrors
309
+ # whatever language each message is written in. Changeable anytime:
310
+ # `rockycode config language zh` or /config in the TUI.
311
+ lang = typer.prompt(
312
+ " reply language / 回复语言 (auto = follow my messages · en · zh)",
313
+ default="auto").strip().lower()
314
+ if lang in {"中文", "cn", "chinese"}:
315
+ lang = "zh"
316
+ elif lang in {"english"}:
317
+ lang = "en"
318
+ if lang not in {"auto", "en", "zh"}:
319
+ info(console, f"'{lang}' not recognized — using auto (follows your messages).")
320
+ lang = "auto"
321
+ from rockycode.config import set_value
322
+ _, lang_err = set_value("language", lang)
323
+ if lang_err:
324
+ info(console, f"could not save language: {lang_err}")
325
+ prefer_keyring = False
326
+ if _keyring() is not None:
327
+ prefer_keyring = typer.confirm(
328
+ " store the key in your OS keychain (encrypted — safer than a file)?", default=True)
329
+ save_credentials(key, base_url, model, prefer_keyring=prefer_keyring)
330
+ where = "your OS keychain" if (prefer_keyring and _keyring_key()) else str(GLOBAL_ENV)
331
+ info(console, f"key saved to {where} — you're set. `rockycode` works in any project now.")
332
+ console.print()
rockycode/palette.py ADDED
@@ -0,0 +1,15 @@
1
+ """rockycode's one palette — shared by the CLI (Rich) and the TUI (Textual).
2
+
3
+ Explicit hex only. ANSI named colors ("magenta", "cyan") render differently
4
+ in every terminal — neon pink, electric blue — and are banned in this repo.
5
+ This file is the single source of truth for both surfaces; if a color isn't
6
+ here, don't use it.
7
+ """
8
+
9
+ VIOLET = "#bb9af7" # brand: titles, headings, "amaze!"
10
+ PURPLE = "#9d7cd8" # structure: borders, progress bars, tool marks
11
+ LAVENDER = "#a9b1d6" # secondary text, quotes
12
+ BLUE = "#7aa2f7" # highlights inside text: paths, dataset names, links
13
+ AMBER = "#d8b27d" # warnings / "i no know"
14
+ RED = "#e06c75" # errors only
15
+ MUTED = "#787c99" # de-emphasized text (works where terminal `dim` doesn't)