agent-relay 1.3.1 → 1.3.3

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 (202) hide show
  1. package/.trajectories/active/traj_3yx9dy148mge.json +42 -0
  2. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +49 -0
  3. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +31 -0
  4. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +49 -0
  5. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +31 -0
  6. package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +109 -0
  7. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +49 -0
  8. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +31 -0
  9. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +66 -0
  10. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +36 -0
  11. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +49 -0
  12. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +31 -0
  13. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +65 -0
  14. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +37 -0
  15. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +36 -0
  16. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +21 -0
  17. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +101 -0
  18. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +52 -0
  19. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +61 -0
  20. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +36 -0
  21. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +73 -0
  22. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +41 -0
  23. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +77 -0
  24. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +42 -0
  25. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +109 -0
  26. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +56 -0
  27. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +113 -0
  28. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +57 -0
  29. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +61 -0
  30. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +36 -0
  31. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +49 -0
  32. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +31 -0
  33. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +49 -0
  34. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +31 -0
  35. package/.trajectories/index.json +140 -1
  36. package/README.md +23 -9
  37. package/TRAIL_GIT_AUTH_FIX.md +113 -0
  38. package/deploy/workspace/codex.config.toml +1 -1
  39. package/deploy/workspace/entrypoint.sh +20 -79
  40. package/deploy/workspace/gh-relay +156 -0
  41. package/deploy/workspace/git-credential-relay +5 -1
  42. package/dist/bridge/multi-project-client.js +13 -10
  43. package/dist/bridge/spawner.d.ts +2 -0
  44. package/dist/bridge/spawner.js +58 -76
  45. package/dist/bridge/types.d.ts +2 -0
  46. package/dist/cli/index.d.ts +8 -6
  47. package/dist/cli/index.js +297 -30
  48. package/dist/cloud/api/admin.js +16 -3
  49. package/dist/cloud/api/codex-auth-helper.js +28 -8
  50. package/dist/cloud/api/consensus.d.ts +13 -0
  51. package/dist/cloud/api/consensus.js +259 -0
  52. package/dist/cloud/api/daemons.js +205 -1
  53. package/dist/cloud/api/git.js +37 -7
  54. package/dist/cloud/api/onboarding.js +4 -1
  55. package/dist/cloud/api/provider-env.d.ts +5 -0
  56. package/dist/cloud/api/provider-env.js +27 -0
  57. package/dist/cloud/api/providers.js +2 -0
  58. package/dist/cloud/api/test-helpers.js +130 -0
  59. package/dist/cloud/api/workspaces.js +38 -3
  60. package/dist/cloud/db/bulk-ingest.d.ts +88 -0
  61. package/dist/cloud/db/bulk-ingest.js +268 -0
  62. package/dist/cloud/db/drizzle.d.ts +33 -0
  63. package/dist/cloud/db/drizzle.js +174 -2
  64. package/dist/cloud/db/index.d.ts +24 -5
  65. package/dist/cloud/db/index.js +19 -4
  66. package/dist/cloud/db/schema.d.ts +397 -3
  67. package/dist/cloud/db/schema.js +75 -1
  68. package/dist/cloud/provisioner/index.d.ts +8 -0
  69. package/dist/cloud/provisioner/index.js +256 -50
  70. package/dist/cloud/server.js +47 -3
  71. package/dist/cloud/services/index.d.ts +1 -0
  72. package/dist/cloud/services/index.js +2 -0
  73. package/dist/cloud/services/nango.d.ts +3 -4
  74. package/dist/cloud/services/nango.js +11 -33
  75. package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
  76. package/dist/cloud/services/workspace-keepalive.js +234 -0
  77. package/dist/config/relay-config.d.ts +23 -0
  78. package/dist/config/relay-config.js +23 -0
  79. package/dist/daemon/agent-manager.d.ts +20 -1
  80. package/dist/daemon/agent-manager.js +51 -0
  81. package/dist/daemon/agent-registry.js +4 -4
  82. package/dist/daemon/agent-signing.d.ts +158 -0
  83. package/dist/daemon/agent-signing.js +523 -0
  84. package/dist/daemon/api.js +18 -1
  85. package/dist/daemon/cli-auth.d.ts +4 -1
  86. package/dist/daemon/cli-auth.js +55 -11
  87. package/dist/daemon/cloud-sync.d.ts +47 -1
  88. package/dist/daemon/cloud-sync.js +152 -3
  89. package/dist/daemon/connection.d.ts +28 -0
  90. package/dist/daemon/connection.js +113 -22
  91. package/dist/daemon/consensus-integration.d.ts +167 -0
  92. package/dist/daemon/consensus-integration.js +371 -0
  93. package/dist/daemon/consensus.d.ts +271 -0
  94. package/dist/daemon/consensus.js +632 -0
  95. package/dist/daemon/delivery-tracker.d.ts +34 -0
  96. package/dist/daemon/delivery-tracker.js +104 -0
  97. package/dist/daemon/enhanced-features.d.ts +118 -0
  98. package/dist/daemon/enhanced-features.js +178 -0
  99. package/dist/daemon/index.d.ts +4 -0
  100. package/dist/daemon/index.js +5 -0
  101. package/dist/daemon/rate-limiter.d.ts +68 -0
  102. package/dist/daemon/rate-limiter.js +130 -0
  103. package/dist/daemon/router.d.ts +18 -11
  104. package/dist/daemon/router.js +57 -113
  105. package/dist/daemon/server.d.ts +13 -1
  106. package/dist/daemon/server.js +71 -9
  107. package/dist/daemon/sync-queue.d.ts +116 -0
  108. package/dist/daemon/sync-queue.js +361 -0
  109. package/dist/dashboard/out/404.html +1 -1
  110. package/dist/dashboard/out/_next/static/chunks/116-de2a4ac06e5000dc.js +1 -0
  111. package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +1 -0
  112. package/dist/dashboard/out/_next/static/chunks/919-87d604a5d76c1fbd.js +1 -0
  113. package/dist/dashboard/out/_next/static/chunks/app/app/{page-c617745b81344f4f.js → page-7f64824ae7d06707.js} +1 -1
  114. package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-3f559d393902aad2.js +1 -0
  115. package/dist/dashboard/out/_next/static/chunks/app/login/page-16d1715ddaa874ee.js +1 -0
  116. package/dist/dashboard/out/_next/static/chunks/app/{page-dc786c183425c2ac.js → page-814efc4d77b4191d.js} +1 -1
  117. package/dist/dashboard/out/_next/static/chunks/{main-2ee6beb2ae96d210.js → main-5a40a5ae29646e1b.js} +1 -1
  118. package/dist/dashboard/out/_next/static/css/44d2b52637b511bc.css +1 -0
  119. package/dist/dashboard/out/app/onboarding.html +1 -1
  120. package/dist/dashboard/out/app/onboarding.txt +1 -1
  121. package/dist/dashboard/out/app.html +1 -1
  122. package/dist/dashboard/out/app.txt +2 -2
  123. package/dist/dashboard/out/cloud/link.html +1 -0
  124. package/dist/dashboard/out/cloud/link.txt +7 -0
  125. package/dist/dashboard/out/connect-repos.html +1 -1
  126. package/dist/dashboard/out/connect-repos.txt +1 -1
  127. package/dist/dashboard/out/history.html +1 -1
  128. package/dist/dashboard/out/history.txt +2 -2
  129. package/dist/dashboard/out/index.html +1 -1
  130. package/dist/dashboard/out/index.txt +2 -2
  131. package/dist/dashboard/out/login.html +2 -3
  132. package/dist/dashboard/out/login.txt +2 -2
  133. package/dist/dashboard/out/metrics.html +1 -1
  134. package/dist/dashboard/out/metrics.txt +2 -2
  135. package/dist/dashboard/out/pricing.html +2 -2
  136. package/dist/dashboard/out/pricing.txt +1 -1
  137. package/dist/dashboard/out/providers/setup/claude.html +1 -1
  138. package/dist/dashboard/out/providers/setup/claude.txt +1 -1
  139. package/dist/dashboard/out/providers/setup/codex.html +1 -1
  140. package/dist/dashboard/out/providers/setup/codex.txt +1 -1
  141. package/dist/dashboard/out/providers.html +1 -1
  142. package/dist/dashboard/out/providers.txt +1 -1
  143. package/dist/dashboard/out/signup.html +2 -2
  144. package/dist/dashboard/out/signup.txt +1 -1
  145. package/dist/dashboard-server/server.js +244 -28
  146. package/dist/health-worker-manager.d.ts +62 -0
  147. package/dist/health-worker-manager.js +144 -0
  148. package/dist/health-worker.d.ts +9 -0
  149. package/dist/health-worker.js +79 -0
  150. package/dist/index.d.ts +2 -1
  151. package/dist/index.js +5 -1
  152. package/dist/memory/context-compaction.d.ts +156 -0
  153. package/dist/memory/context-compaction.js +453 -0
  154. package/dist/memory/index.d.ts +1 -0
  155. package/dist/memory/index.js +1 -0
  156. package/dist/protocol/channels.js +4 -4
  157. package/dist/protocol/framing.d.ts +72 -10
  158. package/dist/protocol/framing.js +194 -25
  159. package/dist/storage/adapter.d.ts +8 -1
  160. package/dist/storage/adapter.js +11 -0
  161. package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
  162. package/dist/storage/batched-sqlite-adapter.js +183 -0
  163. package/dist/storage/dead-letter-queue.d.ts +196 -0
  164. package/dist/storage/dead-letter-queue.js +427 -0
  165. package/dist/storage/dlq-adapter.d.ts +195 -0
  166. package/dist/storage/dlq-adapter.js +664 -0
  167. package/dist/trajectory/config.d.ts +32 -14
  168. package/dist/trajectory/config.js +38 -16
  169. package/dist/trajectory/integration.js +217 -64
  170. package/dist/utils/git-remote.d.ts +47 -0
  171. package/dist/utils/git-remote.js +125 -0
  172. package/dist/utils/id-generator.d.ts +35 -0
  173. package/dist/utils/id-generator.js +60 -0
  174. package/dist/utils/index.d.ts +1 -0
  175. package/dist/utils/index.js +1 -0
  176. package/dist/utils/precompiled-patterns.d.ts +110 -0
  177. package/dist/utils/precompiled-patterns.js +322 -0
  178. package/dist/wrapper/auth-detection.js +1 -1
  179. package/dist/wrapper/base-wrapper.d.ts +40 -0
  180. package/dist/wrapper/base-wrapper.js +60 -6
  181. package/dist/wrapper/client.d.ts +14 -4
  182. package/dist/wrapper/client.js +89 -31
  183. package/dist/wrapper/idle-detector.d.ts +102 -0
  184. package/dist/wrapper/idle-detector.js +279 -0
  185. package/dist/wrapper/parser.d.ts +4 -0
  186. package/dist/wrapper/parser.js +19 -1
  187. package/dist/wrapper/pty-wrapper.d.ts +14 -2
  188. package/dist/wrapper/pty-wrapper.js +132 -32
  189. package/dist/wrapper/shared.d.ts +1 -1
  190. package/dist/wrapper/shared.js +1 -1
  191. package/dist/wrapper/tmux-wrapper.d.ts +20 -2
  192. package/dist/wrapper/tmux-wrapper.js +163 -40
  193. package/package.json +3 -1
  194. package/scripts/run-migrations.js +43 -0
  195. package/scripts/verify-schema.js +134 -0
  196. package/tests/benchmarks/protocol.bench.ts +310 -0
  197. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
  198. package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +0 -1
  199. package/dist/dashboard/out/_next/static/chunks/app/login/page-c22d080201cbd9fb.js +0 -1
  200. package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +0 -1
  201. /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_buildManifest.js +0 -0
  202. /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_ssgManifest.js +0 -0
package/dist/cli/index.js CHANGED
@@ -3,12 +3,14 @@
3
3
  * Agent Relay CLI
4
4
  *
5
5
  * Commands:
6
- * relay <cmd> - Wrap agent with real-time messaging (default)
7
- * relay -n Name cmd - Wrap with specific agent name
8
- * relay up - Start daemon + dashboard
9
- * relay read <id> - Read full message by ID
10
- * relay agents - List connected agents
11
- * relay who - Show currently active agents
6
+ * relay claude - Start daemon + Dashboard coordinator with Claude
7
+ * relay codex - Start daemon + Dasbboard coordinator with Codex
8
+ * relay create-agent <cmd> - Wrap agent with real-time messaging
9
+ * relay create-agent -n Name cmd - Wrap with specific agent name
10
+ * relay up - Start daemon + dashboard
11
+ * relay read <id> - Read full message by ID
12
+ * relay agents - List connected agents
13
+ * relay who - Show currently active agents
12
14
  */
13
15
  import { Command } from 'commander';
14
16
  import { config as dotenvConfig } from 'dotenv';
@@ -36,7 +38,7 @@ const VERSION = packageJson.version;
36
38
  const execAsync = promisify(exec);
37
39
  // Check for updates in background (non-blocking)
38
40
  // Only show notification for interactive commands, not when wrapping agents or running update
39
- const interactiveCommands = ['up', 'down', 'status', 'agents', 'who', 'version', '--version', '-V', '--help', '-h'];
41
+ const interactiveCommands = ['up', 'down', 'status', 'agents', 'who', 'version', '--version', '-V', '--help', '-h', 'create-agent', 'claude', 'codex'];
40
42
  const shouldCheckUpdates = process.argv.length > 2 &&
41
43
  interactiveCommands.includes(process.argv[2]);
42
44
  if (shouldCheckUpdates) {
@@ -50,21 +52,19 @@ program
50
52
  .name('agent-relay')
51
53
  .description('Agent-to-agent messaging')
52
54
  .version(VERSION, '-V, --version', 'Output the version number');
53
- // Default action = wrap agent
55
+ // create-agent - Wrap agent with real-time messaging
54
56
  program
57
+ .command('create-agent')
58
+ .description('Wrap an agent with real-time messaging')
55
59
  .option('-n, --name <name>', 'Agent name (auto-generated if not set)')
56
- .option('-q, --quiet', 'Disable debug output', false)
60
+ .option('-d, --debug', 'Enable debug output')
57
61
  .option('--prefix <pattern>', 'Relay prefix pattern (default: ->relay:)')
58
62
  .option('--dashboard-port <port>', 'Dashboard port for spawn/release API (auto-detected if not set)')
59
63
  .option('--shadow <name>', 'Spawn a shadow agent with this name that monitors the primary')
60
64
  .option('--shadow-role <role>', 'Shadow role: reviewer, auditor, or triggers (comma-separated: SESSION_END,CODE_WRITTEN,REVIEW_REQUEST,EXPLICIT_ASK,ALL_MESSAGES)')
61
- .argument('[command...]', 'Command to wrap (e.g., claude)')
65
+ .option('--skip-instructions', 'Skip initial instruction injection (use with --append-system-prompt)')
66
+ .argument('<command...>', 'Command to wrap (e.g., claude)')
62
67
  .action(async (commandParts, options) => {
63
- // If no command provided, show help
64
- if (!commandParts || commandParts.length === 0) {
65
- program.help();
66
- return;
67
- }
68
68
  const { getProjectPaths } = await import('../utils/project-namespace.js');
69
69
  const { findAgentConfig, isClaudeCli, buildClaudeArgs } = await import('../utils/agent-config.js');
70
70
  const paths = getProjectPaths();
@@ -93,20 +93,29 @@ program
93
93
  dashboardPort = parseInt(options.dashboardPort, 10);
94
94
  }
95
95
  else {
96
- // Try to detect if dashboard is running at default port
97
- const defaultPort = parseInt(DEFAULT_DASHBOARD_PORT, 10);
98
- try {
99
- const response = await fetch(`http://localhost:${defaultPort}/api/status`, {
100
- method: 'GET',
101
- signal: AbortSignal.timeout(500), // Quick timeout for detection
102
- });
103
- if (response.ok) {
104
- dashboardPort = defaultPort;
105
- console.error(`Dashboard detected: http://localhost:${dashboardPort}`);
96
+ // Try to detect if dashboard is running at common ports
97
+ const portsToTry = [
98
+ parseInt(DEFAULT_DASHBOARD_PORT, 10),
99
+ 3889, 3890, 3891, // Common fallback ports when default is in use
100
+ ];
101
+ for (const port of portsToTry) {
102
+ try {
103
+ const response = await fetch(`http://localhost:${port}/api/health`, {
104
+ method: 'GET',
105
+ signal: AbortSignal.timeout(300), // Quick timeout for detection
106
+ });
107
+ if (response.ok) {
108
+ const health = await response.json();
109
+ if (health.status === 'healthy') {
110
+ dashboardPort = port;
111
+ console.error(`Dashboard detected: http://localhost:${dashboardPort}`);
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ catch {
117
+ // Try next port
106
118
  }
107
- }
108
- catch {
109
- // Dashboard not running - spawn/release will use fallback callbacks
110
119
  }
111
120
  }
112
121
  // Create spawner as fallback for direct spawn (if dashboard API not available)
@@ -116,10 +125,11 @@ program
116
125
  command: mainCommand,
117
126
  args: finalArgs,
118
127
  socketPath: paths.socketPath,
119
- debug: false, // Use -q to keep quiet (debug off by default)
128
+ debug: options.debug ?? false,
120
129
  relayPrefix: options.prefix,
121
130
  useInbox: true,
122
131
  inboxDir: paths.dataDir, // Use the project-specific data directory for the inbox
132
+ skipInstructions: options.skipInstructions,
123
133
  // Use dashboard API for spawn/release when available (preferred - works from any context)
124
134
  dashboardPort,
125
135
  // Wire up spawn/release callbacks as fallback (if no dashboardPort)
@@ -448,6 +458,162 @@ program
448
458
  console.log('Cleaned up stale pid');
449
459
  }
450
460
  });
461
+ // System prompt for Dashboard agent - plain text to avoid shell escaping issues
462
+ const MEGA_SYSTEM_PROMPT = [
463
+ 'You are Dashboard, a lead coordinator in agent-relay.',
464
+ 'Your PRIMARY job is to delegate - you should almost NEVER do implementation work yourself.',
465
+ 'ALWAYS SPAWN AGENTS: For any non-trivial task, spawn specialized workers.',
466
+ ].join(' ');
467
+ // Helper function for starting Dashboard coordinator with a specific provider
468
+ async function startDashboardCoordinator(operator) {
469
+ const { spawn } = await import('node:child_process');
470
+ const { getProjectPaths } = await import('../utils/project-namespace.js');
471
+ const { resolveTmux, TmuxNotFoundError } = await import('../utils/tmux-resolver.js');
472
+ // Check for tmux first
473
+ const tmuxInfo = resolveTmux();
474
+ if (!tmuxInfo) {
475
+ const error = new TmuxNotFoundError();
476
+ console.error(`Error: ${error.message}`);
477
+ process.exit(1);
478
+ }
479
+ const paths = getProjectPaths();
480
+ console.log(`Starting Dashboard with ${operator}...`);
481
+ console.log(`Project: ${paths.projectRoot}`);
482
+ console.log(`Tmux: ${tmuxInfo.path} (v${tmuxInfo.version})`);
483
+ // Step 1: Check if daemon is already running, start if needed
484
+ console.log('\n[1/3] Checking daemon...');
485
+ // Check if socket exists (daemon running)
486
+ const socketExists = fs.existsSync(paths.socketPath);
487
+ // Ports to try for dashboard detection
488
+ const portsToTry = [
489
+ parseInt(DEFAULT_DASHBOARD_PORT, 10),
490
+ 3889, 3890, 3891,
491
+ ];
492
+ // Check if dashboard is responding for THIS project
493
+ let dashboardReady = false;
494
+ let detectedPort;
495
+ // Helper to check health at a port
496
+ const checkPort = async (port) => {
497
+ try {
498
+ const response = await fetch(`http://localhost:${port}/api/health`, {
499
+ signal: AbortSignal.timeout(500),
500
+ });
501
+ if (response.ok) {
502
+ const health = await response.json();
503
+ return health.status === 'healthy';
504
+ }
505
+ }
506
+ catch {
507
+ // Port not responding
508
+ }
509
+ return false;
510
+ };
511
+ if (socketExists) {
512
+ for (const port of portsToTry) {
513
+ if (await checkPort(port)) {
514
+ dashboardReady = true;
515
+ detectedPort = port;
516
+ break;
517
+ }
518
+ }
519
+ }
520
+ if (dashboardReady && detectedPort) {
521
+ console.log(`Daemon already running at port ${detectedPort}, reusing...`);
522
+ }
523
+ else {
524
+ console.log('Starting daemon...');
525
+ const daemonProc = spawn(process.execPath, [process.argv[1], 'up'], {
526
+ stdio: 'ignore',
527
+ detached: true,
528
+ });
529
+ daemonProc.unref();
530
+ // Wait for dashboard to be ready (up to 10 seconds)
531
+ const maxWait = 10000;
532
+ const startTime = Date.now();
533
+ while (Date.now() - startTime < maxWait) {
534
+ await new Promise((resolve) => setTimeout(resolve, 500));
535
+ for (const port of portsToTry) {
536
+ if (await checkPort(port)) {
537
+ dashboardReady = true;
538
+ detectedPort = port;
539
+ break;
540
+ }
541
+ }
542
+ if (dashboardReady)
543
+ break;
544
+ }
545
+ if (!dashboardReady) {
546
+ console.error('Warning: Dashboard may not be fully ready. Spawn might not work.');
547
+ detectedPort = parseInt(DEFAULT_DASHBOARD_PORT, 10); // Fallback
548
+ }
549
+ }
550
+ const dashboardPort = detectedPort || parseInt(DEFAULT_DASHBOARD_PORT, 10);
551
+ // Step 2: Install prpm snippet via npx
552
+ console.log('[2/3] Installing agent-relay snippet...');
553
+ const prpmArgs = operator.toLowerCase() === 'claude'
554
+ ? ['prpm', 'install', '@agent-relay/agent-relay-snippet', '--location', 'CLAUDE.md']
555
+ : ['prpm', 'install', '@agent-relay/agent-relay-snippet'];
556
+ try {
557
+ await new Promise((resolve, reject) => {
558
+ const prpmProc = spawn('npx', prpmArgs, {
559
+ stdio: 'inherit',
560
+ });
561
+ prpmProc.on('close', (code) => {
562
+ if (code === 0)
563
+ resolve();
564
+ else
565
+ reject(new Error(`npx prpm exited with code ${code}`));
566
+ });
567
+ prpmProc.on('error', reject);
568
+ });
569
+ }
570
+ catch (err) {
571
+ console.warn(`Warning: prpm install failed: ${err.message}`);
572
+ console.warn('Continuing without snippet installation...');
573
+ }
574
+ // Step 3: Start Dashboard agent with system prompt
575
+ console.log(`[3/3] Starting Dashboard agent with ${operator}...`);
576
+ console.log('');
577
+ const op = operator.toLowerCase();
578
+ // Build CLI-specific arguments for system prompt
579
+ // These args go AFTER the operator command, passed through to the CLI
580
+ let cliArgs = [];
581
+ if (op === 'claude') {
582
+ // Claude: --append-system-prompt <content> (takes content directly, not file)
583
+ cliArgs = ['--append-system-prompt', MEGA_SYSTEM_PROMPT];
584
+ }
585
+ else if (op === 'codex') {
586
+ // Codex: --config developer_instructions="<content>"
587
+ cliArgs = ['--config', `developer_instructions=${MEGA_SYSTEM_PROMPT}`];
588
+ }
589
+ // Use '--' to separate agent-relay options from the command + its args
590
+ // Format: agent-relay create-agent -n Dashboard --skip-instructions --dashboard-port <port> -- claude --append-system-prompt "..."
591
+ const agentProc = spawn(process.execPath, [process.argv[1], 'create-agent', '-n', 'Dashboard', '--skip-instructions', '--dashboard-port', String(dashboardPort), '--', operator, ...cliArgs], { stdio: 'inherit' });
592
+ // Forward signals to agent process
593
+ process.on('SIGINT', () => {
594
+ agentProc.kill('SIGINT');
595
+ });
596
+ process.on('SIGTERM', () => {
597
+ agentProc.kill('SIGTERM');
598
+ });
599
+ agentProc.on('close', (code) => {
600
+ process.exit(code ?? 0);
601
+ });
602
+ }
603
+ // claude - Start daemon and spawn Dashboard coordinator with Claude
604
+ program
605
+ .command('claude')
606
+ .description('Start daemon and Dashboard coordinator with Claude')
607
+ .action(async () => {
608
+ await startDashboardCoordinator('claude');
609
+ });
610
+ // codex - Start daemon and spawn Dashboard coordinator with Codex
611
+ program
612
+ .command('codex')
613
+ .description('Start daemon and Dashboard coordinator with Codex')
614
+ .action(async () => {
615
+ await startDashboardCoordinator('codex');
616
+ });
451
617
  // status - Check daemon status
452
618
  program
453
619
  .command('status')
@@ -1341,6 +1507,105 @@ program
1341
1507
  }
1342
1508
  }
1343
1509
  });
1510
+ // spawn - Spawn an agent via API (works from any context, no tmux required)
1511
+ program
1512
+ .command('spawn', { hidden: true })
1513
+ .description('Spawn an agent via dashboard API (no tmux required, works in containers)')
1514
+ .argument('<name>', 'Agent name')
1515
+ .argument('<cli>', 'CLI to use (claude, codex, gemini, etc.)')
1516
+ .argument('[task]', 'Task description (can also be piped via stdin)')
1517
+ .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
1518
+ .option('--team <team>', 'Team name for the agent')
1519
+ .option('--spawner <name>', 'Name of the agent requesting the spawn (for policy enforcement)')
1520
+ .option('--interactive', 'Disable auto-accept of permission prompts (for auth setup flows)')
1521
+ .option('--cwd <path>', 'Working directory for the agent')
1522
+ .option('--shadow-mode <mode>', 'Shadow execution mode: subagent or process')
1523
+ .option('--shadow-of <name>', 'Primary agent to shadow (if this agent is a shadow)')
1524
+ .option('--shadow-agent <profile>', 'Shadow agent profile to use')
1525
+ .option('--shadow-triggers <triggers>', 'When to trigger shadow (comma-separated: SESSION_END,CODE_WRITTEN,REVIEW_REQUEST,EXPLICIT_ASK,ALL_MESSAGES)')
1526
+ .option('--shadow-speak-on <triggers>', 'When shadow should speak (comma-separated, same values as --shadow-triggers)')
1527
+ .action(async (name, cli, task, options) => {
1528
+ const port = options.port || DEFAULT_DASHBOARD_PORT;
1529
+ // Read task from stdin if not provided as argument
1530
+ let finalTask = task;
1531
+ if (!finalTask && !process.stdin.isTTY) {
1532
+ const chunks = [];
1533
+ for await (const chunk of process.stdin) {
1534
+ chunks.push(chunk);
1535
+ }
1536
+ finalTask = Buffer.concat(chunks).toString('utf-8').trim();
1537
+ }
1538
+ if (!finalTask) {
1539
+ console.error('Error: Task description required (as argument or via stdin)');
1540
+ process.exit(1);
1541
+ }
1542
+ // Validate shadow mode if provided
1543
+ if (options.shadowMode && !['subagent', 'process'].includes(options.shadowMode)) {
1544
+ console.error('Error: --shadow-mode must be "subagent" or "process"');
1545
+ process.exit(1);
1546
+ }
1547
+ // Parse comma-separated trigger lists
1548
+ const parseTriggers = (value) => {
1549
+ if (!value)
1550
+ return undefined;
1551
+ const validTriggers = ['SESSION_END', 'CODE_WRITTEN', 'REVIEW_REQUEST', 'EXPLICIT_ASK', 'ALL_MESSAGES'];
1552
+ const triggers = value.split(',').map(t => t.trim().toUpperCase());
1553
+ const invalid = triggers.filter(t => !validTriggers.includes(t));
1554
+ if (invalid.length > 0) {
1555
+ console.error(`Error: Invalid triggers: ${invalid.join(', ')}`);
1556
+ console.error(`Valid triggers: ${validTriggers.join(', ')}`);
1557
+ process.exit(1);
1558
+ }
1559
+ return triggers;
1560
+ };
1561
+ try {
1562
+ // Build spawn request using the SpawnRequest type for consistency
1563
+ const spawnRequest = {
1564
+ name,
1565
+ cli,
1566
+ task: finalTask,
1567
+ team: options.team,
1568
+ spawnerName: options.spawner,
1569
+ interactive: options.interactive,
1570
+ cwd: options.cwd,
1571
+ shadowMode: options.shadowMode,
1572
+ shadowOf: options.shadowOf,
1573
+ shadowAgent: options.shadowAgent,
1574
+ shadowTriggers: parseTriggers(options.shadowTriggers),
1575
+ shadowSpeakOn: parseTriggers(options.shadowSpeakOn),
1576
+ };
1577
+ const response = await fetch(`http://localhost:${port}/api/spawn`, {
1578
+ method: 'POST',
1579
+ headers: { 'Content-Type': 'application/json' },
1580
+ body: JSON.stringify(spawnRequest),
1581
+ });
1582
+ const result = await response.json();
1583
+ if (result.success) {
1584
+ console.log(`Spawned agent: ${name} (pid: ${result.pid})`);
1585
+ process.exit(0);
1586
+ }
1587
+ else {
1588
+ if (result.policyDecision) {
1589
+ console.error(`Policy denied spawn: ${result.policyDecision.reason}`);
1590
+ console.error(`Policy source: ${result.policyDecision.policySource}`);
1591
+ }
1592
+ else {
1593
+ console.error(`Failed to spawn ${name}: ${result.error || 'Unknown error'}`);
1594
+ }
1595
+ process.exit(1);
1596
+ }
1597
+ }
1598
+ catch (err) {
1599
+ if (err.code === 'ECONNREFUSED') {
1600
+ console.error(`Cannot connect to dashboard at port ${port}. Is the daemon running?`);
1601
+ console.log(`Run 'agent-relay up' to start the daemon.`);
1602
+ }
1603
+ else {
1604
+ console.error(`Failed to spawn ${name}: ${err.message}`);
1605
+ }
1606
+ process.exit(1);
1607
+ }
1608
+ });
1344
1609
  // release - Release a spawned agent via API (works from any context, no terminal required)
1345
1610
  program
1346
1611
  .command('release')
@@ -1439,7 +1704,7 @@ cloudCommand
1439
1704
  .command('link')
1440
1705
  .description('Link this machine to your Agent Relay Cloud account')
1441
1706
  .option('--name <name>', 'Name for this machine')
1442
- .option('--cloud-url <url>', 'Cloud API URL', process.env.AGENT_RELAY_CLOUD_URL || 'https://api.agent-relay.com')
1707
+ .option('--cloud-url <url>', 'Cloud API URL', process.env.AGENT_RELAY_CLOUD_URL || 'https://agent-relay.com')
1443
1708
  .action(async (options) => {
1444
1709
  const os = await import('node:os');
1445
1710
  const crypto = await import('node:crypto');
@@ -1720,6 +1985,8 @@ program
1720
1985
  stdio: 'inherit',
1721
1986
  env: {
1722
1987
  ...process.env,
1988
+ // Trajectory env vars override parent shell settings
1989
+ // This ensures config-based TRAJECTORIES_DATA_DIR takes precedence
1723
1990
  TRAJECTORIES_PROJECT: paths.projectId,
1724
1991
  TRAJECTORIES_DATA_DIR: trajectoriesDir,
1725
1992
  },
@@ -43,7 +43,7 @@ adminRouter.use(requireAdminAuth);
43
43
  * - batchSize?: Number of concurrent updates (default: 5)
44
44
  *
45
45
  * Response:
46
- * - summary: { total, updated, pendingRestart, skippedActiveAgents, skippedNotRunning, errors }
46
+ * - summary: { total, updated, pendingRestart, skippedActiveAgents, skippedVerificationFailed, skippedNotRunning, errors }
47
47
  * - results: Array of per-workspace results
48
48
  */
49
49
  adminRouter.post('/workspaces/update-image', async (req, res) => {
@@ -159,7 +159,9 @@ adminRouter.get('/workspaces/:id/agents', async (req, res) => {
159
159
  return;
160
160
  }
161
161
  try {
162
- const response = await fetch(`${baseUrl}/api/agents`, {
162
+ // Use /api/data endpoint which returns { agents: [...], ... }
163
+ // Note: /api/agents doesn't exist on the workspace dashboard-server
164
+ const response = await fetch(`${baseUrl}/api/data`, {
163
165
  method: 'GET',
164
166
  headers: { 'Accept': 'application/json' },
165
167
  signal: AbortSignal.timeout(10_000),
@@ -170,7 +172,18 @@ adminRouter.get('/workspaces/:id/agents', async (req, res) => {
170
172
  }
171
173
  const data = await response.json();
172
174
  const agents = data.agents || [];
173
- const activeAgents = agents.filter(a => a.status === 'running' || a.activityState === 'active' || a.activityState === 'idle');
175
+ const activeAgents = agents.filter(a => {
176
+ const status = (a.status ?? '').toLowerCase();
177
+ const activityState = (a.activityState ?? '').toLowerCase();
178
+ const isProcessing = a.isProcessing === true;
179
+ if (activityState === 'active' || activityState === 'idle')
180
+ return true;
181
+ if (status && status !== 'disconnected' && status !== 'offline')
182
+ return true;
183
+ if (isProcessing)
184
+ return true;
185
+ return false;
186
+ });
174
187
  res.json({
175
188
  hasActiveAgents: activeAgents.length > 0,
176
189
  agentCount: activeAgents.length,
@@ -19,6 +19,7 @@ import { requireAuth } from './auth.js';
19
19
  import { db } from '../db/index.js';
20
20
  import { deriveSshPassword } from '../services/ssh-security.js';
21
21
  export const codexAuthHelperRouter = Router();
22
+ const WORKSPACE_SSH_PORT = 3022;
22
23
  const pendingAuthSessions = new Map();
23
24
  const pendingCliTokens = new Map();
24
25
  // Clean up old sessions every minute
@@ -201,18 +202,18 @@ codexAuthHelperRouter.get('/tunnel-info/:workspaceId', async (req, res) => {
201
202
  const host = url.hostname;
202
203
  const apiPort = parseInt(url.port, 10) || 80;
203
204
  // SSH connection info varies by environment:
204
- // - Fly.io: Use public fly.dev hostname with port 2222 (exposed via TCP service)
205
+ // - Fly.io: Use public fly.dev hostname with port 3022 (exposed via TCP service)
205
206
  // - Local Docker: Use localhost with derived SSH port (22000 + apiPort - 3000)
206
207
  const isOnFly = !!process.env.FLY_APP_NAME;
207
208
  const isLocalDocker = (host === 'localhost' || host === '127.0.0.1') && apiPort >= 3000;
208
209
  let sshHost;
209
210
  let sshPort;
210
211
  if (isOnFly) {
211
- // Fly.io public hostname - SSH is exposed as a public TCP service on port 2222
212
- // Users can SSH directly from their machine to {app}.fly.dev:2222
212
+ // Fly.io public hostname - SSH is exposed as a public TCP service on port 3022
213
+ // Users can SSH directly from their machine to {app}.fly.dev:3022
213
214
  const appName = `ar-${workspace.id.substring(0, 8)}`;
214
215
  sshHost = `${appName}.fly.dev`;
215
- sshPort = 2222;
216
+ sshPort = WORKSPACE_SSH_PORT;
216
217
  }
217
218
  else if (isLocalDocker) {
218
219
  // Local Docker: SSH port is derived from API port
@@ -223,7 +224,7 @@ codexAuthHelperRouter.get('/tunnel-info/:workspaceId', async (req, res) => {
223
224
  else {
224
225
  // Default fallback
225
226
  sshHost = host;
226
- sshPort = 2222;
227
+ sshPort = WORKSPACE_SSH_PORT;
227
228
  }
228
229
  // SSH password is derived per-workspace for security
229
230
  // Each workspace gets a unique password based on its ID + secret salt
@@ -288,18 +289,37 @@ codexAuthHelperRouter.get('/auth-status/:workspaceId', async (req, res) => {
288
289
  if (!workspace.publicUrl) {
289
290
  return res.status(400).json({ error: 'Workspace URL not available' });
290
291
  }
291
- // Check with workspace daemon if Codex is authenticated
292
- const response = await fetch(`${workspace.publicUrl}/auth/cli/openai/check`, {
292
+ // Check with workspace daemon if Codex is authenticated for this specific user
293
+ // Pass userId to enable per-user credential checking (multiple users can share a workspace)
294
+ const checkUrl = new URL(`${workspace.publicUrl}/auth/cli/openai/check`);
295
+ checkUrl.searchParams.set('userId', userId);
296
+ const response = await fetch(checkUrl.toString(), {
293
297
  method: 'GET',
294
298
  signal: AbortSignal.timeout(5000),
295
299
  });
296
300
  if (response.ok) {
297
301
  const data = await response.json();
302
+ // When authentication is detected, mark the provider as connected in the database
303
+ // This ensures the dashboard shows correct per-user connection status
304
+ if (data.authenticated && userId) {
305
+ try {
306
+ await db.credentials.upsert({
307
+ userId,
308
+ provider: 'codex', // Codex provider for OpenAI
309
+ scopes: [],
310
+ });
311
+ console.log(`[codex-helper] Marked codex as connected for user ${userId}`);
312
+ }
313
+ catch (dbError) {
314
+ console.error('[codex-helper] Failed to mark provider as connected:', dbError);
315
+ // Don't fail the request if DB update fails
316
+ }
317
+ }
298
318
  return res.json({ authenticated: data.authenticated });
299
319
  }
300
320
  res.json({ authenticated: false });
301
321
  }
302
- catch (error) {
322
+ catch (_error) {
303
323
  // Workspace might not be reachable, return false
304
324
  res.json({ authenticated: false });
305
325
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Consensus API Routes (Read-Only)
3
+ *
4
+ * Provides API endpoints for observing multi-agent consensus decisions.
5
+ * The dashboard is read-only - agents handle all consensus activity via relay messages.
6
+ *
7
+ * Architecture:
8
+ * - Agents create proposals and vote via ->relay:_consensus messages
9
+ * - The daemon processes these and syncs state to cloud via /sync endpoint
10
+ * - Dashboard reads consensus state for display only
11
+ */
12
+ export declare const consensusRouter: import("express-serve-static-core").Router;
13
+ //# sourceMappingURL=consensus.d.ts.map