nexus/.planning/phases/31-puter.js-zero-config-cloud/31-03-PLAN.md

21 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
31-puter.js-zero-config-cloud 03 execute 2
31-01
31-02
ui/src/components/onboarding/ProviderSelectionStep.tsx
ui/src/components/onboarding/PuterAuthButton.tsx
ui/src/components/onboarding/GoogleOAuthButton.tsx
ui/src/components/onboarding/ApiKeyEntryForm.tsx
ui/src/components/NexusOnboardingWizard.tsx
ui/src/api/puter-proxy.ts
true
CLOUD-01
CLOUD-03
CLOUD-04
CLOUD-05
truths artifacts key_links
User sees a Provider Selection step (Step 3 of 4) in the onboarding wizard with Puter, Google, and API Key options
Clicking 'Continue with Puter' triggers Puter auth popup, stores token in React state for later server submission
Google option shows policy-risk warning before enabling the Sign in button (3-second gate)
Google OAuth opens popup, callback stores tokens by stateId, stateId captured in React state for later claim
API key option shows inline form with provider dropdown and key input
Skip for now button is always visible and advances to root directory step
Pre-installed tools (Hermes, Claude Code, OpenClaw) show detected badges via adapter probe
Step indicator shows 'Step N of 4' (updated from 3)
handleSubmit posts collected credentials to server AFTER company creation (Puter token, Google claim, API key)
path provides exports
ui/src/components/onboarding/ProviderSelectionStep.tsx Provider selection UI with three option cards and skip button
ProviderSelectionStep
path provides exports
ui/src/components/onboarding/PuterAuthButton.tsx Puter auth button that loads CDN script, calls signIn, captures token in state
PuterAuthButton
path provides exports
ui/src/components/onboarding/GoogleOAuthButton.tsx Google OAuth button with 3-second risk warning gate, opens popup, captures stateId
GoogleOAuthButton
path provides exports
ui/src/components/onboarding/ApiKeyEntryForm.tsx API key entry form with provider dropdown
ApiKeyEntryForm
path provides exports
ui/src/api/puter-proxy.ts API client for puter-proxy, oauth, and api-keys endpoints
puterProxyApi
path provides
ui/src/components/NexusOnboardingWizard.tsx 4-step wizard with provider selection step inserted
from to via pattern
ui/src/components/onboarding/PuterAuthButton.tsx POST /api/puter-proxy/token puterProxyApi.storeToken called in wizard handleSubmit after company creation puter-proxy/token
from to via pattern
ui/src/components/onboarding/GoogleOAuthButton.tsx POST /api/oauth/google/authorize fetch call to get auth URL + stateId, then window.open oauth/google/authorize
from to via pattern
ui/src/components/NexusOnboardingWizard.tsx POST /api/oauth/google/claim claim call in handleSubmit with stateId + companyId after company creation oauth/google/claim
from to via pattern
ui/src/components/onboarding/ApiKeyEntryForm.tsx POST /api/api-keys/store puterProxyApi.storeApiKey called in wizard handleSubmit after company creation api-keys/store
from to via pattern
ui/src/components/NexusOnboardingWizard.tsx ui/src/components/onboarding/ProviderSelectionStep.tsx import and render in step 3 ProviderSelectionStep
from to via pattern
ui/src/components/NexusOnboardingWizard.tsx GET /api/adapters/:type/probe probeAdapter calls for hermes_local, claude_local, openclaw_gateway on mount probeAdapter|adapters.*probe
Build the Provider Selection step UI components and wire them into the existing 3-step onboarding wizard (making it 4 steps). This includes the Puter auth button, Google OAuth button with risk warning, API key entry form, and adapter auto-detection badges.

Purpose: CLOUD-01 (Puter zero-config UI), CLOUD-03 (Google OAuth UI), CLOUD-04 (auto-detect tools), CLOUD-05 (API key entry UI). This is the user-facing surface for all cloud provider paths. Output: ProviderSelectionStep, PuterAuthButton, GoogleOAuthButton, ApiKeyEntryForm, updated wizard

<execution_context> @/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md @/home/mikkel/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/31-puter.js-zero-config-cloud/31-RESEARCH.md @.planning/phases/31-puter.js-zero-config-cloud/31-UI-SPEC.md @.planning/phases/30-hardware-detection-mode-selection/30-02-SUMMARY.md From ui/src/components/NexusOnboardingWizard.tsx (current state): ```typescript // 3-step wizard: step 1 = hardware, step 2 = mode, step 3 = root directory // Step indicator: "Step {step} of 3" // defaultAdapter state: "claude_local" | "hermes_local" // probing state for adapter detection // handleSubmit creates company + agents on step 3 form submit ```

From ui/src/components/onboarding/ModeSelector.tsx (selected state pattern):

// Cards use: border-primary bg-primary/5 when selected
// Vertical stack with gap-3
// Each card is a <button type="button"> element

From ui/src/api/agents.ts:

probeAdapter: (type: string) =>
  api.get<{ available: boolean; status: string }>(`/adapters/${encodeURIComponent(type)}/probe`),

From 31-UI-SPEC.md (copywriting):

Step heading: "Choose a provider"
Step subheading: "No API keys needed for the zero-config path."
Puter card label: "Puter -- free, zero-config"
Puter card description: "Free AI powered by your Puter.com account. No API key needed."
Google card label: "Google -- Gemini free tier"
Google risk warning body: "Google has suspended accounts that used third-party apps with Gemini credentials. This may affect your Gmail and Workspace access. Use a Google AI Studio API key instead if you want to avoid this risk."
API key card label: "API key -- subscription provider"

From Plan 02 routes (pendingTokens pattern):

// POST /oauth/google/authorize returns { url, stateId }
// GET /oauth/google/callback stores tokens by stateId, redirects to /?google_oauth=success&state={stateId}
// POST /oauth/google/claim { stateId, companyId } -> moves tokens to secretService
Task 1: API client + PuterAuthButton + GoogleOAuthButton + ApiKeyEntryForm ui/src/api/puter-proxy.ts, ui/src/components/onboarding/PuterAuthButton.tsx, ui/src/components/onboarding/GoogleOAuthButton.tsx, ui/src/components/onboarding/ApiKeyEntryForm.tsx ui/src/api/agents.ts, ui/src/api/client.ts, ui/src/components/onboarding/ModeSelector.tsx, ui/src/components/NexusOnboardingWizard.tsx, .planning/phases/31-puter.js-zero-config-cloud/31-UI-SPEC.md **Create ui/src/api/puter-proxy.ts:**
Import `api` from `./client`. Export `puterProxyApi` with:
- `storeToken: (companyId: string, token: string) => api.post("/puter-proxy/token", { companyId, token })`
- `getAuthUrl: () => api.post<{ url: string; stateId: string }>("/oauth/google/authorize", {})` — no companyId needed
- `claimGoogleTokens: (stateId: string, companyId: string) => api.post("/oauth/google/claim", { stateId, companyId })`
- `storeApiKey: (companyId: string, provider: string, apiKey: string) => api.post("/api-keys/store", { companyId, provider, apiKey })`

**Create ui/src/components/onboarding/PuterAuthButton.tsx:**

Props: `{ onSuccess: (token: string) => void; onError: (msg: string) => void }`.

State: `loading` boolean, `connected` boolean.

`loadScript` helper: check if `window.puter` exists; if not, create a `<script>` tag with `src="https://js.puter.com/v2/"`, append to `<head>`, wait for `load` event. Return a promise.

`handleClick` (the onClick handler — MUST be synchronous entry point for popup to work):
1. Set `loading(true)`.
2. Call `loadScript()` to ensure Puter SDK is loaded.
3. Call `(window as any).puter.auth.signIn()` — this opens the popup. Must be in the same synchronous-ish call chain from the click event.
4. After signIn resolves, extract token: try `(window as any).puter.authToken` first, then `(window as any).puter.auth?.token`, then call `(window as any).puter.auth?.getUser?.()` and check for token on the returned object. Log a warning if token is undefined (per Pitfall 1 in RESEARCH.md).
5. Call `onSuccess(token)` — parent stores the token in React state for later submission to server after company creation.
6. Set `connected(true)`, `loading(false)`.
7. On any error: `onError("Puter sign-in failed. Check your Puter.com account and try again.")`.

Render a `<Button>` (from @/components/ui/button). When `loading`, show spinner (h-4 w-4 animate-spin SVG) and text "Connecting to Puter...". When `connected`, show CheckCircle icon (from lucide-react) and text "Puter connected". Otherwise show LogIn icon and text "Continue with Puter". Use `aria-busy={loading}`.

**Create ui/src/components/onboarding/GoogleOAuthButton.tsx:**

Props: `{ onSuccess: (stateId: string) => void; onError: (msg: string) => void }`.

State: `loading`, `warningTimer` (boolean, starts false — becomes true after 3 seconds), `connected` boolean.

On mount, start a 3-second timeout that sets `warningTimer = true`. Clean up timeout on unmount.

Render:
1. Policy-risk warning (always visible when this component is rendered):
   ```
   <div role="alert" className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
     <p className="font-medium">Policy risk -- read before continuing</p>
     <p className="mt-1">Google has suspended accounts that used third-party apps with Gemini credentials. This may affect your Gmail and Workspace access. Use a Google AI Studio API key instead if you want to avoid this risk.</p>
   </div>
   ```
2. Button: "Sign in with Google" (disabled until `warningTimer === true`). On click:
   a. Set `loading(true)`.
   b. Call `puterProxyApi.getAuthUrl()` to get `{ url, stateId }`.
   c. Open popup: `window.open(url, "_blank", "popup,width=600,height=700")`.
   d. Poll for popup close or listen for `window.addEventListener("message", ...)`. Simpler: poll every 500ms checking if the popup is closed (`popup.closed`). When popup closes, check `window.location.search` for `google_oauth=success` query param, OR just trust that the callback happened server-side and the tokens are parked.
   e. Actually, the simplest approach: after opening the popup, the Google OAuth callback redirects the popup to `/?google_oauth=success&state={stateId}`. The popup lands on that URL. Use `window.addEventListener("message")` — but since the popup is our own origin, we can poll it. Simplest: after popup opens, poll `popup.closed` every 500ms. When closed, assume success (the callback stored tokens server-side). Call `onSuccess(stateId)`.
   f. Set `connected(true)`, `loading(false)`.
3. On error: call `onError(msg)`.

**Create ui/src/components/onboarding/ApiKeyEntryForm.tsx:**

Props: `{ onSave: (provider: string, apiKey: string) => void; onError: (msg: string) => void }`.

State: `provider` (default "openai"), `apiKey` (string), `saved` (boolean).

Render:
1. `<Label>` + select dropdown with options: OpenAI, Anthropic, Groq.
2. `<Label>` + `<Input>` for API key, type="password".
3. `<Button>` "Save API key". On click: validate apiKey not empty, call `onSave(provider, apiKey)`. Set `saved(true)`.
4. When `saved`, show CheckCircle icon and "API key saved" text.
5. Use shadcn `Label`, `Input`, `Button` components. Inline form (no page navigation).
cd /opt/nexus && ls ui/src/api/puter-proxy.ts ui/src/components/onboarding/PuterAuthButton.tsx ui/src/components/onboarding/GoogleOAuthButton.tsx ui/src/components/onboarding/ApiKeyEntryForm.tsx - grep -q "puterProxyApi" ui/src/api/puter-proxy.ts - grep -q "puter-proxy/token" ui/src/api/puter-proxy.ts - grep -q "oauth/google/claim" ui/src/api/puter-proxy.ts - grep -q "stateId" ui/src/api/puter-proxy.ts - grep -q "js.puter.com/v2" ui/src/components/onboarding/PuterAuthButton.tsx - grep -q "puter.auth.signIn" ui/src/components/onboarding/PuterAuthButton.tsx - grep -q "Continue with Puter" ui/src/components/onboarding/PuterAuthButton.tsx - grep -q "Connecting to Puter" ui/src/components/onboarding/PuterAuthButton.tsx - grep -q "Policy risk" ui/src/components/onboarding/GoogleOAuthButton.tsx - grep -q "suspended accounts" ui/src/components/onboarding/GoogleOAuthButton.tsx - grep -q "Sign in with Google" ui/src/components/onboarding/GoogleOAuthButton.tsx - grep -q "3000\|3 .* second\|setTimeout" ui/src/components/onboarding/GoogleOAuthButton.tsx - grep -q "openai\|anthropic\|groq" ui/src/components/onboarding/ApiKeyEntryForm.tsx - grep -q "Save API key" ui/src/components/onboarding/ApiKeyEntryForm.tsx - grep -q 'type="password"' ui/src/components/onboarding/ApiKeyEntryForm.tsx PuterAuthButton loads CDN and triggers signIn popup (token captured in React state), GoogleOAuthButton shows risk warning with 3s gate and opens OAuth popup (stateId captured), ApiKeyEntryForm captures provider/key pairs — all pass data back to parent via callbacks for post-company-creation submission Task 2: ProviderSelectionStep + wire 4-step wizard ui/src/components/onboarding/ProviderSelectionStep.tsx, ui/src/components/NexusOnboardingWizard.tsx ui/src/components/NexusOnboardingWizard.tsx, ui/src/components/onboarding/ModeSelector.tsx, ui/src/components/onboarding/PuterAuthButton.tsx, ui/src/components/onboarding/GoogleOAuthButton.tsx, ui/src/components/onboarding/ApiKeyEntryForm.tsx, ui/src/api/agents.ts, .planning/phases/31-puter.js-zero-config-cloud/31-UI-SPEC.md **Create ui/src/components/onboarding/ProviderSelectionStep.tsx:**
Props:
```typescript
interface ProviderSelectionStepProps {
  onPuterToken: (token: string) => void;
  onGoogleOAuthState: (stateId: string) => void;
  onApiKey: (provider: string, apiKey: string) => void;
  onSkip: () => void;
  onContinue: () => void;
  detectedAdapters: Record<string, boolean>; // e.g., { hermes_local: true, claude_local: false }
}
```

State: `selectedProvider` ("puter" | "google" | "apikey" | null), `providerReady` boolean (true when auth is complete for selected provider), `error` string | null.

Layout (following ModeSelector card pattern from 30-02):
1. Three vertical cards in a stack (`flex flex-col gap-3`):
   - **Puter card**: `<button type="button">` with `border-primary bg-primary/5` when selected. Label: "Puter -- free, zero-config". Description: "Free AI powered by your Puter.com account. No API key needed." If any adapters detected, show badges inside card: `<span className="text-xs text-primary">{Name} detected</span>`.
   - **Google card**: Same pattern. Label: "Google -- Gemini free tier". Description: "Sign in with Google to access Gemini via your Google account."
   - **API key card**: Same pattern. Label: "API key -- subscription provider". Description: "Use your own OpenAI, Anthropic, or Groq API key."

2. Below cards, conditionally render the action component for the selected provider:
   - Puter selected: `<PuterAuthButton onSuccess={(token) => { onPuterToken(token); setProviderReady(true); }} onError={setError} />`
   - Google selected: `<GoogleOAuthButton onSuccess={(stateId) => { onGoogleOAuthState(stateId); setProviderReady(true); }} onError={setError} />`
   - API key selected: `<ApiKeyEntryForm onSave={(prov, key) => { onApiKey(prov, key); setProviderReady(true); }} onError={setError} />`

3. If `providerReady`: show a "Continue" button calling `onContinue()`.

4. Always show: `<Button variant="ghost" onClick={onSkip}>Skip for now</Button>` with `aria-label="Skip provider setup for now"`.

5. Show detected adapter badges on each card:
   For each adapter in `detectedAdapters` where value is true, show `<span className="text-xs text-primary">Hermes detected</span>` (or "Claude Code detected", "OpenClaw detected") inside the relevant card area.

6. Error display: if `error` is set, show `<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">{error}</p>`.

**Update ui/src/components/NexusOnboardingWizard.tsx:**

Changes:
1. Import `ProviderSelectionStep` from `./onboarding/ProviderSelectionStep`.
2. Import `puterProxyApi` from `../api/puter-proxy`.
3. Change step count from 3 to 4. Step indicator: `"Step {step} of 4"`.
4. Add state: `puterToken` (string | null), `googleOAuthStateId` (string | null), `apiKeyData` ({ provider: string; apiKey: string } | null).
5. Add state: `detectedAdapters` (Record<string, boolean>, default `{}`).
6. Update the adapter probe useEffect to also probe `claude_local` and `openclaw_gateway` (in parallel with existing `hermes_local` probe). Store results in `detectedAdapters`.
7. Renumber steps:
   - Step 1: Hardware Detection (unchanged)
   - Step 2: Mode Selection (unchanged)
   - Step 3: Provider Selection (NEW)
   - Step 4: Root Directory (was step 3)
8. Step 2 "Continue" now goes to step 3 (not step 3/root dir).
9. Step 3 renders `<ProviderSelectionStep>`. "Skip" and "Continue" both advance to step 4. Back goes to step 2.
10. Step 4 is the existing root directory form. Back goes to step 3.
11. In `handleSubmit` (after company creation, before navigate), add credential storage:
   ```typescript
   // Store collected provider credentials after company creation
   if (puterToken) {
     await puterProxyApi.storeToken(company.id, puterToken).catch(() => {});
   }
   if (googleOAuthStateId) {
     // Claim Google OAuth tokens for this company
     await puterProxyApi.claimGoogleTokens(googleOAuthStateId, company.id).catch(() => {});
   }
   if (apiKeyData) {
     await puterProxyApi.storeApiKey(company.id, apiKeyData.provider, apiKeyData.apiKey).catch(() => {});
   }
   ```
   All credential storage is non-blocking (catch(() => {})) — same pattern as mode save.
12. Reset new state variables in the reset effect (when wizard closes): `setPuterToken(null)`, `setGoogleOAuthStateId(null)`, `setApiKeyData(null)`.
13. Update heading for step 3: "Choose a provider" with subheading "No API keys needed for the zero-config path."
cd /opt/nexus && pnpm --filter @paperclipai/ui exec tsc --noEmit 2>&1 | head -20 - grep -q "ProviderSelectionStep" ui/src/components/onboarding/ProviderSelectionStep.tsx - grep -q "Choose a provider" ui/src/components/onboarding/ProviderSelectionStep.tsx - grep -q "Skip for now" ui/src/components/onboarding/ProviderSelectionStep.tsx - grep -q "border-primary bg-primary/5" ui/src/components/onboarding/ProviderSelectionStep.tsx - grep -q "detected" ui/src/components/onboarding/ProviderSelectionStep.tsx - grep -q "Step 3.*Provider\|ProviderSelectionStep" ui/src/components/NexusOnboardingWizard.tsx - grep -q "Step {step} of 4\|of 4" ui/src/components/NexusOnboardingWizard.tsx - grep -q "puterToken" ui/src/components/NexusOnboardingWizard.tsx - grep -q "googleOAuthStateId" ui/src/components/NexusOnboardingWizard.tsx - grep -q "claimGoogleTokens" ui/src/components/NexusOnboardingWizard.tsx - grep -q "openclaw_gateway\|claude_local" ui/src/components/NexusOnboardingWizard.tsx - TypeScript compilation passes (tsc --noEmit exits 0) 4-step wizard with ProviderSelectionStep at step 3, adapter auto-detection badges for hermes/claude/openclaw, Puter token captured in state then posted after company creation, Google OAuth stateId captured then claimed after company creation, API key captured then stored after company creation, Skip always available - `cd /opt/nexus && pnpm --filter @paperclipai/ui exec tsc --noEmit` — no TypeScript errors - All new components exist with correct exports - Wizard shows 4 steps with provider selection at step 3 - Provider cards match ModeSelector visual pattern - All UI copy matches 31-UI-SPEC.md copywriting contract - Google OAuth flow uses stateId/claim pattern (no companyId before company creation)

<success_criteria>

  • Provider Selection step renders three provider cards with correct copy
  • Puter auth triggers popup and captures token in React state
  • Google OAuth shows risk warning with 3-second gate, opens popup, captures stateId
  • API key form captures provider/key pairs
  • Auto-detected adapters show badges on cards
  • Skip for now always works
  • Wizard is 4 steps, all credentials stored after company creation via handleSubmit </success_criteria>
After completion, create `.planning/phases/31-puter.js-zero-config-cloud/31-03-SUMMARY.md`