diff --git a/README.md b/README.md index 212eb98..3f478ae 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,10 @@ Flynn provides a full CLI via the `flynn` binary (or `npx tsx src/cli/index.ts` | `flynn skills` | List/install/manage skills | | `flynn companion` | Run a minimal companion node client against the gateway | -`flynn setup` / `flynn onboard` now print a post-save channel verification checklist (start command, WebChat URL, `/status` smoke test, and channel-specific validation hints). +`flynn setup` / `flynn onboard` now include: +- a **Personal Assistant Mode** first-run preset (announce delivery, proactive memory, talk mode defaults, TTS fallback policy), +- post-save **live readiness checks** (model, channel, memory, automation), +- and a guided **first-success task** sequence after config save. ### Examples @@ -1288,6 +1291,7 @@ Repeated failure/recovery notifications are throttled by `notify_cooldown`. - `automation.minio_sync.notify.channel: webchat` `flynn setup` now includes an Operator Pack option in Automation that preconfigures scheduled backups, heartbeat alerts, a daily briefing, and a default MinIO sync task, with prompts for output channel/peer routing. +After save, setup prints focused live readiness checks and a first-success task path so new installs can validate end-to-end operation immediately. See `docs/operations/OPERATOR_PACK.md` for an operations runbook and verification checklist. Example Operator Pack output routing: diff --git a/docs/api/PROTOCOL.md b/docs/api/PROTOCOL.md index 120f10a..495eccf 100644 --- a/docs/api/PROTOCOL.md +++ b/docs/api/PROTOCOL.md @@ -23,6 +23,8 @@ The gateway provides: - **HTTP Server**: Serves static dashboard and handles webhook endpoints - **Node Capability Negotiation**: Optional companion-node role/capability registration +Operational note: onboarding (`flynn setup` / `flynn onboard`) now runs post-save live readiness checks (model/channel/memory/automation) and prints a guided first-success task flow. This improves setup reliability without changing JSON-RPC method/event shapes. + ### Execution Model (Sessions + Per-Session Queue) Two concepts matter for correct clients: diff --git a/docs/architecture/AGENT_DIAGRAM.md b/docs/architecture/AGENT_DIAGRAM.md index fb386c6..4587405 100644 --- a/docs/architecture/AGENT_DIAGRAM.md +++ b/docs/architecture/AGENT_DIAGRAM.md @@ -158,6 +158,7 @@ Gateway streaming UX signals: - Canvas artifacts are persisted by the gateway so session UI surfaces can recover after daemon restarts. - TTS synthesis uses an ordered provider chain with health cooldown tracking; if all providers fail, replies degrade to text-only without dropping the response. - Talk mode accepts spoken/text `stop`/`cancel` while active and maps it onto the same `/stop` run-control cancellation path used for text sessions. +- Setup/onboarding now applies a first-run Personal Assistant Mode preset, prints model/channel/memory/automation live readiness checks, and emits a guided first-success task path after config save. Key files: diff --git a/docs/architecture/GATEWAY_SESSIONS_AND_QUEUE.md b/docs/architecture/GATEWAY_SESSIONS_AND_QUEUE.md index 0c45fd9..7abaeec 100644 --- a/docs/architecture/GATEWAY_SESSIONS_AND_QUEUE.md +++ b/docs/architecture/GATEWAY_SESSIONS_AND_QUEUE.md @@ -23,6 +23,7 @@ If you only want the protocol surface, see `docs/api/PROTOCOL.md`. - Canvas artifacts are persisted per session under the gateway data directory for UI recovery across restarts. - TTS output is best-effort with ordered provider fallback + per-provider cooldown tracking; synthesis failures still fall back to text-only responses. - Talk mode voice sessions share the same cancel/replace semantics as text lanes (`/stop`, interrupt mode preemption), including spoken `stop`/`cancel` mapping while talk mode is active. +- Setup/onboarding UX now adds post-save live readiness checks (model/channel/memory/automation) and a guided first-success task flow, improving zero-to-first-automation path reliability before sustained gateway use. ## Component Map diff --git a/docs/plans/state.json b/docs/plans/state.json index ec996e1..e2ffa03 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -6789,7 +6789,7 @@ "status": "in_progress", "date": "2026-02-26", "updated": "2026-02-27", - "summary": "Rebaselined Flynn's OpenClaw-style personal-assistant gaps and defined an execution-ready 8-10 week roadmap. Phase 3 browser reliability, Phase 1 companion reconnect/handoff reliability, and Phase 2 voice daily-driver reliability (talk controls + TTS provider fallback/health + interruption-safe voice cancel semantics) are now shipped.", + "summary": "Rebaselined Flynn's OpenClaw-style personal-assistant gaps and defined an execution-ready 8-10 week roadmap. Phase 3 browser reliability, Phase 1 companion reconnect/handoff reliability, Phase 2 voice daily-driver reliability (talk controls + TTS provider fallback/health + interruption-safe voice cancel semantics), and Phase 4 onboarding first-success funnel improvements are now shipped.", "files_modified": [ "docs/plans/2026-02-26-personal-assistant-productization-plan.md", "docs/plans/state.json" @@ -6868,6 +6868,29 @@ ], "test_status": "pnpm test:run src/models/tts.test.ts src/daemon/routing.test.ts src/gateway/handlers/handlers.test.ts src/gateway/ui/pages/settings.test.ts src/gateway/ui/pages/dashboard.test.ts src/gateway/ui/pages/chat.test.ts src/config/schema.test.ts + pnpm typecheck passing" }, + "personal-assistant-productization-phase4-onboarding-first-success": { + "status": "completed", + "date": "2026-02-27", + "updated": "2026-02-27", + "summary": "Implemented Phase 4 onboarding 2.0 first-success slice: setup/onboard first-run flow now offers a Personal Assistant Mode preset, runs live readiness checks (model/channel/memory/automation) after save, and prints a guided first-success task sequence to validate end-to-end operation quickly.", + "files_modified": [ + "src/cli/setup/config.ts", + "src/cli/setup/config.test.ts", + "src/cli/setup/orchestrator.ts", + "src/cli/setup/integration.test.ts", + "src/cli/setup/liveChecks.ts", + "src/cli/setup/liveChecks.test.ts", + "src/cli/setup/summary.ts", + "src/cli/setup/summary.test.ts", + "src/cli/setup.ts", + "README.md", + "docs/api/PROTOCOL.md", + "docs/architecture/AGENT_DIAGRAM.md", + "docs/architecture/GATEWAY_SESSIONS_AND_QUEUE.md", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/cli/setup/config.test.ts src/cli/setup/integration.test.ts src/cli/setup/summary.test.ts src/cli/setup/liveChecks.test.ts src/cli/setup/orchestrator.test.ts src/cli/setup/sections.test.ts + pnpm typecheck passing" + }, "subagents-support-phase1": { "status": "completed", "date": "2026-02-26", @@ -6902,7 +6925,7 @@ } }, "overall_progress": { - "total_test_count": 2559, + "total_test_count": 2568, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", @@ -6917,7 +6940,7 @@ "tier2_completion": "4/4 (100%) \u2014 inbound webhooks, vector memory search, Dockerfile, heartbeat monitor", "tier3_completion": "5/5 (100%) \u2014 lane queue, credential redaction, web UI token dashboard, xAI (Grok) provider, Voyage AI embeddings", "tier4_completion": "4/4 (100%) \u2014 gateway lock, shell completion, Tailscale Serve/Funnel, DM pairing codes", - "feature_gap_scorecard": "rebaselined 2026-02-26 and updated 2026-02-27 (phase 3 + phase 1 + phase 2 reliability slices) — channel breadth, setup wizard, baseline browser automation, subagent controls, browser workflow reliability primitives (wait/assert/extract/retries/checkpoints/guardrails/budgets), companion reconnect/runtime-handoff foundations, and voice reliability hardening (talk controls + TTS fallback/health + interruption-safe cancel semantics) are implemented; remaining high-impact personal-assistant gaps center on shipped desktop/mobile companion apps and first-success onboarding funnel optimization.", + "feature_gap_scorecard": "rebaselined 2026-02-26 and updated 2026-02-27 (phase 3 + phase 1 + phase 2 + phase 4 slices) — channel breadth, setup wizard, baseline browser automation, subagent controls, browser workflow reliability primitives (wait/assert/extract/retries/checkpoints/guardrails/budgets), companion reconnect/runtime-handoff foundations, voice reliability hardening (talk controls + TTS fallback/health + interruption-safe cancel semantics), and onboarding first-success funnel improvements are implemented; remaining high-impact personal-assistant gaps center on shipped desktop/mobile companion app surfaces and packaging.", "operator_dx_milestone": "Phase 3 (Live Ops Dashboard): 2/2 plans complete \u2014 milestone done", "dashboard_observability": "completed \u2014 service health graphs + core service log viewer added to web UI via observability RPCs and bounded backend sampling", "gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram", @@ -6950,7 +6973,7 @@ "deeper_surfaces_phase3_companion_canvas_voice": "completed \u2014 companion reconnect resilience (auto-reconnect with backoff, pending-wait cancellation on disconnect), canvas artifact persistence (SQLite-backed store, daemon-restart durability), voice TTS fallback coverage (text-only reply on TTS failure, no dropped responses)", "deeper_surfaces_phase4_rollout": "completed \u2014 phase 4 rollout and operator readiness plan documented: canary rollout plan by feature flag/surface, explicit rollback playbook, operator docs and architecture/protocol docs synchronized", "post_phase_test_fixes": "completed \u2014 fixed 4 test failures introduced by phases 1-3: iOS/Android push listNodes (missing publishHeartbeat before platform-filtered query), server.test agent.send (run_state events now precede done; added sendAndWaitForDone helper), httpBody 413 (req.destroy() closed socket before response could be sent; replaced with Connection: close header on 413 responses)", - "personal_assistant_productization_plan": "in_progress \u2014 8-10 week phased roadmap active; Phase 3 browser workflow reliability shipped, Phase 1 companion runtime reliability includes reconnect state replay + typed handoff support, and Phase 2 voice reliability now ships talk controls + TTS provider fallback/health + interruption-safe voice cancel mapping. Remaining phases: companion app packaging/surfaces and onboarding 2.0 first-success funnel.", + "personal_assistant_productization_plan": "in_progress \u2014 8-10 week phased roadmap active; Phase 3 browser workflow reliability shipped, Phase 1 companion runtime reliability includes reconnect state replay + typed handoff support, Phase 2 voice reliability ships talk controls + TTS provider fallback/health + interruption-safe voice cancel mapping, and Phase 4 onboarding now includes Personal Assistant Mode preset + live readiness checks + first-success guidance. Remaining phase focus: companion app packaging/surfaces.", "subagents_support": "completed \u2014 subagent phases 1-3 shipped with `subagent.spawn/send/list/cancel/delete/summary`, per-child queue mode (`followup|interrupt`), budgets (`max_turns`, `max_total_tokens`, `turn_timeout_ms`), tool-profile overrides, trace-linked audit events, `/subagents` inspection commands, and focused regression tests." }, "soul_md_and_cron_create": { diff --git a/src/cli/setup.ts b/src/cli/setup.ts index 862744f..bdc0650 100644 --- a/src/cli/setup.ts +++ b/src/cli/setup.ts @@ -13,7 +13,8 @@ import { getMinioExtractorInstallHints, renderMinioExtractorSetupLines, } from './minioExtractors.js'; -import { renderOnboardingChecklist } from './setup/summary.js'; +import { renderFirstSuccessTask, renderOnboardingChecklist } from './setup/summary.js'; +import { countSetupLiveCheckStatuses, renderSetupLiveChecks, runSetupLiveChecks } from './setup/liveChecks.js'; import type { SetupConfig } from './setup/config.js'; export async function runSetup(configPath: string): Promise { @@ -30,6 +31,8 @@ export async function runSetup(configPath: string): Promise { saveConfig(configPath, builder, p); const config = builder.build(); printOnboardingChecklist(p, config as Record, configPath); + printSetupLiveChecks(p, config as Record); + printFirstSuccessTask(p, config as Record); await printMinioExtractorSetupStatus(p, config as Record); await runGoogleAuth(p, config); } else { @@ -38,6 +41,8 @@ export async function runSetup(configPath: string): Promise { saveConfig(configPath, builder, p); const config = builder.build(); printOnboardingChecklist(p, config as Record, configPath); + printSetupLiveChecks(p, config as Record); + printFirstSuccessTask(p, config as Record); await printMinioExtractorSetupStatus(p, config as Record); await runGoogleAuth(p, config); @@ -62,6 +67,8 @@ export async function runSetup(configPath: string): Promise { saveConfig(configPath, menuBuilder, p); const config = menuBuilder.build(); printOnboardingChecklist(p, config as Record, configPath); + printSetupLiveChecks(p, config as Record); + printFirstSuccessTask(p, config as Record); await printMinioExtractorSetupStatus(p, config as Record); await runGoogleAuth(p, config); } @@ -88,6 +95,26 @@ function printOnboardingChecklist( p.println(renderOnboardingChecklist(config as SetupConfig, { configPath })); } +function printSetupLiveChecks( + p: { println(msg?: string): void }, + config: Record, +): void { + const results = runSetupLiveChecks(config as SetupConfig); + const counts = countSetupLiveCheckStatuses(results); + + p.println(); + p.println(renderSetupLiveChecks(results)); + p.println(` Summary: ${counts.pass} pass, ${counts.fail} fail, ${counts.warn} warn, ${counts.skip} skip`); +} + +function printFirstSuccessTask( + p: { println(msg?: string): void }, + config: Record, +): void { + p.println(); + p.println(renderFirstSuccessTask(config as SetupConfig)); +} + async function printMinioExtractorSetupStatus( p: { println(msg?: string): void }, config: Record, diff --git a/src/cli/setup/config.test.ts b/src/cli/setup/config.test.ts index 49c9700..7bc899b 100644 --- a/src/cli/setup/config.test.ts +++ b/src/cli/setup/config.test.ts @@ -127,4 +127,34 @@ describe('ConfigBuilder', () => { namespace: 'global', }); }); + + it('applies personal assistant mode defaults', () => { + const builder = new ConfigBuilder(); + builder.applyPersonalAssistantMode(); + + const obj = builder.build(); + expect((obj.automation as Record)?.delivery_mode).toBe('announce'); + expect((obj.memory as Record)?.daily_log).toEqual({ + enabled: true, + namespace_prefix: 'daily', + }); + expect((obj.memory as Record)?.proactive_extract).toEqual({ + enabled: true, + min_tool_calls: 2, + namespace: 'global', + }); + expect((obj.audio as Record)?.talk_mode).toMatchObject({ + enabled: true, + wake_phrase: 'hey flynn', + timeout_ms: 120000, + allow_manual_toggle: true, + }); + expect((obj.tts as Record)?.fallback).toEqual({ + max_attempts: 3, + failure_cooldown_ms: 60000, + }); + expect((obj.server as Record)?.queue).toMatchObject({ + mode: 'interrupt', + }); + }); }); diff --git a/src/cli/setup/config.ts b/src/cli/setup/config.ts index 434936c..9882214 100644 --- a/src/cli/setup/config.ts +++ b/src/cli/setup/config.ts @@ -23,6 +23,9 @@ export interface SetupConfig { token?: string; lock?: boolean; tailscale?: { serve?: boolean }; + queue?: { + mode?: 'collect' | 'followup' | 'steer' | 'steer_backlog' | 'interrupt'; + }; } & Record; hooks?: Record; telegram?: { bot_token: string; allowed_chat_ids: number[] }; @@ -30,6 +33,22 @@ export interface SetupConfig { slack?: { bot_token: string; app_token: string; signing_secret: string; allowed_channel_ids: string[] }; whatsapp?: { allowed_numbers: string[] }; memory?: { embedding?: { enabled?: boolean; provider?: string; api_key?: string; endpoint?: string } }; + audio?: { + enabled?: boolean; + talk_mode?: { + enabled?: boolean; + wake_phrase?: string; + timeout_ms?: number; + allow_manual_toggle?: boolean; + }; + }; + tts?: { + enabled?: boolean; + fallback?: { + max_attempts?: number; + failure_cooldown_ms?: number; + }; + }; sandbox?: { enabled?: boolean }; pairing?: { enabled?: boolean }; tools?: { profile?: string }; @@ -55,7 +74,15 @@ export interface SetupConfig { gdrive?: { enabled?: boolean }; gtasks?: { enabled?: boolean }; heartbeat?: { enabled?: boolean }; - daily_briefing?: { enabled?: boolean }; + daily_briefing?: { + enabled?: boolean; + schedule?: string; + output?: { + channel?: string; + peer?: string; + }; + model_tier?: 'fast' | 'default' | 'complex' | 'local'; + }; minio_sync?: { enabled?: boolean }; } & Record; backup?: { @@ -79,6 +106,11 @@ interface ResearchAgentOptions { modelTier: 'fast' | 'default' | 'complex' | 'local'; } +interface PersonalAssistantModeOptions { + enableTalkMode?: boolean; + enableTts?: boolean; +} + export class ConfigBuilder { private config: SetupConfig; @@ -317,6 +349,49 @@ export class ConfigBuilder { this.config.memory = memory; } + applyPersonalAssistantMode(options?: PersonalAssistantModeOptions): void { + const automation = (this.config.automation ?? {}) as Record; + const memory = (this.config.memory ?? {}) as Record; + const audio = (this.config.audio ?? {}) as Record; + const talkMode = (audio.talk_mode ?? {}) as Record; + const tts = (this.config.tts ?? {}) as Record; + const ttsFallback = (tts.fallback ?? {}) as Record; + const server = (this.config.server ?? {}) as Record; + const queue = (server.queue ?? {}) as Record; + + automation.delivery_mode = 'announce'; + memory.daily_log = { + enabled: true, + namespace_prefix: 'daily', + }; + memory.proactive_extract = { + enabled: true, + min_tool_calls: 2, + namespace: 'global', + }; + + talkMode.enabled = options?.enableTalkMode ?? true; + talkMode.wake_phrase = 'hey flynn'; + talkMode.timeout_ms = 120000; + talkMode.allow_manual_toggle = true; + audio.talk_mode = talkMode; + audio.enabled = Boolean(audio.enabled || talkMode.enabled); + + tts.enabled = options?.enableTts ?? true; + ttsFallback.max_attempts = 3; + ttsFallback.failure_cooldown_ms = 60000; + tts.fallback = ttsFallback; + + queue.mode = 'interrupt'; + server.queue = queue; + + this.config.automation = automation; + this.config.memory = memory; + this.config.audio = audio; + this.config.tts = tts; + this.config.server = server as SetupConfig['server']; + } + build(): SetupConfig { return structuredClone(this.config) as SetupConfig; } diff --git a/src/cli/setup/integration.test.ts b/src/cli/setup/integration.test.ts index e44f104..4526e62 100644 --- a/src/cli/setup/integration.test.ts +++ b/src/cli/setup/integration.test.ts @@ -26,6 +26,7 @@ describe('first-run wizard integration', () => { 'n', // confirm: Configure a fast tier? (no) '', // ask: Gateway port (default) 'n', // confirm: Add a messaging channel? (no) + 'n', // confirm: Enable Personal Assistant Mode defaults? (no) 'n', // confirm: Configure automation now? (no) ]); const p = createPrompter(rl); @@ -54,6 +55,7 @@ describe('first-run wizard integration', () => { '123:ABCdef', // password: Bot token '12345678', // ask: Allowed chat IDs 'n', // confirm: Add another channel? (no) + 'n', // confirm: Enable Personal Assistant Mode defaults? (no) 'n', // confirm: Configure automation now? (no) ]); const p = createPrompter(rl); @@ -78,6 +80,7 @@ describe('first-run wizard integration', () => { '123:ABCdef', // password: Bot token '12345678', // ask: Allowed chat IDs 'n', // confirm: Add another channel? (no) + 'y', // confirm: Enable Personal Assistant Mode defaults? (yes) 'y', // confirm: Configure automation now? (yes) 'y', // confirm: Enable operator automation pack? (yes) '', // ask: Operator notifications output channel (default) @@ -101,4 +104,31 @@ describe('first-run wizard integration', () => { expect(backup.schedule).toBe('0 2 * * *'); expect(heartbeat.enabled).toBe(true); }); + + it('applies personal assistant mode defaults when accepted', async () => { + const rl = mockReadline([ + '1', // choose: Anthropic + 'sk-ant-key', // password: API key + '', // ask: Model (default) + 'n', // confirm: Configure a fast tier? (no) + '', // ask: Gateway port (default) + 'n', // confirm: Add a messaging channel? (no) + '', // confirm: Enable Personal Assistant Mode defaults? (yes, default) + 'n', // confirm: Configure automation now? (no) + ]); + const p = createPrompter(rl); + + const builder = await runFirstRunWizard(p); + const config = builder.build() as Record; + const automation = config.automation as Record; + const memory = config.memory as Record; + const audio = config.audio as Record; + + expect(automation.delivery_mode).toBe('announce'); + expect(memory.daily_log).toEqual({ + enabled: true, + namespace_prefix: 'daily', + }); + expect((audio.talk_mode as Record)?.enabled).toBe(true); + }); }); diff --git a/src/cli/setup/liveChecks.test.ts b/src/cli/setup/liveChecks.test.ts new file mode 100644 index 0000000..38187d1 --- /dev/null +++ b/src/cli/setup/liveChecks.test.ts @@ -0,0 +1,98 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { countSetupLiveCheckStatuses, renderSetupLiveChecks, runSetupLiveChecks } from './liveChecks.js'; +import type { SetupConfig } from './config.js'; + +function createBaseConfig(): SetupConfig { + return { + models: { + default: { + provider: 'openai', + model: 'gpt-4.1-mini', + api_key: 'sk-test', + }, + }, + server: { + port: 18800, + localhost: true, + }, + }; +} + +describe('setup live checks', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('returns pass/skip checks for a minimal healthy setup', () => { + const results = runSetupLiveChecks(createBaseConfig()); + expect(results).toHaveLength(4); + expect(results[0]).toMatchObject({ status: 'pass', label: 'Model connectivity' }); + expect(results[1]).toMatchObject({ status: 'pass', label: 'Channel connectivity' }); + expect(results[2]).toMatchObject({ status: 'skip', label: 'Memory readiness' }); + expect(results[3]).toMatchObject({ status: 'skip', label: 'Automation readiness' }); + }); + + it('fails model readiness when api-key provider has no credentials', () => { + const config = createBaseConfig(); + config.models.default.api_key = undefined; + + const results = runSetupLiveChecks(config); + const model = results.find((entry) => entry.label === 'Model connectivity'); + expect(model).toMatchObject({ status: 'fail' }); + expect(String(model?.detail ?? '')).toContain('missing credentials'); + }); + + it('fails automation readiness when daily briefing output is missing', () => { + const config = createBaseConfig(); + config.automation = { + daily_briefing: { + enabled: true, + }, + } as SetupConfig['automation']; + + const results = runSetupLiveChecks(config); + const automation = results.find((entry) => entry.label === 'Automation readiness'); + expect(automation).toMatchObject({ status: 'fail' }); + expect(String(automation?.detail ?? '')).toContain('output.channel/output.peer'); + }); + + it('warns memory readiness for local embeddings without endpoint', () => { + const config = createBaseConfig(); + config.memory = { + embedding: { + enabled: true, + provider: 'ollama', + }, + }; + + const results = runSetupLiveChecks(config); + const memory = results.find((entry) => entry.label === 'Memory readiness'); + expect(memory).toMatchObject({ status: 'warn' }); + }); + + it('renders and counts check statuses', () => { + const config = createBaseConfig(); + config.memory = { + embedding: { + enabled: true, + provider: 'openai', + api_key: 'sk-embed', + }, + }; + config.automation = { + daily_briefing: { + enabled: true, + output: { channel: 'telegram', peer: '123' }, + }, + } as SetupConfig['automation']; + + const results = runSetupLiveChecks(config); + const summary = countSetupLiveCheckStatuses(results); + const rendered = renderSetupLiveChecks(results); + + expect(summary.pass).toBeGreaterThanOrEqual(3); + expect(summary.fail).toBe(0); + expect(rendered).toContain('Setup live checks:'); + expect(rendered).toContain('[PASS] Model connectivity'); + }); +}); diff --git a/src/cli/setup/liveChecks.ts b/src/cli/setup/liveChecks.ts new file mode 100644 index 0000000..0cf6562 --- /dev/null +++ b/src/cli/setup/liveChecks.ts @@ -0,0 +1,267 @@ +import type { SetupConfig } from './config.js'; + +export interface SetupLiveCheckResult { + status: 'pass' | 'warn' | 'fail' | 'skip'; + label: string; + detail?: string; +} + +const PROVIDER_ENV_KEY: Record = { + openai: 'OPENAI_API_KEY', + anthropic: 'ANTHROPIC_API_KEY', + gemini: 'GEMINI_API_KEY', + openrouter: 'OPENROUTER_API_KEY', + vercel: 'AI_GATEWAY_API_KEY', + zhipuai: 'ZHIPUAI_API_KEY', + xai: 'XAI_API_KEY', + minimax: 'MINIMAX_API_KEY', + moonshot: 'MOONSHOT_API_KEY', + voyage: 'VOYAGE_API_KEY', +}; + +function hasNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value.trim().length > 0; +} + +function runModelReadinessCheck(config: SetupConfig): SetupLiveCheckResult { + const defaultModel = config.models?.default; + if (!defaultModel || !hasNonEmptyString(defaultModel.provider) || !hasNonEmptyString(defaultModel.model)) { + return { + status: 'fail', + label: 'Model connectivity', + detail: 'default model provider/model is missing', + }; + } + + const provider = defaultModel.provider; + const modelName = defaultModel.model; + const envVar = PROVIDER_ENV_KEY[provider]; + const envHasKey = envVar ? hasNonEmptyString(process.env[envVar]) : false; + const configHasKey = hasNonEmptyString(defaultModel.api_key); + + if (envVar && !envHasKey && !configHasKey) { + return { + status: 'fail', + label: 'Model connectivity', + detail: `${provider}/${modelName} missing credentials (set ${envVar} or configure api_key)`, + }; + } + + const keySource = configHasKey ? 'config' : envHasKey ? 'env' : 'n/a'; + return { + status: 'pass', + label: 'Model connectivity', + detail: `${provider}/${modelName} (auth=${keySource})`, + }; +} + +function runChannelReadinessCheck(config: SetupConfig): SetupLiveCheckResult { + const port = Number(config.server?.port ?? 18800); + const issues: string[] = []; + const enabledChannels: string[] = []; + + if (config.telegram) { + enabledChannels.push('telegram'); + if (!hasNonEmptyString(config.telegram.bot_token)) { + issues.push('telegram.bot_token missing'); + } + if (!Array.isArray(config.telegram.allowed_chat_ids) || config.telegram.allowed_chat_ids.length === 0) { + issues.push('telegram.allowed_chat_ids missing'); + } + } + + if (config.discord) { + enabledChannels.push('discord'); + if (!hasNonEmptyString(config.discord.bot_token)) { + issues.push('discord.bot_token missing'); + } + } + + if (config.slack) { + enabledChannels.push('slack'); + if (!hasNonEmptyString(config.slack.bot_token)) { + issues.push('slack.bot_token missing'); + } + if (!hasNonEmptyString(config.slack.app_token)) { + issues.push('slack.app_token missing'); + } + if (!hasNonEmptyString(config.slack.signing_secret)) { + issues.push('slack.signing_secret missing'); + } + } + + if (config.whatsapp) { + enabledChannels.push('whatsapp'); + } + + if (issues.length > 0) { + return { + status: 'fail', + label: 'Channel connectivity', + detail: issues.join('; '), + }; + } + + const channelSummary = enabledChannels.length > 0 + ? enabledChannels.join(', ') + : 'webchat'; + return { + status: 'pass', + label: 'Channel connectivity', + detail: `${channelSummary} (webchat http://localhost:${port}/#/chat)`, + }; +} + +function runMemoryReadinessCheck(config: SetupConfig): SetupLiveCheckResult { + const embedding = config.memory?.embedding; + if (!embedding?.enabled) { + return { + status: 'skip', + label: 'Memory readiness', + detail: 'vector memory disabled (keyword memory still available)', + }; + } + + const provider = embedding.provider ?? 'openai'; + const envVar = PROVIDER_ENV_KEY[provider]; + const configHasKey = hasNonEmptyString(embedding.api_key); + const envHasKey = envVar ? hasNonEmptyString(process.env[envVar]) : false; + + if ((provider === 'openai' || provider === 'gemini' || provider === 'voyage') && !configHasKey && !envHasKey) { + return { + status: 'fail', + label: 'Memory readiness', + detail: `${provider} embeddings enabled but credentials are missing${envVar ? ` (set ${envVar} or embedding.api_key)` : ''}`, + }; + } + + if ((provider === 'ollama' || provider === 'llamacpp') && !hasNonEmptyString(embedding.endpoint)) { + return { + status: 'warn', + label: 'Memory readiness', + detail: `${provider} embeddings enabled without endpoint (set memory.embedding.endpoint for reliability)`, + }; + } + + return { + status: 'pass', + label: 'Memory readiness', + detail: `vector memory enabled (${provider})`, + }; +} + +function runAutomationReadinessCheck(config: SetupConfig): SetupLiveCheckResult { + const automation = config.automation ?? {}; + const enabledFeatures: string[] = []; + + if ((automation.daily_briefing as { enabled?: boolean } | undefined)?.enabled) { + enabledFeatures.push('daily_briefing'); + } + if ((automation.heartbeat as { enabled?: boolean } | undefined)?.enabled) { + enabledFeatures.push('heartbeat'); + } + if ((automation.gmail as { enabled?: boolean } | undefined)?.enabled) { + enabledFeatures.push('gmail'); + } + if ((automation.gcal as { enabled?: boolean } | undefined)?.enabled) { + enabledFeatures.push('gcal'); + } + if ((automation.gdocs as { enabled?: boolean } | undefined)?.enabled) { + enabledFeatures.push('gdocs'); + } + if ((automation.gdrive as { enabled?: boolean } | undefined)?.enabled) { + enabledFeatures.push('gdrive'); + } + if ((automation.gtasks as { enabled?: boolean } | undefined)?.enabled) { + enabledFeatures.push('gtasks'); + } + if ((automation.minio_sync as { enabled?: boolean } | undefined)?.enabled) { + enabledFeatures.push('minio_sync'); + } + if (Array.isArray(automation.cron) && automation.cron.length > 0) { + enabledFeatures.push(`cron:${automation.cron.length}`); + } + if (Array.isArray(automation.webhooks) && automation.webhooks.length > 0) { + enabledFeatures.push(`webhooks:${automation.webhooks.length}`); + } + if (config.backup?.enabled) { + enabledFeatures.push('backup'); + } + + if (enabledFeatures.length === 0) { + return { + status: 'skip', + label: 'Automation readiness', + detail: 'no automation enabled', + }; + } + + const dailyBriefing = automation.daily_briefing as { + enabled?: boolean; + output?: { channel?: string; peer?: string }; + } | undefined; + if (dailyBriefing?.enabled) { + const output = dailyBriefing.output; + if (!hasNonEmptyString(output?.channel) || !hasNonEmptyString(output?.peer)) { + return { + status: 'fail', + label: 'Automation readiness', + detail: 'daily_briefing enabled but output.channel/output.peer are missing', + }; + } + } + + const backupEnabled = config.backup?.enabled; + if (backupEnabled && !hasNonEmptyString(config.backup?.schedule)) { + return { + status: 'warn', + label: 'Automation readiness', + detail: `enabled features: ${enabledFeatures.join(', ')} (backup schedule missing)`, + }; + } + + return { + status: 'pass', + label: 'Automation readiness', + detail: `enabled features: ${enabledFeatures.join(', ')}`, + }; +} + +export function runSetupLiveChecks(config: SetupConfig): SetupLiveCheckResult[] { + return [ + runModelReadinessCheck(config), + runChannelReadinessCheck(config), + runMemoryReadinessCheck(config), + runAutomationReadinessCheck(config), + ]; +} + +export function renderSetupLiveChecks(results: SetupLiveCheckResult[]): string { + const lines: string[] = ['Setup live checks:']; + const marker: Record = { + pass: 'PASS', + warn: 'WARN', + fail: 'FAIL', + skip: 'SKIP', + }; + for (const result of results) { + const detail = result.detail ? ` — ${result.detail}` : ''; + lines.push(` [${marker[result.status]}] ${result.label}${detail}`); + } + return lines.join('\n'); +} + +export function countSetupLiveCheckStatuses(results: SetupLiveCheckResult[]): { + pass: number; + warn: number; + fail: number; + skip: number; +} { + return results.reduce( + (acc, result) => { + acc[result.status] += 1; + return acc; + }, + { pass: 0, warn: 0, fail: 0, skip: 0 }, + ); +} diff --git a/src/cli/setup/orchestrator.ts b/src/cli/setup/orchestrator.ts index a715780..741a8f5 100644 --- a/src/cli/setup/orchestrator.ts +++ b/src/cli/setup/orchestrator.ts @@ -71,7 +71,18 @@ export async function runFirstRunWizard(p: Prompter): Promise { p.println(); await setupChannels(p, builder); - // Step 3: Optional automation pack + // Step 3: Personal Assistant Mode defaults + p.println(); + const enablePersonalAssistantMode = await p.confirm( + 'Enable Personal Assistant Mode defaults (announce delivery, proactive memory, talk mode, TTS fallback)?', + true, + ); + if (enablePersonalAssistantMode) { + builder.applyPersonalAssistantMode(); + p.println('✓ Personal Assistant Mode defaults enabled'); + } + + // Step 4: Optional automation pack p.println(); const configureAutomation = await p.confirm('Configure automation now (operator pack, cron, webhooks, Google services)?', false); if (configureAutomation) { diff --git a/src/cli/setup/summary.test.ts b/src/cli/setup/summary.test.ts index fcb6b9f..ce91f38 100644 --- a/src/cli/setup/summary.test.ts +++ b/src/cli/setup/summary.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { renderOnboardingChecklist, renderOperatorPackStatus, renderSummary } from './summary.js'; +import { renderFirstSuccessTask, renderOnboardingChecklist, renderOperatorPackStatus, renderSummary } from './summary.js'; import type { SetupConfig } from './config.js'; describe('renderSummary', () => { @@ -89,3 +89,30 @@ describe('renderOnboardingChecklist', () => { expect(output).toContain('Slack: message the bot'); }); }); + +describe('renderFirstSuccessTask', () => { + it('renders daily-briefing-first guidance when daily briefing is enabled', () => { + const config = { + server: { port: 18800, localhost: true }, + automation: { + daily_briefing: { + enabled: true, + output: { channel: 'telegram', peer: '123' }, + }, + }, + } as unknown as SetupConfig; + + const output = renderFirstSuccessTask(config); + expect(output).toContain('Create my daily briefing now'); + expect(output).toContain('telegram/123'); + }); + + it('falls back to proposal guidance when no automation is enabled', () => { + const config = { + server: { port: 18800, localhost: true }, + } as unknown as SetupConfig; + + const output = renderFirstSuccessTask(config); + expect(output).toContain('Propose one useful automation'); + }); +}); diff --git a/src/cli/setup/summary.ts b/src/cli/setup/summary.ts index 80af12a..8f8b1e2 100644 --- a/src/cli/setup/summary.ts +++ b/src/cli/setup/summary.ts @@ -112,3 +112,44 @@ export function renderOnboardingChecklist(config: SetupConfig, opts?: { configPa lines.push(' 5. Run `flynn doctor` if any channel test fails'); return lines.join('\n'); } + +function hasGoogleAutomationEnabled(config: SetupConfig): boolean { + const automation = config.automation ?? {}; + return Boolean( + automation.gmail?.enabled + || automation.gcal?.enabled + || automation.gdocs?.enabled + || automation.gdrive?.enabled + || automation.gtasks?.enabled, + ); +} + +export function renderFirstSuccessTask(config: SetupConfig): string { + const lines: string[] = []; + const port = config.server?.port ?? 18800; + const automation = config.automation ?? {}; + const dailyBriefingEnabled = Boolean(automation.daily_briefing?.enabled); + const dailyBriefingOutput = automation.daily_briefing?.output as + | { channel?: string; peer?: string } + | undefined; + + lines.push('Guided first-success task (under 5 minutes):'); + lines.push(` 1. Open WebChat: http://localhost:${port}/#/chat`); + lines.push(' 2. Send `/status` and confirm Flynn replies.'); + + if (dailyBriefingEnabled && dailyBriefingOutput?.channel && dailyBriefingOutput?.peer) { + lines.push(' 3. Send: "Create my daily briefing now. Use available tools and output: Schedule, Priorities, Risks, First actions."'); + lines.push(` 4. Confirm the reply is actionable and routed for ${dailyBriefingOutput.channel}/${dailyBriefingOutput.peer}.`); + } else if (hasGoogleAutomationEnabled(config)) { + lines.push(' 3. Send: "Use my connected Google services to summarize today\'s schedule and top 3 priorities."'); + lines.push(' 4. Confirm at least one connected service signal appears in the reply.'); + } else if (Array.isArray(automation.cron) && automation.cron.length > 0) { + lines.push(' 3. Send: "List my active automation jobs and recommend which one to run now."'); + lines.push(' 4. Confirm Flynn identifies at least one configured automation by name.'); + } else { + lines.push(' 3. Send: "Propose one useful automation I can enable today with exact config fields and values."'); + lines.push(' 4. Confirm the reply includes channel + schedule/output details you can apply directly.'); + } + + return lines.join('\n'); +}