openclaw/src/crestodian/crestodian.ts
Peter Steinberger e2a112a556
feat(onboard): guided CLI onboarding with live AI verification and classic fallback (#101880)
* feat(onboard): guided CLI onboarding with live AI verification and classic fallback

Interactive `openclaw onboard` (and bare `openclaw` on a fresh install) now
runs a guided flow with macOS-app parity: detect existing AI access, live-test
candidates with a real completion before persisting anything, walk down the
ladder on failure with mapped reasons, and offer verified manual API-key entry
from installed provider manifests (masked input). In-flow escapes: classic
wizard, Crestodian chat, skip-AI. Classic wizard gains an optional post-auth
live verification step. `--classic`, `--modern`, and `--non-interactive`
contracts unchanged. Docs corrected for post-#99935 routing.

Closes #101851

* improve(onboard): quiet probe diagnostics in wizard TTY, carry risk ack into classic escape

Candidate live-tests during guided setup are probes: rename their run id and
lane to the existing probe conventions (logging/subsystem.ts console
suppression, command-queue quiet probe lanes) so expected failures stop
leaking raw diagnostics into the Clack UI; file diagnostics unchanged. The
classic-wizard escape now passes the already-collected risk acknowledgement
through instead of re-prompting in the same session.

* fix(onboard): quiet the session-derived setup-inference probe lane too

The live-test run enqueues on two lanes: the explicit probe lane and one
derived from its temp session key. Extend the shared quiet-probe predicate to
cover the derived lane so a failing candidate cannot leak lane-task
diagnostics into the wizard TTY.

* improve(onboard): suppress subsystem console output during wizard live tests

Provider-transport subsystem loggers (model-fetch start/response, transport
errors) carry no run id, so probe suppression cannot catch them and a failing
candidate printed raw log lines into the Clack TTY. Reuse the TUI console
subsystem-filter seam via a finally-safe scoped helper around guided
activation and the classic live-verify; file logging is unchanged and the
gateway (macOS app) surface is unaffected.

* fix(onboard): never auto-replace a configured model when its live check fails

The re-run verification probe executes outside the configured workspace (setup
never runs workspace plugins), so a workspace-backed current model can fail
the check while working fine in the agent. Stop the auto ladder on an
existing-model failure and hand the decision to the manual stage instead of
silently persisting a different candidate as the default. Docs note the
fail-safe and the workspace caveat.

* feat(onboard): two-way switching between Crestodian chat and the menu wizards

From the chat, `open setup wizard`, `open classic wizard`, and `open channel
wizard for <channel>` hand off to the guided flow, the classic wizard, or the
masked `channels add` wizard after the chat TUI tears down (mirrors the
open-tui handoff; gateway surface gets a text pointer instead). The hosted
channel wizard no longer dead-ends at sensitive steps — it offers the switch
and remembers the channel. New read-only `channel info <channel>` operation
and ring-zero action surface label, blurb, configured state, and the real
docs URL from channel-setup discovery so the assistant can explain Slack or
Telegram prerequisites instead of guessing; both prompts instruct it to use
them. `channels add --channel <id>` now preselects the channel. Docs cover
the interchangeable flows.

* fix(onboard): avoid param reassignment in open-setup handoff

* improve(onboard): separate ask-about vs connect intent in channel prompt guidance

Live test showed the agent detouring an explicit connect request through
channel_info because the guidance said to consult it first. Both prompts now
distinguish asking about a channel (channel info + docs link) from asking to
connect (connect right away).

* fix(channels): mark channel token entry as sensitive input

The shared single-token prompt lacked sensitive:true, so terminal wizards
echoed pasted channel tokens and the Crestodian chat bridge (which refuses
plain-text secrets based on this flag) hosted the Telegram token step in
visible chat. Found live-testing the chat-to-wizard switch; pre-existing on
main but load-bearing for the masked-wizard contract this PR documents.

* fix(onboard): restore terminal state around the guided flow's TUI launch

Mirror the classic finalize handoff so the chat TUI never inherits the wizard
prompter's raw/paused terminal state on the default first-run path.

* fix(channels): type the token prompter mock for the sensitive-flag assertion

* fix(gateway): map the TUI-only open-setup action to none for app clients

Engine-side surface gating already prevents open-setup replies on the gateway
surface; this keeps the client-visible action enum stable even if that gate
ever regresses. (Reviewed with the switching round; missed in its commit.)

* docs: regenerate docs map for onboarding page changes
2026-07-09 12:40:55 +01:00

120 lines
4.1 KiB
TypeScript

// Crestodian CLI runner selects JSON, one-shot, or interactive setup-helper mode.
import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
import { withProgress } from "../cli/progress.js";
import { defaultRuntime, writeRuntimeJson, type RuntimeEnv } from "../runtime.js";
import type { CrestodianAssistantPlanner } from "./assistant.js";
import { resolveCrestodianOperation } from "./dialogue.js";
import {
executeCrestodianOperation,
isPersistentCrestodianOperation,
type CrestodianCommandDeps,
} from "./operations.js";
import {
formatCrestodianOverview,
loadCrestodianOverview,
type CrestodianOverview,
} from "./overview.js";
/**
* CLI entry point for Crestodian.
*
* This module chooses JSON, one-shot, or interactive TUI mode and delegates all
* command parsing/execution to dialogue and operation modules.
*/
type CrestodianInteractiveRunner = (
opts: RunCrestodianOptions,
runtime: RuntimeEnv,
) => Promise<void>;
/** Options accepted by the Crestodian command runner. */
export type RunCrestodianOptions = {
message?: string;
yes?: boolean;
json?: boolean;
interactive?: boolean;
/** "onboarding" swaps the greeting for the first-run setup proposal. */
welcomeVariant?: "onboarding";
/** Workspace override for the proposed first-run setup (from --workspace). */
setupWorkspace?: string;
/** Risk acknowledgement already collected by the calling onboarding flow. */
setupAcceptRisk?: boolean;
onReady?: () => void;
deps?: CrestodianCommandDeps;
formatOverview?: (overview: CrestodianOverview) => string;
loadOverview?: typeof loadCrestodianOverview;
planWithAssistant?: CrestodianAssistantPlanner;
input?: NodeJS.ReadableStream;
output?: NodeJS.WritableStream;
runInteractiveTui?: CrestodianInteractiveRunner;
};
function crestodianCommandDepsFromOptions(
opts: RunCrestodianOptions,
): CrestodianCommandDeps | undefined {
if (!opts.deps && !opts.formatOverview && !opts.loadOverview) {
return undefined;
}
return {
...opts.deps,
...(opts.formatOverview ? { formatOverview: opts.formatOverview } : {}),
...(opts.loadOverview ? { loadOverview: opts.loadOverview } : {}),
};
}
async function runOneShot(
input: string,
runtime: RuntimeEnv,
opts: RunCrestodianOptions,
): Promise<void> {
const operation = await resolveCrestodianOperation(input, runtime, opts);
await executeCrestodianOperation(operation, runtime, {
approved: opts.yes === true || !isPersistentCrestodianOperation(operation),
deps: crestodianCommandDepsFromOptions(opts),
});
}
/** Run Crestodian in JSON, one-shot message, or interactive TUI mode. */
export async function runCrestodian(
opts: RunCrestodianOptions = {},
runtime: RuntimeEnv = defaultRuntime,
): Promise<void> {
if (opts.json) {
const overview = await (opts.loadOverview ?? loadCrestodianOverview)();
writeRuntimeJson(runtime, overview);
return;
}
if (opts.message?.trim()) {
// One-shot mode always shows the overview first so planned changes have local context.
const overview = await withProgress(
{
label: "Loading Crestodian overview…",
indeterminate: true,
delayMs: 0,
fallback: "none",
},
async () => await (opts.loadOverview ?? loadCrestodianOverview)(),
);
runtime.log((opts.formatOverview ?? formatCrestodianOverview)(overview));
runtime.log("");
await runOneShot(opts.message, runtime, opts);
return;
}
const interactive = opts.interactive ?? true;
const input = opts.input ?? defaultStdin;
const output = opts.output ?? defaultStdout;
const inputIsTty = (input as { isTTY?: boolean }).isTTY === true;
const outputIsTty = (output as { isTTY?: boolean }).isTTY === true;
if (!interactive || !inputIsTty || !outputIsTty) {
// Without a TTY, Crestodian cannot safely ask for confirmation; require --message instead.
runtime.error("Crestodian needs an interactive TTY. Use --message for one command.");
runtime.exit(1);
return;
}
const runInteractiveTui =
opts.runInteractiveTui ?? (await import("./tui-backend.js")).runCrestodianTui;
opts.onReady?.();
await runInteractiveTui(opts, runtime);
}