feat: add quickstart onboarding defaults
This commit is contained in:
@@ -3,9 +3,38 @@ import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
import type { AuthChoice } from "./onboard-types.js";
|
||||
|
||||
export type AuthChoiceOption = { value: AuthChoice; label: string };
|
||||
export type AuthChoiceOption = {
|
||||
value: AuthChoice;
|
||||
label: string;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
function formatOAuthHint(expires?: number): string {
|
||||
const rich = isRich();
|
||||
if (!expires) {
|
||||
return colorize(rich, theme.muted, "token unavailable");
|
||||
}
|
||||
const now = Date.now();
|
||||
const remaining = expires - now;
|
||||
if (remaining <= 0) {
|
||||
return colorize(rich, theme.error, "token expired");
|
||||
}
|
||||
const minutes = Math.round(remaining / (60 * 1000));
|
||||
const duration =
|
||||
minutes >= 120
|
||||
? `${Math.round(minutes / 60)}h`
|
||||
: minutes >= 60
|
||||
? "1h"
|
||||
: `${Math.max(minutes, 1)}m`;
|
||||
const label = `token ok · expires in ${duration}`;
|
||||
if (minutes <= 10) {
|
||||
return colorize(rich, theme.warn, label);
|
||||
}
|
||||
return colorize(rich, theme.success, label);
|
||||
}
|
||||
|
||||
export function buildAuthChoiceOptions(params: {
|
||||
store: AuthProfileStore;
|
||||
@@ -13,24 +42,26 @@ export function buildAuthChoiceOptions(params: {
|
||||
}): AuthChoiceOption[] {
|
||||
const options: AuthChoiceOption[] = [];
|
||||
|
||||
const claudeCli = params.store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
if (claudeCli?.type === "oauth") {
|
||||
options.push({
|
||||
value: "claude-cli",
|
||||
label: "Anthropic OAuth (Claude CLI)",
|
||||
});
|
||||
}
|
||||
|
||||
options.push({ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" });
|
||||
|
||||
const codexCli = params.store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
if (codexCli?.type === "oauth") {
|
||||
options.push({
|
||||
value: "codex-cli",
|
||||
label: "OpenAI Codex OAuth (Codex CLI)",
|
||||
hint: formatOAuthHint(codexCli.expires),
|
||||
});
|
||||
}
|
||||
|
||||
const claudeCli = params.store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
if (claudeCli?.type === "oauth") {
|
||||
options.push({
|
||||
value: "claude-cli",
|
||||
label: "Anthropic OAuth (Claude CLI)",
|
||||
hint: formatOAuthHint(claudeCli.expires),
|
||||
});
|
||||
}
|
||||
|
||||
options.push({ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" });
|
||||
|
||||
options.push({
|
||||
value: "openai-codex",
|
||||
label: "OpenAI Codex (ChatGPT OAuth)",
|
||||
|
||||
@@ -11,12 +11,18 @@ import {
|
||||
} from "../config/config.js";
|
||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
||||
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { resolveUserPath, sleep } from "../utils.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
type GatewayDaemonRuntime,
|
||||
} from "./daemon-runtime.js";
|
||||
import { maybeRepairAnthropicOAuthProfileId } from "./doctor-auth.js";
|
||||
import {
|
||||
buildGatewayRuntimeHints,
|
||||
@@ -56,13 +62,7 @@ import {
|
||||
DEFAULT_WORKSPACE,
|
||||
printWizardHeader,
|
||||
} from "./onboard-helpers.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
type GatewayDaemonRuntime,
|
||||
} from "./daemon-runtime.js";
|
||||
|
||||
function resolveMode(cfg: ClawdbotConfig): "local" | "remote" {
|
||||
return cfg.gateway?.mode === "remote" ? "remote" : "local";
|
||||
|
||||
@@ -470,10 +470,69 @@ async function maybeConfigureDmPolicies(params: {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
async function promptTelegramAllowFrom(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId: string;
|
||||
}): Promise<ClawdbotConfig> {
|
||||
const { cfg, prompter, accountId } = params;
|
||||
const resolved = resolveTelegramAccount({ cfg, accountId });
|
||||
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
||||
const entry = await prompter.text({
|
||||
message: "Telegram allowFrom (user id)",
|
||||
placeholder: "123456789",
|
||||
initialValue: existingAllowFrom[0]
|
||||
? String(existingAllowFrom[0])
|
||||
: undefined,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "Required";
|
||||
if (!/^\d+$/.test(raw)) return "Use a numeric Telegram user id";
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
const normalized = String(entry).trim();
|
||||
const merged = [
|
||||
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
||||
normalized,
|
||||
];
|
||||
const unique = [...new Set(merged)];
|
||||
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
telegram: {
|
||||
...cfg.telegram,
|
||||
enabled: true,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
telegram: {
|
||||
...cfg.telegram,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...cfg.telegram?.accounts,
|
||||
[accountId]: {
|
||||
...cfg.telegram?.accounts?.[accountId],
|
||||
enabled: cfg.telegram?.accounts?.[accountId]?.enabled ?? true,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function promptWhatsAppAllowFrom(
|
||||
cfg: ClawdbotConfig,
|
||||
_runtime: RuntimeEnv,
|
||||
prompter: WizardPrompter,
|
||||
options?: { forceAllowlist?: boolean },
|
||||
): Promise<ClawdbotConfig> {
|
||||
const existingPolicy = cfg.whatsapp?.dmPolicy ?? "pairing";
|
||||
const existingAllowFrom = cfg.whatsapp?.allowFrom ?? [];
|
||||
@@ -481,6 +540,47 @@ async function promptWhatsAppAllowFrom(
|
||||
existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset";
|
||||
const existingResponsePrefix = cfg.messages?.responsePrefix;
|
||||
|
||||
if (options?.forceAllowlist) {
|
||||
const entry = await prompter.text({
|
||||
message: "Your WhatsApp number (E.164)",
|
||||
placeholder: "+15555550123",
|
||||
initialValue: existingAllowFrom[0],
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "Required";
|
||||
const normalized = normalizeE164(raw);
|
||||
if (!normalized) return `Invalid number: ${raw}`;
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
const normalized = normalizeE164(String(entry).trim());
|
||||
const merged = [
|
||||
...existingAllowFrom
|
||||
.filter((item) => item !== "*")
|
||||
.map((item) => normalizeE164(item))
|
||||
.filter(Boolean),
|
||||
normalized,
|
||||
];
|
||||
const unique = [...new Set(merged.filter(Boolean))];
|
||||
let next = setWhatsAppSelfChatMode(cfg, true);
|
||||
next = setWhatsAppDmPolicy(next, "allowlist");
|
||||
next = setWhatsAppAllowFrom(next, unique);
|
||||
if (existingResponsePrefix === undefined) {
|
||||
next = setMessagesResponsePrefix(next, "[clawdbot]");
|
||||
}
|
||||
await prompter.note(
|
||||
[
|
||||
"Allowlist mode enabled.",
|
||||
`- allowFrom includes ${normalized}`,
|
||||
existingResponsePrefix === undefined
|
||||
? "- responsePrefix set to [clawdbot]"
|
||||
: "- responsePrefix left unchanged",
|
||||
].join("\n"),
|
||||
"WhatsApp allowlist",
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
await prompter.note(
|
||||
[
|
||||
"WhatsApp direct chats are gated by `whatsapp.dmPolicy` + `whatsapp.allowFrom`.",
|
||||
@@ -562,7 +662,7 @@ async function promptWhatsAppAllowFrom(
|
||||
}
|
||||
if (policy === "disabled") return next;
|
||||
|
||||
const options =
|
||||
const allowOptions =
|
||||
existingAllowFrom.length > 0
|
||||
? ([
|
||||
{ value: "keep", label: "Keep current allowFrom" },
|
||||
@@ -579,8 +679,11 @@ async function promptWhatsAppAllowFrom(
|
||||
|
||||
const mode = (await prompter.select({
|
||||
message: "WhatsApp allowFrom (optional pre-allowlist)",
|
||||
options: options.map((opt) => ({ value: opt.value, label: opt.label })),
|
||||
})) as (typeof options)[number]["value"];
|
||||
options: allowOptions.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
})),
|
||||
})) as (typeof allowOptions)[number]["value"];
|
||||
|
||||
if (mode === "keep") {
|
||||
// Keep allowFrom as-is.
|
||||
@@ -631,6 +734,8 @@ type SetupProvidersOptions = {
|
||||
whatsappAccountId?: string;
|
||||
promptWhatsAppAccountId?: boolean;
|
||||
onWhatsAppAccountId?: (accountId: string) => void;
|
||||
forceAllowFromProviders?: ProviderChoice[];
|
||||
skipDmPolicyPrompt?: boolean;
|
||||
};
|
||||
|
||||
export async function setupProviders(
|
||||
@@ -639,6 +744,12 @@ export async function setupProviders(
|
||||
prompter: WizardPrompter,
|
||||
options?: SetupProvidersOptions,
|
||||
): Promise<ClawdbotConfig> {
|
||||
const forceAllowFromProviders = new Set(
|
||||
options?.forceAllowFromProviders ?? [],
|
||||
);
|
||||
const forceTelegramAllowFrom = forceAllowFromProviders.has("telegram");
|
||||
const forceWhatsAppAllowFrom = forceAllowFromProviders.has("whatsapp");
|
||||
|
||||
let whatsappAccountId =
|
||||
options?.whatsappAccountId?.trim() || resolveDefaultWhatsAppAccountId(cfg);
|
||||
let whatsappLinked = await detectWhatsAppLinked(cfg, whatsappAccountId);
|
||||
@@ -854,7 +965,9 @@ export async function setupProviders(
|
||||
);
|
||||
}
|
||||
|
||||
next = await promptWhatsAppAllowFrom(next, runtime, prompter);
|
||||
next = await promptWhatsAppAllowFrom(next, runtime, prompter, {
|
||||
forceAllowlist: forceWhatsAppAllowFrom,
|
||||
});
|
||||
}
|
||||
|
||||
if (selection.includes("telegram")) {
|
||||
@@ -962,6 +1075,14 @@ export async function setupProviders(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (forceTelegramAllowFrom) {
|
||||
next = await promptTelegramAllowFrom({
|
||||
cfg: next,
|
||||
prompter,
|
||||
accountId: telegramAccountId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (selection.includes("discord")) {
|
||||
@@ -1414,7 +1535,9 @@ export async function setupProviders(
|
||||
);
|
||||
}
|
||||
|
||||
next = await maybeConfigureDmPolicies({ cfg: next, selection, prompter });
|
||||
if (!options?.skipDmPolicyPrompt) {
|
||||
next = await maybeConfigureDmPolicies({ cfg: next, selection, prompter });
|
||||
}
|
||||
|
||||
if (options?.allowDisable) {
|
||||
if (!selection.includes("telegram") && telegramConfigured) {
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import { withProgress } from "../../cli/progress.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { listChatProviders } from "../../providers/registry.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import {
|
||||
type ChatProvider,
|
||||
formatProviderAccountLabel,
|
||||
requireValidConfig,
|
||||
} from "./shared.js";
|
||||
type ClawdbotConfig,
|
||||
readConfigFileSnapshot,
|
||||
} from "../../config/config.js";
|
||||
import {
|
||||
listDiscordAccountIds,
|
||||
resolveDiscordAccount,
|
||||
} from "../../discord/accounts.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import {
|
||||
listIMessageAccountIds,
|
||||
resolveIMessageAccount,
|
||||
} from "../../imessage/accounts.js";
|
||||
import { formatAge } from "../../infra/provider-summary.js";
|
||||
import { listChatProviders } from "../../providers/registry.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
resolveSignalAccount,
|
||||
} from "../../signal/accounts.js";
|
||||
import { listSlackAccountIds, resolveSlackAccount } from "../../slack/accounts.js";
|
||||
import {
|
||||
listSlackAccountIds,
|
||||
resolveSlackAccount,
|
||||
} from "../../slack/accounts.js";
|
||||
import {
|
||||
listTelegramAccountIds,
|
||||
resolveTelegramAccount,
|
||||
} from "../../telegram/accounts.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
import {
|
||||
listWhatsAppAccountIds,
|
||||
resolveWhatsAppAccount,
|
||||
@@ -35,9 +39,11 @@ import {
|
||||
readWebSelfId,
|
||||
webAuthExists,
|
||||
} from "../../web/session.js";
|
||||
import { formatAge } from "../../infra/provider-summary.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
import { readConfigFileSnapshot, type ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
type ChatProvider,
|
||||
formatProviderAccountLabel,
|
||||
requireValidConfig,
|
||||
} from "./shared.js";
|
||||
|
||||
export type ProvidersStatusOptions = {
|
||||
json?: boolean;
|
||||
@@ -80,10 +86,16 @@ export function formatGatewayProvidersStatusLines(
|
||||
if (typeof account.tokenSource === "string" && account.tokenSource) {
|
||||
bits.push(`token:${account.tokenSource}`);
|
||||
}
|
||||
if (typeof account.botTokenSource === "string" && account.botTokenSource) {
|
||||
if (
|
||||
typeof account.botTokenSource === "string" &&
|
||||
account.botTokenSource
|
||||
) {
|
||||
bits.push(`bot:${account.botTokenSource}`);
|
||||
}
|
||||
if (typeof account.appTokenSource === "string" && account.appTokenSource) {
|
||||
if (
|
||||
typeof account.appTokenSource === "string" &&
|
||||
account.appTokenSource
|
||||
) {
|
||||
bits.push(`app:${account.appTokenSource}`);
|
||||
}
|
||||
if (typeof account.baseUrl === "string" && account.baseUrl) {
|
||||
@@ -176,10 +188,16 @@ async function formatConfigProvidersStatusLines(
|
||||
if (typeof account.tokenSource === "string" && account.tokenSource) {
|
||||
bits.push(`token:${account.tokenSource}`);
|
||||
}
|
||||
if (typeof account.botTokenSource === "string" && account.botTokenSource) {
|
||||
if (
|
||||
typeof account.botTokenSource === "string" &&
|
||||
account.botTokenSource
|
||||
) {
|
||||
bits.push(`bot:${account.botTokenSource}`);
|
||||
}
|
||||
if (typeof account.appTokenSource === "string" && account.appTokenSource) {
|
||||
if (
|
||||
typeof account.appTokenSource === "string" &&
|
||||
account.appTokenSource
|
||||
) {
|
||||
bits.push(`app:${account.appTokenSource}`);
|
||||
}
|
||||
if (typeof account.baseUrl === "string" && account.baseUrl) {
|
||||
@@ -242,7 +260,8 @@ async function formatConfigProvidersStatusLines(
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured:
|
||||
Boolean(account.botToken?.trim()) && Boolean(account.appToken?.trim()),
|
||||
Boolean(account.botToken?.trim()) &&
|
||||
Boolean(account.appToken?.trim()),
|
||||
botTokenSource: account.botTokenSource,
|
||||
appTokenSource: account.appTokenSource,
|
||||
};
|
||||
@@ -259,11 +278,18 @@ async function formatConfigProvidersStatusLines(
|
||||
}),
|
||||
imessage: listIMessageAccountIds(cfg).map((accountId) => {
|
||||
const account = resolveIMessageAccount({ cfg, accountId });
|
||||
const imsgConfigured = Boolean(
|
||||
account.config.cliPath ||
|
||||
account.config.dbPath ||
|
||||
account.config.allowFrom ||
|
||||
account.config.service ||
|
||||
account.config.region,
|
||||
);
|
||||
return {
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
configured: imsgConfigured,
|
||||
};
|
||||
}),
|
||||
} satisfies Partial<Record<ChatProvider, Array<Record<string, unknown>>>>;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
} from "../config/sessions.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
import { info } from "../globals.js";
|
||||
import { buildProviderSummary } from "../infra/provider-summary.js";
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
loadProviderUsageSummary,
|
||||
} from "../infra/provider-usage.js";
|
||||
import { peekSystemEvents } from "../infra/system-events.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { resolveWhatsAppAccount } from "../web/accounts.js";
|
||||
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
||||
@@ -29,8 +29,8 @@ import {
|
||||
logWebSelfId,
|
||||
webAuthExists,
|
||||
} from "../web/session.js";
|
||||
import { resolveControlUiLinks } from "./onboard-helpers.js";
|
||||
import type { HealthSummary } from "./health.js";
|
||||
import { resolveControlUiLinks } from "./onboard-helpers.js";
|
||||
|
||||
export type SessionStatus = {
|
||||
key: string;
|
||||
|
||||
Reference in New Issue
Block a user