Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4,933 changes: 2,101 additions & 2,832 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
"@aws-sdk/client-xray": "^3.1003.0",
"@aws-sdk/credential-providers": "^3.893.0",
"@commander-js/extra-typings": "^14.0.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/otlp-transformer": "^0.213.0",
"@smithy/shared-ini-file-loader": "^4.4.2",
"commander": "^14.0.2",
"dotenv": "^17.2.3",
Expand All @@ -98,6 +100,7 @@
"js-yaml": "^4.1.1",
"react": "^19.2.3",
"yaml": "^2.8.3",
"@aws/agent-inspector": "0.1.0",
"zod": "^4.3.5"
},
"peerDependencies": {
Expand Down
13 changes: 13 additions & 0 deletions scripts/copy-assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const __dirname = path.dirname(__filename);

const srcDir = path.join(__dirname, '..', 'src', 'assets');
const destDir = path.join(__dirname, '..', 'dist', 'assets');
const inspectorSrcDir = path.join(__dirname, '..', 'node_modules', '@aws', 'agent-inspector', 'dist-assets');
const inspectorDestDir = path.join(__dirname, '..', 'dist', 'agent-inspector');

/**
* Recursively copy directory contents, excluding specified files at root level only
Expand Down Expand Up @@ -44,6 +46,17 @@ try {
console.log('Copying assets...');
copyDir(srcDir, destDir, ['AGENTS.md']);
console.log('Assets copied successfully!');

// Copy @aws/agent-inspector built assets into dist/agent-inspector/ for bundled CLI
if (fs.existsSync(inspectorSrcDir)) {
console.log('Copying @aws/agent-inspector assets...');
copyDir(inspectorSrcDir, inspectorDestDir);
console.log('@aws/agent-inspector assets copied successfully!');
} else {
console.warn(
'Warning: @aws/agent-inspector dist-assets/ not found — skipping. Run "npm run build" in the agent-inspector package.'
);
}
} catch (error) {
console.error('Error copying assets:', error);
process.exit(1);
Expand Down
11 changes: 11 additions & 0 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ALL_PRIMITIVES } from './primitives';
import { App } from './tui/App';
import { LayoutProvider } from './tui/context';
import { COMMAND_DESCRIPTIONS } from './tui/copy';
import { clearExitAction, getExitAction } from './tui/exit-action';
import { clearExitMessage, getExitMessage } from './tui/exit-message';
import { CommandListScreen } from './tui/screens/home';
import { getCommandsForUI } from './tui/utils';
Expand Down Expand Up @@ -104,6 +105,16 @@ function renderTUI(updateCheck: Promise<UpdateCheckResult | null>, isFirstRun: b
process.stdout.write(EXIT_ALT_SCREEN);
process.stdout.write(SHOW_CURSOR);

// Check if the TUI requested a post-exit action (e.g., launch browser dev mode)
const action = getExitAction();
clearExitAction();

if (action?.type === 'dev') {
const { launchBrowserDev } = await import('./commands/dev/browser-mode');
await launchBrowserDev();
return;
}

// Print any exit message set by screens (e.g., after successful project creation)
const exitMessage = getExitMessage();
if (exitMessage) {
Expand Down
207 changes: 207 additions & 0 deletions src/cli/commands/dev/browser-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { ConfigIO, findConfigRoot, getWorkingDirectory } from '../../../lib';
import type { AgentCoreProjectSpec } from '../../../schema';
import { getDevConfig, getDevSupportedAgents, loadDevEnv, loadProjectConfig } from '../../operations/dev';
import { type OtelCollector, startOtelCollector } from '../../operations/dev/otel';
import {
type AgentInfo,
type ListMemoryRecordsHandler,
type RetrieveMemoryRecordsHandler,
runWebUI,
} from '../../operations/dev/web-ui';
import { listMemoryRecords, retrieveMemoryRecords } from '../../operations/memory';
import path from 'node:path';

interface DeployedHandlers {
onListMemoryRecords?: ListMemoryRecordsHandler;
onRetrieveMemoryRecords?: RetrieveMemoryRecordsHandler;
}

/**
* Resolve deployed resources (memories, agents) from config and return handlers
* that query them via the AWS SDK. Only resources with "deployed" status are available.
*/
async function resolveDeployedHandlers(
baseDir: string,
onLog: (level: 'info' | 'warn' | 'error', msg: string) => void
): Promise<DeployedHandlers> {
const configIO = new ConfigIO({ baseDir });

if (!configIO.configExists('state') || !configIO.configExists('awsTargets')) {
return {};
}

try {
const deployedState = await configIO.readDeployedState();
const awsTargets = await configIO.readAWSDeploymentTargets();

const targetName = Object.keys(deployedState.targets)[0];
if (!targetName) return {};

const targetState = deployedState.targets[targetName];
const targetConfig = awsTargets.find(t => t.name === targetName);
if (!targetConfig) return {};

const region = targetConfig.region;
const result: DeployedHandlers = {};

// Memory handlers
const memoryEntries = targetState?.resources?.memories ?? {};
const memories = Object.entries(memoryEntries).map(([name, state]) => ({
name,
memoryId: state.memoryId,
region,
}));

if (memories.length > 0) {
onLog(
'info',
`Memory browsing enabled for ${memories.length} deployed memory(ies): ${memories.map(m => m.name).join(', ')}`
);

result.onListMemoryRecords = async (memoryName, namespace, strategyId) => {
const memory = memories.find(m => m.name === memoryName);
if (!memory) return { success: false, error: `Memory "${memoryName}" not found in deployed state` };
return listMemoryRecords({
region: memory.region,
memoryId: memory.memoryId,
namespace,
memoryStrategyId: strategyId,
});
};

result.onRetrieveMemoryRecords = async (memoryName, namespace, searchQuery, strategyId) => {
const memory = memories.find(m => m.name === memoryName);
if (!memory) return { success: false, error: `Memory "${memoryName}" not found in deployed state` };
return retrieveMemoryRecords({
region: memory.region,
memoryId: memory.memoryId,
namespace,
searchQuery,
memoryStrategyId: strategyId,
});
};
}

return result;
} catch (err) {
onLog('warn', `Could not resolve deployed resources: ${err instanceof Error ? err.message : String(err)}`);
return {};
}
}

export interface BrowserModeOptions {
workingDir: string;
project: AgentCoreProjectSpec;
port: number;
agentName?: string;
/** OTEL env vars to pass to dev servers (set by the dev command when collector is active) */
otelEnvVars?: Record<string, string>;
/** OTEL collector instance for local trace collection */
collector?: OtelCollector;
}

/**
* Standalone entry point for launching browser dev mode from the TUI.
* Handles all setup (project loading, OTEL collector, etc.) internally.
*/
export async function launchBrowserDev(): Promise<void> {
const workingDir = getWorkingDirectory();
const project = await loadProjectConfig(workingDir);

if (!project?.runtimes || project.runtimes.length === 0) {
console.error('Error: No agents defined in project.');
process.exit(1);
}

const configRoot = findConfigRoot(workingDir);
const persistTracesDir = path.join(configRoot ?? workingDir, '.cli', 'traces');
const { collector, otelEnvVars } = await startOtelCollector(persistTracesDir);

await runBrowserMode({
workingDir,
project,
port: 8080,
otelEnvVars,
collector,
});
}

export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
const { workingDir, project, agentName, otelEnvVars = {}, collector } = opts;

const configRoot = findConfigRoot(workingDir);
const { envVars } = await loadDevEnv(workingDir);

const supportedAgents = getDevSupportedAgents(project);

if (supportedAgents.length === 0) {
console.error('Error: No dev-supported agents found.');
process.exit(1);
}
Comment thread
avi-alpert marked this conversation as resolved.

if (agentName && !supportedAgents.some(a => a.name === agentName)) {
console.error(`Error: Agent "${agentName}" not found or does not support dev mode.`);
process.exit(1);
}

const onLog = (level: 'info' | 'warn' | 'error', msg: string) => {
if (level === 'error') console.error(`Web UI: ${msg}`);
};

const mergedEnvVars = { ...envVars, ...otelEnvVars };

const agentInfoList: AgentInfo[] = supportedAgents.map(a => ({
name: a.name,
buildType: a.build,
protocol: a.protocol ?? 'HTTP',
}));

// Resolve deployed resources (memories, agents) so memory browsing and
// CloudWatch traces work in dev mode alongside local traces.
// Handlers re-resolve on each call so newly deployed memories are picked up.
const baseDir = configRoot ?? workingDir;

await runWebUI({
logLabel: 'dev',
onLog,
serverOptions: {
mode: 'dev',
Comment thread
avi-alpert marked this conversation as resolved.
agents: agentInfoList,
selectedAgent: agentName,
envVars: mergedEnvVars,
getEnvVars: async () => {
const { envVars: freshEnvVars } = await loadDevEnv(workingDir);
return { ...freshEnvVars, ...otelEnvVars };
},
configRoot: configRoot ?? undefined,
getDevConfig: async name => {
const freshProject = await loadProjectConfig(workingDir);
return getDevConfig(workingDir, freshProject, configRoot ?? undefined, name);
},
reloadAgents: configRoot
? async () => {
const freshProject = await loadProjectConfig(workingDir);
return getDevSupportedAgents(freshProject).map(a => ({
name: a.name,
buildType: a.build,
protocol: a.protocol ?? 'HTTP',
}));
}
: undefined,
onListTraces: collector
? (agentNameParam, startTime, endTime) => collector.listTraces(agentNameParam, startTime, endTime)
: undefined,
onGetTrace: collector ? (agentNameParam, traceId) => collector.getTraceSpans(agentNameParam, traceId) : undefined,
onListMemoryRecords: async (memoryName, namespace, strategyId) => {
const deployed = await resolveDeployedHandlers(baseDir, onLog);
if (!deployed.onListMemoryRecords) return { success: false, error: 'No deployed AgentCore Memory found' };
return deployed.onListMemoryRecords(memoryName, namespace, strategyId);
},
onRetrieveMemoryRecords: async (memoryName, namespace, searchQuery, strategyId) => {
const deployed = await resolveDeployedHandlers(baseDir, onLog);
if (!deployed.onRetrieveMemoryRecords) return { success: false, error: 'No deployed AgentCore Memory found' };
return deployed.onRetrieveMemoryRecords(memoryName, namespace, searchQuery, strategyId);
},
},
});
}
Loading
Loading