@agentled/cli
Version:
CLI for Agentled — manage workflows, apps, and knowledge from the command line. Zero context-window cost for AI agents.
391 lines • 18 kB
JavaScript
/**
* `agentled setup` — single canonical entrypoint for new users.
*
* Orchestrates the seven-step flow:
* 1. Version check (warns if outdated)
* 2. Browser sign-in + workspace selection
* 3. Workspace folder scaffold (agentled_<slug>/)
* 4. MCP auto-config (Claude Code, Codex, Cursor, Claude Desktop, Windsurf)
* 5. Skill install (writes SKILL.md to the client's skill directory)
* 6. Knowledge probe (company.profile; if missing, prompt + write)
* 7. Restart prompt (printed at end of setup)
*
* Replaces the legacy `agentled setup` (company-profile-only) which required
* prior auth and only wrote workspace company info. The legacy company-profile
* prompt logic is preserved as `runOnboarding()` and called from step 6 —
* but it now writes to `company.profile` (the same key the probe
* reads), so the prompt does not re-fire on the next run.
*/
import { existsSync } from 'node:fs';
import { resolve } from 'node:path';
import * as readline from 'node:readline';
import { AgentledApiClient } from '@agentled/core';
import { AgentledClient, getBaseUrl, resolveAuthContext, saveWorkspaceProfile } from '../client.js';
import { browserLogin } from '../utils/browser-auth.js';
import { installSkills, getSkillsTargetDir, getCodexSkillsDir, summarizeForLoginBanner, } from '../utils/skills.js';
import { configureMcp, getClientLabel } from '../utils/mcp-config.js';
import { checkCliVersion, summarizeVersionCheck } from '../utils/version-check.js';
import { probeCompanyKnowledge, summarizeKnowledgeProbe } from '../utils/knowledge-probe.js';
import { bootstrapAgentledHome } from '../utils/home-bootstrap.js';
import { createWorkspaceFolder, resolveWorkspaceMeta } from '../utils/workspace-folder.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 promptSelect(question, options) {
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
return new Promise(resolve => {
console.error(question);
options.forEach((opt, i) => console.error(` ${i + 1}. ${opt}`));
rl.question(` Choice [1-${options.length}]: `, answer => {
rl.close();
const idx = parseInt(answer.trim(), 10) - 1;
resolve(idx >= 0 && idx < options.length ? options[idx] : options[options.length - 1]);
});
});
}
const USE_CASES = [
'Lead enrichment & outreach',
'Market research & competitive analysis',
'Investor sourcing & deal flow (VC)',
'Portfolio monitoring & due diligence (VC)',
'Recruiting & talent sourcing',
'Data pipelines & automation',
'General process automation',
'Other',
];
async function fetchSiteMetadata(rawUrl) {
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 8000);
try {
const url = rawUrl.startsWith('http') ? rawUrl : `https://${rawUrl}`;
const res = await fetch(url, {
signal: ac.signal,
headers: { 'User-Agent': 'agentled-cli/onboarding' },
redirect: 'follow',
});
if (!res.ok)
return null;
const html = (await res.text()).slice(0, 200_000);
const title = html.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.trim();
const desc = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i)?.[1] ||
html.match(/<meta[^>]+property=["']og:description["'][^>]+content=["']([^"']+)["']/i)?.[1];
return { title, description: desc?.trim() };
}
catch {
return null;
}
finally {
clearTimeout(timer);
}
}
function buildProfileText(company) {
return [
company.name ? `Company: ${company.name}` : null,
company.description ? `\n${company.description}` : null,
company.urls?.length ? `\nWebsite: ${company.urls.join(', ')}` : null,
company.additionalInformation ? `\n${company.additionalInformation}` : null,
].filter(Boolean).join('\n').trim();
}
/**
* Interactive prompt to populate the workspace company profile. Called by
* step 6 of `agentled setup` when the workspace knowledge probe finds no
* existing `company.profile`, and by `auth login` for new users.
*/
export async function runOnboarding(mode = 'new-user') {
console.error('\n Welcome! Let\'s finish setting up your workspace.\n');
if (!process.stdin.isTTY || !process.stderr.isTTY) {
if (mode === 'new-user') {
console.error(' Company profile setup is required for first-time onboarding, but this terminal is non-interactive.');
console.error(' Re-run "agentled setup" from an interactive shell to complete onboarding.\n');
throw new Error('First-time onboarding requires an interactive terminal for company profile setup.');
}
console.error(' Existing user detected. Skipping interactive company profile setup in non-interactive mode.');
console.error(' You can run "agentled workspace company-profile" later from an interactive shell if needed.\n');
return;
}
const companyName = await prompt(' ? Company name: ');
const website = await prompt(' ? Company website (optional): ');
const useCase = await promptSelect(' ? Primary use case:', USE_CASES);
const company = {};
if (companyName)
company.name = companyName;
if (website)
company.urls = [website];
if (website) {
process.stderr.write(' Reading website to seed knowledge… ');
const meta = await fetchSiteMetadata(website);
if (meta?.description || meta?.title) {
if (meta.description)
company.description = meta.description;
if (meta.title && !company.name)
company.name = meta.title;
console.error('done.');
}
else {
console.error('skipped (could not read).');
}
}
const additional = [];
if (useCase && useCase !== 'Other')
additional.push(`Primary use case: ${useCase}`);
if (additional.length)
company.additionalInformation = additional.join('\n');
if (Object.keys(company).length === 0) {
console.error('\n Skipped — no company info provided.');
console.error(' You can set this up later with "agentled workspace company-profile".\n');
return;
}
// AgentledClient extends AgentledApiClient, so all KG / workspace / etc.
// methods are directly callable on this instance — no second client needed.
const cliClient = new AgentledClient();
try {
await cliClient.updateWorkspaceCompanyProfile(company);
}
catch (err) {
console.error(` ⚠ Could not save workspace profile record (${err.message}).`);
}
const profileText = buildProfileText(company);
if (profileText) {
try {
await cliClient.upsertKnowledgeText({
key: 'company.profile',
content: profileText,
title: company.name || 'Company profile',
});
console.error('\n Workspace configured! ✓ (company.profile written)');
}
catch (err) {
console.error(`\n ⚠ Could not write company.profile (${err.message}).`);
console.error(' The probe will re-prompt on the next \`agentled setup\` run.');
}
}
console.error('\n Next:');
console.error(' • Read the company website thoroughly (about/team/blog/pricing).');
console.error(' • Pull LinkedIn (get-linkedin-company-from-url) and recent posts.');
console.error(' • Use upsert_knowledge_text to enrich company.profile (long-form),');
console.error(' company.products. Skip .icp / .tone for now — add when needed.\n');
}
// ---------------------------------------------------------------------------
// `agentled setup` orchestration
// ---------------------------------------------------------------------------
function printSection(label, step, total) {
console.error('');
console.error(` [${step}/${total}] ${label}`);
console.error('');
}
function skillTargetForClient(client) {
if (client === 'claude-code' || client === 'claude-desktop') {
return { kind: 'claude', dir: getSkillsTargetDir(true) };
}
if (client === 'codex') {
return { kind: 'codex', dir: getCodexSkillsDir() };
}
return { kind: 'skip', dir: null };
}
function parseMcpClientTarget(value) {
const normalized = value.trim().toLowerCase();
const valid = ['auto', 'codex', 'claude-code', 'claude-desktop', 'cursor', 'windsurf'];
if (valid.includes(normalized)) {
return normalized;
}
throw new Error(`Invalid MCP client "${value}". Use one of: ${valid.join(', ')}`);
}
async function runSetup(options = {}) {
console.error('');
console.error(' ◆ Agentled Setup');
console.error(' One command: auth → workspace folder → MCP → skill → knowledge probe.');
console.error('');
const baseUrl = getBaseUrl().replace(/\/$/, '');
const TOTAL_STEPS = 7;
// Step 1
printSection('Version check', 1, TOTAL_STEPS);
const versionResult = await checkCliVersion();
const versionLine = summarizeVersionCheck(versionResult);
if (versionLine) {
console.error(` ⚠ ${versionLine}`);
}
else {
console.error(` ✓ @agentled/cli v${versionResult.installed}${versionResult.latest ? ' (latest)' : ''}`);
}
// Step 2
printSection('Sign in & select workspace', 2, TOTAL_STEPS);
// Skip the browser workspace picker if we already have an active workspace
// in ~/.agentled/config.json (the user picked it on a prior run or via
// `agentled auth use`). Pass --reauth to force re-running the browser flow.
const existingAuth = resolveAuthContext();
const savedWorkspace = !options.reauth &&
existingAuth.source === 'config' &&
existingAuth.workspace?.apiKey
? existingAuth.workspace
: null;
let loginResult;
if (savedWorkspace) {
loginResult = {
apiKey: savedWorkspace.apiKey,
workspaceId: savedWorkspace.id,
workspaceName: savedWorkspace.name,
workspaceSlug: savedWorkspace.alias,
userId: savedWorkspace.userId,
userEmail: savedWorkspace.userEmail,
userName: savedWorkspace.userName,
isNewUser: false,
baseUrl: savedWorkspace.baseUrl || baseUrl,
};
console.error(` ✓ Detected active workspace "${savedWorkspace.name}" — skipping browser sign-in.`);
console.error(' Re-run with `agentled setup --reauth` to switch workspace.');
}
else {
loginResult = await browserLogin(baseUrl);
saveWorkspaceProfile({
id: loginResult.workspaceId,
name: loginResult.workspaceName,
apiKey: loginResult.apiKey,
baseUrl: loginResult.baseUrl || baseUrl,
userId: loginResult.userId,
userEmail: loginResult.userEmail,
userName: loginResult.userName,
});
console.error(` ✓ Authenticated to workspace "${loginResult.workspaceName}"`);
}
const homeBootstrap = bootstrapAgentledHome();
if (homeBootstrap.refreshed) {
console.error(` ✓ Wrote ~/.agentled/ docs and ${homeBootstrap.scaffoldsCopied} scaffolds (content v${homeBootstrap.version})`);
}
// Step 3
printSection('Scaffold workspace folder', 3, TOTAL_STEPS);
const apiClient = new AgentledClient();
let folderName = null;
try {
const [workspace, appsList, modelsList] = await Promise.allSettled([
apiClient.getWorkspace(),
apiClient.listApps(),
apiClient.listModels(),
]);
if (workspace.status === 'fulfilled') {
const meta = resolveWorkspaceMeta({
response: workspace.value,
authWorkspace: {
id: loginResult.workspaceId,
name: loginResult.workspaceName,
alias: loginResult.workspaceSlug,
},
slugOverride: loginResult.workspaceSlug,
apiBase: loginResult.baseUrl || baseUrl,
});
folderName = `agentled_${meta.slug}`;
const folderPath = resolve(process.cwd(), folderName);
if (existsSync(folderPath)) {
console.error(` • ${folderName}/ already exists — leaving as-is. Run \`agentled workspace sync\` to refresh.`);
}
else {
createWorkspaceFolder(folderPath, meta, {
appsList: appsList.status === 'fulfilled' ? appsList.value : undefined,
modelsList: modelsList.status === 'fulfilled' ? modelsList.value : undefined,
});
console.error(` ✓ Created ${folderName}/ in current directory`);
}
}
else {
console.error(` ⚠ Could not load workspace metadata (${workspace.reason?.message ?? 'unknown'}). Run \`agentled init\` later.`);
}
}
catch (err) {
console.error(` ⚠ Folder scaffold skipped (${err?.message ?? err}). Run \`agentled init\` later.`);
}
// Step 4
printSection('Configure MCP client', 4, TOTAL_STEPS);
const mcpResult = await configureMcp(loginResult.apiKey, loginResult.baseUrl || baseUrl, options.mcpClient || 'auto');
if (mcpResult.success) {
console.error(` ✓ Registered "agentled" MCP server in ${getClientLabel(mcpResult.client)}${mcpResult.configPath ? ` (${mcpResult.configPath})` : ''}`);
}
else if (mcpResult.fallbackHint) {
console.error(` ⚠ Could not auto-configure MCP. Manual instructions:`);
console.error('');
for (const line of mcpResult.fallbackHint.split('\n'))
console.error(` ${line}`);
}
// Step 5
printSection('Install Agentled skill', 5, TOTAL_STEPS);
const skillTarget = skillTargetForClient(mcpResult.client);
if (skillTarget.kind === 'skip') {
console.error(` • Skill auto-install not supported for ${getClientLabel(mcpResult.client)} — skipped.`);
console.error(` Run \`agentled skills install\` if your client supports Claude-format skills.`);
}
else {
try {
const results = installSkills({ targetDir: skillTarget.dir });
const banner = summarizeForLoginBanner(results, skillTarget.dir);
if (banner) {
console.error(` ✓ ${banner}`);
}
else {
console.error(` • Skill already installed in ${skillTarget.dir}`);
}
}
catch (err) {
console.error(` ⚠ Skill install skipped (${err?.message ?? err}). Run \`agentled skills install\` later.`);
}
}
// Step 6 — the probe explicitly uses loginResult.apiKey/baseUrl rather
// than the saved-config-resolved `apiClient`, so a stale AGENTLED_API_KEY
// env var pointing at a different workspace cannot mask the
// just-authenticated workspace's knowledge.
printSection('Probe workspace knowledge', 6, TOTAL_STEPS);
const probe = await probeCompanyKnowledge(new AgentledApiClient({
apiKey: loginResult.apiKey,
baseUrl: loginResult.baseUrl || baseUrl,
missingAuthMessage: 'API key is required.',
}));
console.error(` ${probe.profilePresent ? '✓' : '•'} ${summarizeKnowledgeProbe(probe)}`);
if (!probe.profilePresent && !probe.error) {
if (loginResult.isNewUser) {
await runOnboarding('new-user');
}
else {
console.error(' • Existing user detected with missing company profile.');
await runOnboarding('existing-user-missing-profile');
}
}
// Step 7
printSection('Restart your MCP client', 7, TOTAL_STEPS);
console.error(' Your MCP client must restart to pick up the new connection:');
console.error(' • Claude Code: /mcp → reconnect, or restart the session');
console.error(' • Codex / Cursor: quit and reopen the app');
console.error('');
console.error(' ——— Setup complete ———');
console.error('');
if (folderName) {
console.error(` Workspace folder: ./${folderName}/`);
}
console.error(' Global home: ~/.agentled/ (config, docs, scaffolds)');
console.error('');
console.error(' Try: "agentled --help" or talk to your agent.');
console.error('');
}
export function registerOnboardingCommands(program) {
program
.command('setup')
.description('Connect Agentled end-to-end: auth → workspace folder → MCP → skill → knowledge probe')
.option('--reauth', 'Force the browser sign-in step even if an active workspace is already saved')
.option('--mcp-client <client>', 'Target MCP client to configure: auto, codex, claude-code, claude-desktop, cursor, windsurf', parseMcpClientTarget, 'auto')
.action(async (opts) => {
try {
await runSetup({ reauth: !!opts.reauth, mcpClient: opts.mcpClient || 'auto' });
}
catch (err) {
console.error(`\n ✗ Setup failed: ${err?.message ?? err}`);
console.error(' You can re-run \`agentled setup\`, or run components individually:');
console.error(' agentled auth login # browser auth');
console.error(' agentled init # workspace folder');
console.error(' agentled skills install # install Claude Code skill');
process.exit(1);
}
});
}
//# sourceMappingURL=onboarding.js.map