diff --git a/surfaces/cli/src/commands/app.test.ts b/surfaces/cli/src/commands/app.test.ts index 42c96c308..2d09a5d28 100644 --- a/surfaces/cli/src/commands/app.test.ts +++ b/surfaces/cli/src/commands/app.test.ts @@ -25,6 +25,50 @@ describe("registerAppCommands", () => { expect(calls).toEqual([[]]); }); + test("passes setup mode to the setup wizard", async () => { + const calls: unknown[] = []; + const program = new Command(); + + registerAppCommands(program, { + collectListOption: (value, previous) => [...previous, value], + configureAgent: async () => {}, + launchDashboard: async () => {}, + migrateSchema: async () => {}, + setupWizard: async (options) => { + calls.push(options); + }, + showDoctor: async () => {}, + showStatus: async () => {}, + syncTemplates: async () => {}, + }); + + await program.parseAsync(["node", "test", "setup", "--setup-mode", "dashboard"]); + + expect(calls).toEqual([expect.objectContaining({ setupMode: "dashboard" })]); + }); + + test("rejects malformed setup option tokens as excess arguments", async () => { + const calls: unknown[] = []; + const program = new Command(); + program.exitOverride(); + + registerAppCommands(program, { + collectListOption: (value, previous) => [...previous, value], + configureAgent: async () => {}, + launchDashboard: async () => {}, + migrateSchema: async () => {}, + setupWizard: async (options) => { + calls.push(options); + }, + showDoctor: async () => {}, + showStatus: async () => {}, + syncTemplates: async () => {}, + }); + + await expect(program.parseAsync(["node", "test", "setup", " --setup-mode", "dashboard"])).rejects.toThrow(); + expect(calls).toEqual([]); + }); + test("routes doctor target into doctor options", async () => { const calls: unknown[] = []; const program = new Command(); diff --git a/surfaces/cli/src/commands/app.ts b/surfaces/cli/src/commands/app.ts index e88e2e689..1e9480bfe 100644 --- a/surfaces/cli/src/commands/app.ts +++ b/surfaces/cli/src/commands/app.ts @@ -24,6 +24,7 @@ interface SetupOptions { disableSignetSecrets?: boolean; withGraphiq?: boolean; disableGraphiq?: boolean; + setupMode?: string; } interface PathOptions { @@ -49,9 +50,11 @@ interface AppDeps { export function registerAppCommands(program: Command, deps: AppDeps): void { program .command("setup") + .allowExcessArguments(false) .description("Setup wizard (interactive by default)") .option("-p, --path ", "Base path for agent files") .option("--non-interactive", "Run setup without prompts") + .option("--setup-mode ", "Interactive setup surface (terminal, dashboard)") .option("--name ", "Agent name (non-interactive mode)") .option("--description ", "Agent description (non-interactive mode)") .option( @@ -72,7 +75,7 @@ export function registerAppCommands(program: Command, deps: AppDeps): void { .option("--embedding-model ", "Embedding model in non-interactive mode") .option( "--extraction-provider ", - "Extraction provider in non-interactive mode (claude-code, codex, llama-cpp, ollama, opencode, openrouter, openai-compatible, none)", + "Extraction provider in non-interactive mode (acpx, claude-code, codex, llama-cpp, ollama, opencode, openrouter, openai-compatible, none)", ) .option("--extraction-model ", "Extraction model in non-interactive mode") .option("--extraction-endpoint ", "OpenAI-compatible extraction endpoint in non-interactive mode") diff --git a/surfaces/cli/src/features/setup-types.ts b/surfaces/cli/src/features/setup-types.ts index 5aa7a6588..1af2293cf 100644 --- a/surfaces/cli/src/features/setup-types.ts +++ b/surfaces/cli/src/features/setup-types.ts @@ -30,7 +30,8 @@ export interface SetupWizardOptions { disableSignetSecrets?: boolean; withGraphiq?: boolean; disableGraphiq?: boolean; - identityPreset?: string; + identityPreset?: string; + setupMode?: string; } export interface SetupDeps { diff --git a/surfaces/cli/src/features/setup.ts b/surfaces/cli/src/features/setup.ts index d280f0b3b..fa5b7fadd 100644 --- a/surfaces/cli/src/features/setup.ts +++ b/surfaces/cli/src/features/setup.ts @@ -180,6 +180,8 @@ export async function setupWizard(options: SetupWizardOptions, deps: SetupDeps): const requestedExtractionProvider = deps.normalizeChoice(rawExtractionProvider, EXTRACTION_PROVIDER_CHOICES); const rawExtractionEndpoint = deps.normalizeStringValue(options.extractionEndpoint); const requestedExtractionEndpoint = normalizeHttpEndpoint(rawExtractionEndpoint); + const rawSetupMode = deps.normalizeStringValue(options.setupMode); + const requestedSetupMode = deps.normalizeChoice(rawSetupMode, ["terminal", "dashboard"] as const); const existingName = readString(existingConfig.name) ?? readString(existingAgent.name) ?? "My Agent"; const existingDesc = readString(existingConfig.description) ?? readString(existingAgent.description) ?? "Personal AI assistant"; @@ -228,6 +230,9 @@ export async function setupWizard(options: SetupWizardOptions, deps: SetupDeps): if (rawExtractionEndpoint && !requestedExtractionEndpoint) { failSetupValidation("--extraction-endpoint must be an http:// or https:// URL."); } + if (rawSetupMode && !requestedSetupMode) { + failSetupValidation(`Unknown --setup-mode value: ${rawSetupMode}. Valid choices: terminal, dashboard.`); + } const unknownHarnessValues = findUnknownHarnessValues(options.harness, deps); if (nonInteractive && unknownHarnessValues.length > 0) { failNonInteractiveSetup( @@ -497,13 +502,16 @@ export async function setupWizard(options: SetupWizardOptions, deps: SetupDeps): const setupMethod = nonInteractive ? "new" - : await select({ - message: "How would you like to set up?", - choices: [ - { value: "new", name: "Create new agent identity" }, - { value: "github", name: "Import from GitHub repository" }, - ], - }); + : requestedSetupMode === "dashboard" + ? "dashboard" + : await select({ + message: "How would you like to set up?", + choices: [ + { value: "new", name: "Create new agent identity in terminal" }, + { value: "dashboard", name: "Create defaults and finish in dashboard" }, + { value: "github", name: "Import from GitHub repository" }, + ], + }); if (setupMethod === "github") { mkdirSync(basePath, { recursive: true }); @@ -511,6 +519,61 @@ export async function setupWizard(options: SetupWizardOptions, deps: SetupDeps): await deps.importFromGitHub(basePath); return; } + if (setupMethod === "dashboard") { + const deploymentType = requestedDeploymentType ?? "local"; + const embeddingProvider = requestedEmbeddingProvider ?? defaultEmbeddingProviderForDeployment(deploymentType); + let embeddingModel = deps.normalizeStringValue(options.embeddingModel) || "nomic-embed-text-v1.5"; + let embeddingDimensions = getEmbeddingDimensions(embeddingModel); + if (embeddingProvider === "native") { + embeddingModel = "nomic-embed-text-v1.5"; + embeddingDimensions = 768; + } + const harnesses = normalizeHarnessList(options.harness, deps); + const extractionProvider = resolveSetupExtractionProvider({ + deploymentType, + requestedProvider: requestedExtractionProvider, + preserveExisting: false, + detectedProvider, + availableProviders: availableToolExtractionProviders, + preferredHarnesses: harnesses, + }); + const extractionModel = + deps.normalizeStringValue(options.extractionModel) || defaultExtractionModel(extractionProvider); + await runFreshSetup( + { + basePath, + agentName: deps.normalizeStringValue(options.name) || existingName, + agentDescription: deps.normalizeStringValue(options.description) || existingDesc, + networkMode: deps.normalizeChoice(options.networkMode, NETWORK_MODES) ?? "localhost", + harnesses, + openclawRuntimePath: deps.normalizeChoice(options.openclawRuntimePath, OPENCLAW_RUNTIME_CHOICES) ?? "plugin", + configureOpenClawWs: options.configureOpenclawWorkspace === true, + openclawConfigCount: new OpenClawConnector().getDiscoveredConfigPaths().length, + embeddingProvider, + embeddingModel, + embeddingDimensions, + extractionProvider, + extractionModel, + availableExtractionProviders: availableToolExtractionProviders, + acpxBin, + searchBalance: deps.parseSearchBalanceValue(options.searchBalance) ?? 0.7, + searchTopK: 20, + searchMinScore: 0.3, + memorySessionBudget: 2000, + memoryDecayRate: 0.95, + gitEnabled: options.skipGit !== true, + existingAgentsDir: existing.agentsDir, + nonInteractive: true, + openDashboard: true, + allowUnprotectedWorkspace: options.allowUnprotectedWorkspace === true, + createLocalBackup: options.createLocalBackup === true, + signetSecretsEnabled: await resolveSignetSecretsCorePluginSelection(basePath, true, options), + graphiqEnabled: await resolveGraphiqPluginSelection(basePath, true, options), + }, + deps, + ); + return; + } console.log(); } diff --git a/surfaces/dashboard/src/lib/components/onboarding/OnboardingModal.svelte b/surfaces/dashboard/src/lib/components/onboarding/OnboardingModal.svelte new file mode 100644 index 000000000..6bb8547f2 --- /dev/null +++ b/surfaces/dashboard/src/lib/components/onboarding/OnboardingModal.svelte @@ -0,0 +1,590 @@ + + +{#if open} + +{/if} + + diff --git a/surfaces/dashboard/src/lib/components/onboarding/OnboardingStepShell.svelte b/surfaces/dashboard/src/lib/components/onboarding/OnboardingStepShell.svelte new file mode 100644 index 000000000..043333a88 --- /dev/null +++ b/surfaces/dashboard/src/lib/components/onboarding/OnboardingStepShell.svelte @@ -0,0 +1,75 @@ + + +
+
+ {stepNumber} +
+

{title}

+

{subtitle}

+
+
+
+ {@render children()} +
+
+ + diff --git a/surfaces/dashboard/src/lib/components/onboarding/onboarding-state.svelte.ts b/surfaces/dashboard/src/lib/components/onboarding/onboarding-state.svelte.ts new file mode 100644 index 000000000..2d995f7f0 --- /dev/null +++ b/surfaces/dashboard/src/lib/components/onboarding/onboarding-state.svelte.ts @@ -0,0 +1,229 @@ +import type { PipelineProviderChoice } from "@signet/core/pipeline-providers"; + +export type IdentityPresetName = "minimal" | "hermes" | "openclaw" | "custom"; + +export const IDENTITY_PRESET_META: Array<{ + name: IdentityPresetName; + title: string; + subtitle: string; + files: string; +}> = [ + { + name: "minimal", + title: "Minimal", + subtitle: "AGENTS.md operating instructions plus dreaming. Light and focused.", + files: "AGENTS.md + DREAMING.md", + }, + { + name: "hermes", + title: "Hermes", + subtitle: "SOUL.md primary identity with project-context discovery.", + files: "SOUL.md + AGENTS.md + DREAMING.md", + }, + { + name: "openclaw", + title: "OpenClaw", + subtitle: "Rich identity stack for character-forward agents.", + files: "AGENTS.md + SOUL.md + IDENTITY.md + USER.md + MEMORY.md", + }, + { + name: "custom", + title: "Custom", + subtitle: "Select your own startup files later in settings.", + files: "AGENTS.md + DREAMING.md", + }, +]; + +export interface OnboardingState { + identityPreset: IdentityPresetName; + agentName: string; + agentDescription: string; + selectedHarnesses: string[]; + selectedHarness: string; + embeddingProvider: EmbeddingProvider; + embeddingModel: string; + embeddingEndpoint: string; + extractionProvider: PipelineProviderChoice; + extractionModel: string; + extractionEndpoint: string; + synthesisEnabled: boolean; + showAdvancedProviders: boolean; + currentStep: number; + saving: boolean; +} + +export type EmbeddingProvider = "native" | "llama-cpp" | "ollama" | "openai" | "none"; + +export const EMBEDDING_PROVIDER_OPTIONS: Array<{ + readonly value: EmbeddingProvider; + readonly label: string; + readonly detail: string; + readonly defaultModel: string; + readonly defaultEndpoint: string; +}> = [ + { + value: "native", + label: "Native (built-in)", + detail: "On-device embedding with no external server. Recommended.", + defaultModel: "nomic-embed-text-v1.5", + defaultEndpoint: "", + }, + { + value: "llama-cpp", + label: "llama.cpp", + detail: "OpenAI-compatible server bundled with llama.cpp.", + defaultModel: "nomic-embed-text", + defaultEndpoint: "http://localhost:8080/v1", + }, + { + value: "ollama", + label: "Ollama", + detail: "Ollama daemon on this machine or LAN.", + defaultModel: "nomic-embed-text", + defaultEndpoint: "http://localhost:11434", + }, + { + value: "openai", + label: "OpenAI", + detail: "OpenAI text-embedding API. Requires API key in secrets.", + defaultModel: "text-embedding-3-small", + defaultEndpoint: "https://api.openai.com/v1", + }, + { + value: "none", + label: "Off", + detail: "Disable vector search. Memory will be keyword-only.", + defaultModel: "", + defaultEndpoint: "", + }, +]; + +export const EMBEDDING_MODEL_PRESETS: Record> = { + native: [{ value: "nomic-embed-text-v1.5", label: "nomic-embed-text-v1.5" }], + "llama-cpp": [ + { value: "nomic-embed-text", label: "nomic-embed-text" }, + { value: "all-minilm", label: "all-minilm" }, + { value: "mxbai-embed-large", label: "mxbai-embed-large" }, + ], + ollama: [ + { value: "nomic-embed-text", label: "nomic-embed-text" }, + { value: "all-minilm", label: "all-minilm" }, + { value: "mxbai-embed-large", label: "mxbai-embed-large" }, + ], + openai: [ + { value: "text-embedding-3-small", label: "text-embedding-3-small" }, + { value: "text-embedding-3-large", label: "text-embedding-3-large" }, + ], + none: [], +}; + +export type ExtractionProviderOption = { + value: PipelineProviderChoice; + label: string; + detail: string; + mode: "agent" | "local" | "api" | "off" | "custom"; + endpointPlaceholder?: string; +}; + +export const EXTRACTION_PROVIDER_OPTIONS: ExtractionProviderOption[] = [ + { + value: "acpx", + label: "ACPX", + detail: "Route extraction through a selected installed agent harness.", + mode: "agent", + }, + { + value: "llama-cpp", + label: "llama.cpp", + detail: "Use a local llama.cpp OpenAI-compatible server.", + mode: "local", + endpointPlaceholder: "http://127.0.0.1:8080/v1", + }, + { + value: "ollama", + label: "Ollama", + detail: "Use an Ollama daemon running on this machine or LAN.", + mode: "local", + endpointPlaceholder: "http://127.0.0.1:11434", + }, + { + value: "claude-code", + label: "Claude Code", + detail: "Call the local Claude Code CLI directly for extraction.", + mode: "agent", + }, + { + value: "codex", + label: "Codex", + detail: "Call the local Codex CLI directly for extraction.", + mode: "agent", + }, + { + value: "opencode", + label: "OpenCode", + detail: "Use OpenCode as the extraction backend.", + mode: "agent", + }, + { + value: "anthropic", + label: "Anthropic API", + detail: "Use Anthropic directly with configured secrets.", + mode: "api", + endpointPlaceholder: "https://api.anthropic.com", + }, + { + value: "openrouter", + label: "OpenRouter", + detail: "Use OpenRouter with configured secrets.", + mode: "api", + endpointPlaceholder: "https://openrouter.ai/api/v1", + }, + { + value: "command", + label: "Command", + detail: "Use a custom command provider configured in advanced settings.", + mode: "custom", + }, + { + value: "none", + label: "Off", + detail: "Leave extraction disabled for now.", + mode: "off", + }, +]; + +export const EXTRACTION_MODEL_PRESETS: Partial> = { + acpx: ["gpt-5-codex-mini", "gpt-5-codex", "claude-haiku-4-5"], + "llama-cpp": ["qwen3.5:4b", "qwen3:8b", "llama-3.1-8b"], + ollama: ["qwen3:4b", "qwen3:8b", "glm-4.7-flash"], + "claude-code": ["haiku", "sonnet", "opus"], + codex: ["gpt-5-codex-mini", "gpt-5-codex", "gpt-5.4"], + opencode: ["anthropic/claude-haiku-4-5-20251001", "google/gemini-2.5-flash"], + anthropic: ["haiku", "sonnet", "opus"], + openrouter: ["openai/gpt-4o-mini", "anthropic/claude-haiku-4-5-20251001"], +}; + +export const EXTRACTION_SAFETY_TEXT = + "Remote API extraction can stack up extreme fees fast. Intended usage: Claude Code on haiku, Codex CLI on gpt-5-codex-mini with a pro/max subscription, or local providers (llama.cpp or Ollama) at qwen3:4b or larger."; + +export const RECOMMENDED_EXTRACTION: PipelineProviderChoice[] = ["acpx", "ollama", "codex", "claude-code"]; + +export function createDefaultState(): OnboardingState { + return { + identityPreset: "minimal", + agentName: "My Agent", + agentDescription: "Personal AI assistant", + selectedHarnesses: [], + selectedHarness: "", + embeddingProvider: "native", + embeddingModel: "nomic-embed-text-v1.5", + embeddingEndpoint: "", + extractionProvider: "acpx", + extractionModel: "gpt-5-codex-mini", + extractionEndpoint: "", + synthesisEnabled: true, + showAdvancedProviders: false, + currentStep: 0, + saving: false, + }; +} diff --git a/surfaces/dashboard/src/lib/components/onboarding/steps/EmbeddingStep.svelte b/surfaces/dashboard/src/lib/components/onboarding/steps/EmbeddingStep.svelte new file mode 100644 index 000000000..282940517 --- /dev/null +++ b/surfaces/dashboard/src/lib/components/onboarding/steps/EmbeddingStep.svelte @@ -0,0 +1,237 @@ + + +
+

+ Embedding turns text into vectors for semantic search. The built-in native provider works out of + the box with no setup. +

+ +
+ {#each EMBEDDING_PROVIDER_OPTIONS as option (option.value)} + + {/each} +
+ + {#if state.embeddingProvider !== "none"} +
+
+ + + {#if needsEndpoint} + + {/if} +
+
+ {/if} +
+ + diff --git a/surfaces/dashboard/src/lib/components/onboarding/steps/ExtractionStep.svelte b/surfaces/dashboard/src/lib/components/onboarding/steps/ExtractionStep.svelte new file mode 100644 index 000000000..4454447e2 --- /dev/null +++ b/surfaces/dashboard/src/lib/components/onboarding/steps/ExtractionStep.svelte @@ -0,0 +1,420 @@ + + +
+

{EXTRACTION_SAFETY_TEXT}

+ +
+

Recommended routes

+ {#each EXTRACTION_PROVIDER_OPTIONS as option (option.value)} + {#if isRecommended(option.value)} + + {/if} + {/each} + + + + {#if state.showAdvancedProviders} +

Advanced routes

+ {#each EXTRACTION_PROVIDER_OPTIONS as option (option.value)} + {#if !isRecommended(option.value)} + + {/if} + {/each} + {/if} +
+ +
+
+ {providerOption.mode} +

{providerOption.detail}

+
+ + {#if state.extractionProvider === "acpx"} +
+ ACPX harness +
+ {#each state.selectedHarnesses as h (h)} + + {/each} + {#if state.selectedHarnesses.length === 0} + Select at least one harness in the previous step. + {/if} +
+
+ {/if} + +
+ + {#if needsEndpoint} + + {/if} +
+ + {#if modelPresets.length > 0 && state.extractionProvider !== "none"} +
+ {#each modelPresets as preset (preset)} + + {/each} +
+ {/if} + + +
+
+ + diff --git a/surfaces/dashboard/src/lib/components/onboarding/steps/IdentityStep.svelte b/surfaces/dashboard/src/lib/components/onboarding/steps/IdentityStep.svelte new file mode 100644 index 000000000..c87e3737b --- /dev/null +++ b/surfaces/dashboard/src/lib/components/onboarding/steps/IdentityStep.svelte @@ -0,0 +1,212 @@ + + +
+
+ +