Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
HOST=127.0.0.1
PORT=4310
DATABASE_PATH=data/opendots.sqlite
# Optional: absolute path to the Codex CLI executable on this same desktop.
# CODEX_CLI_PATH=/absolute/path/to/codex
# Keep OWNER_ID stable: it owns the persistent Intelligence conversations.
OWNER_ID=opendots-owner
# npm run dev allows http://127.0.0.1:5173; override for a different exact origin.
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

# OpenDots

### Always-on AI coworkers that move between text, calls, and Slack.
### A self-hosted desktop AI workspace for your own Codex CLI account.

**An open-source template for persistent AI agents, each with its own computer. Available on Web and Mobile.**
**Run a personal AI workspace with your signed-in OpenAI Codex CLI account. Each user keeps their Codex session on their own computer.**

Built with [CopilotKit](https://github.com/CopilotKit/CopilotKit) and [AG-UI](https://docs.ag-ui.com/introduction). · [Get started](#get-started) · [Overview](#overview) · [Architecture](#architecture) · [Features](#features) · [Contributing](CONTRIBUTING.md)
OpenDots is an open-source, self-hosted AI agent workspace built with [CopilotKit](https://github.com/CopilotKit/CopilotKit) and [AG-UI](https://docs.ag-ui.com/introduction). This version connects locally to Codex CLI, so each person runs their own server and keeps Codex credentials in their desktop profile. · [Get started](#get-started) · [Connect Codex](docs/SETUP.md#use-your-local-codex-account) · [Features](#features) · [Contributing](CONTRIBUTING.md)

[![CI](https://github.com/CopilotKit/OpenDots/actions/workflows/ci.yml/badge.svg)](https://github.com/CopilotKit/OpenDots/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
![Alpha](https://img.shields.io/badge/status-alpha-orange.svg)

Fully self-hostable. Clone this template and customize it however you want.
Self-host it on the same desktop as Codex CLI. Your Codex credentials stay in Codex's local sign-in profile; OpenDots does not ask you to paste or upload a token.

[**Building on OpenDots? Meet with the CopilotKit team →**](https://www.copilotkit.ai/talk-to-an-engineer?ref=opendots_readme)

Expand Down Expand Up @@ -160,7 +160,7 @@ cp .env.example .env
npm run dev
```

Open **http://127.0.0.1:5173**. You can create Spaces, write pages, and configure Dots before connecting services. Add your conversation and model settings to `.env` to start chatting.
Open **http://127.0.0.1:5173**. You can create Spaces, write pages, and configure Dots before connecting services. To chat using the Codex account on this desktop, install and sign in to Codex CLI, then choose **Connect Codex** in Settings; see [local Codex setup](docs/SETUP.md#use-your-local-codex-account). You can also configure a server-side conversation and model provider in `.env`.

See [Setup](docs/SETUP.md) for configuration, Slack, calls, the browser service, and Docker.

Expand All @@ -171,6 +171,7 @@ See [Setup](docs/SETUP.md) for configuration, Slack, calls, the browser service,
| Spaces and Specialist Dots | Saved names, role instructions, and per-Dot research and memory permissions |
| Pages | Searchable library, visual editor, slash commands, autosave, and revision checks |
| Conversations | React SDK chat and Threads integration, page-specific conversations, and source links |
| Local Codex account | Connect the Codex CLI session on this desktop; each self-hosted user runs their own local server and account |
| Slack | Managed Channels SDK declaration with workspace and user allowlists |
| Calls | WebRTC speech, delegated compute, bounded sessions, hangup, and timeline receipts |
| Background work | Scheduled server-side turns in their original conversation, with pause and retry controls |
Expand Down
60 changes: 49 additions & 11 deletions docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,44 @@ cp .env.example .env
npm run dev
```

Open http://127.0.0.1:5173. The API runs on port 4310. Without service credentials, the app shows its setup state; it does not generate simulated replies.
Open http://127.0.0.1:5173. The API runs on port 4310. Without service credentials or a local Codex sign-in, the app shows its setup state; it does not generate simulated replies.

## Use your local Codex account

OpenDots can use the Codex account already signed in with Codex CLI. Run both Codex CLI and the OpenDots server on the same desktop. The server starts Codex's local app-server over standard input/output; it does not send your Codex sign-in to OpenDots, the browser, or a hosted service. Codex keeps its own sign-in on your device.

This is a per-desktop setup, not a shared hosted login. Each person who wants to use their own Codex account runs their own OpenDots server on their computer. Do not expose the server to the public internet or share a server process between people: this template has one local workspace and one local Codex account, not per-user identity or account isolation. Docker and remote hosting do not inherit the desktop's Codex sign-in.

1. Install Codex CLI using the [official Codex installation instructions](https://github.com/openai/codex#readme). The npm installation command is:

```sh
npm install -g @openai/codex
```

2. Sign in with your ChatGPT account. Running `codex login` starts the browser sign-in flow:

```sh
codex login
codex login status
```

`codex login status` should report that you are logged in using ChatGPT. Complete the sign-in on the same desktop account that will run OpenDots.

3. Start OpenDots locally as described above. If the server cannot find Codex CLI, put its executable path in `.env`:

```dotenv
CODEX_CLI_PATH=/absolute/path/to/codex
```

Find the path with `command -v codex` (macOS/Linux) or `where.exe codex` (Windows). Restart OpenDots after changing `.env`.

4. In OpenDots, open **Settings & setup** and choose **Connect Codex**. A connected status means OpenDots can read the local Codex account. Start a new conversation and send a message to confirm model access.

OpenDots checks the account through Codex's local app-server and chooses a model that the account currently reports as available. Conversation text and thread history remain in the local Codex profile; OpenDots stores the mapping from its conversation to the Codex thread in its SQLite database. To remove the connection, sign out with `codex logout` or stop the OpenDots server.

If the account check fails, confirm `codex login status` succeeds in the same operating-system account, set `CODEX_CLI_PATH` if needed, and restart OpenDots. No API key or pasted Codex token is required for this local connection.

For a built local app, keep the server bound to loopback (`HOST=127.0.0.1`) so only apps on that desktop can reach it.

For a built local app:

Expand All @@ -27,16 +64,17 @@ Open http://127.0.0.1:4310. Keep the server running for background work.

Edit `.env` on the server and restart after changes:

| Variable | Purpose |
| --------------------------------------------- | --------------------------------------------------------- |
| `INTELLIGENCE_API_KEY` | Project credential for conversation persistence |
| `INTELLIGENCE_API_URL`, `INTELLIGENCE_WS_URL` | Endpoint overrides for your Intelligence deployment |
| `OPENAI_API_KEY`, `OPENAI_MODEL` | Model credential and model identifier |
| `OPENAI_BASE_URL` | Compatible model API endpoint |
| `OWNER_ID` | Stable identity used for this deployment's conversations |
| `DATABASE_PATH` | SQLite file containing pages, workspace and work metadata |
| `OWNER_TOKEN` | Application access token; required for external bindings |
| `APP_ORIGIN` | Exact browser origin when using a proxy or custom domain |
| Variable | Purpose |
| --------------------------------------------- | ------------------------------------------------------------------ |
| `INTELLIGENCE_API_KEY` | Project credential for conversation persistence |
| `INTELLIGENCE_API_URL`, `INTELLIGENCE_WS_URL` | Endpoint overrides for your Intelligence deployment |
| `OPENAI_API_KEY`, `OPENAI_MODEL` | Model credential and model identifier |
| `OPENAI_BASE_URL` | Compatible model API endpoint |
| `CODEX_CLI_PATH` | Optional path to the Codex CLI executable for local account access |
| `OWNER_ID` | Stable identity used for this deployment's conversations |
| `DATABASE_PATH` | SQLite file containing pages, workspace and work metadata |
| `OWNER_TOKEN` | Application access token; required for external bindings |
| `APP_ORIGIN` | Exact browser origin when using a proxy or custom domain |

The model environment variable names follow the configured provider adapter. Provider credentials belong in `.env`, not client-side variables or source code. Conversation history lives in the configured Intelligence project; copying the SQLite file alone does not back up that history.

Expand Down
22 changes: 20 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,27 @@
<meta name="theme-color" content="#f8f7f4" />
<meta
name="description"
content="OpenDots is a self-hosted template for your documents and personal assistants."
content="Run OpenDots on your desktop with your own signed-in OpenAI Codex CLI account. A self-hosted AI workspace for personal agents, documents, and conversations."
/>
<title>OpenDots</title>
<meta property="og:type" content="website" />
<meta
property="og:title"
content="OpenDots — Local Codex desktop workspace"
/>
<meta
property="og:description"
content="Connect OpenDots to the Codex CLI session on your own desktop. Each user runs their own local workspace and keeps their credentials on-device."
/>
<meta name="twitter:card" content="summary" />
<meta
name="twitter:title"
content="OpenDots — Local Codex desktop workspace"
/>
<meta
name="twitter:description"
content="A self-hosted desktop AI workspace connected to your own OpenAI Codex CLI account."
/>
<title>OpenDots — Local Codex desktop workspace</title>
</head>
<body>
<div id="root"></div>
Expand Down
5 changes: 4 additions & 1 deletion src/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,9 @@ export function App() {
{error}
</p>
)}
<p className="muted">The token stays in this tab’s session storage.</p>
<p className="muted">
Your access token is stored in this browser on this device.
</p>
</main>
);
if (!state || !workspace || !dot)
Expand Down Expand Up @@ -581,6 +583,7 @@ export function App() {
paused={state.settings.paused}
onSaved={refresh}
onComputer={() => setPane(true)}
codexConnected={workspace.setup.codex ?? false}
onSchedule={() =>
setDialog({ type: 'schedule', threadId: thread.id })
}
Expand Down
17 changes: 13 additions & 4 deletions src/client/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { pageReviewSchema, pageReviewTool } from '../shared/page-review';
import { contextualMessage, type PageContext } from './page-context';
import { api } from './api';
import type { Page } from '../server/pages';
import type { Message } from '@ag-ui/core';
import { useEffect, useRef, useState } from 'react';
import {
CopilotChatToolCallsView,
Expand Down Expand Up @@ -41,6 +42,7 @@ export function Chat({
onSaved,
onSchedule,
onComputer,
codexConnected,
}: {
thread: Conversation;
dot: Dot;
Expand All @@ -52,6 +54,7 @@ export function Chat({
onSaved: () => void;
onSchedule: () => void;
onComputer?: () => void;
codexConnected: boolean;
}) {
const { agent, isReady } = useAgent({
agentId: `chat-${thread.id}`,
Expand Down Expand Up @@ -112,7 +115,13 @@ export function Chat({
let active = true;
void copilotkit
.connectAgent({ agent })
.then(() => {
.then(async () => {
if (codexConnected) {
const history = await api<
Array<{ id: string; role: string; content: string }>
>(`/conversations/${thread.id}/history`);
agent.setMessages(history as unknown as typeof agent.messages);
}
if (active) setLoaded(true);
})
.catch((e) => {
Expand All @@ -124,7 +133,7 @@ export function Chat({
return () => {
active = false;
};
}, [agent, copilotkit, isReady]);
}, [agent, copilotkit, isReady, codexConnected, thread.id]);
const send = async (text: string) => {
if (!text.trim() || running || !loaded || !contextReady || paused) return;
setError('');
Expand Down Expand Up @@ -212,7 +221,7 @@ export function Chat({
);
const visible = agent.messages.filter(
(message) =>
!isInternalVoiceReceipt(message) &&
!isInternalVoiceReceipt(message as unknown as Message) &&
['user', 'assistant'].includes(message.role) &&
((typeof message.content === 'string' && message.content.trim()) ||
(message.role === 'assistant' &&
Expand Down Expand Up @@ -319,7 +328,7 @@ export function Chat({
</div>
)}
<ChatTranscript
messages={visible}
messages={visible as unknown as Message[]}
calls={calls}
renderTools={(message) => (
<CopilotChatToolCallsView
Expand Down
1 change: 1 addition & 0 deletions src/client/PageConversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export function PageConversation({
initialPrompt={pending}
onConsumed={() => setPending(undefined)}
voiceReady={workspace.setup.voice}
codexConnected={workspace.setup.codex ?? false}
calls={workspace.calls.filter((call) => call.threadId === thread.id)}
paused={paused}
onSaved={onRefresh}
Expand Down
22 changes: 22 additions & 0 deletions src/client/WorkspaceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function WorkspaceDialog({
dialog.type === 'dot' ? (dialog.dot?.skillDeliveryEnabled ?? false) : false,
);
const [busy, setBusy] = useState(false);
const [codexBusy, setCodexBusy] = useState(false);
const [error, setError] = useState('');
const container = useRef<HTMLElement>(null);
useEffect(() => {
Expand Down Expand Up @@ -373,6 +374,27 @@ export function WorkspaceDialog({
: 'needs VOICE_API_KEY and VOICE_MODEL'}
.
</p>
<strong>Codex account</strong>
<p>
{workspace.setup.codex
? 'Connected using the Codex account session stored on this device.'
: 'Connect using the Codex CLI session on this device. OpenDots does not copy or store your Codex credentials.'}
</p>
<button
type="button"
disabled={codexBusy}
onClick={async () => {
setCodexBusy(true);
await mutate('/codex/verify', 'POST');
setCodexBusy(false);
}}
>
{codexBusy
? 'Checking…'
: workspace.setup.codex
? 'Verify connection'
: 'Connect Codex'}
</button>
<a
href="https://github.com/CopilotKit/OpenDots/blob/main/docs/SETUP.md"
target="_blank"
Expand Down
Loading