CLI
Before anything else: on macOS we recommend the desktop app. Download it, drag Lore into Applications, and sign in; it installs the CLI for you automatically, along with the plugin and background session capture, so there's nothing to set up by hand. The rest of this section is for installing and scripting the CLI directly.
Download the macOS app
The Lore CLI is the local interface to Lore. It finds and uploads your coding-agent sessions and gives you scriptable access to the threads you can see. The npm package is @loredotlink/cli and the binary it installs is lore. (Slash commands like /lore:share come from the Lore plugin, not the CLI.)
Install
npm install -g @loredotlink/cli
The CLI requires Node 20 or newer.
Authenticate
lore login
This opens a browser to finish the auth flow. Signing in with an email on a domain that already has a Lore workspace adds you to that workspace automatically; otherwise it creates one for the domain. Related commands:
lore whoami shows the signed-in account and active workspace.
lore status inspects the background uploader.
lore logout clears local credentials.
On macOS, after login the CLI starts a background process that uploads your Claude Code and Cowork sessions according to your consent settings and upload filters. Nothing is uploaded that you have not allowed.
Work with threads
Thread operations live under the threads resource:
lore threads list # list threads you can see
lore threads get <thread_id_or_url> # fetch one thread
lore threads ask "<question>" # ask across your threads
lore threads fork <thread_id_or_url> # get a handoff summary to continue a thread
Every operation derives its flags and --help text from the API contract, so lore threads list --help is always accurate for the version you have installed.
Share a session
lore export # share the current Claude Code session
lore export --visibility workspace # override the visibility for this share
lore share-cowork # share a Cowork session
Other commands
lore workspaces list lists workspaces you belong to.
lore skills manages skill sharing across your team: publish, install, uninstall, sync, status, list, and scan, plus proposals, approve, and reject for reviewing proposed changes. See Skills.
Run lore --help for the full command tree, or lore <command> --help for any command.
Beyond the CLI
- Authentication covers how
lore login obtains and refreshes your token.
- MCP Server connects an agent to Lore over the Model Context Protocol.
- API Reference documents the REST API the CLI is built on.
MCP Server
Lore runs a hosted Model Context Protocol (MCP) server, so any MCP-capable agent (Claude Code, Claude Desktop, and others) can read, search, and create Lore threads directly. The server speaks MCP over streamable HTTP.
Endpoint
https://mcp.lore.link/mcp
POST /mcp is the JSON-RPC entry point for tool calls.
GET /mcp exists for protocol compliance; the server runs stateless streamable HTTP, so there is no server-initiated notification stream today.
- Authentication uses OAuth: on first connect the server returns its authorization metadata and the client walks you through sign-in. The same workspace permissions that apply on the web apply over MCP, so you only ever see threads you are allowed to see.
Connect a client
Using the Lore plugin? The plugin already bundles the MCP server, so its tools are available as soon as it's installed. You can skip the manual setup below; it's only for clients that aren't running the plugin. See Setup.
Every client connects to the same remote URL (https://mcp.lore.link/mcp) and runs the OAuth sign-in on first use.
Claude Code
claude mcp add --transport http lore https://mcp.lore.link/mcp
Then run /mcp to complete authentication.
VS Code, Claude Desktop, and other clients take a config block wherever they accept a remote (HTTP) MCP server:
{
"mcpServers": {
"lore": {
"type": "http",
"url": "https://mcp.lore.link/mcp"
}
}
}
Claude Code can instead install the Lore plugin, which bundles this MCP server alongside the slash commands so there's nothing else to wire up; see the plugin README.
Clients that auto-discover servers can read the server card (SEP-2127) for the name, icon, description, and remote URL.
Tools
| Tool |
What it does |
list_threads |
List Lore threads visible to the caller across every workspace in the token. |
get_thread |
Fetch one thread by id. Returns the thread if visible, an error otherwise. |
search_threads |
Search threads by title across the caller's workspaces. Returns a small page of matches. |
fork_thread |
Generate an intent-conditioned handoff summary for continuing a thread. |
share_session |
Persist a coding-agent session transcript as a Lore thread. Takes the transcript and harness, plus optional title, visibility (private, workspace, or public), workspace_id, and a natural-language highlight that resolves to an anchored URL. |
list_threads, get_thread, search_threads, and fork_thread are read-only. share_session writes a new thread. When visibility is omitted, it follows the author's Share new threads with my workspace preference (on by default), the same default used everywhere in Lore: workspace when the author belongs to a workspace and the setting is on, otherwise private.
Related pages
Authentication
Lore authenticates with WorkOS-issued OAuth tokens. Every request carries a short-lived bearer token in the Authorization header. (The one exception is Upload API keys, which exist only for upload integrations and are managed with lore upload-api-keys.) The same workspace permissions apply everywhere, so a token only ever sees the threads and workspaces its owner is allowed to see.
Permissions and access
The public API is read-only: it lists, reads, and searches threads; it never creates, edits, or deletes them. There are no scopes to request. A token simply carries your identity, and every endpoint returns only what that identity is already allowed to see.
Access granularity comes entirely from per-thread visibility, not from token scopes:
- Private: visible only to the thread's owner.
- Workspace: visible to everyone in the owner's workspace (the same verified email domain).
- Public: visible to anyone.
So a token can read its owner's private threads, their workspace's threads, and public threads, and nothing else. A request for a thread the caller can't see returns 404 rather than 403, so the API never reveals that a hidden thread exists; a request without a valid token returns 401.
With the CLI
lore login runs WorkOS CLI Auth in your browser, stores the result locally, and keeps the CLI's cli token slot fresh on disk (at ~/.lore/tokens.json). From then on the CLI attaches a valid bearer token to every request automatically.
lore login: authenticate with WorkOS CLI Auth (opens a browser/device prompt).
lore whoami: show the signed-in account and active workspace.
lore logout: clear stored credentials.
Signing in with an email whose domain already has a Lore workspace adds you to it; otherwise it creates one for that domain.
With the MCP server
The MCP server uses standard OAuth discovery, so MCP clients authenticate without any manual token handling. On first connect the server advertises its authorization metadata (RFC 9728 protected-resource metadata at /.well-known/oauth-protected-resource) and the client walks you through sign-in.
claude mcp add --transport http lore https://mcp.lore.link/mcp
Then run /mcp inside Claude Code to complete authentication. See the MCP Server page for the full tool list.
Connect an agent (device authorization flow)
An agent that is neither the CLI nor an MCP client can authenticate with the OAuth 2.0 device authorization grant (RFC 8628), the most agent-friendly path, since the user approves in a browser without the agent embedding one. Every endpoint is discovered at runtime from the metadata chain, so nothing is hardcoded and the flow keeps working if the authorization server moves.
Discover the endpoints, register a client once (RFC 7591), then run the device flow:
# 1. Protected-resource metadata names the authorization server; its metadata
# gives the endpoints. (When the authorization server enables dynamic client
# registration, its metadata advertises a registration_endpoint.)
AS=$(curl -s https://lore.link/.well-known/oauth-protected-resource | jq -r '.authorization_servers[0]')
META=$(curl -s "$AS/.well-known/oauth-authorization-server")
REGISTER=$(echo "$META" | jq -r '.registration_endpoint')
DEVICE=$(echo "$META" | jq -r '.device_authorization_endpoint')
TOKEN=$(echo "$META" | jq -r '.token_endpoint')
# 2. Register a public client, then start the device flow.
CLIENT_ID=$(curl -s -X POST "$REGISTER" -H 'content-type: application/json' \
-d '{"client_name":"my-agent","grant_types":["urn:ietf:params:oauth:grant-type:device_code","refresh_token"],"token_endpoint_auth_method":"none"}' \
| jq -r '.client_id')
curl -s -X POST "$DEVICE" -d "client_id=$CLIENT_ID" -d 'scope=openid email profile offline_access'
# -> { "device_code": "...", "user_code": "ABCD-EFGH", "verification_uri": "...", "interval": 5 }
# 3. Show the user verification_uri + user_code, then poll the token endpoint.
curl -s -X POST "$TOKEN" \
-d 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \
-d "device_code=$DEVICE_CODE" -d "client_id=$CLIENT_ID"
# pending -> { "error": "authorization_pending" }; wait `interval` seconds and retry.
# done -> { "access_token": "...", "refresh_token": "...", "expires_in": ... }
The same flow in TypeScript:
// Discover endpoints; no hardcoded URLs.
const prm = await (await fetch("https://lore.link/.well-known/oauth-protected-resource")).json();
const meta = await (await fetch(`${prm.authorization_servers[0]}/.well-known/oauth-authorization-server`)).json();
// Register a public client once (RFC 7591).
const { client_id } = await (await fetch(meta.registration_endpoint, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
client_name: "my-agent",
grant_types: ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
token_endpoint_auth_method: "none",
}),
})).json();
// Start the device flow and show the user where to approve.
const device = await (await fetch(meta.device_authorization_endpoint, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ client_id, scope: "openid email profile offline_access" }),
})).json();
console.log(`Visit ${device.verification_uri} and enter ${device.user_code}`);
// Poll until the user approves.
let tokens;
for (;;) {
await new Promise((r) => setTimeout(r, device.interval * 1000));
const res = await (await fetch(meta.token_endpoint, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: device.device_code,
client_id,
}),
})).json();
if (res.access_token) { tokens = res; break; }
if (res.error !== "authorization_pending" && res.error !== "slow_down") throw new Error(res.error);
}
await fetch("https://api.lore.link/api/whoami", {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
Raw HTTP
If you call the REST API directly, send the bearer token in the Authorization header:
curl -H "Authorization: Bearer $LORE_TOKEN" https://api.lore.link/api/whoami
await fetch("https://api.lore.link/api/whoami", {
headers: { Authorization: `Bearer ${process.env.LORE_TOKEN}` },
});
import os, requests
requests.get(
"https://api.lore.link/api/whoami",
headers={"Authorization": f"Bearer {os.environ['LORE_TOKEN']}"},
)
Tokens are issued through the WorkOS flow above; the CLI is the supported way to obtain and refresh one. Requests to a protected endpoint without a valid token return 401.
Token lifetime, refresh, and scopes
- Access tokens are short-lived JWTs. Treat the token's
exp claim as authoritative and refresh before it expires rather than assuming a fixed lifetime. The CLI refreshes automatically once a token is within ten seconds of expiry.
- Refresh tokens are single-use and rotated. Request the
offline_access scope to receive one. Each refresh returns a new access token and a new refresh token, and the previous refresh token is consumed, so reusing it fails with invalid_grant. Always persist the latest refresh token from each response.
- Scopes are the standard OpenID Connect set:
openid, email, profile, and offline_access. There are no custom Lore API scopes to request; the public API is read-only and access is governed entirely by identity and per-thread visibility.
- Signing out.
lore logout clears the locally stored tokens. There is no public token-revocation endpoint; refresh-token rotation and short access-token lifetimes bound the exposure of a leaked token.
See also
- CLI: installs and manages your token.
- API Reference: the endpoints these tokens unlock.
API Reference
The Lore REST API is a single typed contract (ts-rest) that the CLI, the web app, and the MCP tools are all built on. This page documents the base URL, authentication, response conventions, and the core thread endpoints. For the complete, always-current surface (search, mentions, favorites, and every field), the CLI is generated from the same contract, so lore <command> --help is authoritative.
A machine-readable OpenAPI 3 description of the public endpoints below is published at /openapi.json (also at /swagger.json). Import it into Insomnia or any OpenAPI-aware client directly, or load the ready-made Postman collection (also at /postman.json) and set its loreToken variable to a bearer token.
Base URL
https://api.lore.link/api
Authentication
Every request carries a bearer token: Authorization: Bearer <token>. Most tokens come from the WorkOS OAuth flow the CLI and MCP server run; upload integrations may also use Upload API keys on upload-session routes and the matching thread parse-status poll. See Authentication for how to obtain a WorkOS token. Requests without a valid token return 401.
Response conventions
A single resource returns a JSON object. List endpoints return a list envelope:
{
"type": "list",
"list_type": "thread",
"has_more": true,
"objects": []
}
- Results are newest-first on each endpoint's timeline field, with
id as the tiebreaker.
- Pages hold 20 items.
- Paginate with cursors that are plain resource IDs.
after=<id> returns items older than that item; before=<id> returns items newer than it. Both are exclusive.
- An invalid or non-visible cursor returns
200 with an empty list, not an error.
Endpoints
All endpoints require authentication and respect workspace visibility, so you only ever see what you are allowed to.
| Method |
Path |
Description |
GET |
/whoami |
The authenticated user and active workspace. |
GET |
/threads |
List threads visible to you. Paginated; supports after and before. |
GET |
/threads/:id |
Fetch a single thread by id. |
GET |
/threads/:id/parse_status |
Fetch only transcript_parsing_state and transcript_parsed_at for SDK upload polling. |
GET |
/threads/:id/similar |
Threads similar to a given one. |
GET |
/threads/live |
Recently active threads. |
POST |
/threads/ask |
Ask a natural-language question across your threads. |
GET /threads uses descending activity timestamp (last_activity_at when present, otherwise started_at) for ordering and cursor bounds.
To create a thread, share a session through the CLI or the MCP share_session tool rather than posting a raw transcript.
Errors
Every error is a JSON object with a message string and the matching HTTP status:
{ "message": "Thread not found or not visible to the caller." }
| Status |
Meaning |
401 |
Missing or invalid bearer token. |
404 |
Thread not found, or not visible to you. The API returns 404 (never 403) for a thread you can't see, so it never reveals that a hidden thread exists. |
500 |
Unexpected server error. |
A malformed request (for example, POST /threads/ask without a question) is rejected at the validation layer with a 400 whose body names the fields that failed, rather than the { "message": … } envelope above. There are no scopes, so you'll never see a permissions 403 on these read endpoints; the one extra status to expect is 409 from GET /threads/:id/similar while a thread's summary is still being prepared.
Rate limits
The public read endpoints documented here are not rate-limited. Even so, write an integration defensively: treat a 429 Too Many Requests as retryable, honor the Retry-After header, and back off before retrying. That keeps a client working if protective limits are introduced later, or if an upstream dependency throttles a burst (for example, the WorkOS authorization server while issuing or refreshing a token).
Webhooks
Lore does not currently emit outbound webhooks. To stay current with new threads, poll GET /threads or GET /threads/live, or use the MCP Server tools.
Example
curl -H "Authorization: Bearer $LORE_TOKEN" \
"https://api.lore.link/api/threads"
const res = await fetch("https://api.lore.link/api/threads", {
headers: { Authorization: `Bearer ${process.env.LORE_TOKEN}` },
});
if (!res.ok) throw new Error((await res.json()).message);
const { objects, has_more } = await res.json();
import os, requests
res = requests.get(
"https://api.lore.link/api/threads",
headers={"Authorization": f"Bearer {os.environ['LORE_TOKEN']}"},
)
res.raise_for_status()
page = res.json()
threads, has_more = page["objects"], page["has_more"]
Higher-level clients
Most integrations should use one of these instead of raw HTTP:
- The CLI:
lore <resource> <op>, generated from this contract, with JSON output.
- The MCP Server:
list_threads, get_thread, search_threads, fork_thread, and share_session.