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.
- rockycode/__init__.py +1 -0
- rockycode/banner.py +37 -0
- rockycode/cli.py +1386 -0
- rockycode/config.py +178 -0
- rockycode/dream/__init__.py +9 -0
- rockycode/dream/core.py +523 -0
- rockycode/dream/judge.py +134 -0
- rockycode/dream/mining.py +152 -0
- rockycode/dream/proposals.py +440 -0
- rockycode/engine/__init__.py +10 -0
- rockycode/engine/artifact.py +367 -0
- rockycode/engine/budget.py +90 -0
- rockycode/engine/checks.py +157 -0
- rockycode/engine/compaction.py +181 -0
- rockycode/engine/container.py +225 -0
- rockycode/engine/effort.py +46 -0
- rockycode/engine/events.py +101 -0
- rockycode/engine/explore.py +592 -0
- rockycode/engine/goal.py +541 -0
- rockycode/engine/goal_review.py +161 -0
- rockycode/engine/goal_session.py +259 -0
- rockycode/engine/headless.py +481 -0
- rockycode/engine/loop.py +711 -0
- rockycode/engine/lsp.py +473 -0
- rockycode/engine/mcp.py +364 -0
- rockycode/engine/modes.py +123 -0
- rockycode/engine/outcome.py +81 -0
- rockycode/engine/permission.py +198 -0
- rockycode/engine/planmode.py +249 -0
- rockycode/engine/providers.py +196 -0
- rockycode/engine/redact.py +83 -0
- rockycode/engine/safety.py +139 -0
- rockycode/engine/sandbox.py +219 -0
- rockycode/engine/server.py +431 -0
- rockycode/engine/skills.py +178 -0
- rockycode/engine/titler.py +46 -0
- rockycode/engine/tools.py +479 -0
- rockycode/engine/trajectory.py +131 -0
- rockycode/engine/web.py +431 -0
- rockycode/engine/worktree.py +128 -0
- rockycode/memory/__init__.py +7 -0
- rockycode/memory/index.py +260 -0
- rockycode/memory/store.py +331 -0
- rockycode/modes/learn/learn.md +46 -0
- rockycode/modes/research/deep-research.md +53 -0
- rockycode/modes/research/paper-reading.md +49 -0
- rockycode/modes/research/prove.md +60 -0
- rockycode/modes/research/whiteboard.md +64 -0
- rockycode/onboarding.py +332 -0
- rockycode/palette.py +15 -0
- rockycode/pricing.py +178 -0
- rockycode/prompts/__init__.py +0 -0
- rockycode/prompts/rocky.py +257 -0
- rockycode/routines.py +287 -0
- rockycode/runners/__init__.py +0 -0
- rockycode/runners/agent.py +273 -0
- rockycode/runners/data.py +61 -0
- rockycode/runners/raw.py +176 -0
- rockycode/score.py +114 -0
- rockycode/session.py +298 -0
- rockycode/skills/architecture-viz/SKILL.md +71 -0
- rockycode/skills/architecture-viz/template.html +87 -0
- rockycode/skills/lean-prover/SKILL.md +155 -0
- rockycode/skills/lean-prover/torchlean-api.md +85 -0
- rockycode/tui/__init__.py +1 -0
- rockycode/tui/app.py +2450 -0
- rockycode/tui/exitsheet.py +181 -0
- rockycode/tui/goal_screen.py +315 -0
- rockycode/tui/mdterm.py +232 -0
- rockycode/tui/mdview.py +99 -0
- rockycode/tui/modepicker.py +103 -0
- rockycode/tui/permission.py +154 -0
- rockycode/tui/plangate.py +110 -0
- rockycode/tui/prompt_history.py +77 -0
- rockycode/tui/proposalcard.py +126 -0
- rockycode/tui/resume.py +142 -0
- rockycode/tui/rocky_pet.py +96 -0
- rockycode/tui/routinecard.py +123 -0
- rockycode-0.1.0.dist-info/METADATA +488 -0
- rockycode-0.1.0.dist-info/RECORD +83 -0
- rockycode-0.1.0.dist-info/WHEEL +4 -0
- rockycode-0.1.0.dist-info/entry_points.txt +2 -0
- 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.
|
rockycode/onboarding.py
ADDED
|
@@ -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)
|