···11+# CRUSH.md
22+33+Build/Lint/Test
44+- Install: bun install
55+- Typecheck: bun x tsc --noEmit
66+- Run: bun run index.ts or bun index.ts
77+- Test all: bun test
88+- Test watch: bun test --watch
99+- Test single: bun test path/to/file.test.ts -t "name"
1010+- Lint: bun x biome check --write || bun x eslint . (if configured)
1111+1212+Conventions
1313+- Runtime: Bun (see CLAUDE.md). Prefer Bun APIs (Bun.serve, Bun.file, Bun.$) over Node shims. Bun auto-loads .env.
1414+- Modules: ESM only ("type": "module"). Use extensionless TS imports within project.
1515+- Formatting: Prettier/biome if present; otherwise 2-space indent, trailing commas where valid, semicolons optional but consistent.
1616+- Types: Strict TypeScript. Prefer explicit types on public APIs; infer locals via const. Use unknown over any. Narrow with guards.
1717+- Imports: Group std/bun, third-party, then local. Use named imports; avoid default exports for libs.
1818+- Naming: camelCase for vars/functions, PascalCase for types/classes, UPPER_SNAKE for env constants.
1919+- Errors: Throw Error (or subclasses) with actionable messages; never swallow. Use Result-like returns only if established.
2020+- Async: Prefer async/await. Always handle rejections. Avoid top-level await outside Bun entrypoints.
2121+- Logging: Use console.* sparingly; no secrets in logs. Prefer structured messages.
2222+- Env/config: Read via process.env or Bun.env at startup; validate and fail fast.
2323+- Files: Prefer Bun.file and Response over fs. Avoid sync IO.
2424+- Tests: bun:test (import { test, expect } from "bun:test"). Keep tests deterministic, no network without mocking.
2525+2626+Repo Notes
2727+- No Cursor/Copilot rules detected.
2828+- Add ".crush" dir to .gitignore (keeps agent scratch files untracked).
+15
README.md
···11+# anthropic-api-key
22+33+To install dependencies:
44+55+```bash
66+bun install
77+```
88+99+To run:
1010+1111+```bash
1212+bun run index.ts
1313+```
1414+1515+This project was created using `bun init` in bun v1.2.19. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+531
anthropic.sh
···11+#!/bin/sh
22+33+# Anthropic OAuth client ID
44+CLIENT_ID="9d1c250a-e61b-44d9-88ed-5944d1962f5e"
55+66+# Token cache file location
77+CACHE_DIR="${HOME}/.config/crush/anthropic"
88+CACHE_FILE="${CACHE_DIR}/bearer_token"
99+REFRESH_TOKEN_FILE="${CACHE_DIR}/refresh_token"
1010+1111+# Function to extract expiration from cached token file
1212+extract_expiration() {
1313+ if [ -f "${CACHE_FILE}.expires" ]; then
1414+ cat "${CACHE_FILE}.expires"
1515+ fi
1616+}
1717+1818+# Function to check if token is valid
1919+is_token_valid() {
2020+ local expires="$1"
2121+2222+ if [ -z "$expires" ]; then
2323+ return 1
2424+ fi
2525+2626+ local current_time=$(date +%s)
2727+ # Add 60 second buffer before expiration
2828+ local buffer_time=$((expires - 60))
2929+3030+ if [ "$current_time" -lt "$buffer_time" ]; then
3131+ return 0
3232+ else
3333+ return 1
3434+ fi
3535+}
3636+3737+# Function to generate PKCE challenge (requires openssl)
3838+generate_pkce() {
3939+ # Generate 32 random bytes, base64url encode
4040+ local verifier=$(openssl rand -base64 32 | tr -d "=" | tr "/" "_" | tr "+" "-" | tr -d "\n")
4141+ # Create SHA256 hash of verifier, base64url encode
4242+ local challenge=$(printf '%s' "$verifier" | openssl dgst -sha256 -binary | openssl base64 | tr -d "=" | tr "/" "_" | tr "+" "-" | tr -d "\n")
4343+4444+ echo "$verifier|$challenge"
4545+}
4646+4747+# Function to exchange refresh token for new access token
4848+exchange_refresh_token() {
4949+ local refresh_token="$1"
5050+5151+ local bearer_response=$(curl -s -X POST "https://console.anthropic.com/v1/oauth/token" \
5252+ -H "Content-Type: application/json" \
5353+ -H "User-Agent: CRUSH/1.0" \
5454+ -d "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"${refresh_token}\",\"client_id\":\"${CLIENT_ID}\"}")
5555+5656+ # Parse JSON response - try jq first, fallback to sed
5757+ local access_token=""
5858+ local new_refresh_token=""
5959+ local expires_in=""
6060+6161+ if command -v jq >/dev/null 2>&1; then
6262+ access_token=$(echo "$bearer_response" | jq -r '.access_token // empty')
6363+ new_refresh_token=$(echo "$bearer_response" | jq -r '.refresh_token // empty')
6464+ expires_in=$(echo "$bearer_response" | jq -r '.expires_in // empty')
6565+ else
6666+ # Fallback to sed parsing
6767+ access_token=$(echo "$bearer_response" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
6868+ new_refresh_token=$(echo "$bearer_response" | sed -n 's/.*"refresh_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
6969+ expires_in=$(echo "$bearer_response" | sed -n 's/.*"expires_in"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p')
7070+ fi
7171+7272+ if [ -n "$access_token" ] && [ -n "$expires_in" ]; then
7373+ # Calculate expiration timestamp
7474+ local current_time=$(date +%s)
7575+ local expires_timestamp=$((current_time + expires_in))
7676+7777+ # Cache the new tokens
7878+ mkdir -p "$CACHE_DIR"
7979+ echo "$access_token" > "$CACHE_FILE"
8080+ chmod 600 "$CACHE_FILE"
8181+8282+ if [ -n "$new_refresh_token" ]; then
8383+ echo "$new_refresh_token" > "$REFRESH_TOKEN_FILE"
8484+ chmod 600 "$REFRESH_TOKEN_FILE"
8585+ fi
8686+8787+ # Store expiration for future reference
8888+ echo "$expires_timestamp" > "${CACHE_FILE}.expires"
8989+ chmod 600 "${CACHE_FILE}.expires"
9090+9191+ echo "$access_token"
9292+ return 0
9393+ fi
9494+9595+ return 1
9696+}
9797+9898+# Function to exchange authorization code for tokens
9999+exchange_authorization_code() {
100100+ local auth_code="$1"
101101+ local verifier="$2"
102102+103103+ # Split code if it contains state (format: code#state)
104104+ local code=$(echo "$auth_code" | cut -d'#' -f1)
105105+ local state=""
106106+ if echo "$auth_code" | grep -q '#'; then
107107+ state=$(echo "$auth_code" | cut -d'#' -f2)
108108+ fi
109109+110110+ # Use the working endpoint
111111+ local bearer_response=$(curl -s -X POST "https://console.anthropic.com/v1/oauth/token" \
112112+ -H "Content-Type: application/json" \
113113+ -H "User-Agent: CRUSH/1.0" \
114114+ -d "{\"code\":\"${code}\",\"state\":\"${state}\",\"grant_type\":\"authorization_code\",\"client_id\":\"${CLIENT_ID}\",\"redirect_uri\":\"https://console.anthropic.com/oauth/code/callback\",\"code_verifier\":\"${verifier}\"}")
115115+116116+ # Parse JSON response - try jq first, fallback to sed
117117+ local access_token=""
118118+ local refresh_token=""
119119+ local expires_in=""
120120+121121+ if command -v jq >/dev/null 2>&1; then
122122+ access_token=$(echo "$bearer_response" | jq -r '.access_token // empty')
123123+ refresh_token=$(echo "$bearer_response" | jq -r '.refresh_token // empty')
124124+ expires_in=$(echo "$bearer_response" | jq -r '.expires_in // empty')
125125+ else
126126+ # Fallback to sed parsing
127127+ access_token=$(echo "$bearer_response" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
128128+ refresh_token=$(echo "$bearer_response" | sed -n 's/.*"refresh_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
129129+ expires_in=$(echo "$bearer_response" | sed -n 's/.*"expires_in"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p')
130130+ fi
131131+132132+ if [ -n "$access_token" ] && [ -n "$refresh_token" ] && [ -n "$expires_in" ]; then
133133+ # Calculate expiration timestamp
134134+ local current_time=$(date +%s)
135135+ local expires_timestamp=$((current_time + expires_in))
136136+137137+ # Cache the tokens
138138+ mkdir -p "$CACHE_DIR"
139139+ echo "$access_token" > "$CACHE_FILE"
140140+ echo "$refresh_token" > "$REFRESH_TOKEN_FILE"
141141+ echo "$expires_timestamp" > "${CACHE_FILE}.expires"
142142+ chmod 600 "$CACHE_FILE" "$REFRESH_TOKEN_FILE" "${CACHE_FILE}.expires"
143143+144144+ echo "$access_token"
145145+ return 0
146146+ else
147147+ return 1
148148+ fi
149149+}
150150+151151+# Check for cached bearer token
152152+if [ -f "$CACHE_FILE" ] && [ -f "${CACHE_FILE}.expires" ]; then
153153+ CACHED_TOKEN=$(cat "$CACHE_FILE")
154154+ CACHED_EXPIRES=$(cat "${CACHE_FILE}.expires")
155155+ if is_token_valid "$CACHED_EXPIRES"; then
156156+ # Token is still valid, output and exit
157157+ echo "$CACHED_TOKEN"
158158+ exit 0
159159+ fi
160160+fi
161161+162162+# Bearer token is expired/missing, try to use cached refresh token
163163+if [ -f "$REFRESH_TOKEN_FILE" ]; then
164164+ REFRESH_TOKEN=$(cat "$REFRESH_TOKEN_FILE")
165165+ if [ -n "$REFRESH_TOKEN" ]; then
166166+ # Try to exchange refresh token for new bearer token
167167+ BEARER_TOKEN=$(exchange_refresh_token "$REFRESH_TOKEN")
168168+ if [ -n "$BEARER_TOKEN" ]; then
169169+ # Successfully got new bearer token, output and exit
170170+ echo "$BEARER_TOKEN"
171171+ exit 0
172172+ fi
173173+ fi
174174+fi
175175+176176+# No valid tokens found, start OAuth flow
177177+# Check if openssl is available for PKCE
178178+if ! command -v openssl >/dev/null 2>&1; then
179179+ exit 1
180180+fi
181181+182182+# Generate PKCE challenge
183183+PKCE_DATA=$(generate_pkce)
184184+VERIFIER=$(echo "$PKCE_DATA" | cut -d'|' -f1)
185185+CHALLENGE=$(echo "$PKCE_DATA" | cut -d'|' -f2)
186186+187187+# Build OAuth URL
188188+AUTH_URL="https://claude.ai/oauth/authorize"
189189+AUTH_URL="${AUTH_URL}?response_type=code"
190190+AUTH_URL="${AUTH_URL}&client_id=${CLIENT_ID}"
191191+AUTH_URL="${AUTH_URL}&redirect_uri=https://console.anthropic.com/oauth/code/callback"
192192+AUTH_URL="${AUTH_URL}&scope=org:create_api_key%20user:profile%20user:inference"
193193+AUTH_URL="${AUTH_URL}&code_challenge=${CHALLENGE}"
194194+AUTH_URL="${AUTH_URL}&code_challenge_method=S256"
195195+AUTH_URL="${AUTH_URL}&state=${VERIFIER}"
196196+197197+# Create a temporary HTML file with the authentication form
198198+TEMP_HTML="/tmp/anthropic_auth_$$.html"
199199+cat > "$TEMP_HTML" << EOF
200200+<!DOCTYPE html>
201201+<html>
202202+<head>
203203+ <title>Anthropic Authentication</title>
204204+ <style>
205205+ * {
206206+ box-sizing: border-box;
207207+ margin: 0;
208208+ padding: 0;
209209+ }
210210+211211+ body {
212212+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
213213+ background: linear-gradient(135deg, #1a1a1a 0%, #2d1810 100%);
214214+ color: #ffffff;
215215+ min-height: 100vh;
216216+ display: flex;
217217+ align-items: center;
218218+ justify-content: center;
219219+ padding: 20px;
220220+ }
221221+222222+ .container {
223223+ background: rgba(40, 40, 40, 0.95);
224224+ border: 1px solid #4a4a4a;
225225+ border-radius: 16px;
226226+ padding: 48px;
227227+ max-width: 480px;
228228+ width: 100%;
229229+ text-align: center;
230230+ backdrop-filter: blur(10px);
231231+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
232232+ }
233233+234234+ .logo {
235235+ width: 48px;
236236+ height: 48px;
237237+ margin: 0 auto 24px;
238238+ background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%);
239239+ border-radius: 12px;
240240+ display: flex;
241241+ align-items: center;
242242+ justify-content: center;
243243+ font-weight: bold;
244244+ font-size: 24px;
245245+ color: white;
246246+ }
247247+248248+ h1 {
249249+ font-size: 28px;
250250+ font-weight: 600;
251251+ margin-bottom: 12px;
252252+ color: #ffffff;
253253+ }
254254+255255+ .subtitle {
256256+ color: #a0a0a0;
257257+ margin-bottom: 32px;
258258+ font-size: 16px;
259259+ line-height: 1.5;
260260+ }
261261+262262+ .step {
263263+ margin-bottom: 32px;
264264+ text-align: left;
265265+ }
266266+267267+ .step-number {
268268+ display: inline-flex;
269269+ align-items: center;
270270+ justify-content: center;
271271+ width: 24px;
272272+ height: 24px;
273273+ background: #ff6b35;
274274+ color: white;
275275+ border-radius: 50%;
276276+ font-size: 14px;
277277+ font-weight: 600;
278278+ margin-right: 12px;
279279+ }
280280+281281+ .step-title {
282282+ font-weight: 600;
283283+ margin-bottom: 8px;
284284+ color: #ffffff;
285285+ }
286286+287287+ .step-description {
288288+ color: #a0a0a0;
289289+ font-size: 14px;
290290+ margin-left: 36px;
291291+ }
292292+293293+ .button {
294294+ display: inline-block;
295295+ background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%);
296296+ color: white;
297297+ padding: 16px 32px;
298298+ text-decoration: none;
299299+ border-radius: 12px;
300300+ font-weight: 600;
301301+ font-size: 16px;
302302+ margin-bottom: 24px;
303303+ transition: all 0.2s ease;
304304+ box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
305305+ }
306306+307307+ .button:hover {
308308+ transform: translateY(-2px);
309309+ box-shadow: 0 8px 20px rgba(255, 107, 53, 0.4);
310310+ }
311311+312312+ .input-group {
313313+ margin-bottom: 24px;
314314+ text-align: left;
315315+ }
316316+317317+ label {
318318+ display: block;
319319+ margin-bottom: 8px;
320320+ font-weight: 500;
321321+ color: #ffffff;
322322+ }
323323+324324+ textarea {
325325+ width: 100%;
326326+ background: #2a2a2a;
327327+ border: 2px solid #4a4a4a;
328328+ border-radius: 8px;
329329+ padding: 16px;
330330+ color: #ffffff;
331331+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
332332+ font-size: 14px;
333333+ line-height: 1.4;
334334+ resize: vertical;
335335+ min-height: 120px;
336336+ transition: border-color 0.2s ease;
337337+ }
338338+339339+ textarea:focus {
340340+ outline: none;
341341+ border-color: #ff6b35;
342342+ box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1);
343343+ }
344344+345345+ textarea::placeholder {
346346+ color: #666;
347347+ }
348348+349349+ .submit-btn {
350350+ background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%);
351351+ color: white;
352352+ border: none;
353353+ padding: 16px 32px;
354354+ border-radius: 12px;
355355+ font-weight: 600;
356356+ font-size: 16px;
357357+ cursor: pointer;
358358+ transition: all 0.2s ease;
359359+ box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
360360+ width: 100%;
361361+ }
362362+363363+ .submit-btn:hover {
364364+ transform: translateY(-2px);
365365+ box-shadow: 0 8px 20px rgba(255, 107, 53, 0.4);
366366+ }
367367+368368+ .submit-btn:disabled {
369369+ opacity: 0.6;
370370+ cursor: not-allowed;
371371+ transform: none;
372372+ }
373373+374374+ .status {
375375+ margin-top: 16px;
376376+ padding: 12px;
377377+ border-radius: 8px;
378378+ font-size: 14px;
379379+ display: none;
380380+ }
381381+382382+ .status.success {
383383+ background: rgba(52, 168, 83, 0.1);
384384+ border: 1px solid rgba(52, 168, 83, 0.3);
385385+ color: #34a853;
386386+ }
387387+388388+ .status.error {
389389+ background: rgba(234, 67, 53, 0.1);
390390+ border: 1px solid rgba(234, 67, 53, 0.3);
391391+ color: #ea4335;
392392+ }
393393+ </style>
394394+</head>
395395+<body>
396396+ <div class="container">
397397+ <div class="logo">A</div>
398398+ <h1>Anthropic Authentication</h1>
399399+ <p class="subtitle">Connect your Anthropic account to continue</p>
400400+401401+ <div class="step">
402402+ <div class="step-title">
403403+ <span class="step-number">1</span>
404404+ Authorize with Anthropic
405405+ </div>
406406+ <div class="step-description">
407407+ Click the button below to open the Anthropic authorization page
408408+ </div>
409409+ </div>
410410+411411+ <a href="$AUTH_URL" class="button" target="_blank">
412412+ Open Anthropic Authorization
413413+ </a>
414414+415415+ <div class="step">
416416+ <div class="step-title">
417417+ <span class="step-number">2</span>
418418+ Paste your authorization token
419419+ </div>
420420+ <div class="step-description">
421421+ After authorizing, copy the token and paste it below
422422+ </div>
423423+ </div>
424424+425425+ <form id="tokenForm">
426426+ <div class="input-group">
427427+ <label for="token">Authorization Token:</label>
428428+ <textarea
429429+ id="token"
430430+ name="token"
431431+ placeholder="Paste your token here..."
432432+ required
433433+ ></textarea>
434434+ </div>
435435+ <button type="submit" class="submit-btn" id="submitBtn">
436436+ Complete Authentication
437437+ </button>
438438+ </form>
439439+440440+ <div id="status" class="status"></div>
441441+ </div>
442442+443443+ <script>
444444+ document.getElementById('tokenForm').addEventListener('submit', function(e) {
445445+ e.preventDefault();
446446+447447+ const token = document.getElementById('token').value.trim();
448448+ if (!token) {
449449+ showStatus('Please paste your authorization token', 'error');
450450+ return;
451451+ }
452452+453453+ // Ensure token has content before creating file
454454+ if (token.length > 0) {
455455+ // Save the token as a downloadable file
456456+ const blob = new Blob([token], { type: 'text/plain' });
457457+ const a = document.createElement('a');
458458+ a.href = URL.createObjectURL(blob);
459459+ a.download = "anthropic_token.txt";
460460+ document.body.appendChild(a); // Append to body to ensure it works in all browsers
461461+ a.click();
462462+ document.body.removeChild(a); // Clean up
463463+464464+ // Verify file creation
465465+ console.log("Token file created with content length: " + token.length);
466466+ } else {
467467+ showStatus('Empty token detected, please provide a valid token', 'error');
468468+ return;
469469+ }
470470+471471+ document.getElementById('submitBtn').disabled = true;
472472+ document.getElementById('submitBtn').textContent = "Token saved, you may close this tab.";
473473+ showStatus('Token file downloaded! You can close this window.', 'success');
474474+475475+ // setTimeout(() => {
476476+ // window.close();
477477+ // }, 2000);
478478+ });
479479+480480+ function showStatus(message, type) {
481481+ const status = document.getElementById('status');
482482+ status.textContent = message;
483483+ status.className = 'status ' + type;
484484+ status.style.display = 'block';
485485+ }
486486+487487+ // Auto-close after 10 minutes
488488+ setTimeout(() => {
489489+ window.close();
490490+ }, 600000);
491491+ </script>
492492+</body>
493493+</html>
494494+EOF
495495+496496+# Open the HTML file
497497+if command -v xdg-open >/dev/null 2>&1; then
498498+ xdg-open "$TEMP_HTML" >/dev/null 2>&1 &
499499+elif command -v open >/dev/null 2>&1; then
500500+ open "$TEMP_HTML" >/dev/null 2>&1 &
501501+elif command -v start >/dev/null 2>&1; then
502502+ start "$TEMP_HTML" >/dev/null 2>&1 &
503503+fi
504504+505505+# Wait for user to download the token file
506506+TOKEN_FILE="$HOME/Downloads/anthropic_token.txt"
507507+508508+for i in $(seq 1 60); do
509509+ if [ -f "$TOKEN_FILE" ]; then
510510+ AUTH_CODE=$(cat "$TOKEN_FILE" | tr -d '\r\n')
511511+ rm -f "$TOKEN_FILE"
512512+ break
513513+ fi
514514+ sleep 2
515515+done
516516+517517+# Clean up the temporary HTML file
518518+rm -f "$TEMP_HTML"
519519+520520+if [ -z "$AUTH_CODE" ]; then
521521+ exit 1
522522+fi
523523+524524+# Exchange code for tokens
525525+ACCESS_TOKEN=$(exchange_authorization_code "$AUTH_CODE" "$VERIFIER")
526526+if [ -n "$ACCESS_TOKEN" ]; then
527527+ echo "$ACCESS_TOKEN"
528528+ exit 0
529529+else
530530+ exit 1
531531+fi