agent-relay 1.2.3 → 1.3.0

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.
Files changed (200) hide show
  1. package/.trajectories/agent-relay-322-324.md +17 -0
  2. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
  3. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
  4. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
  5. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
  6. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
  7. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
  8. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
  9. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
  10. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
  11. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
  12. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
  13. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
  14. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
  15. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
  16. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
  17. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
  18. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
  19. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
  20. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
  21. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
  22. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
  23. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
  24. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
  25. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
  26. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
  27. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
  28. package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
  29. package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
  30. package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
  31. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
  32. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
  33. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
  34. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
  35. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
  36. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
  37. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
  38. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
  39. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
  40. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
  41. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
  42. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
  43. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
  44. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
  45. package/.trajectories/consolidate-settings-panel.md +24 -0
  46. package/.trajectories/gh-cli-user-token.md +26 -0
  47. package/.trajectories/index.json +155 -1
  48. package/deploy/workspace/codex.config.toml +15 -0
  49. package/deploy/workspace/entrypoint.sh +167 -7
  50. package/deploy/workspace/git-credential-relay +17 -2
  51. package/dist/bridge/spawner.d.ts +7 -0
  52. package/dist/bridge/spawner.js +40 -9
  53. package/dist/bridge/types.d.ts +2 -0
  54. package/dist/cli/index.js +210 -168
  55. package/dist/cloud/api/admin.d.ts +8 -0
  56. package/dist/cloud/api/admin.js +212 -0
  57. package/dist/cloud/api/auth.js +8 -0
  58. package/dist/cloud/api/billing.d.ts +0 -10
  59. package/dist/cloud/api/billing.js +248 -58
  60. package/dist/cloud/api/codex-auth-helper.d.ts +10 -4
  61. package/dist/cloud/api/codex-auth-helper.js +215 -8
  62. package/dist/cloud/api/coordinators.js +402 -0
  63. package/dist/cloud/api/daemons.js +15 -11
  64. package/dist/cloud/api/git.js +104 -17
  65. package/dist/cloud/api/github-app.js +42 -8
  66. package/dist/cloud/api/nango-auth.js +297 -16
  67. package/dist/cloud/api/onboarding.js +97 -33
  68. package/dist/cloud/api/providers.js +12 -16
  69. package/dist/cloud/api/repos.js +200 -124
  70. package/dist/cloud/api/test-helpers.js +40 -0
  71. package/dist/cloud/api/usage.js +13 -0
  72. package/dist/cloud/api/webhooks.js +1 -1
  73. package/dist/cloud/api/workspaces.d.ts +18 -0
  74. package/dist/cloud/api/workspaces.js +945 -15
  75. package/dist/cloud/config.d.ts +8 -0
  76. package/dist/cloud/config.js +15 -0
  77. package/dist/cloud/db/drizzle.d.ts +5 -2
  78. package/dist/cloud/db/drizzle.js +27 -20
  79. package/dist/cloud/db/schema.d.ts +19 -51
  80. package/dist/cloud/db/schema.js +5 -4
  81. package/dist/cloud/index.d.ts +0 -1
  82. package/dist/cloud/index.js +0 -1
  83. package/dist/cloud/provisioner/index.d.ts +93 -1
  84. package/dist/cloud/provisioner/index.js +608 -63
  85. package/dist/cloud/server.js +156 -16
  86. package/dist/cloud/services/compute-enforcement.d.ts +57 -0
  87. package/dist/cloud/services/compute-enforcement.js +175 -0
  88. package/dist/cloud/services/index.d.ts +2 -0
  89. package/dist/cloud/services/index.js +4 -0
  90. package/dist/cloud/services/intro-expiration.d.ts +55 -0
  91. package/dist/cloud/services/intro-expiration.js +211 -0
  92. package/dist/cloud/services/nango.d.ts +14 -0
  93. package/dist/cloud/services/nango.js +74 -14
  94. package/dist/cloud/services/ssh-security.d.ts +31 -0
  95. package/dist/cloud/services/ssh-security.js +63 -0
  96. package/dist/continuity/manager.d.ts +5 -0
  97. package/dist/continuity/manager.js +56 -2
  98. package/dist/daemon/api.d.ts +2 -0
  99. package/dist/daemon/api.js +214 -5
  100. package/dist/daemon/cli-auth.d.ts +13 -1
  101. package/dist/daemon/cli-auth.js +166 -47
  102. package/dist/daemon/connection.d.ts +7 -1
  103. package/dist/daemon/connection.js +15 -0
  104. package/dist/daemon/orchestrator.d.ts +2 -0
  105. package/dist/daemon/orchestrator.js +26 -0
  106. package/dist/daemon/repo-manager.d.ts +116 -0
  107. package/dist/daemon/repo-manager.js +384 -0
  108. package/dist/daemon/router.d.ts +60 -1
  109. package/dist/daemon/router.js +281 -20
  110. package/dist/daemon/user-directory.d.ts +111 -0
  111. package/dist/daemon/user-directory.js +233 -0
  112. package/dist/dashboard/out/404.html +1 -1
  113. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +1 -0
  114. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
  115. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
  116. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
  117. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
  118. package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +1 -0
  119. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +1 -0
  120. package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +1 -0
  121. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-f45ecbc3e06134fc.js +1 -0
  122. package/dist/dashboard/out/_next/static/chunks/app/history/{page-abb9ab2d329f56e9.js → page-8c8bed33beb2bf1c.js} +1 -1
  123. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
  124. package/dist/dashboard/out/_next/static/chunks/app/login/{page-c22d080201cbd9fb.js → page-16f3b49e55b1e0ed.js} +1 -1
  125. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +1 -0
  126. package/dist/dashboard/out/_next/static/chunks/app/{page-77e9c65420a06cfb.js → page-4a5938c18a11a654.js} +1 -1
  127. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-982a7000fee44014.js +1 -0
  128. package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +1 -0
  129. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +1 -0
  130. package/dist/dashboard/out/_next/static/chunks/app/signup/{page-68d34f50baa8ab6b.js → page-547dd0ca55ecd0ba.js} +1 -1
  131. package/dist/dashboard/out/_next/static/chunks/{main-ed4e1fb6f29c34cf.js → main-2ee6beb2ae96d210.js} +1 -1
  132. package/dist/dashboard/out/_next/static/chunks/{main-app-6e8e8d3ef4e0192a.js → main-app-5d692157a8eb1fd9.js} +1 -1
  133. package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +1 -0
  134. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
  135. package/dist/dashboard/out/app/onboarding.html +1 -1
  136. package/dist/dashboard/out/app/onboarding.txt +3 -3
  137. package/dist/dashboard/out/app.html +1 -1
  138. package/dist/dashboard/out/app.txt +3 -3
  139. package/dist/dashboard/out/apple-icon.png +0 -0
  140. package/dist/dashboard/out/connect-repos.html +1 -1
  141. package/dist/dashboard/out/connect-repos.txt +3 -3
  142. package/dist/dashboard/out/history.html +1 -1
  143. package/dist/dashboard/out/history.txt +3 -3
  144. package/dist/dashboard/out/index.html +1 -1
  145. package/dist/dashboard/out/index.txt +3 -3
  146. package/dist/dashboard/out/login.html +2 -2
  147. package/dist/dashboard/out/login.txt +3 -3
  148. package/dist/dashboard/out/metrics.html +1 -1
  149. package/dist/dashboard/out/metrics.txt +3 -3
  150. package/dist/dashboard/out/pricing.html +2 -2
  151. package/dist/dashboard/out/pricing.txt +3 -3
  152. package/dist/dashboard/out/providers/setup/claude.html +1 -0
  153. package/dist/dashboard/out/providers/setup/claude.txt +8 -0
  154. package/dist/dashboard/out/providers/setup/codex.html +1 -0
  155. package/dist/dashboard/out/providers/setup/codex.txt +8 -0
  156. package/dist/dashboard/out/providers.html +1 -1
  157. package/dist/dashboard/out/providers.txt +3 -3
  158. package/dist/dashboard/out/signup.html +2 -2
  159. package/dist/dashboard/out/signup.txt +3 -3
  160. package/dist/dashboard-server/server.js +316 -12
  161. package/dist/dashboard-server/user-bridge.d.ts +103 -0
  162. package/dist/dashboard-server/user-bridge.js +189 -0
  163. package/dist/protocol/channels.d.ts +205 -0
  164. package/dist/protocol/channels.js +154 -0
  165. package/dist/protocol/types.d.ts +13 -1
  166. package/dist/resiliency/provider-context.js +2 -0
  167. package/dist/shared/cli-auth-config.d.ts +19 -0
  168. package/dist/shared/cli-auth-config.js +58 -2
  169. package/dist/utils/agent-config.js +1 -1
  170. package/dist/wrapper/auth-detection.d.ts +49 -0
  171. package/dist/wrapper/auth-detection.js +192 -0
  172. package/dist/wrapper/base-wrapper.d.ts +153 -0
  173. package/dist/wrapper/base-wrapper.js +393 -0
  174. package/dist/wrapper/client.d.ts +7 -1
  175. package/dist/wrapper/client.js +3 -0
  176. package/dist/wrapper/index.d.ts +1 -0
  177. package/dist/wrapper/index.js +4 -3
  178. package/dist/wrapper/pty-wrapper.d.ts +62 -84
  179. package/dist/wrapper/pty-wrapper.js +154 -180
  180. package/dist/wrapper/tmux-wrapper.d.ts +41 -66
  181. package/dist/wrapper/tmux-wrapper.js +90 -134
  182. package/package.json +4 -2
  183. package/scripts/postinstall.js +11 -155
  184. package/scripts/test-interactive-terminal.sh +248 -0
  185. package/dist/cloud/vault/index.d.ts +0 -76
  186. package/dist/cloud/vault/index.js +0 -219
  187. package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +0 -1
  188. package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +0 -9
  189. package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +0 -1
  190. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-3fdfa60e53f2810d.js +0 -1
  191. package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +0 -1
  192. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-3538dfe0ffe984b8.js +0 -1
  193. package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +0 -1
  194. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +0 -1
  195. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-b08ed1c34d14434a.js +0 -1
  196. package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +0 -1
  197. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
  198. package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +0 -1
  199. package/dist/dashboard/out/_next/static/wPgKJtcOmTFLpUncDg16A/_ssgManifest.js +0 -1
  200. /package/dist/dashboard/out/_next/static/{wPgKJtcOmTFLpUncDg16A → T1tgCqVWHFIkV7ClEtzD7}/_buildManifest.js +0 -0
@@ -9,7 +9,8 @@ import { createLogger } from '../resiliency/logger.js';
9
9
  import { metrics } from '../resiliency/metrics.js';
10
10
  import { getWorkspaceManager } from './workspace-manager.js';
11
11
  import { getAgentManager } from './agent-manager.js';
12
- import { startCLIAuth, getAuthSession, cancelAuthSession, getSupportedProviders, } from './cli-auth.js';
12
+ import { startCLIAuth, getAuthSession, submitAuthCode, cancelAuthSession, getSupportedProviders, } from './cli-auth.js';
13
+ import { getRepoManager, initRepoManager } from './repo-manager.js';
13
14
  const logger = createLogger('daemon-api');
14
15
  export class DaemonApi extends EventEmitter {
15
16
  server;
@@ -21,6 +22,9 @@ export class DaemonApi extends EventEmitter {
21
22
  config;
22
23
  allowedOrigins;
23
24
  allowAllOrigins;
25
+ // Track alive status for ping/pong keepalive
26
+ clientAlive = new WeakMap();
27
+ pingInterval;
24
28
  constructor(config) {
25
29
  super();
26
30
  this.config = config;
@@ -80,11 +84,28 @@ export class DaemonApi extends EventEmitter {
80
84
  * Start the API server
81
85
  */
82
86
  async start() {
87
+ // Initialize repo manager (scans for existing repos, syncs from env)
88
+ // This runs in background - don't block server startup
89
+ initRepoManager().catch((err) => {
90
+ logger.warn('Failed to initialize repo manager', { error: String(err) });
91
+ });
83
92
  return new Promise((resolve) => {
84
93
  this.server = http.createServer((req, res) => this.handleRequest(req, res));
85
- // Setup WebSocket server
86
- this.wss = new WebSocketServer({ server: this.server });
94
+ // Setup WebSocket server (disable compression for compatibility)
95
+ this.wss = new WebSocketServer({ server: this.server, perMessageDeflate: false });
87
96
  this.wss.on('connection', (ws, req) => this.handleWebSocketConnection(ws, req));
97
+ // Setup ping/pong keepalive (30 second interval)
98
+ this.pingInterval = setInterval(() => {
99
+ this.wss?.clients.forEach((ws) => {
100
+ if (this.clientAlive.get(ws) === false) {
101
+ logger.info('WebSocket client unresponsive, closing');
102
+ ws.terminate();
103
+ return;
104
+ }
105
+ this.clientAlive.set(ws, false);
106
+ ws.ping();
107
+ });
108
+ }, 30000);
88
109
  this.server.listen(this.config.port, this.config.host, () => {
89
110
  logger.info('Daemon API started', { port: this.config.port, host: this.config.host });
90
111
  resolve();
@@ -95,6 +116,11 @@ export class DaemonApi extends EventEmitter {
95
116
  * Stop the API server
96
117
  */
97
118
  async stop() {
119
+ // Clear ping interval
120
+ if (this.pingInterval) {
121
+ clearInterval(this.pingInterval);
122
+ this.pingInterval = undefined;
123
+ }
98
124
  // Close all WebSocket connections
99
125
  if (this.wss) {
100
126
  for (const ws of this.wss.clients) {
@@ -295,6 +321,8 @@ export class DaemonApi extends EventEmitter {
295
321
  status: session.status,
296
322
  authUrl: session.authUrl,
297
323
  error: session.error,
324
+ errorHint: session.errorHint,
325
+ recoverable: session.recoverable,
298
326
  promptsHandled: session.promptsHandled,
299
327
  },
300
328
  };
@@ -306,17 +334,109 @@ export class DaemonApi extends EventEmitter {
306
334
  if (!session) {
307
335
  return { status: 404, body: { error: 'Session not found' } };
308
336
  }
309
- if (session.status !== 'success') {
310
- return { status: 400, body: { error: 'Auth not complete', status: session.status } };
337
+ // Check for error state first
338
+ if (session.status === 'error') {
339
+ return {
340
+ status: 400,
341
+ body: {
342
+ error: session.error || 'Authentication failed',
343
+ errorHint: session.errorHint,
344
+ recoverable: session.recoverable,
345
+ status: session.status,
346
+ },
347
+ };
348
+ }
349
+ // Check if auth is complete AND we have credentials
350
+ // Status can be 'success' before credentials are extracted (race condition)
351
+ if (session.status !== 'success' || !session.token) {
352
+ return {
353
+ status: 400,
354
+ body: {
355
+ error: 'Auth not complete or credentials not yet available',
356
+ status: session.status,
357
+ hasToken: !!session.token,
358
+ },
359
+ };
311
360
  }
312
361
  return {
313
362
  status: 200,
314
363
  body: {
315
364
  token: session.token,
365
+ refreshToken: session.refreshToken,
366
+ tokenExpiresAt: session.tokenExpiresAt,
316
367
  provider: session.provider,
317
368
  },
318
369
  };
319
370
  });
371
+ // Submit auth code to PTY session
372
+ this.routes.set('POST /auth/cli/:provider/code/:sessionId', async (req) => {
373
+ const { sessionId } = req.params;
374
+ const { code, state } = req.body;
375
+ if (!code || typeof code !== 'string') {
376
+ return { status: 400, body: { error: 'Auth code is required' } };
377
+ }
378
+ const result = await submitAuthCode(sessionId, code, state);
379
+ if (!result.success) {
380
+ return {
381
+ status: 400,
382
+ body: {
383
+ error: result.error || 'Failed to submit auth code',
384
+ needsRestart: result.needsRestart,
385
+ },
386
+ };
387
+ }
388
+ return { status: 200, body: { success: true, message: 'Auth code submitted' } };
389
+ });
390
+ // Complete auth and wait for credentials
391
+ this.routes.set('POST /auth/cli/:provider/complete/:sessionId', async (req) => {
392
+ const { sessionId } = req.params;
393
+ const { authCode, state } = req.body;
394
+ // For Codex, we need to forward the authCode to the CLI's callback server
395
+ // The Codex CLI starts a callback server at localhost:1455
396
+ if (authCode) {
397
+ try {
398
+ // Forward the OAuth callback to the Codex CLI's callback server
399
+ const callbackUrl = `http://localhost:1455/auth/callback?code=${encodeURIComponent(authCode)}${state ? `&state=${encodeURIComponent(state)}` : ''}`;
400
+ logger.info('Forwarding OAuth callback to Codex CLI', { callbackUrl: callbackUrl.replace(authCode, '[REDACTED]') });
401
+ const callbackResponse = await fetch(callbackUrl, {
402
+ signal: AbortSignal.timeout(5000),
403
+ });
404
+ if (!callbackResponse.ok) {
405
+ logger.error('Failed to forward callback to Codex CLI', { status: callbackResponse.status });
406
+ return {
407
+ status: 400,
408
+ body: {
409
+ error: 'Failed to deliver OAuth callback to Codex CLI. The CLI may have timed out.',
410
+ needsRestart: true,
411
+ },
412
+ };
413
+ }
414
+ logger.info('Successfully forwarded OAuth callback to Codex CLI');
415
+ }
416
+ catch (err) {
417
+ logger.error('Error forwarding callback to Codex CLI', { error: String(err) });
418
+ return {
419
+ status: 500,
420
+ body: {
421
+ error: 'Failed to reach Codex CLI callback server. The CLI may not be running.',
422
+ needsRestart: true,
423
+ },
424
+ };
425
+ }
426
+ }
427
+ // Wait for credentials to be available (polls for up to 15 seconds)
428
+ const { completeAuthSession } = await import('./cli-auth.js');
429
+ const completeResult = await completeAuthSession(sessionId);
430
+ if (!completeResult.success) {
431
+ return {
432
+ status: 400,
433
+ body: {
434
+ error: completeResult.error || 'Authentication failed',
435
+ },
436
+ };
437
+ }
438
+ return { status: 200, body: { success: true, token: completeResult.token } };
439
+ });
320
440
  // Cancel auth session
321
441
  this.routes.set('POST /auth/cli/:provider/cancel/:sessionId', async (req) => {
322
442
  const { sessionId } = req.params;
@@ -326,6 +446,89 @@ export class DaemonApi extends EventEmitter {
326
446
  }
327
447
  return { status: 200, body: { success: true } };
328
448
  });
449
+ // Check if provider is authenticated (credentials exist)
450
+ this.routes.set('GET /auth/cli/:provider/check', async (req) => {
451
+ const { provider } = req.params;
452
+ const { checkProviderAuth } = await import('./cli-auth.js');
453
+ const authenticated = await checkProviderAuth(provider);
454
+ return { status: 200, body: { authenticated } };
455
+ });
456
+ // === Repository Management ===
457
+ // Dynamic repo management without workspace restart
458
+ // List all repos
459
+ this.routes.set('GET /repos', async () => {
460
+ try {
461
+ const repoManager = getRepoManager();
462
+ const repos = repoManager.getRepos();
463
+ return { status: 200, body: { repos } };
464
+ }
465
+ catch (err) {
466
+ logger.error('Failed to list repos', { error: String(err) });
467
+ return { status: 500, body: { error: 'Failed to list repositories' } };
468
+ }
469
+ });
470
+ // Get a specific repo
471
+ this.routes.set('GET /repos/:name', async (req) => {
472
+ try {
473
+ const repoManager = getRepoManager();
474
+ // Handle encoded slashes (e.g., "owner%2Frepo" -> "owner/repo")
475
+ const fullName = decodeURIComponent(req.params.name);
476
+ const repo = repoManager.getRepo(fullName);
477
+ if (!repo) {
478
+ return { status: 404, body: { error: 'Repository not found' } };
479
+ }
480
+ return { status: 200, body: repo };
481
+ }
482
+ catch (err) {
483
+ logger.error('Failed to get repo', { error: String(err) });
484
+ return { status: 500, body: { error: 'Failed to get repository' } };
485
+ }
486
+ });
487
+ // Sync (clone or update) a repo
488
+ this.routes.set('POST /repos/sync', async (req) => {
489
+ const body = req.body;
490
+ // Support single repo or batch
491
+ const reposToSync = [];
492
+ if (body.repo) {
493
+ reposToSync.push(body.repo);
494
+ }
495
+ if (body.repos && Array.isArray(body.repos)) {
496
+ reposToSync.push(...body.repos);
497
+ }
498
+ if (reposToSync.length === 0) {
499
+ return { status: 400, body: { error: 'repo or repos field is required' } };
500
+ }
501
+ try {
502
+ const repoManager = getRepoManager();
503
+ const results = await repoManager.syncRepos(reposToSync);
504
+ const allSuccess = results.every(r => r.success);
505
+ return {
506
+ status: allSuccess ? 200 : 207, // 207 Multi-Status if partial success
507
+ body: { results },
508
+ };
509
+ }
510
+ catch (err) {
511
+ logger.error('Failed to sync repos', { error: String(err) });
512
+ return { status: 500, body: { error: 'Failed to sync repositories' } };
513
+ }
514
+ });
515
+ // Remove a repo
516
+ this.routes.set('DELETE /repos/:name', async (req) => {
517
+ try {
518
+ const repoManager = getRepoManager();
519
+ const fullName = decodeURIComponent(req.params.name);
520
+ const deleteFiles = req.query.deleteFiles === 'true';
521
+ const removed = await repoManager.removeRepo(fullName, deleteFiles);
522
+ if (!removed) {
523
+ return { status: 404, body: { error: 'Repository not found' } };
524
+ }
525
+ return { status: 200, body: { success: true, deleted: deleteFiles } };
526
+ }
527
+ catch (err) {
528
+ logger.error('Failed to remove repo', { error: String(err) });
529
+ return { status: 500, body: { error: 'Failed to remove repository' } };
530
+ }
531
+ });
329
532
  }
330
533
  /**
331
534
  * Handle HTTP request
@@ -461,6 +664,12 @@ export class DaemonApi extends EventEmitter {
461
664
  */
462
665
  handleWebSocketConnection(ws, req) {
463
666
  logger.info('WebSocket client connected', { url: req.url });
667
+ // Mark client as alive for ping/pong keepalive
668
+ this.clientAlive.set(ws, true);
669
+ // Handle pong responses
670
+ ws.on('pong', () => {
671
+ this.clientAlive.set(ws, true);
672
+ });
464
673
  // Create session
465
674
  const session = {
466
675
  userId: 'anonymous', // Would be set from auth
@@ -20,6 +20,10 @@ interface AuthSession {
20
20
  refreshToken?: string;
21
21
  tokenExpiresAt?: Date;
22
22
  error?: string;
23
+ /** User-friendly hint for resolving the error */
24
+ errorHint?: string;
25
+ /** Whether the error can be resolved by retrying */
26
+ recoverable?: boolean;
23
27
  output: string;
24
28
  promptsHandled: string[];
25
29
  createdAt: Date;
@@ -44,9 +48,12 @@ export declare function getAuthSession(sessionId: string): AuthSession | null;
44
48
  * Submit auth code to a waiting session
45
49
  * This writes the code to the PTY process stdin
46
50
  *
51
+ * @param sessionId - The auth session ID
52
+ * @param code - The OAuth authorization code
53
+ * @param state - Optional OAuth state parameter for CSRF validation (used by Codex)
47
54
  * @returns Object with success status and optional error message
48
55
  */
49
- export declare function submitAuthCode(sessionId: string, code: string): Promise<{
56
+ export declare function submitAuthCode(sessionId: string, code: string, state?: string): Promise<{
50
57
  success: boolean;
51
58
  error?: string;
52
59
  needsRestart?: boolean;
@@ -64,4 +71,9 @@ export declare function completeAuthSession(sessionId: string): Promise<{
64
71
  * Cancel auth session
65
72
  */
66
73
  export declare function cancelAuthSession(sessionId: string): boolean;
74
+ /**
75
+ * Check if a provider is authenticated (credentials exist)
76
+ * Used by the auth check endpoint for SSH tunnel flow
77
+ */
78
+ export declare function checkProviderAuth(provider: string): Promise<boolean>;
67
79
  //# sourceMappingURL=cli-auth.d.ts.map
@@ -9,7 +9,7 @@ import * as crypto from 'crypto';
9
9
  import * as fs from 'fs/promises';
10
10
  import * as os from 'os';
11
11
  import { createLogger } from '../resiliency/logger.js';
12
- import { CLI_AUTH_CONFIG, stripAnsiCodes, matchesSuccessPattern, findMatchingPrompt, getSupportedProviders, } from '../shared/cli-auth-config.js';
12
+ import { CLI_AUTH_CONFIG, stripAnsiCodes, matchesSuccessPattern, findMatchingPrompt, findMatchingError, getSupportedProviders, } from '../shared/cli-auth-config.js';
13
13
  const logger = createLogger('cli-auth');
14
14
  // Re-export for consumers
15
15
  export { CLI_AUTH_CONFIG, getSupportedProviders };
@@ -123,46 +123,49 @@ export async function startCLIAuth(provider, options = {}) {
123
123
  session.error = 'Timeout waiting for auth completion (5 minutes). Please try again.';
124
124
  }
125
125
  }, config.waitTimeout + OAUTH_COMPLETION_TIMEOUT);
126
- // Keep-alive: Some CLIs timeout if they don't receive stdin input
127
- // Send a space+backspace every 20 seconds to simulate user presence
128
- const keepAliveInterval = setInterval(() => {
129
- if (session.status === 'waiting_auth' && session.process) {
130
- try {
131
- // Send space then backspace - appears as user typing but no net effect
132
- session.process.write(' \b');
133
- logger.debug('Keep-alive ping sent', {
134
- sessionId,
135
- status: session.status,
136
- ageSeconds: Math.round((Date.now() - session.createdAt.getTime()) / 1000),
137
- });
138
- }
139
- catch {
140
- // Process may have exited
141
- }
142
- }
143
- }, 20000);
126
+ // Note: Removed keep-alive mechanism that sent ' \b' every 20 seconds
127
+ // It was interfering with OAuth code paste, causing "invalid code" errors
128
+ // CLIs like Claude don't actually need stdin keep-alive during auth wait
144
129
  proc.onData((data) => {
145
130
  session.output += data;
146
- // Handle prompts
147
- const matchingPrompt = findMatchingPrompt(data, config.prompts, respondedPrompts);
148
- if (matchingPrompt) {
149
- respondedPrompts.add(matchingPrompt.description);
150
- session.promptsHandled.push(matchingPrompt.description);
151
- logger.info('Auto-responding to prompt', { description: matchingPrompt.description });
152
- const delay = matchingPrompt.delay ?? 100;
153
- setTimeout(() => {
154
- try {
155
- proc.write(matchingPrompt.response);
156
- }
157
- catch {
158
- // Process may have exited
159
- }
160
- }, delay);
161
- }
162
- // Extract auth URL
163
131
  const cleanText = stripAnsiCodes(data);
132
+ // Check for error patterns FIRST - if error detected, don't auto-respond to prompts
133
+ // This prevents us from auto-responding to "Press Enter to retry" in error messages
134
+ const matchedError = findMatchingError(data, config.errorPatterns);
135
+ if (matchedError && session.status !== 'error') {
136
+ logger.warn('Auth error detected', {
137
+ provider,
138
+ sessionId,
139
+ errorMessage: matchedError.message,
140
+ recoverable: matchedError.recoverable,
141
+ });
142
+ session.status = 'error';
143
+ session.error = matchedError.message;
144
+ session.errorHint = matchedError.hint;
145
+ session.recoverable = matchedError.recoverable;
146
+ }
147
+ // Don't auto-respond to prompts if we're in error state
148
+ // This prevents responding to "Press Enter to retry" after an error
149
+ if (session.status !== 'error') {
150
+ const matchingPrompt = findMatchingPrompt(data, config.prompts, respondedPrompts);
151
+ if (matchingPrompt) {
152
+ respondedPrompts.add(matchingPrompt.description);
153
+ session.promptsHandled.push(matchingPrompt.description);
154
+ logger.info('Auto-responding to prompt', { description: matchingPrompt.description });
155
+ const delay = matchingPrompt.delay ?? 100;
156
+ setTimeout(() => {
157
+ try {
158
+ proc.write(matchingPrompt.response);
159
+ }
160
+ catch {
161
+ // Process may have exited
162
+ }
163
+ }, delay);
164
+ }
165
+ }
166
+ // Extract auth URL (only if not in error state and don't have URL yet)
164
167
  const match = cleanText.match(config.urlPattern);
165
- if (match && match[1] && !session.authUrl) {
168
+ if (match && match[1] && !session.authUrl && session.status !== 'error') {
166
169
  session.authUrl = match[1];
167
170
  session.status = 'waiting_auth';
168
171
  logger.info('Auth URL captured', { provider, url: session.authUrl });
@@ -172,7 +175,7 @@ export async function startCLIAuth(provider, options = {}) {
172
175
  }
173
176
  // Log all output after auth URL is captured (for debugging)
174
177
  if (session.authUrl) {
175
- const trimmedData = stripAnsiCodes(data).trim();
178
+ const trimmedData = cleanText.trim();
176
179
  if (trimmedData.length > 0) {
177
180
  logger.info('PTY output after auth URL', {
178
181
  provider,
@@ -182,12 +185,18 @@ export async function startCLIAuth(provider, options = {}) {
182
185
  }
183
186
  }
184
187
  // Check for success and try to extract credentials
185
- if (matchesSuccessPattern(data, config.successPatterns)) {
188
+ // Don't override error status - if there was an error, keep it
189
+ if (session.status !== 'error' && matchesSuccessPattern(data, config.successPatterns)) {
186
190
  session.status = 'success';
187
191
  logger.info('Success pattern detected, attempting credential extraction', { provider });
188
192
  // Try to extract credentials immediately (CLI may not exit after success)
189
193
  // Use a small delay to let the CLI finish writing the file
190
194
  setTimeout(async () => {
195
+ // Don't extract if status changed to error (e.g., error detected after success pattern)
196
+ if (session.status === 'error') {
197
+ logger.info('Skipping credential extraction - session is in error state', { provider });
198
+ return;
199
+ }
191
200
  try {
192
201
  const creds = await extractCredentials(provider, config);
193
202
  if (creds) {
@@ -206,7 +215,6 @@ export async function startCLIAuth(provider, options = {}) {
206
215
  proc.onExit(async ({ exitCode }) => {
207
216
  clearTimeout(timeout);
208
217
  clearTimeout(authUrlTimeout);
209
- clearInterval(keepAliveInterval);
210
218
  // Clear process reference so submitAuthCode knows PTY is gone
211
219
  session.process = undefined;
212
220
  // Log full output for debugging PTY exit issues
@@ -221,8 +229,9 @@ export async function startCLIAuth(provider, options = {}) {
221
229
  // Last 500 chars of output for debugging
222
230
  outputTail: cleanOutput.slice(-500),
223
231
  });
224
- // Try to extract credentials
225
- if (session.authUrl || exitCode === 0) {
232
+ // Try to extract credentials (but don't override error status)
233
+ // CLI might exit cleanly (code 0) even after an OAuth error
234
+ if ((session.authUrl || exitCode === 0) && session.status !== 'error') {
226
235
  try {
227
236
  const creds = await extractCredentials(provider, config);
228
237
  if (creds) {
@@ -265,9 +274,12 @@ export function getAuthSession(sessionId) {
265
274
  * Submit auth code to a waiting session
266
275
  * This writes the code to the PTY process stdin
267
276
  *
277
+ * @param sessionId - The auth session ID
278
+ * @param code - The OAuth authorization code
279
+ * @param state - Optional OAuth state parameter for CSRF validation (used by Codex)
268
280
  * @returns Object with success status and optional error message
269
281
  */
270
- export async function submitAuthCode(sessionId, code) {
282
+ export async function submitAuthCode(sessionId, code, state) {
271
283
  // Log all active sessions for debugging
272
284
  const activeSessionIds = Array.from(sessions.keys());
273
285
  logger.info('submitAuthCode called', {
@@ -305,11 +317,14 @@ export async function submitAuthCode(sessionId, code) {
305
317
  outputTail: session.output ? stripAnsiCodes(session.output).slice(-500) : 'no output',
306
318
  });
307
319
  // Try to extract credentials as a fallback - maybe auth completed in browser
320
+ // But don't override error status
308
321
  const config = CLI_AUTH_CONFIG[session.provider];
309
- if (config) {
322
+ if (config && session.status !== 'error') {
310
323
  try {
311
324
  const creds = await extractCredentials(session.provider, config);
312
- if (creds) {
325
+ // Re-check status after async operation (race condition protection)
326
+ // Use type assertion because TypeScript narrowing doesn't account for async race conditions
327
+ if (creds && session.status !== 'error') {
313
328
  session.token = creds.token;
314
329
  session.refreshToken = creds.refreshToken;
315
330
  session.tokenExpiresAt = creds.expiresAt;
@@ -331,8 +346,78 @@ export async function submitAuthCode(sessionId, code) {
331
346
  };
332
347
  }
333
348
  try {
334
- // Clean the code - trim whitespace
335
- const cleanCode = code.trim();
349
+ // Clean the code - trim whitespace and strip state parameter if present
350
+ // Claude OAuth codes come as "CODE#STATE" - we only need the code part
351
+ let cleanCode = code.trim();
352
+ if (cleanCode.includes('#')) {
353
+ const originalCode = cleanCode;
354
+ cleanCode = cleanCode.split('#')[0];
355
+ logger.info('Stripped state parameter from auth code', {
356
+ sessionId,
357
+ originalLength: originalCode.length,
358
+ cleanLength: cleanCode.length,
359
+ });
360
+ }
361
+ // For Codex (openai), forward the callback to the CLI's localhost server
362
+ // instead of writing to PTY stdin. The CLI spawns a localhost server
363
+ // waiting for the OAuth callback.
364
+ if (session.provider === 'openai' && session.authUrl) {
365
+ // Extract the redirect port from the auth URL (usually 1455)
366
+ const redirectMatch = session.authUrl.match(/redirect_uri=http%3A%2F%2Flocalhost%3A(\d+)/);
367
+ const port = redirectMatch ? redirectMatch[1] : '1455';
368
+ logger.info('Forwarding OAuth callback to Codex CLI localhost server', {
369
+ sessionId,
370
+ port,
371
+ codeLength: cleanCode.length,
372
+ hasState: !!state,
373
+ });
374
+ try {
375
+ // Forward the callback to the CLI's localhost server
376
+ // Include state parameter for CSRF validation if provided
377
+ let callbackUrl = `http://localhost:${port}/auth/callback?code=${encodeURIComponent(cleanCode)}`;
378
+ if (state) {
379
+ callbackUrl += `&state=${encodeURIComponent(state)}`;
380
+ }
381
+ const response = await fetch(callbackUrl, {
382
+ method: 'GET',
383
+ signal: AbortSignal.timeout(5000),
384
+ });
385
+ if (response.ok) {
386
+ logger.info('OAuth callback forwarded successfully to Codex CLI', { sessionId, status: response.status });
387
+ // Start polling for credentials
388
+ const config = CLI_AUTH_CONFIG[session.provider];
389
+ if (config) {
390
+ pollForCredentials(session, config);
391
+ }
392
+ return { success: true };
393
+ }
394
+ else {
395
+ // Try to get error details from response body
396
+ let errorBody = '';
397
+ try {
398
+ errorBody = await response.text();
399
+ }
400
+ catch {
401
+ // Ignore
402
+ }
403
+ logger.warn('Codex CLI localhost server returned error', {
404
+ sessionId,
405
+ status: response.status,
406
+ statusText: response.statusText,
407
+ errorBody: errorBody.substring(0, 500), // Limit log size
408
+ callbackUrl: callbackUrl.replace(/code=[^&]+/, 'code=***'), // Redact code
409
+ });
410
+ // Fall through to PTY write as fallback
411
+ }
412
+ }
413
+ catch (err) {
414
+ logger.warn('Failed to forward callback to Codex CLI localhost server', {
415
+ sessionId,
416
+ error: String(err),
417
+ });
418
+ // Fall through to PTY write as fallback
419
+ }
420
+ }
336
421
  logger.info('Writing auth code to PTY', {
337
422
  sessionId,
338
423
  originalLength: code.length,
@@ -382,6 +467,14 @@ async function pollForCredentials(session, config) {
382
467
  try {
383
468
  const creds = await extractCredentials(session.provider, config);
384
469
  if (creds) {
470
+ // Double-check we're not in error state (race condition protection)
471
+ // Use type assertion because TypeScript narrowing doesn't account for async race conditions
472
+ if (session.status === 'error') {
473
+ logger.info('Credentials found but session is in error state, not overriding', {
474
+ provider: session.provider,
475
+ });
476
+ return;
477
+ }
385
478
  session.token = creds.token;
386
479
  session.refreshToken = creds.refreshToken;
387
480
  session.tokenExpiresAt = creds.expiresAt;
@@ -424,9 +517,18 @@ export async function completeAuthSession(sessionId) {
424
517
  const maxAttempts = 15;
425
518
  const pollInterval = 1000;
426
519
  for (let i = 0; i < maxAttempts; i++) {
520
+ // Check if session went into error state
521
+ if (session.status === 'error') {
522
+ return { success: false, error: session.error || 'Authentication failed' };
523
+ }
427
524
  try {
428
525
  const creds = await extractCredentials(session.provider, config);
429
526
  if (creds) {
527
+ // Double-check we're not in error state (race condition protection)
528
+ // Use type assertion because TypeScript narrowing doesn't account for async race conditions
529
+ if (session.status === 'error') {
530
+ return { success: false, error: session.error || 'Authentication failed' };
531
+ }
430
532
  session.token = creds.token;
431
533
  session.refreshToken = creds.refreshToken;
432
534
  session.tokenExpiresAt = creds.expiresAt;
@@ -534,4 +636,21 @@ async function extractCredentials(provider, config) {
534
636
  return null;
535
637
  }
536
638
  }
639
+ /**
640
+ * Check if a provider is authenticated (credentials exist)
641
+ * Used by the auth check endpoint for SSH tunnel flow
642
+ */
643
+ export async function checkProviderAuth(provider) {
644
+ const config = CLI_AUTH_CONFIG[provider];
645
+ if (!config) {
646
+ return false;
647
+ }
648
+ try {
649
+ const creds = await extractCredentials(provider, config);
650
+ return !!creds?.token;
651
+ }
652
+ catch {
653
+ return false;
654
+ }
655
+ }
537
656
  //# sourceMappingURL=cli-auth.js.map
@@ -8,7 +8,7 @@
8
8
  * ERROR -------> CLOSED
9
9
  */
10
10
  import net from 'node:net';
11
- import { type Envelope, type AckPayload } from '../protocol/types.js';
11
+ import { type Envelope, type AckPayload, type EntityType } from '../protocol/types.js';
12
12
  export type ConnectionState = 'CONNECTING' | 'HANDSHAKING' | 'ACTIVE' | 'CLOSING' | 'CLOSED' | 'ERROR';
13
13
  export interface ConnectionConfig {
14
14
  maxFrameBytes: number;
@@ -39,11 +39,14 @@ export declare class Connection {
39
39
  private config;
40
40
  private _state;
41
41
  private _agentName?;
42
+ private _entityType?;
42
43
  private _cli?;
43
44
  private _program?;
44
45
  private _model?;
45
46
  private _task?;
46
47
  private _workingDirectory?;
48
+ private _displayName?;
49
+ private _avatarUrl?;
47
50
  private _sessionId;
48
51
  private _resumeToken;
49
52
  private _isResumed;
@@ -59,11 +62,14 @@ export declare class Connection {
59
62
  constructor(socket: net.Socket, config?: Partial<ConnectionConfig>);
60
63
  get state(): ConnectionState;
61
64
  get agentName(): string | undefined;
65
+ get entityType(): EntityType | undefined;
62
66
  get cli(): string | undefined;
63
67
  get program(): string | undefined;
64
68
  get model(): string | undefined;
65
69
  get task(): string | undefined;
66
70
  get workingDirectory(): string | undefined;
71
+ get displayName(): string | undefined;
72
+ get avatarUrl(): string | undefined;
67
73
  get sessionId(): string;
68
74
  get resumeToken(): string;
69
75
  get isResumed(): boolean;