feat: vibed out some slop over here
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# raycast-api
|
||||
[](https://sladge.net)
|
||||
|
||||
Python client for `backend.raycast.com`. Bring-your-own-credentials: the
|
||||
signing secret and algorithm are extracted from a local Raycast install at
|
||||
setup time - nothing sensitive ships with the package.
|
||||
|
||||
Not affiliated with Raycast. You need your own account, subscription, and a
|
||||
copy of the desktop app on macOS to run discovery once.
|
||||
|
||||
## Install
|
||||
|
||||
As a CLI tool (pulls in the discovery extra):
|
||||
|
||||
```bash
|
||||
uv tool install "raycast-api[discovery] @ git+https://git.kotikot.com/beaver/raycast-api"
|
||||
```
|
||||
|
||||
As a library inside another project (runtime only, no discovery deps):
|
||||
|
||||
```bash
|
||||
uv add "raycast-api @ git+https://git.kotikot.com/beaver/raycast-api"
|
||||
```
|
||||
|
||||
The runtime needs only `aiohttp`. The `discovery` extra adds `esprima` (a JS
|
||||
AST parser) and is required only for `init` / `refresh`.
|
||||
|
||||
## Credentials
|
||||
|
||||
```bash
|
||||
raycast-api init # extract signing spec + secret from the local app → ./config.json
|
||||
export RAYCAST_BEARER='rca_...' # your OAuth access token, sniffed from the desktop client
|
||||
raycast-api ask --stream 'hello, raycast'
|
||||
```
|
||||
|
||||
`init` writes `config.json` (chmod 600) containing the signing spec, the
|
||||
launcher-derived secret, and bundle/launcher hashes used as a cache key.
|
||||
Re-run `raycast-api refresh` after a Raycast update - the cache invalidates
|
||||
automatically when either hash changes.
|
||||
|
||||
## Use
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from raycast_api import Client, Config, Message
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(
|
||||
config=Config.load("config.json"),
|
||||
bearer_token="rca_...",
|
||||
device_id="<64 hex>", # any stable per-install id
|
||||
) as client:
|
||||
result = await client.chat.complete(
|
||||
model="Claude Sonnet 4.6",
|
||||
messages=[Message.user("hello, raycast")],
|
||||
)
|
||||
print(result.text)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Streaming:
|
||||
|
||||
```python
|
||||
async for chunk in client.chat.stream(model="...", messages=[...]):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
```
|
||||
|
||||
Endpoints: `client.chat`, `client.models`, `client.me`, `client.files`.
|
||||
Interrupted streams resume via `client.chat.resume(buffer_id=..., last_event_id=...)`.
|
||||
|
||||
## CLI
|
||||
|
||||
```
|
||||
raycast-api init [--app-path PATH] [--output config.json] [--force] [--no-cache]
|
||||
raycast-api refresh [--app-path PATH] [--config config.json]
|
||||
raycast-api inspect [--config config.json] [--verify | --app-path PATH] [--quiet]
|
||||
raycast-api ask PROMPT [--bearer ...] [--device-id ...] [--model ...] [--stream]
|
||||
```
|
||||
|
||||
- `init` - run discovery, write `config.json`. Cached by `(bundle_hash, launcher_hash)`.
|
||||
- `refresh` - bypass the cache and re-derive. Use after a launcher rebuild
|
||||
that didn't touch the JS bundle.
|
||||
- `inspect` - print the saved config with the secret redacted. `--verify`
|
||||
rechecks hashes against a live Raycast install; `--quiet` collapses to an
|
||||
exit code (`0` current, `1` stale, `2` unverifiable) for scripting.
|
||||
- `ask` - one-shot smoke test against `chat.complete`.
|
||||
|
||||
## How the signing scheme is recovered
|
||||
|
||||
Raycast signs every request with a rotated-alphabet HMAC over a canonical
|
||||
string. `init` recovers this end-to-end from your local install, without
|
||||
running any of Raycast's code:
|
||||
|
||||
1. **Launcher.** Read the Mach-O binary, find the 64-hex `signature_secret`
|
||||
by anchored byte-pattern.
|
||||
2. **Bundle.** Locate `index.mjs` inside the app, hash it for the cache key.
|
||||
3. **AST.** Parse the bundle with `esprima`. Walk for the signing function
|
||||
by structural shape - it takes `(method, path, body, ts, key)`, calls
|
||||
`crypto.createHmac`, and joins its inputs with a single character. From
|
||||
its caller recover the rot transform: alphabet ranges and shift counts
|
||||
are literals in the source.
|
||||
4. **Spec.** Emit a portable `signing_spec` (ranges, join char, HMAC and
|
||||
body-hash algorithms, key/output encodings). The runtime signer reads
|
||||
only that - no version-specific code anywhere in the package.
|
||||
|
||||
As long as Raycast keeps a rot-transform over an HMAC, a future build only
|
||||
needs `raycast-api refresh`, not a release.
|
||||
Reference in New Issue
Block a user