From bfc08664c3c4dc8b4e1f637077be6f56b2af010a Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Tue, 31 Mar 2026 10:48:53 +0200 Subject: [PATCH] feat(04-03): add Nexus agent bootstrap to CLI onboarding - Add bootstrapNexusAgents function with health-check poll (max 30s) - Create workspace (company) then PM agent (role:ceo) and Engineer agent - Idempotent: skips if workspace already exists - Bootstrap runs concurrently before runCommand starts server - Failures are warnings, not errors - [nexus] comments on all new lines --- cli/src/commands/onboard.ts | 90 +++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/cli/src/commands/onboard.ts b/cli/src/commands/onboard.ts index f3fa8cd6..596d1c5b 100644 --- a/cli/src/commands/onboard.ts +++ b/cli/src/commands/onboard.ts @@ -35,6 +35,89 @@ import { bootstrapCeoInvite } from "./auth-bootstrap-ceo.js"; import { printNexusCliBanner } from "../utils/banner.js"; import { VOCAB } from "@paperclipai/branding"; // [nexus] +// [nexus] Auto-create PM and Engineer agents on first run +async function bootstrapNexusAgents(serverUrl: string, rootDir: string): Promise { + // [nexus] Health-check poll — wait for server to be ready (max 30 seconds) + const maxRetries = 30; + let serverReady = false; + for (let i = 0; i < maxRetries; i++) { + try { + const res = await fetch(`${serverUrl}/api/health`); + if (res.ok) { + serverReady = true; + break; + } + } catch { + // [nexus] Server not ready yet + } + if (i < maxRetries - 1) { + await new Promise((r) => setTimeout(r, 1000)); + } + } + + if (!serverReady) { + console.warn("[nexus] Server did not become ready in 30s, skipping agent bootstrap"); + return; + } + + try { + // [nexus] Check if workspace already exists (idempotent — skip if already bootstrapped) + const companiesRes = await fetch(`${serverUrl}/api/companies`); + if (!companiesRes.ok) { + console.warn("[nexus] Could not fetch workspaces, skipping agent bootstrap"); + return; + } + const companies = (await companiesRes.json()) as unknown[]; + if (companies.length > 0) { + return; // [nexus] Already bootstrapped — skip + } + + // [nexus] Create workspace + p.log.step(`Creating your ${VOCAB.company} workspace...`); + const companyRes = await fetch(`${serverUrl}/api/companies`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: VOCAB.appName }), + }); + if (!companyRes.ok) { + console.warn("[nexus] Could not create workspace, skipping agent bootstrap"); + return; + } + const company = (await companyRes.json()) as { id: string }; + + // [nexus] Create PM agent (role: "ceo" for elevated permissions — displays as Project Manager) + p.log.step(`Adding ${VOCAB.ceo} agent...`); + await fetch(`${serverUrl}/api/companies/${company.id}/agents`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "Project Manager", + role: "ceo", + adapterType: "claude_local", + adapterConfig: { cwd: rootDir }, + }), + }); + + // [nexus] Create Engineer agent + p.log.step("Adding Engineer agent..."); + await fetch(`${serverUrl}/api/companies/${company.id}/agents`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "Engineer", + role: "engineer", + adapterType: "claude_local", + adapterConfig: { cwd: rootDir }, + }), + }); + + p.log.success("Workspace and agents created — you're ready to go!"); + } catch (err) { + // [nexus] Bootstrap failures are warnings, not errors — user can create agents manually + console.warn("[nexus] Agent bootstrap failed:", err instanceof Error ? err.message : String(err)); + } +} + type SetupMode = "quickstart" | "advanced"; type OnboardOptions = { @@ -545,6 +628,13 @@ export async function onboard(opts: OnboardOptions): Promise { if (shouldRunNow && !opts.invokedByRun) { process.env.PAPERCLIP_OPEN_ON_LISTEN = "true"; const { runCommand } = await import("./run.js"); + // [nexus] Start bootstrap concurrently — health-check poll waits for server readiness + const serverUrl = `http://${server.host}:${server.port}`; + const rootDir = process.cwd(); + bootstrapNexusAgents(serverUrl, rootDir).catch((err: unknown) => { + // [nexus] Bootstrap failures are non-fatal + console.warn("[nexus] Agent bootstrap error:", err instanceof Error ? err.message : String(err)); + }); await runCommand({ config: configPath, repair: true, yes: true }); return; }