feat(setup): add onboarding live checks and first-success guidance
This commit is contained in:
@@ -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 skills` | List/install/manage skills |
|
||||||
| `flynn companion` | Run a minimal companion node client against the gateway |
|
| `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
|
### Examples
|
||||||
|
|
||||||
@@ -1288,6 +1291,7 @@ Repeated failure/recovery notifications are throttled by `notify_cooldown`.
|
|||||||
- `automation.minio_sync.notify.channel: webchat`
|
- `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.
|
`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.
|
See `docs/operations/OPERATOR_PACK.md` for an operations runbook and verification checklist.
|
||||||
|
|
||||||
Example Operator Pack output routing:
|
Example Operator Pack output routing:
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ The gateway provides:
|
|||||||
- **HTTP Server**: Serves static dashboard and handles webhook endpoints
|
- **HTTP Server**: Serves static dashboard and handles webhook endpoints
|
||||||
- **Node Capability Negotiation**: Optional companion-node role/capability registration
|
- **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)
|
### Execution Model (Sessions + Per-Session Queue)
|
||||||
|
|
||||||
Two concepts matter for correct clients:
|
Two concepts matter for correct clients:
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ Gateway streaming UX signals:
|
|||||||
- Canvas artifacts are persisted by the gateway so session UI surfaces can recover after daemon restarts.
|
- 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.
|
- 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.
|
- 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:
|
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.
|
- 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.
|
- 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.
|
- 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
|
## Component Map
|
||||||
|
|
||||||
|
|||||||
+27
-4
@@ -6789,7 +6789,7 @@
|
|||||||
"status": "in_progress",
|
"status": "in_progress",
|
||||||
"date": "2026-02-26",
|
"date": "2026-02-26",
|
||||||
"updated": "2026-02-27",
|
"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": [
|
"files_modified": [
|
||||||
"docs/plans/2026-02-26-personal-assistant-productization-plan.md",
|
"docs/plans/2026-02-26-personal-assistant-productization-plan.md",
|
||||||
"docs/plans/state.json"
|
"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"
|
"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": {
|
"subagents-support-phase1": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-26",
|
"date": "2026-02-26",
|
||||||
@@ -6902,7 +6925,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
"total_test_count": 2559,
|
"total_test_count": 2568,
|
||||||
"all_tests_passing": true,
|
"all_tests_passing": true,
|
||||||
"p0_completion": "3/3 (100%)",
|
"p0_completion": "3/3 (100%)",
|
||||||
"p1_completion": "4/4 (100%)",
|
"p1_completion": "4/4 (100%)",
|
||||||
@@ -6917,7 +6940,7 @@
|
|||||||
"tier2_completion": "4/4 (100%) \u2014 inbound webhooks, vector memory search, Dockerfile, heartbeat monitor",
|
"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",
|
"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",
|
"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",
|
"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",
|
"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",
|
"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_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",
|
"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)",
|
"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."
|
"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": {
|
"soul_md_and_cron_create": {
|
||||||
|
|||||||
+28
-1
@@ -13,7 +13,8 @@ import {
|
|||||||
getMinioExtractorInstallHints,
|
getMinioExtractorInstallHints,
|
||||||
renderMinioExtractorSetupLines,
|
renderMinioExtractorSetupLines,
|
||||||
} from './minioExtractors.js';
|
} 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';
|
import type { SetupConfig } from './setup/config.js';
|
||||||
|
|
||||||
export async function runSetup(configPath: string): Promise<void> {
|
export async function runSetup(configPath: string): Promise<void> {
|
||||||
@@ -30,6 +31,8 @@ export async function runSetup(configPath: string): Promise<void> {
|
|||||||
saveConfig(configPath, builder, p);
|
saveConfig(configPath, builder, p);
|
||||||
const config = builder.build();
|
const config = builder.build();
|
||||||
printOnboardingChecklist(p, config as Record<string, unknown>, configPath);
|
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 printMinioExtractorSetupStatus(p, config as Record<string, unknown>);
|
||||||
await runGoogleAuth(p, config);
|
await runGoogleAuth(p, config);
|
||||||
} else {
|
} else {
|
||||||
@@ -38,6 +41,8 @@ export async function runSetup(configPath: string): Promise<void> {
|
|||||||
saveConfig(configPath, builder, p);
|
saveConfig(configPath, builder, p);
|
||||||
const config = builder.build();
|
const config = builder.build();
|
||||||
printOnboardingChecklist(p, config as Record<string, unknown>, configPath);
|
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 printMinioExtractorSetupStatus(p, config as Record<string, unknown>);
|
||||||
await runGoogleAuth(p, config);
|
await runGoogleAuth(p, config);
|
||||||
|
|
||||||
@@ -62,6 +67,8 @@ export async function runSetup(configPath: string): Promise<void> {
|
|||||||
saveConfig(configPath, menuBuilder, p);
|
saveConfig(configPath, menuBuilder, p);
|
||||||
const config = menuBuilder.build();
|
const config = menuBuilder.build();
|
||||||
printOnboardingChecklist(p, config as Record<string, unknown>, configPath);
|
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 printMinioExtractorSetupStatus(p, config as Record<string, unknown>);
|
||||||
await runGoogleAuth(p, config);
|
await runGoogleAuth(p, config);
|
||||||
}
|
}
|
||||||
@@ -88,6 +95,26 @@ function printOnboardingChecklist(
|
|||||||
p.println(renderOnboardingChecklist(config as SetupConfig, { configPath }));
|
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(
|
async function printMinioExtractorSetupStatus(
|
||||||
p: { println(msg?: string): void },
|
p: { println(msg?: string): void },
|
||||||
config: Record<string, unknown>,
|
config: Record<string, unknown>,
|
||||||
|
|||||||
@@ -127,4 +127,34 @@ describe('ConfigBuilder', () => {
|
|||||||
namespace: 'global',
|
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
@@ -23,6 +23,9 @@ export interface SetupConfig {
|
|||||||
token?: string;
|
token?: string;
|
||||||
lock?: boolean;
|
lock?: boolean;
|
||||||
tailscale?: { serve?: boolean };
|
tailscale?: { serve?: boolean };
|
||||||
|
queue?: {
|
||||||
|
mode?: 'collect' | 'followup' | 'steer' | 'steer_backlog' | 'interrupt';
|
||||||
|
};
|
||||||
} & Record<string, unknown>;
|
} & Record<string, unknown>;
|
||||||
hooks?: Record<string, unknown>;
|
hooks?: Record<string, unknown>;
|
||||||
telegram?: { bot_token: string; allowed_chat_ids: number[] };
|
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[] };
|
slack?: { bot_token: string; app_token: string; signing_secret: string; allowed_channel_ids: string[] };
|
||||||
whatsapp?: { allowed_numbers: string[] };
|
whatsapp?: { allowed_numbers: string[] };
|
||||||
memory?: { embedding?: { enabled?: boolean; provider?: string; api_key?: string; endpoint?: 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 };
|
sandbox?: { enabled?: boolean };
|
||||||
pairing?: { enabled?: boolean };
|
pairing?: { enabled?: boolean };
|
||||||
tools?: { profile?: string };
|
tools?: { profile?: string };
|
||||||
@@ -55,7 +74,15 @@ export interface SetupConfig {
|
|||||||
gdrive?: { enabled?: boolean };
|
gdrive?: { enabled?: boolean };
|
||||||
gtasks?: { enabled?: boolean };
|
gtasks?: { enabled?: boolean };
|
||||||
heartbeat?: { 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 };
|
minio_sync?: { enabled?: boolean };
|
||||||
} & Record<string, unknown>;
|
} & Record<string, unknown>;
|
||||||
backup?: {
|
backup?: {
|
||||||
@@ -79,6 +106,11 @@ interface ResearchAgentOptions {
|
|||||||
modelTier: 'fast' | 'default' | 'complex' | 'local';
|
modelTier: 'fast' | 'default' | 'complex' | 'local';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PersonalAssistantModeOptions {
|
||||||
|
enableTalkMode?: boolean;
|
||||||
|
enableTts?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class ConfigBuilder {
|
export class ConfigBuilder {
|
||||||
private config: SetupConfig;
|
private config: SetupConfig;
|
||||||
|
|
||||||
@@ -317,6 +349,49 @@ export class ConfigBuilder {
|
|||||||
this.config.memory = memory;
|
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 {
|
build(): SetupConfig {
|
||||||
return structuredClone(this.config) as SetupConfig;
|
return structuredClone(this.config) as SetupConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ describe('first-run wizard integration', () => {
|
|||||||
'n', // confirm: Configure a fast tier? (no)
|
'n', // confirm: Configure a fast tier? (no)
|
||||||
'', // ask: Gateway port (default)
|
'', // ask: Gateway port (default)
|
||||||
'n', // confirm: Add a messaging channel? (no)
|
'n', // confirm: Add a messaging channel? (no)
|
||||||
|
'n', // confirm: Enable Personal Assistant Mode defaults? (no)
|
||||||
'n', // confirm: Configure automation now? (no)
|
'n', // confirm: Configure automation now? (no)
|
||||||
]);
|
]);
|
||||||
const p = createPrompter(rl);
|
const p = createPrompter(rl);
|
||||||
@@ -54,6 +55,7 @@ describe('first-run wizard integration', () => {
|
|||||||
'123:ABCdef', // password: Bot token
|
'123:ABCdef', // password: Bot token
|
||||||
'12345678', // ask: Allowed chat IDs
|
'12345678', // ask: Allowed chat IDs
|
||||||
'n', // confirm: Add another channel? (no)
|
'n', // confirm: Add another channel? (no)
|
||||||
|
'n', // confirm: Enable Personal Assistant Mode defaults? (no)
|
||||||
'n', // confirm: Configure automation now? (no)
|
'n', // confirm: Configure automation now? (no)
|
||||||
]);
|
]);
|
||||||
const p = createPrompter(rl);
|
const p = createPrompter(rl);
|
||||||
@@ -78,6 +80,7 @@ describe('first-run wizard integration', () => {
|
|||||||
'123:ABCdef', // password: Bot token
|
'123:ABCdef', // password: Bot token
|
||||||
'12345678', // ask: Allowed chat IDs
|
'12345678', // ask: Allowed chat IDs
|
||||||
'n', // confirm: Add another channel? (no)
|
'n', // confirm: Add another channel? (no)
|
||||||
|
'y', // confirm: Enable Personal Assistant Mode defaults? (yes)
|
||||||
'y', // confirm: Configure automation now? (yes)
|
'y', // confirm: Configure automation now? (yes)
|
||||||
'y', // confirm: Enable operator automation pack? (yes)
|
'y', // confirm: Enable operator automation pack? (yes)
|
||||||
'', // ask: Operator notifications output channel (default)
|
'', // ask: Operator notifications output channel (default)
|
||||||
@@ -101,4 +104,31 @@ describe('first-run wizard integration', () => {
|
|||||||
expect(backup.schedule).toBe('0 2 * * *');
|
expect(backup.schedule).toBe('0 2 * * *');
|
||||||
expect(heartbeat.enabled).toBe(true);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -71,7 +71,18 @@ export async function runFirstRunWizard(p: Prompter): Promise<ConfigBuilder> {
|
|||||||
p.println();
|
p.println();
|
||||||
await setupChannels(p, builder);
|
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();
|
p.println();
|
||||||
const configureAutomation = await p.confirm('Configure automation now (operator pack, cron, webhooks, Google services)?', false);
|
const configureAutomation = await p.confirm('Configure automation now (operator pack, cron, webhooks, Google services)?', false);
|
||||||
if (configureAutomation) {
|
if (configureAutomation) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
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';
|
import type { SetupConfig } from './config.js';
|
||||||
|
|
||||||
describe('renderSummary', () => {
|
describe('renderSummary', () => {
|
||||||
@@ -89,3 +89,30 @@ describe('renderOnboardingChecklist', () => {
|
|||||||
expect(output).toContain('Slack: message the bot');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -112,3 +112,44 @@ export function renderOnboardingChecklist(config: SetupConfig, opts?: { configPa
|
|||||||
lines.push(' 5. Run `flynn doctor` if any channel test fails');
|
lines.push(' 5. Run `flynn doctor` if any channel test fails');
|
||||||
return lines.join('\n');
|
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');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user