110 lines
4.0 KiB
Markdown
110 lines
4.0 KiB
Markdown
# 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.
|