@agentled/cli
Version:
CLI for Agentled — manage workflows, apps, and knowledge from the command line. Zero context-window cost for AI agents.
295 lines • 12.8 kB
JavaScript
/* eslint-disable no-console */
import * as readline from 'node:readline';
import { AgentledApiClient } from '@agentled/core';
import { getBaseUrl, getConfigPath, getSelectedWorkspace, listWorkspaces, loadConfig, removeWorkspace, resolveAuthContext, saveWorkspaceProfile, setActiveWorkspace, deleteConfig, } from '../client.js';
import { browserLogin } from '../utils/browser-auth.js';
import { printError, printOutput } from '../utils/output.js';
import { runOnboarding } from './onboarding.js';
import { getSkillsTargetDir, installSkills, summarizeForLoginBanner, } from '../utils/skills.js';
function prompt(question) {
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
return new Promise(resolve => {
rl.question(question, answer => {
rl.close();
resolve(answer.trim());
});
});
}
function pluralize(count, singular, plural) {
return count === 1 ? singular : plural;
}
function describeWorkspace(workspace) {
if (!workspace)
return 'unknown workspace';
return workspace.alias ? `${workspace.name} (${workspace.alias})` : workspace.name;
}
function listWorkspaceHints(workspaces) {
if (!workspaces.length)
return 'No saved workspaces.';
return workspaces
.map(workspace => workspace.alias || workspace.name || workspace.id)
.join(', ');
}
async function resolveWorkspaceFromApiKey(apiKey, baseUrl) {
const client = new AgentledApiClient({
apiKey,
baseUrl,
missingAuthMessage: 'API key is required.',
});
const result = await client.getWorkspace();
const workspaceId = result?.workspace?.id;
const workspaceName = result?.workspace?.name || result?.company?.name || 'Workspace';
if (!workspaceId) {
throw new Error('Authenticated, but the workspace response did not include workspace.id.');
}
return { workspaceId, workspaceName };
}
function printSavedWorkspaceSummary() {
const savedWorkspaces = listWorkspaces();
const count = savedWorkspaces.length;
console.log(`Saved ${count} ${pluralize(count, 'workspace', 'workspaces')}. Use "agentled auth list" to inspect them.`);
}
/**
* Installs the Agentled Claude Code skill into ~/.claude/skills on successful
* login. Silently no-op'd by --skip-skills. Never blocks login on failure —
* skills are a convenience, not a hard requirement.
*
* This is what closes silent-strip-class bugs: without the skill auto-loaded,
* agents invent step types (`type: "ai"`, `knowledge_graph_query`) that the
* API silently strips.
*/
function installSkillsAfterLogin(skipSkills) {
if (skipSkills)
return;
try {
const targetDir = getSkillsTargetDir(true); // default: global
const results = installSkills({ global: true });
const banner = summarizeForLoginBanner(results, targetDir);
if (banner) {
console.log(banner);
}
}
catch (err) {
// Never fail login because of skill install. Surface a hint instead.
console.log(`(Skill auto-install skipped: ${err?.message ?? String(err)}. Run \`agentled skills install\` manually.)`);
}
}
/**
* After a successful login, the agentled MCP server (if registered with
* Claude Code / Codex / Cursor) is still bound to whatever workspace was
* active before login. The MCP client must be restarted to pick up the new
* auth — without this, the agent will silently keep using the prior
* workspace's tools (or none at all). Tell the user explicitly.
*/
function printMcpRestartNotice() {
console.log('');
console.log('Restart your MCP client to use this workspace via MCP tools:');
console.log(' • Claude Code: /mcp → reconnect, or restart the session');
console.log(' • Codex / Cursor: restart the app (or reload the MCP server)');
console.log('Until you restart, the agent keeps using the previous workspace\'s MCP connection.');
console.log('(The CLI itself works immediately — no restart needed for `agentled` commands.)');
}
export function registerAuthCommands(program) {
const auth = program.command('auth').description('Manage authentication');
auth
.command('login')
.description('Authenticate with Agentled (opens browser by default)')
.option('--key <apiKey>', 'API key (wsk_...) — skip browser flow')
.option('--alias <alias>', 'Optional local alias for this workspace profile')
.option('--url <baseUrl>', 'Base URL (default: https://www.agentled.app)')
.option('--no-browser', 'Skip browser flow, prompt for API key instead')
.option('--skip-skills', 'Skip auto-installing the Claude Code skill on successful login')
.action(async (opts) => {
const baseUrl = opts.url || getBaseUrl();
if (opts.key) {
if (!opts.key.startsWith('wsk_')) {
console.error('Error: API key should start with "wsk_". Generate one in Workspace Settings > Developer.');
process.exit(1);
}
try {
const workspace = await resolveWorkspaceFromApiKey(opts.key, baseUrl);
saveWorkspaceProfile({
id: workspace.workspaceId,
name: workspace.workspaceName,
apiKey: opts.key,
baseUrl,
alias: opts.alias,
});
console.log(`Authenticated to workspace "${workspace.workspaceName}" ✓`);
printSavedWorkspaceSummary();
installSkillsAfterLogin(Boolean(opts.skipSkills));
printMcpRestartNotice();
return;
}
catch (error) {
printError(error.message);
}
}
if (opts.browser === false) {
const apiKey = await prompt('Enter your API key (wsk_...): ');
if (!apiKey.startsWith('wsk_')) {
console.error('Error: API key should start with "wsk_". Generate one in Workspace Settings > Developer.');
process.exit(1);
}
try {
const workspace = await resolveWorkspaceFromApiKey(apiKey, baseUrl);
saveWorkspaceProfile({
id: workspace.workspaceId,
name: workspace.workspaceName,
apiKey,
baseUrl,
alias: opts.alias,
});
console.log(`Authenticated to workspace "${workspace.workspaceName}" ✓`);
printSavedWorkspaceSummary();
installSkillsAfterLogin(Boolean(opts.skipSkills));
printMcpRestartNotice();
return;
}
catch (error) {
printError(error.message);
}
}
try {
const result = await browserLogin(baseUrl);
saveWorkspaceProfile({
id: result.workspaceId,
name: result.workspaceName,
apiKey: result.apiKey,
baseUrl: result.baseUrl || baseUrl,
userId: result.userId,
userEmail: result.userEmail,
userName: result.userName,
alias: opts.alias,
});
console.log(`Authenticated to workspace "${result.workspaceName}" ✓`);
printSavedWorkspaceSummary();
installSkillsAfterLogin(Boolean(opts.skipSkills));
if (result.isNewUser) {
await runOnboarding();
}
printMcpRestartNotice();
console.log('Run `agentled --help` to get started.');
}
catch (err) {
console.error(`\nLogin failed: ${err.message}`);
console.error('\nAlternatives:');
console.error(' agentled auth login --no-browser (prompt for API key)');
console.error(' agentled auth login --key wsk_... (direct API key)');
process.exit(1);
}
});
auth
.command('status')
.description('Check authentication status')
.action(() => {
const authContext = resolveAuthContext();
if (!authContext.authenticated) {
console.log('Not authenticated. Run "agentled auth login" or set AGENTLED_API_KEY.');
process.exit(1);
}
const savedCount = listWorkspaces(authContext.config).length;
const viaEnv = authContext.source === 'environment';
const workspaceLabel = describeWorkspace(authContext.workspace);
if (viaEnv) {
console.log(`Authenticated via AGENTLED_API_KEY (${workspaceLabel}).`);
return;
}
console.log(`Authenticated to workspace "${workspaceLabel}" (${savedCount} saved ${pluralize(savedCount, 'workspace', 'workspaces')}).`);
});
auth
.command('current')
.description('Show the current authenticated workspace profile')
.option('--format <fmt>', 'Output format', 'json')
.action((opts) => {
const authContext = resolveAuthContext();
const savedCount = listWorkspaces(authContext.config).length;
printOutput({
authenticated: authContext.authenticated,
source: authContext.source,
baseUrl: authContext.baseUrl,
configPath: getConfigPath(),
activeWorkspace: authContext.workspace
? {
id: authContext.workspace.id,
name: authContext.workspace.name,
alias: authContext.workspace.alias,
userId: authContext.workspace.userId,
userEmail: authContext.workspace.userEmail,
userName: authContext.workspace.userName,
}
: null,
savedWorkspaceCount: savedCount,
}, opts.format);
});
auth
.command('list')
.description('List saved workspace profiles')
.option('--format <fmt>', 'Output format', 'json')
.action((opts) => {
const config = loadConfig();
const workspaces = listWorkspaces(config).map(workspace => ({
active: workspace.id === config.activeWorkspaceId,
id: workspace.id,
name: workspace.name,
alias: workspace.alias,
userEmail: workspace.userEmail,
baseUrl: workspace.baseUrl,
lastUsedAt: workspace.lastUsedAt,
}));
printOutput(workspaces, opts.format);
});
auth
.command('use <workspace>')
.description('Make a saved workspace profile active')
.action((workspaceRef) => {
const config = loadConfig();
const workspace = getSelectedWorkspace(config, workspaceRef);
if (!workspace) {
printError(`Unknown workspace "${workspaceRef}". Saved workspaces: ${listWorkspaceHints(listWorkspaces(config))}`);
}
setActiveWorkspace(workspaceRef);
console.log(`Active workspace set to "${describeWorkspace(workspace)}".`);
});
auth
.command('remove <workspace>')
.description('Remove a saved workspace profile')
.action((workspaceRef) => {
const config = loadConfig();
const workspace = getSelectedWorkspace(config, workspaceRef);
if (!workspace) {
printError(`Unknown workspace "${workspaceRef}". Saved workspaces: ${listWorkspaceHints(listWorkspaces(config))}`);
}
const nextConfig = removeWorkspace(workspaceRef);
const remaining = listWorkspaces(nextConfig).length;
console.log(`Removed workspace "${describeWorkspace(workspace)}". ${remaining} ${pluralize(remaining, 'workspace remains', 'workspaces remain')}.`);
});
auth
.command('logout')
.description('Remove all stored credentials')
.action(() => {
if (deleteConfig()) {
console.log('Logged out. All saved workspaces removed.');
}
else {
console.log('No stored credentials found.');
}
});
auth
.command('whoami')
.description('Alias for auth current')
.option('--format <fmt>', 'Output format', 'json')
.action((opts) => {
const authContext = resolveAuthContext();
const savedCount = listWorkspaces(authContext.config).length;
printOutput({
authenticated: authContext.authenticated,
source: authContext.source,
baseUrl: authContext.baseUrl,
configPath: getConfigPath(),
activeWorkspace: authContext.workspace ?? null,
savedWorkspaceCount: savedCount,
}, opts.format);
});
}
//# sourceMappingURL=auth.js.map