feat(setup): add onboarding live checks and first-success guidance

This commit is contained in:
William Valentin
2026-02-26 18:18:12 -08:00
parent 03926a81eb
commit 62c427da4a
14 changed files with 646 additions and 9 deletions
+5 -1
View File
@@ -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:
+2
View File
@@ -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:
+1
View File
@@ -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:
@@ -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
+27 -4
View File
@@ -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": {
+28 -1
View File
@@ -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<void> {
@@ -30,6 +31,8 @@ export async function runSetup(configPath: string): Promise<void> {
saveConfig(configPath, builder, p);
const config = builder.build();
printOnboardingChecklist(p, config as Record<string, unknown>, configPath);
printSetupLiveChecks(p, config as Record<string, unknown>);
printFirstSuccessTask(p, config as Record<string, unknown>);
await printMinioExtractorSetupStatus(p, config as Record<string, unknown>);
await runGoogleAuth(p, config);
} else {
@@ -38,6 +41,8 @@ export async function runSetup(configPath: string): Promise<void> {
saveConfig(configPath, builder, p);
const config = builder.build();
printOnboardingChecklist(p, config as Record<string, unknown>, configPath);
printSetupLiveChecks(p, config as Record<string, unknown>);
printFirstSuccessTask(p, config as Record<string, unknown>);
await printMinioExtractorSetupStatus(p, config as Record<string, unknown>);
await runGoogleAuth(p, config);
@@ -62,6 +67,8 @@ export async function runSetup(configPath: string): Promise<void> {
saveConfig(configPath, menuBuilder, p);
const config = menuBuilder.build();
printOnboardingChecklist(p, config as Record<string, unknown>, configPath);
printSetupLiveChecks(p, config as Record<string, unknown>);
printFirstSuccessTask(p, config as Record<string, unknown>);
await printMinioExtractorSetupStatus(p, config as Record<string, unknown>);
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<string, unknown>,
): 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<string, unknown>,
): void {
p.println();
p.println(renderFirstSuccessTask(config as SetupConfig));
}
async function printMinioExtractorSetupStatus(
p: { println(msg?: string): void },
config: Record<string, unknown>,
+30
View File
@@ -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<string, unknown>)?.delivery_mode).toBe('announce');
expect((obj.memory as Record<string, unknown>)?.daily_log).toEqual({
enabled: true,
namespace_prefix: 'daily',
});
expect((obj.memory as Record<string, unknown>)?.proactive_extract).toEqual({
enabled: true,
min_tool_calls: 2,
namespace: 'global',
});
expect((obj.audio as Record<string, unknown>)?.talk_mode).toMatchObject({
enabled: true,
wake_phrase: 'hey flynn',
timeout_ms: 120000,
allow_manual_toggle: true,
});
expect((obj.tts as Record<string, unknown>)?.fallback).toEqual({
max_attempts: 3,
failure_cooldown_ms: 60000,
});
expect((obj.server as Record<string, unknown>)?.queue).toMatchObject({
mode: 'interrupt',
});
});
});
+76 -1
View File
@@ -23,6 +23,9 @@ export interface SetupConfig {
token?: string;
lock?: boolean;
tailscale?: { serve?: boolean };
queue?: {
mode?: 'collect' | 'followup' | 'steer' | 'steer_backlog' | 'interrupt';
};
} & Record<string, unknown>;
hooks?: Record<string, unknown>;
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<string, unknown>;
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<string, unknown>;
const memory = (this.config.memory ?? {}) as Record<string, unknown>;
const audio = (this.config.audio ?? {}) as Record<string, unknown>;
const talkMode = (audio.talk_mode ?? {}) as Record<string, unknown>;
const tts = (this.config.tts ?? {}) as Record<string, unknown>;
const ttsFallback = (tts.fallback ?? {}) as Record<string, unknown>;
const server = (this.config.server ?? {}) as Record<string, unknown>;
const queue = (server.queue ?? {}) as Record<string, unknown>;
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;
}
+30
View File
@@ -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<string, unknown>;
const automation = config.automation as Record<string, unknown>;
const memory = config.memory as Record<string, unknown>;
const audio = config.audio as Record<string, unknown>;
expect(automation.delivery_mode).toBe('announce');
expect(memory.daily_log).toEqual({
enabled: true,
namespace_prefix: 'daily',
});
expect((audio.talk_mode as Record<string, unknown>)?.enabled).toBe(true);
});
});
+98
View File
@@ -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');
});
});
+267
View File
@@ -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<string, string> = {
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<SetupLiveCheckResult['status'], string> = {
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 },
);
}
+12 -1
View File
@@ -71,7 +71,18 @@ export async function runFirstRunWizard(p: Prompter): Promise<ConfigBuilder> {
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) {
+28 -1
View File
@@ -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');
});
});
+41
View File
@@ -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');
}