UNPKG

@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
/* 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