1515 */
1616
1717import { resolve , join } from 'node:path' ;
18+ import { readdirSync , statSync } from 'node:fs' ;
1819import { SfCommand , Flags } from '@salesforce/sf-plugins-core' ;
19- import { AuthInfo , Connection , Messages , SfError } from '@salesforce/core' ;
20+ import { AuthInfo , Connection , Messages , SfError , SfProject } from '@salesforce/core' ;
2021import React from 'react' ;
2122import { render } from 'ink' ;
2223import { env } from '@salesforce/kit' ;
23- import { AgentPreview as Preview } from '@salesforce/agents' ;
24+ import { AgentPreview as Preview , AgentSimulate , findAuthoringBundle } from '@salesforce/agents' ;
2425import { select , confirm , input } from '@inquirer/prompts' ;
2526import { AgentPreviewReact } from '../../components/agent-preview-react.js' ;
2627
@@ -43,10 +44,17 @@ type Choice<Value> = {
4344 disabled ?: boolean | string ;
4445} ;
4546
47+ enum AgentSource {
48+ ORG = 'org' ,
49+ LOCAL = 'local' ,
50+ }
51+
4652type AgentValue = {
4753 Id : string ;
4854 DeveloperName : string ;
49- } ;
55+ source : AgentSource . ORG ;
56+ } |
57+ { DeveloperName : string ; source : AgentSource . LOCAL ; path : string } ;
5058
5159// https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#prerequisites
5260export const UNSUPPORTED_AGENTS = [ 'Copilot_for_Salesforce' ] ;
@@ -93,13 +101,30 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
93101 const authInfo = await AuthInfo . create ( {
94102 username : flags [ 'target-org' ] . getUsername ( ) ,
95103 } ) ;
96- if ( ! ( flags [ 'client-app' ] ?? env . getString ( 'SF_DEMO_AGENT_CLIENT_APP' ) ) ) {
97- throw new SfError ( 'SF_DEMO_AGENT_CLIENT_APP is unset!' ) ;
104+ // Get client app - check flag first, then auth file, then env var
105+ let clientApp = flags [ 'client-app' ] ;
106+
107+ if ( ! clientApp ) {
108+ const clientApps = getClientAppsFromAuth ( authInfo ) ;
109+
110+ if ( clientApps . length === 1 ) {
111+ clientApp = clientApps [ 0 ] ;
112+ } else if ( clientApps . length > 1 ) {
113+ clientApp = await select ( {
114+ message : 'Select a client app' ,
115+ choices : clientApps . map ( ( app ) => ( { value : app , name : app } ) ) ,
116+ } ) ;
117+ }
118+ }
119+
120+ if ( ! clientApp ) {
121+ // at this point we should throw an error
122+ throw new SfError ( 'No client app found.' ) ;
98123 }
99124
100125 const jwtConn = await Connection . create ( {
101126 authInfo,
102- clientApp : env . getString ( 'SF_DEMO_AGENT_CLIENT_APP' ) ?? flags [ 'client-app' ] ,
127+ clientApp,
103128 } ) ;
104129
105130 const agentsQuery = await conn . query < AgentData > (
@@ -110,32 +135,42 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
110135
111136 const agentsInOrg = agentsQuery . records ;
112137
113- let selectedAgent ;
138+ let selectedAgent : AgentValue | undefined ;
114139
115140 if ( flags [ 'authoring-bundle' ] ) {
116- const envAgentName = env . getString ( 'SF_DEMO_AGENT' ) ;
117- const agent = agentsQuery . records . find ( ( a ) => a . DeveloperName === envAgentName ) ;
141+ const bundlePath = findAuthoringBundle ( this . project ! . getPath ( ) , flags [ 'authoring-bundle' ] ) ;
142+ if ( ! bundlePath ) {
143+ throw new SfError ( `Could not find authoring bundle for ${ flags [ 'authoring-bundle' ] } ` ) ;
144+ }
118145 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- } `,
124146 DeveloperName : flags [ 'authoring-bundle' ] ,
147+ source : AgentSource . LOCAL ,
148+ path : bundlePath ,
125149 } ;
126150 } else if ( apiNameFlag ) {
127- selectedAgent = agentsInOrg . find ( ( agent ) => agent . DeveloperName === apiNameFlag ) ;
151+ const agent = agentsInOrg . find ( ( a ) => a . DeveloperName === apiNameFlag ) ;
152+ if ( ! agent ) throw new Error ( `No valid Agents were found with the Api Name ${ apiNameFlag } .` ) ;
153+ validateAgent ( agent ) ;
154+ selectedAgent = {
155+ Id : agent . Id ,
156+ DeveloperName : agent . DeveloperName ,
157+ source : AgentSource . ORG ,
158+ } ;
128159 if ( ! selectedAgent ) throw new Error ( `No valid Agents were found with the Api Name ${ apiNameFlag } .` ) ;
129- validateAgent ( selectedAgent ) ;
130160 } else {
131161 selectedAgent = await select ( {
132162 message : 'Select an agent' ,
133- choices : getAgentChoices ( agentsInOrg ) ,
163+ choices : getAgentChoices ( agentsInOrg , this . project ! ) ,
134164 } ) ;
135165 }
136166
137167 const outputDir = await resolveOutputDir ( flags [ 'output-dir' ] , flags [ 'apex-debug' ] ) ;
138- const agentPreview = new Preview ( jwtConn , selectedAgent . Id ) ;
168+ // Both classes share the same interface for the methods we need
169+ const agentPreview = selectedAgent . source === AgentSource . ORG ?
170+ new Preview ( jwtConn , selectedAgent . Id ) :
171+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
172+ new AgentSimulate ( jwtConn , selectedAgent . path , true ) as unknown as Preview ;
173+
139174 agentPreview . toggleApexDebugMode ( flags [ 'apex-debug' ] ) ;
140175
141176 const instance = render (
@@ -172,22 +207,59 @@ export const validateAgent = (agent: AgentData): boolean => {
172207 return true ;
173208} ;
174209
175- export const getAgentChoices = ( agents : AgentData [ ] ) : Array < Choice < AgentValue > > =>
176- agents . map ( ( agent ) => {
177- let disabled : string | boolean = false ;
210+ export const getAgentChoices = (
211+ agents : AgentData [ ] ,
212+ project : SfProject
213+ ) : Array < Choice < AgentValue > > => {
214+ const choices : Array < Choice < AgentValue > > = [ ] ;
178215
179- if ( agentIsInactive ( agent ) ) disabled = '(Inactive)' ;
180- if ( agentIsUnsupported ( agent . DeveloperName ) ) disabled = '(Not Supported)' ;
216+ // Add org agents
217+ for ( const agent of agents ) {
218+ if ( agentIsInactive ( agent ) || agentIsUnsupported ( agent . DeveloperName ) ) {
219+ continue ;
220+ }
181221
182- return {
183- name : agent . DeveloperName ,
222+ choices . push ( {
223+ name : ` ${ agent . DeveloperName } (org)` ,
184224 value : {
185225 Id : agent . Id ,
186226 DeveloperName : agent . DeveloperName ,
227+ source : AgentSource . ORG ,
187228 } ,
188- disabled,
189- } ;
190- } ) ;
229+ } ) ;
230+ }
231+
232+ // Add local agents from authoring bundles
233+ const localAgents = findAuthoringBundle ( project . getPath ( ) , '*' ) ;
234+ if ( localAgents ) {
235+ const bundlePath = localAgents . replace ( / \/ [ ^ / ] + $ / , '' ) ; // Get parent directory
236+ const agentDirs = readdirSync ( bundlePath ) . filter ( ( dir ) =>
237+ statSync ( join ( bundlePath , dir ) ) . isDirectory ( )
238+ ) ;
239+
240+ agentDirs . forEach ( ( agentDir ) => {
241+ choices . push ( {
242+ name : `${ agentDir } (local)` ,
243+ value : {
244+ DeveloperName : agentDir ,
245+ source : AgentSource . LOCAL ,
246+ path : join ( bundlePath , agentDir ) ,
247+ } ,
248+ } ) ;
249+ } ) ;
250+ }
251+
252+ return choices ;
253+ } ;
254+
255+
256+ export const getClientAppsFromAuth = ( authInfo : AuthInfo ) : string [ ] => {
257+ const config = authInfo . getConnectionOptions ( ) ;
258+ const clientApps = Object . entries ( config )
259+ . filter ( ( [ key ] ) => key . startsWith ( 'oauthClientApp_' ) )
260+ . map ( ( [ , value ] ) => value as string ) ;
261+ return clientApps ;
262+ } ;
191263
192264export const resolveOutputDir = async (
193265 outputDir : string | undefined ,
0 commit comments