agentgui 1.0.702 → 1.0.704

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -82,6 +82,7 @@ XState v5 machines provide formal state tracking alongside (not replacing) the e
82
82
  - `BASE_URL` - URL prefix (default: /gm)
83
83
  - `STARTUP_CWD` - Working directory passed to agents
84
84
  - `HOT_RELOAD` - Set to "false" to disable watch mode
85
+ - `CODEX_HOME` - Override Codex CLI home directory (default: `~/.codex`)
85
86
 
86
87
  ## ACP Tool Lifecycle
87
88
 
@@ -123,6 +124,11 @@ All routes are prefixed with `BASE_URL` (default `/gm`).
123
124
  - `GET /api/tools/:id/history` - Get tool install/update history (query: limit, offset)
124
125
  - `POST /api/tools/update` - Batch update all tools with available updates
125
126
  - `POST /api/tools/refresh-all` - Refresh all tool statuses from package manager
127
+ - `POST /api/codex-oauth/start` - Start Codex CLI OAuth flow (returns `{ authUrl, mode }`)
128
+ - `GET /api/codex-oauth/status` - Get current Codex OAuth state `{ status, email, error }`
129
+ - `POST /api/codex-oauth/relay` - Relay OAuth code+state from remote browser (body: `{ code, state }`)
130
+ - `POST /api/codex-oauth/complete` - Complete OAuth by pasting redirect URL (body: `{ url }`)
131
+ - `GET /codex-oauth2callback` - OAuth callback endpoint (redirect_uri for local flows)
126
132
 
127
133
  ## Tool Update System
128
134
 
@@ -286,6 +292,27 @@ Speech models (~470MB total) are downloaded automatically on server startup. No
286
292
  - **Client init:** `loadAgents()`, `loadConversations()`, `checkSpeechStatus()` run in parallel via `Promise.all()`.
287
293
  - **`perMessageDeflate: false`** on WebSocket server — msgpack binary doesn't compress well, and zlib was blocking the event loop on every streaming_progress send.
288
294
 
295
+ ## Codex CLI OAuth
296
+
297
+ OpenAI Codex CLI uses PKCE authorization code flow against `https://auth.openai.com`.
298
+
299
+ **Flow:**
300
+ 1. `POST /api/codex-oauth/start` generates PKCE (SHA-256 S256 challenge), CSRF state, returns `authUrl`
301
+ 2. User opens `authUrl` in browser and authenticates via OpenAI/ChatGPT
302
+ 3. **Local**: Browser redirects to `http://localhost:1455/auth/callback` — but since agentgui's server is on a different port, the redirect goes to `GET /codex-oauth2callback` (agentgui intercepts via matching route). Token exchange happens server-side.
303
+ 4. **Remote**: Redirect goes to `/codex-oauth2callback` which serves a relay page. Relay POSTs `{ code, state }` to `/api/codex-oauth/relay`. Token exchange happens on the server.
304
+ 5. Tokens saved to `$CODEX_HOME/auth.json` (default: `~/.codex/auth.json`) as `{ auth_mode: "chatgpt", tokens: { id_token, access_token, refresh_token }, last_refresh }`
305
+
306
+ **Constants (in server.js):**
307
+ - Issuer: `https://auth.openai.com`
308
+ - Client ID: `app_EMoamEEZ73f0CkXaXp7hrann`
309
+ - Scopes: `openid profile email offline_access api.connectors.read api.connectors.invoke`
310
+ - Redirect URI (local): `http://localhost:1455/auth/callback` (actual callback goes to agentgui's `/codex-oauth2callback`)
311
+
312
+ **WebSocket handlers** (in `lib/ws-handlers-util.js`): `codex.start`, `codex.status`, `codex.relay`, `codex.complete`
313
+
314
+ **Agent auth**: `POST /api/agents/codex/auth` starts OAuth flow same as Gemini — broadcasts `script_started`/`script_output`/`script_stopped` events as OAuth progresses.
315
+
289
316
  ## ACP SDK Integration
290
317
 
291
318
  - **@agentclientprotocol/sdk** (`^0.4.1`) added to dependencies
@@ -9,7 +9,7 @@ export function register(router, deps) {
9
9
  const { queries, wsOptimizer, modelDownloadState, ensureModelsDownloaded,
10
10
  broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
11
11
  startGeminiOAuth, exchangeGeminiOAuthCode, geminiOAuthState,
12
- startCodexDeviceAuth, codexDeviceAuthState,
12
+ startCodexOAuth, exchangeCodexOAuthCode, codexOAuthState,
13
13
  STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents } = deps;
14
14
 
15
15
  router.handle('home', () => ({ home: os.homedir(), cwd: STARTUP_CWD }));
@@ -158,31 +158,46 @@ export function register(router, deps) {
158
158
  } catch (e) { err(400, e.message); }
159
159
  });
160
160
 
161
+ router.handle('gemini.complete', async (p) => {
162
+ const pastedUrl = (p.url || '').trim();
163
+ if (!pastedUrl) err(400, 'No URL provided');
164
+ let parsed;
165
+ try { parsed = new URL(pastedUrl); } catch { err(400, 'Invalid URL. Paste the full URL from the browser address bar.'); }
166
+ const urlError = parsed.searchParams.get('error');
167
+ if (urlError) {
168
+ const desc = parsed.searchParams.get('error_description') || urlError;
169
+ return { error: desc };
170
+ }
171
+ const code = parsed.searchParams.get('code');
172
+ const state = parsed.searchParams.get('state');
173
+ try {
174
+ const email = await exchangeGeminiOAuthCode(code, state);
175
+ return { success: true, email };
176
+ } catch (e) { err(400, e.message); }
177
+ });
178
+
161
179
  router.handle('codex.start', async () => {
162
180
  try {
163
- const result = startCodexDeviceAuth();
164
- const st = typeof codexDeviceAuthState === 'function' ? codexDeviceAuthState() : codexDeviceAuthState;
165
- if (result.alreadyAuthenticated) return { status: 'success', authenticated: true };
166
- const waitForCode = () => new Promise((resolve) => {
167
- let tries = 12;
168
- const poll = () => {
169
- const state = typeof codexDeviceAuthState === 'function' ? codexDeviceAuthState() : codexDeviceAuthState;
170
- if (state.authUrl || tries-- <= 0) resolve(state);
171
- else setTimeout(poll, 400);
172
- };
173
- poll();
174
- });
175
- const state = await waitForCode();
176
- return { status: state.status, authUrl: state.authUrl, userCode: state.userCode };
181
+ const result = await startCodexOAuth();
182
+ return { authUrl: result.authUrl, mode: result.mode };
177
183
  } catch (e) { err(500, e.message); }
178
184
  });
179
185
 
180
186
  router.handle('codex.status', () => {
181
- const st = typeof codexDeviceAuthState === 'function' ? codexDeviceAuthState() : codexDeviceAuthState;
187
+ const st = typeof codexOAuthState === 'function' ? codexOAuthState() : codexOAuthState;
182
188
  return st;
183
189
  });
184
190
 
185
- router.handle('gemini.complete', async (p) => {
191
+ router.handle('codex.relay', async (p) => {
192
+ const { code, state } = p;
193
+ if (!code || !state) err(400, 'Missing code or state');
194
+ try {
195
+ const email = await exchangeCodexOAuthCode(code, state);
196
+ return { success: true, email };
197
+ } catch (e) { err(400, e.message); }
198
+ });
199
+
200
+ router.handle('codex.complete', async (p) => {
186
201
  const pastedUrl = (p.url || '').trim();
187
202
  if (!pastedUrl) err(400, 'No URL provided');
188
203
  let parsed;
@@ -195,7 +210,7 @@ export function register(router, deps) {
195
210
  const code = parsed.searchParams.get('code');
196
211
  const state = parsed.searchParams.get('state');
197
212
  try {
198
- const email = await exchangeGeminiOAuthCode(code, state);
213
+ const email = await exchangeCodexOAuthCode(code, state);
199
214
  return { success: true, email };
200
215
  } catch (e) { err(400, e.message); }
201
216
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.702",
3
+ "version": "1.0.704",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",