Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5478d76
test: enabling NUTs against a devhub for now
WillieRuemmele Dec 12, 2025
47f18c4
chore: remove env from pjson
WillieRuemmele Dec 12, 2025
cf80719
chore: skip failing NUT until 12/16
WillieRuemmele Dec 12, 2025
41ace9e
chore: working on refactor
WillieRuemmele Dec 12, 2025
8f5b242
chore: more refactoring consumption
WillieRuemmele Dec 12, 2025
4141efb
chore: refactoring, builds
WillieRuemmele Dec 15, 2025
25b6bb5
chore: working on consumption
WillieRuemmele Dec 15, 2025
1004182
chore: fix selection types
WillieRuemmele Dec 15, 2025
9bcefd2
chore: let library save traces
WillieRuemmele Dec 15, 2025
65e541d
chore: use correct names
WillieRuemmele Dec 16, 2025
631999b
chore: update snapshot
WillieRuemmele Dec 19, 2025
aed5169
test: fix tests
WillieRuemmele Dec 19, 2025
d6ddb8e
chore: merge main, resolve conflicts
WillieRuemmele Dec 19, 2025
f170922
chore: merge main, resolve conflicts
WillieRuemmele Jan 8, 2026
46f5100
test: downgrade SDR to avoid AAB deploy bug
WillieRuemmele Jan 8, 2026
1996550
chore: delete unused NUT project files
WillieRuemmele Jan 8, 2026
ea7516e
chore: bump SDR
WillieRuemmele Jan 9, 2026
c47dc90
chore: merge main, resolve conflicts
WillieRuemmele Jan 9, 2026
bf8a4a3
chore: try pinning SDR to before AAB validation on deploy
WillieRuemmele Jan 12, 2026
432b784
chore: bump agents
WillieRuemmele Jan 13, 2026
8c5a819
chore: set skip aab in test setup
WillieRuemmele Jan 13, 2026
dd2b33e
test: add timeouts to befores for shared test-setup
WillieRuemmele Jan 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,12 @@
"alias": [],
"command": "agent:preview",
"flagAliases": [],
"flagChars": ["c", "d", "n", "o", "x"],
"flagChars": ["d", "n", "o", "x"],
"flags": [
"apex-debug",
"api-name",
"api-version",
"authoring-bundle",
"client-app",
"flags-dir",
"output-dir",
"target-org",
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
"version": "1.26.2",
"author": "Salesforce",
"bugs": "https://github.com/forcedotcom/cli/issues",
"resolutions": {
"@salesforce/source-deploy-retrieve": "12.30.0"
},
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/prompts": "^7.10.1",
"@oclif/core": "^4",
"@oclif/multi-stage-output": "^0.8.29",
"@salesforce/agents": "^0.20.0",
"@salesforce/core": "^8.23.7",
"@salesforce/agents": "0.20.0-beta.3",
"@salesforce/core": "^8.24.0",
"@salesforce/kit": "^3.2.3",
"@salesforce/sf-plugins-core": "^12.2.6",
"@salesforce/source-deploy-retrieve": "^12.31.2",
"@salesforce/source-deploy-retrieve": "^12.31.6",
"@salesforce/types": "^1.5.0",
"ansis": "^3.3.2",
"fast-xml-parser": "^4.5.1",
Expand Down
17 changes: 10 additions & 7 deletions src/agentActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* limitations under the License.
*/

import { Connection, Messages, Org, SfError } from '@salesforce/core';
import { Agent, type BotMetadata } from '@salesforce/agents';
import { Messages, Org, SfError, SfProject } from '@salesforce/core';
import { Agent, type BotMetadata, ProductionAgent } from '@salesforce/agents';
import { select } from '@inquirer/prompts';

type Choice<Value> = {
Expand Down Expand Up @@ -67,16 +67,15 @@ export const getAgentChoices = (agents: BotMetadata[], status: 'Active' | 'Inact
});

export const getAgentForActivation = async (config: {
conn: Connection;
targetOrg: Org;
status: 'Active' | 'Inactive';
apiNameFlag?: string;
}): Promise<Agent> => {
const { conn, targetOrg, status, apiNameFlag } = config;
}): Promise<ProductionAgent> => {
const { targetOrg, status, apiNameFlag } = config;

let agentsInOrg: BotMetadata[] = [];
try {
agentsInOrg = await Agent.listRemote(conn);
agentsInOrg = await Agent.listRemote(targetOrg.getConnection());
} catch (error) {
throw SfError.create({
message: 'Error listing agents in org',
Expand Down Expand Up @@ -105,5 +104,9 @@ export const getAgentForActivation = async (config: {
selectedAgent = agentsInOrg.find((agent) => agent.DeveloperName === agentChoice.DeveloperName);
}

return new Agent({ connection: conn, nameOrId: selectedAgent!.Id });
return Agent.init({
connection: targetOrg.getConnection(),
apiNameOrId: selectedAgent!.Id,
project: SfProject.getInstance(),
});
};
3 changes: 1 addition & 2 deletions src/commands/agent/activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@ export default class AgentActivate extends SfCommand<void> {

const apiNameFlag = flags['api-name'];
const targetOrg = flags['target-org'];
const conn = targetOrg.getConnection(flags['api-version']);

if (!apiNameFlag && this.jsonEnabled()) {
throw messages.createError('error.missingRequiredFlags', ['api-name']);
}

const agent = await getAgentForActivation({ conn, targetOrg, status: 'Active', apiNameFlag });
const agent = await getAgentForActivation({ targetOrg, status: 'Active', apiNameFlag });
await agent.activate();
const agentName = (await agent.getBotMetadata()).DeveloperName;

Expand Down
3 changes: 1 addition & 2 deletions src/commands/agent/deactivate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@ export default class AgentDeactivate extends SfCommand<void> {

const apiNameFlag = flags['api-name'];
const targetOrg = flags['target-org'];
const conn = targetOrg.getConnection(flags['api-version']);

if (!apiNameFlag && this.jsonEnabled()) {
throw messages.createError('error.missingRequiredFlags', ['api-name']);
}

const agent = await getAgentForActivation({ conn, targetOrg, status: 'Inactive', apiNameFlag });
const agent = await getAgentForActivation({ targetOrg, status: 'Inactive', apiNameFlag });
await agent.deactivate();
const agentName = (await agent.getBotMetadata()).DeveloperName;

Expand Down
8 changes: 3 additions & 5 deletions src/commands/agent/generate/authoring-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { join, resolve } from 'node:path';
import { readFileSync, existsSync } from 'node:fs';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { generateApiName, Messages, SfError } from '@salesforce/core';
import { Agent, AgentJobSpec } from '@salesforce/agents';
import { AgentJobSpec, ScriptAgent } from '@salesforce/agents';
import YAML from 'yaml';
import { input as inquirerInput } from '@inquirer/prompts';
import { theme } from '../../../inquirer-theme.js';
Expand Down Expand Up @@ -102,7 +102,7 @@ export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerat

public async run(): Promise<AgentGenerateAuthoringBundleResult> {
const { flags } = await this.parse(AgentGenerateAuthoringBundle);
const { 'output-dir': outputDir, 'target-org': targetOrg } = flags;
const { 'output-dir': outputDir } = flags;

// If we don't have a spec yet, prompt for it
const spec = flags.spec ?? (await promptForYamlFile(AgentGenerateAuthoringBundle.FLAGGABLE_PROMPTS['spec']));
Expand Down Expand Up @@ -135,10 +135,8 @@ export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerat
const metaXmlPath = join(targetOutputDir, `${bundleApiName}.bundle-meta.xml`);

// Write Agent file
const conn = targetOrg.getConnection(flags['api-version']);
const specContents = YAML.parse(readFileSync(spec, 'utf8')) as AgentJobSpec;
await Agent.createAuthoringBundle({
connection: conn,
await ScriptAgent.createAuthoringBundle({
agentSpec: { ...specContents, ...{ name, developerName: bundleApiName } },
project: this.project!,
bundleApiName,
Expand Down
162 changes: 33 additions & 129 deletions src/commands/agent/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,25 @@
* limitations under the License.
*/

import * as path from 'node:path';
import { join, resolve } from 'node:path';
import { globSync } from 'glob';
import { resolve } from 'node:path';
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
import { AuthInfo, Connection, Lifecycle, Logger, Messages, SfError } from '@salesforce/core';
import React from 'react';
import { render } from 'ink';
import {
AgentPreview as Preview,
AgentSimulate,
Agent,
AgentSource,
findAuthoringBundle,
PublishedAgent,
PreviewableAgent,
ProductionAgent,
ScriptAgent,
} from '@salesforce/agents';
import { select } from '@inquirer/prompts';
import { Lifecycle, Messages, SfError } from '@salesforce/core';
import { AgentPreviewReact } from '../../components/agent-preview-react.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.preview');

let logger: Logger;
const getLogger = (): Logger => {
if (!logger) {
logger = Logger.childFromRoot('plugin-agent-preview');
}
return logger;
};

type BotVersionStatus = { Status: 'Active' | 'Inactive' };

export type AgentData = {
Expand All @@ -53,12 +43,6 @@ export type AgentData = {
};
};

type Choice<Value> = {
value: Value;
name?: string;
disabled?: boolean | string;
};

// https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#prerequisites
export const UNSUPPORTED_AGENTS = ['Copilot_for_Salesforce'];

Expand All @@ -74,11 +58,6 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
public static readonly flags = {
'target-org': Flags.requiredOrg(),
'api-version': Flags.orgApiVersion(),
'client-app': Flags.string({
char: 'c',
summary: messages.getMessage('flags.client-app.summary'),
dependsOn: ['target-org'],
}),
'api-name': Flags.string({
summary: messages.getMessage('flags.api-name.summary'),
char: 'n',
Expand All @@ -105,146 +84,74 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
// get user's agent selection either from flags, or interaction
// if .agent selected, use the AgentSimulate class to preview
// if published agent, use AgentPreview for preview
// based on agent, differing auth mechanisms required
const { flags } = await this.parse(AgentPreview);

const { 'api-name': apiNameFlag, 'use-live-actions': useLiveActions } = flags;
const conn = flags['target-org'].getConnection(flags['api-version']);

const agentsInOrg = (
await conn.query<AgentData>(
'SELECT Id, DeveloperName, (SELECT Status FROM BotVersions) FROM BotDefinition WHERE IsDeleted = false'
)
).records;

let selectedAgent: ScriptAgent | PublishedAgent;
let selectedAgent: ScriptAgent | ProductionAgent;

if (flags['authoring-bundle']) {
// user specified --authoring-bundle, we'll find the script and use it
const bundlePath = findAuthoringBundle(this.project!.getPath(), flags['authoring-bundle']);
if (!bundlePath) {
throw new SfError(`Could not find authoring bundle for ${flags['authoring-bundle']}`);
}
selectedAgent = {
DeveloperName: flags['authoring-bundle'],
source: AgentSource.SCRIPT,
path: join(bundlePath, `${flags['authoring-bundle']}.agent`),
};
selectedAgent = await Agent.init({ connection: conn, project: this.project!, aabDirectory: bundlePath });
} else if (apiNameFlag) {
// user specified --api-name, it should be in the list of agents from the org
const agent = agentsInOrg.find((a) => a.DeveloperName === apiNameFlag);
if (!agent) throw new Error(`No valid Agents were found with the Api Name ${apiNameFlag}.`);
validateAgent(agent);
selectedAgent = {
Id: agent.Id,
DeveloperName: agent.DeveloperName,
source: AgentSource.PUBLISHED,
};
if (!selectedAgent) throw new Error(`No valid Agents were found with the Api Name ${apiNameFlag}.`);
selectedAgent = await Agent.init({ connection: conn, project: this.project!, apiNameOrId: apiNameFlag });
} else {
selectedAgent = await select<ScriptAgent | PublishedAgent>({
const previewableAgents = await Agent.listPreviewable(conn, this.project!);
const choices = previewableAgents.map((agent) => ({
name: agent.source === AgentSource.PUBLISHED ? `${agent.name} (Published)` : `${agent.name} (Agent Script)`,
value: agent,
}));
const choice = await select<PreviewableAgent>({
message: 'Select an agent',
choices: this.getAgentChoices(agentsInOrg),
choices,
});
}

// we have the selected agent, create the appropriate connection
const authInfo = await AuthInfo.create({
username: flags['target-org'].getUsername(),
});
// Get client app - check flag first, then auth file, then env var
let clientApp = flags['client-app'];

if (!clientApp && selectedAgent?.source === AgentSource.PUBLISHED) {
const clientApps = getClientAppsFromAuth(authInfo);

if (clientApps.length === 1) {
clientApp = clientApps[0];
} else if (clientApps.length > 1) {
clientApp = await select({
message: 'Select a client app',
choices: clientApps.map((app) => ({ value: app, name: app })),
if (choice.source === AgentSource.SCRIPT && choice.aabDirectory) {
// aabDirectory should be the directory path, not the filename
selectedAgent = await Agent.init({
connection: conn,
project: this.project!,
aabDirectory: choice.aabDirectory,
});
selectedAgent.preview.setMockMode(flags['use-live-actions'] ? 'Live Test' : 'Mock');
} else {
throw new SfError('No client app found.');
selectedAgent = await Agent.init({
connection: conn,
project: this.project!,
// developerName will be set at this point since the user selected a production agent, even ID will be defined
apiNameOrId: choice.developerName ?? choice.id ?? '',
});
}
}

if (useLiveActions && selectedAgent.source === AgentSource.PUBLISHED) {
if (useLiveActions && selectedAgent instanceof ProductionAgent) {
void Lifecycle.getInstance().emitWarning(
'Published agents will always use real actions in your org, specifying --use-live-actions and selecting a published agent has no effect'
);
}

const jwtConn =
selectedAgent?.source === AgentSource.PUBLISHED
? await Connection.create({
authInfo,
clientApp,
})
: await Connection.create({ authInfo });

// Only resolve outputDir if explicitly provided via flag
// Otherwise, let user decide when exiting
const outputDir = flags['output-dir'] ? resolve(flags['output-dir']) : undefined;
// Both classes share the same interface for the methods we need
const agentPreview =
selectedAgent.source === AgentSource.PUBLISHED
? new Preview(jwtConn, selectedAgent.Id)
: new AgentSimulate(jwtConn, selectedAgent.path, !useLiveActions);

agentPreview.setApexDebugMode(flags['apex-debug']);
selectedAgent.preview.setApexDebugging(flags['apex-debug']);

const instance = render(
React.createElement(AgentPreviewReact, {
connection: conn,
agent: agentPreview,
name: selectedAgent.DeveloperName,
agent: selectedAgent.preview,
name: selectedAgent.name ?? '',
outputDir,
isLocalAgent: selectedAgent.source === AgentSource.SCRIPT,
apexDebug: flags['apex-debug'],
logger: getLogger(),
isLocalAgent: selectedAgent instanceof ScriptAgent,
}),
{ exitOnCtrlC: false }
);
await instance.waitUntilExit();
}

private getAgentChoices(agents: AgentData[]): Array<Choice<ScriptAgent | PublishedAgent>> {
const choices: Array<Choice<ScriptAgent | PublishedAgent>> = [];

// Add org agents
for (const agent of agents) {
if (agentIsInactive(agent) || agentIsUnsupported(agent.DeveloperName)) {
continue;
}

choices.push({
name: `${agent.DeveloperName} (Published)`,
value: {
Id: agent.Id,
DeveloperName: agent.DeveloperName,
source: AgentSource.PUBLISHED,
},
});
}

// Add local agents from .agent files
const localAgentPaths = globSync('**/*.agent', { cwd: this.project!.getPath() });
for (const agentPath of localAgentPaths) {
const agentName = path.basename(agentPath, '.agent');
choices.push({
name: `${agentName} (Agent Script)`,
value: {
DeveloperName: agentName,
source: AgentSource.SCRIPT,
path: path.join(this.project!.getPath(), agentPath),
},
});
}

return choices;
}
}

export const agentIsUnsupported = (devName: string): boolean => UNSUPPORTED_AGENTS.includes(devName);
Expand All @@ -267,6 +174,3 @@ export const validateAgent = (agent: AgentData): boolean => {

return true;
};

export const getClientAppsFromAuth = (authInfo: AuthInfo): string[] =>
Object.keys(authInfo.getFields().clientApps ?? {});
Loading
Loading