@@ -2,7 +2,7 @@ import { type Command } from 'commander';
22import chalk from 'chalk' ;
33import { type RunEvent } from '@agentage/core' ;
44import { ensureDaemon } from '../utils/ensure-daemon.js' ;
5- import { connectWs , post } from '../utils/daemon-client.js' ;
5+ import { connectWs , get , post } from '../utils/daemon-client.js' ;
66import { renderEvent } from '../utils/render.js' ;
77
88interface RunResponse {
@@ -24,10 +24,21 @@ type WsMessage = WsRunEventMessage | WsRunStateMessage;
2424
2525const TERMINAL_STATES = [ 'completed' , 'failed' , 'canceled' ] ;
2626
27+ const parseAgentTarget = ( input : string ) : { agentName : string ; machineName ?: string } => {
28+ const atIndex = input . lastIndexOf ( '@' ) ;
29+ if ( atIndex > 0 ) {
30+ return {
31+ agentName : input . substring ( 0 , atIndex ) ,
32+ machineName : input . substring ( atIndex + 1 ) ,
33+ } ;
34+ }
35+ return { agentName : input } ;
36+ } ;
37+
2738export const registerRun = ( program : Command ) : void => {
2839 program
2940 . command ( 'run' )
30- . argument ( '<agent>' , 'Agent name' )
41+ . argument ( '<agent>' , 'Agent name (or agent@machine for remote) ' )
3142 . argument ( '[prompt]' , 'Task/prompt for the agent' )
3243 . description ( 'Run an agent' )
3344 . option ( '-d, --detach' , 'Run in background, print run ID' )
@@ -48,27 +59,96 @@ export const registerRun = (program: Command): void => {
4859 return ;
4960 }
5061
51- const config = opts . config
52- ? ( JSON . parse ( opts . config ) as Record < string , unknown > )
53- : undefined ;
62+ const { agentName, machineName } = parseAgentTarget ( agent ) ;
5463
55- const { runId } = await post < RunResponse > ( `/api/agents/${ agent } /run` , {
56- task : prompt ,
57- config,
58- context : opts . context ,
59- } ) ;
60-
61- if ( opts . detach ) {
62- console . log ( runId ) ;
63- return ;
64+ if ( machineName ) {
65+ await runRemote ( agentName , machineName , prompt , opts ) ;
66+ } else {
67+ await runLocal ( agentName , prompt , opts ) ;
6468 }
65-
66- // Stream events via WebSocket
67- await streamRun ( runId , opts . json ?? false ) ;
6869 }
6970 ) ;
7071} ;
7172
73+ const runLocal = async (
74+ agentName : string ,
75+ prompt : string ,
76+ opts : { detach ?: boolean ; json ?: boolean ; config ?: string ; context ?: string [ ] }
77+ ) : Promise < void > => {
78+ const config = opts . config ? ( JSON . parse ( opts . config ) as Record < string , unknown > ) : undefined ;
79+
80+ const { runId } = await post < RunResponse > ( `/api/agents/${ agentName } /run` , {
81+ task : prompt ,
82+ config,
83+ context : opts . context ,
84+ } ) ;
85+
86+ if ( opts . detach ) {
87+ console . log ( runId ) ;
88+ return ;
89+ }
90+
91+ await streamRun ( runId , opts . json ?? false ) ;
92+ } ;
93+
94+ const runRemote = async (
95+ agentName : string ,
96+ machineName : string ,
97+ prompt : string ,
98+ opts : { detach ?: boolean ; json ?: boolean }
99+ ) : Promise < void > => {
100+ // Resolve machine name to machine ID
101+ let machines : Array < { id : string ; name : string } > ;
102+ try {
103+ machines = await get < Array < { id : string ; name : string } > > ( '/api/hub/machines' ) ;
104+ } catch {
105+ console . error ( chalk . red ( "Not connected to hub. Run 'agentage login' first." ) ) ;
106+ process . exitCode = 1 ;
107+ return ;
108+ }
109+
110+ const machine = machines . find ( ( m ) => m . name === machineName ) ;
111+ if ( ! machine ) {
112+ console . error ( chalk . red ( `Machine "${ machineName } " not found.` ) ) ;
113+ console . error ( chalk . dim ( `Available: ${ machines . map ( ( m ) => m . name ) . join ( ', ' ) || 'none' } ` ) ) ;
114+ process . exitCode = 1 ;
115+ return ;
116+ }
117+
118+ // Create run via hub
119+ let result : { runId ?: string } ;
120+ try {
121+ result = await post < { runId ?: string } > ( '/api/hub/runs' , {
122+ machineId : machine . id ,
123+ agentName,
124+ input : prompt ,
125+ } ) ;
126+ } catch ( err ) {
127+ console . error (
128+ chalk . red ( `Failed to start remote run: ${ err instanceof Error ? err . message : String ( err ) } ` )
129+ ) ;
130+ process . exitCode = 1 ;
131+ return ;
132+ }
133+
134+ const runId = result . runId ?? ( result as Record < string , unknown > ) . runId ;
135+ if ( ! runId ) {
136+ console . error ( chalk . red ( 'Failed to get run ID from hub' ) ) ;
137+ process . exitCode = 1 ;
138+ return ;
139+ }
140+
141+ if ( opts . detach ) {
142+ console . log ( runId ) ;
143+ return ;
144+ }
145+
146+ console . log ( chalk . dim ( `Running ${ agentName } on ${ machineName } ...` ) ) ;
147+
148+ // Poll for events from hub (MVP approach — daemon proxies)
149+ await pollRemoteRun ( runId as string , opts . json ?? false ) ;
150+ } ;
151+
72152const streamRun = ( runId : string , jsonMode : boolean ) : Promise < void > =>
73153 new Promise ( ( resolve ) => {
74154 const ws = connectWs ( ( data ) => {
@@ -109,3 +189,42 @@ const streamRun = (runId: string, jsonMode: boolean): Promise<void> =>
109189 resolve ( ) ;
110190 } ) ;
111191 } ) ;
192+
193+ const pollRemoteRun = async ( runId : string , jsonMode : boolean ) : Promise < void > => {
194+ let lastEventId : string | undefined ;
195+ const pollInterval = 1000 ;
196+
197+ const poll = async ( ) : Promise < boolean > => {
198+ try {
199+ const url = lastEventId
200+ ? `/api/hub/runs/${ runId } /events?after=${ lastEventId } `
201+ : `/api/hub/runs/${ runId } /events` ;
202+
203+ const events = await get < Array < { id : string ; type : string ; data : unknown } > > ( url ) ;
204+
205+ for ( const event of events ) {
206+ if ( jsonMode ) {
207+ console . log ( JSON . stringify ( event ) ) ;
208+ } else {
209+ renderEvent ( event as unknown as RunEvent ) ;
210+ }
211+ lastEventId = event . id ;
212+ }
213+
214+ // Check run state
215+ const run = await get < { state : string } > ( `/api/hub/runs/${ runId } ` ) ;
216+ if ( TERMINAL_STATES . includes ( run . state ) ) {
217+ return true ;
218+ }
219+ } catch {
220+ // Hub may be temporarily unreachable
221+ }
222+ return false ;
223+ } ;
224+
225+ while ( true ) {
226+ const done = await poll ( ) ;
227+ if ( done ) break ;
228+ await new Promise ( ( r ) => setTimeout ( r , pollInterval ) ) ;
229+ }
230+ } ;
0 commit comments