1414 * limitations under the License.
1515 */
1616
17- import { resolve , join } from 'node:path' ;
18- import { SfCommand , Flags } from '@salesforce/sf-plugins-core' ;
19- import { AuthInfo , Connection , Messages , SfError } from '@salesforce/core' ;
17+ import * as path from 'node:path' ;
18+ import { join , resolve } from 'node:path' ;
19+ import { globSync } from 'glob' ;
20+ import { Flags , SfCommand } from '@salesforce/sf-plugins-core' ;
21+ import { AuthInfo , Connection , Lifecycle , Messages , SfError } from '@salesforce/core' ;
2022import React from 'react' ;
2123import { render } from 'ink' ;
2224import { env } from '@salesforce/kit' ;
23- import { AgentPreview as Preview } from '@salesforce/agents' ;
24- import { select , confirm , input } from '@inquirer/prompts' ;
25+ import {
26+ AgentPreview as Preview ,
27+ AgentSimulate ,
28+ AgentSource ,
29+ findAuthoringBundle ,
30+ PublishedAgent ,
31+ ScriptAgent ,
32+ } from '@salesforce/agents' ;
33+ import { confirm , input , select } from '@inquirer/prompts' ;
2534import { AgentPreviewReact } from '../../components/agent-preview-react.js' ;
2635
2736Messages . importMessagesDirectoryFromMetaUrl ( import . meta. url ) ;
@@ -43,11 +52,6 @@ type Choice<Value> = {
4352 disabled ?: boolean | string ;
4453} ;
4554
46- type AgentValue = {
47- Id : string ;
48- DeveloperName : string ;
49- } ;
50-
5155// https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#prerequisites
5256export const UNSUPPORTED_AGENTS = [ 'Copilot_for_Salesforce' ] ;
5357
@@ -82,73 +86,153 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
8286 summary : messages . getMessage ( 'flags.apex-debug.summary' ) ,
8387 char : 'x' ,
8488 } ) ,
89+ 'use-live-actions' : Flags . boolean ( {
90+ summary : messages . getMessage ( 'flags.use-live-actions.summary' ) ,
91+ default : false ,
92+ } ) ,
8593 } ;
8694
8795 public async run ( ) : Promise < AgentPreviewResult > {
96+ // STAGES OF PREVIEW
97+ // get user's agent selection either from flags, or interaction
98+ // if .agent selected, use the AgentSimulate class to preview
99+ // if published agent, use AgentPreview for preview
100+ // based on agent, differing auth mechanisms required
88101 const { flags } = await this . parse ( AgentPreview ) ;
89102
90- const { 'api-name' : apiNameFlag } = flags ;
103+ const { 'api-name' : apiNameFlag , 'use-live-actions' : useLiveActions } = flags ;
91104 const conn = flags [ 'target-org' ] . getConnection ( flags [ 'api-version' ] ) ;
92105
93- const authInfo = await AuthInfo . create ( {
94- username : flags [ 'target-org' ] . getUsername ( ) ,
95- } ) ;
96- if ( ! ( flags [ 'client-app' ] ?? env . getString ( 'SF_DEMO_AGENT_CLIENT_APP' ) ) ) {
97- throw new SfError ( 'SF_DEMO_AGENT_CLIENT_APP is unset!' ) ;
98- }
99-
100- const jwtConn = await Connection . create ( {
101- authInfo,
102- clientApp : env . getString ( 'SF_DEMO_AGENT_CLIENT_APP' ) ?? flags [ 'client-app' ] ,
103- } ) ;
106+ const agentsInOrg = (
107+ await conn . query < AgentData > (
108+ 'SELECT Id, DeveloperName, (SELECT Status FROM BotVersions) FROM BotDefinition WHERE IsDeleted = false'
109+ )
110+ ) . records ;
104111
105- const agentsQuery = await conn . query < AgentData > (
106- 'SELECT Id, DeveloperName, (SELECT Status FROM BotVersions) FROM BotDefinition WHERE IsDeleted = false'
107- ) ;
108-
109- if ( agentsQuery . totalSize === 0 ) throw new SfError ( 'No Agents found in the org' ) ;
110-
111- const agentsInOrg = agentsQuery . records ;
112-
113- let selectedAgent ;
112+ let selectedAgent : ScriptAgent | PublishedAgent ;
114113
115114 if ( flags [ 'authoring-bundle' ] ) {
116- const envAgentName = env . getString ( 'SF_DEMO_AGENT' ) ;
117- const agent = agentsQuery . records . find ( ( a ) => a . DeveloperName === envAgentName ) ;
115+ // user specified --authoring-bundle, we'll find the script and use it
116+ const bundlePath = findAuthoringBundle ( this . project ! . getPath ( ) , flags [ 'authoring-bundle' ] ) ;
117+ if ( ! bundlePath ) {
118+ throw new SfError ( `Could not find authoring bundle for ${ flags [ 'authoring-bundle' ] } ` ) ;
119+ }
118120 selectedAgent = {
119- Id :
120- agent ?. Id ??
121- `Couldn't find an agent in ${ agentsQuery . records . map ( ( a ) => a . DeveloperName ) . join ( ', ' ) } matching ${
122- envAgentName ?? '!SF_DEMO_AGENT is unset!'
123- } `,
124121 DeveloperName : flags [ 'authoring-bundle' ] ,
122+ source : AgentSource . SCRIPT ,
123+ path : join ( bundlePath , `${ flags [ 'authoring-bundle' ] } .agent` ) ,
125124 } ;
126125 } else if ( apiNameFlag ) {
127- selectedAgent = agentsInOrg . find ( ( agent ) => agent . DeveloperName === apiNameFlag ) ;
126+ // user specified --api-name, it should be in the list of agents from the org
127+ const agent = agentsInOrg . find ( ( a ) => a . DeveloperName === apiNameFlag ) ;
128+ if ( ! agent ) throw new Error ( `No valid Agents were found with the Api Name ${ apiNameFlag } .` ) ;
129+ validateAgent ( agent ) ;
130+ selectedAgent = {
131+ Id : agent . Id ,
132+ DeveloperName : agent . DeveloperName ,
133+ source : AgentSource . PUBLISHED ,
134+ } ;
128135 if ( ! selectedAgent ) throw new Error ( `No valid Agents were found with the Api Name ${ apiNameFlag } .` ) ;
129- validateAgent ( selectedAgent ) ;
130136 } else {
131- selectedAgent = await select ( {
137+ selectedAgent = await select < ScriptAgent | PublishedAgent > ( {
132138 message : 'Select an agent' ,
133- choices : getAgentChoices ( agentsInOrg ) ,
139+ choices : this . getAgentChoices ( agentsInOrg ) ,
134140 } ) ;
135141 }
136142
143+ // we have the selected agent, create the appropriate connection
144+ const authInfo = await AuthInfo . create ( {
145+ username : flags [ 'target-org' ] . getUsername ( ) ,
146+ } ) ;
147+ // Get client app - check flag first, then auth file, then env var
148+ let clientApp = flags [ 'client-app' ] ;
149+
150+ if ( ! clientApp && selectedAgent ?. source === AgentSource . PUBLISHED ) {
151+ const clientApps = getClientAppsFromAuth ( authInfo ) ;
152+
153+ if ( clientApps . length === 1 ) {
154+ clientApp = clientApps [ 0 ] ;
155+ } else if ( clientApps . length > 1 ) {
156+ clientApp = await select ( {
157+ message : 'Select a client app' ,
158+ choices : clientApps . map ( ( app ) => ( { value : app , name : app } ) ) ,
159+ } ) ;
160+ } else {
161+ throw new SfError ( 'No client app found.' ) ;
162+ }
163+ }
164+
165+ if ( useLiveActions && selectedAgent . source === AgentSource . PUBLISHED ) {
166+ void Lifecycle . getInstance ( ) . emitWarning (
167+ 'Published agents will always use real actions in your org, specifying --use-live-actions and selecting a published agent has no effect'
168+ ) ;
169+ }
170+
171+ const jwtConn =
172+ selectedAgent ?. source === AgentSource . PUBLISHED
173+ ? await Connection . create ( {
174+ authInfo,
175+ clientApp,
176+ } )
177+ : await Connection . create ( { authInfo } ) ;
178+
137179 const outputDir = await resolveOutputDir ( flags [ 'output-dir' ] , flags [ 'apex-debug' ] ) ;
138- const agentPreview = new Preview ( jwtConn , selectedAgent . Id ) ;
139- agentPreview . toggleApexDebugMode ( flags [ 'apex-debug' ] ) ;
180+ // Both classes share the same interface for the methods we need
181+ const agentPreview =
182+ selectedAgent . source === AgentSource . PUBLISHED
183+ ? new Preview ( jwtConn , selectedAgent . Id )
184+ : new AgentSimulate ( jwtConn , selectedAgent . path , useLiveActions ) ;
185+
186+ agentPreview . setApexDebugMode ( flags [ 'apex-debug' ] ) ;
140187
141188 const instance = render (
142189 React . createElement ( AgentPreviewReact , {
143190 connection : conn ,
144191 agent : agentPreview ,
145192 name : selectedAgent . DeveloperName ,
146193 outputDir,
194+ isLocalAgent : selectedAgent . source === AgentSource . SCRIPT ,
147195 } ) ,
148196 { exitOnCtrlC : false }
149197 ) ;
150198 await instance . waitUntilExit ( ) ;
151199 }
200+
201+ private getAgentChoices ( agents : AgentData [ ] ) : Array < Choice < ScriptAgent | PublishedAgent > > {
202+ const choices : Array < Choice < ScriptAgent | PublishedAgent > > = [ ] ;
203+
204+ // Add org agents
205+ for ( const agent of agents ) {
206+ if ( agentIsInactive ( agent ) || agentIsUnsupported ( agent . DeveloperName ) ) {
207+ continue ;
208+ }
209+
210+ choices . push ( {
211+ name : `${ agent . DeveloperName } (Published)` ,
212+ value : {
213+ Id : agent . Id ,
214+ DeveloperName : agent . DeveloperName ,
215+ source : AgentSource . PUBLISHED ,
216+ } ,
217+ } ) ;
218+ }
219+
220+ // Add local agents from .agent files
221+ const localAgentPaths = globSync ( '**/*.agent' , { cwd : this . project ! . getPath ( ) } ) ;
222+ for ( const agentPath of localAgentPaths ) {
223+ const agentName = path . basename ( agentPath , '.agent' ) ;
224+ choices . push ( {
225+ name : `${ agentName } (Agent Script)` ,
226+ value : {
227+ DeveloperName : agentName ,
228+ source : AgentSource . SCRIPT ,
229+ path : path . join ( this . project ! . getPath ( ) , agentPath ) ,
230+ } ,
231+ } ) ;
232+ }
233+
234+ return choices ;
235+ }
152236}
153237
154238export const agentIsUnsupported = ( devName : string ) : boolean => UNSUPPORTED_AGENTS . includes ( devName ) ;
@@ -172,22 +256,8 @@ export const validateAgent = (agent: AgentData): boolean => {
172256 return true ;
173257} ;
174258
175- export const getAgentChoices = ( agents : AgentData [ ] ) : Array < Choice < AgentValue > > =>
176- agents . map ( ( agent ) => {
177- let disabled : string | boolean = false ;
178-
179- if ( agentIsInactive ( agent ) ) disabled = '(Inactive)' ;
180- if ( agentIsUnsupported ( agent . DeveloperName ) ) disabled = '(Not Supported)' ;
181-
182- return {
183- name : agent . DeveloperName ,
184- value : {
185- Id : agent . Id ,
186- DeveloperName : agent . DeveloperName ,
187- } ,
188- disabled,
189- } ;
190- } ) ;
259+ export const getClientAppsFromAuth = ( authInfo : AuthInfo ) : string [ ] =>
260+ Object . keys ( authInfo . getFields ( ) . clientApps ?? { } ) ;
191261
192262export const resolveOutputDir = async (
193263 outputDir : string | undefined ,
0 commit comments