UNPKG

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