mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 19:18:26 +00:00
refactor!: rename chat providers to channels
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_CHAT_CHANNEL } from "../channels/registry.js";
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
@@ -7,14 +8,13 @@ import {
|
||||
resolveStorePath,
|
||||
} from "../config/sessions.js";
|
||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||
import { DEFAULT_CHAT_PROVIDER } from "../providers/registry.js";
|
||||
import { normalizeMainKey } from "../routing/session-key.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
normalizeMessageProvider,
|
||||
} from "../utils/message-provider.js";
|
||||
normalizeMessageChannel,
|
||||
} from "../utils/message-channel.js";
|
||||
import { agentCommand } from "./agent.js";
|
||||
|
||||
type AgentGatewayResult = {
|
||||
@@ -42,7 +42,7 @@ export type AgentCliOpts = {
|
||||
json?: boolean;
|
||||
timeout?: string;
|
||||
deliver?: boolean;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
bestEffortDeliver?: boolean;
|
||||
lane?: string;
|
||||
runId?: string;
|
||||
@@ -129,8 +129,7 @@ export async function agentViaGatewayCommand(
|
||||
sessionId: opts.sessionId,
|
||||
});
|
||||
|
||||
const provider =
|
||||
normalizeMessageProvider(opts.provider) ?? DEFAULT_CHAT_PROVIDER;
|
||||
const channel = normalizeMessageChannel(opts.channel) ?? DEFAULT_CHAT_CHANNEL;
|
||||
const idempotencyKey = opts.runId?.trim() || randomIdempotencyKey();
|
||||
|
||||
const response = await withProgress(
|
||||
@@ -149,7 +148,7 @@ export async function agentViaGatewayCommand(
|
||||
sessionKey,
|
||||
thinking: opts.thinking,
|
||||
deliver: Boolean(opts.deliver),
|
||||
provider,
|
||||
channel,
|
||||
timeout: timeoutSeconds,
|
||||
lane: opts.lane,
|
||||
extraSystemPrompt: opts.extraSystemPrompt,
|
||||
|
||||
@@ -287,7 +287,7 @@ describe("agentCommand", () => {
|
||||
message: "hi",
|
||||
to: "123",
|
||||
deliver: true,
|
||||
provider: "telegram",
|
||||
channel: "telegram",
|
||||
},
|
||||
runtime,
|
||||
deps,
|
||||
|
||||
@@ -38,6 +38,12 @@ import {
|
||||
type ThinkLevel,
|
||||
type VerboseLevel,
|
||||
} from "../auto-reply/thinking.js";
|
||||
import {
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../channels/plugins/index.js";
|
||||
import type { ChannelOutboundTargetMode } from "../channels/plugins/types.js";
|
||||
import { DEFAULT_CHAT_CHANNEL } from "../channels/registry.js";
|
||||
import {
|
||||
type CliDeps,
|
||||
createDefaultDeps,
|
||||
@@ -67,21 +73,15 @@ import {
|
||||
normalizeOutboundPayloadsForJson,
|
||||
} from "../infra/outbound/payloads.js";
|
||||
import { resolveOutboundTarget } from "../infra/outbound/targets.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
normalizeProviderId,
|
||||
} from "../providers/plugins/index.js";
|
||||
import type { ProviderOutboundTargetMode } from "../providers/plugins/types.js";
|
||||
import { DEFAULT_CHAT_PROVIDER } from "../providers/registry.js";
|
||||
import { normalizeMainKey } from "../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { applyVerboseOverride } from "../sessions/level-overrides.js";
|
||||
import { resolveSendPolicy } from "../sessions/send-policy.js";
|
||||
import {
|
||||
isInternalMessageProvider,
|
||||
resolveGatewayMessageProvider,
|
||||
resolveMessageProvider,
|
||||
} from "../utils/message-provider.js";
|
||||
isInternalMessageChannel,
|
||||
resolveGatewayMessageChannel,
|
||||
resolveMessageChannel,
|
||||
} from "../utils/message-channel.js";
|
||||
|
||||
/** Image content block for Claude API multimodal messages. */
|
||||
type ImageContent = {
|
||||
@@ -103,10 +103,10 @@ type AgentCommandOpts = {
|
||||
json?: boolean;
|
||||
timeout?: string;
|
||||
deliver?: boolean;
|
||||
/** Message provider context (webchat|voicewake|whatsapp|...). */
|
||||
messageProvider?: string;
|
||||
provider?: string; // delivery provider (whatsapp|telegram|...)
|
||||
deliveryTargetMode?: ProviderOutboundTargetMode;
|
||||
/** Message channel context (webchat|voicewake|whatsapp|...). */
|
||||
messageChannel?: string;
|
||||
channel?: string; // delivery channel (whatsapp|telegram|...)
|
||||
deliveryTargetMode?: ChannelOutboundTargetMode;
|
||||
bestEffortDeliver?: boolean;
|
||||
abortSignal?: AbortSignal;
|
||||
lane?: string;
|
||||
@@ -287,7 +287,7 @@ export async function agentCommand(
|
||||
cfg,
|
||||
entry: sessionEntry,
|
||||
sessionKey,
|
||||
provider: sessionEntry?.provider,
|
||||
channel: sessionEntry?.channel,
|
||||
chatType: sessionEntry?.chatType,
|
||||
});
|
||||
if (sendPolicy === "deny") {
|
||||
@@ -490,9 +490,9 @@ export async function agentCommand(
|
||||
let fallbackProvider = provider;
|
||||
let fallbackModel = model;
|
||||
try {
|
||||
const messageProvider = resolveMessageProvider(
|
||||
opts.messageProvider,
|
||||
opts.provider,
|
||||
const messageChannel = resolveMessageChannel(
|
||||
opts.messageChannel,
|
||||
opts.channel,
|
||||
);
|
||||
const fallbackResult = await runWithModelFallback({
|
||||
cfg,
|
||||
@@ -525,7 +525,7 @@ export async function agentCommand(
|
||||
return runEmbeddedPiAgent({
|
||||
sessionId,
|
||||
sessionKey,
|
||||
messageProvider,
|
||||
messageChannel,
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
@@ -636,30 +636,28 @@ export async function agentCommand(
|
||||
const payloads = result.payloads ?? [];
|
||||
const deliver = opts.deliver === true;
|
||||
const bestEffortDeliver = opts.bestEffortDeliver === true;
|
||||
const deliveryProvider =
|
||||
resolveGatewayMessageProvider(opts.provider) ?? DEFAULT_CHAT_PROVIDER;
|
||||
// Provider docking: delivery providers are resolved via plugin registry.
|
||||
const deliveryPlugin = !isInternalMessageProvider(deliveryProvider)
|
||||
? getProviderPlugin(
|
||||
normalizeProviderId(deliveryProvider) ?? deliveryProvider,
|
||||
)
|
||||
const deliveryChannel =
|
||||
resolveGatewayMessageChannel(opts.channel) ?? DEFAULT_CHAT_CHANNEL;
|
||||
// Channel docking: delivery channels are resolved via plugin registry.
|
||||
const deliveryPlugin = !isInternalMessageChannel(deliveryChannel)
|
||||
? getChannelPlugin(normalizeChannelId(deliveryChannel) ?? deliveryChannel)
|
||||
: undefined;
|
||||
|
||||
const logDeliveryError = (err: unknown) => {
|
||||
const message = `Delivery failed (${deliveryProvider}${deliveryTarget ? ` to ${deliveryTarget}` : ""}): ${String(err)}`;
|
||||
const message = `Delivery failed (${deliveryChannel}${deliveryTarget ? ` to ${deliveryTarget}` : ""}): ${String(err)}`;
|
||||
runtime.error?.(message);
|
||||
if (!runtime.error) runtime.log(message);
|
||||
};
|
||||
|
||||
const isDeliveryProviderKnown =
|
||||
isInternalMessageProvider(deliveryProvider) || Boolean(deliveryPlugin);
|
||||
const isDeliveryChannelKnown =
|
||||
isInternalMessageChannel(deliveryChannel) || Boolean(deliveryPlugin);
|
||||
|
||||
const targetMode: ProviderOutboundTargetMode =
|
||||
const targetMode: ChannelOutboundTargetMode =
|
||||
opts.deliveryTargetMode ?? (opts.to ? "explicit" : "implicit");
|
||||
const resolvedTarget =
|
||||
deliver && isDeliveryProviderKnown && deliveryProvider
|
||||
deliver && isDeliveryChannelKnown && deliveryChannel
|
||||
? resolveOutboundTarget({
|
||||
provider: deliveryProvider,
|
||||
channel: deliveryChannel,
|
||||
to: opts.to,
|
||||
cfg,
|
||||
accountId:
|
||||
@@ -670,8 +668,8 @@ export async function agentCommand(
|
||||
const deliveryTarget = resolvedTarget?.ok ? resolvedTarget.to : undefined;
|
||||
|
||||
if (deliver) {
|
||||
if (!isDeliveryProviderKnown) {
|
||||
const err = new Error(`Unknown provider: ${deliveryProvider}`);
|
||||
if (!isDeliveryChannelKnown) {
|
||||
const err = new Error(`Unknown channel: ${deliveryChannel}`);
|
||||
if (!bestEffortDeliver) throw err;
|
||||
logDeliveryError(err);
|
||||
} else if (resolvedTarget && !resolvedTarget.ok) {
|
||||
@@ -715,13 +713,13 @@ export async function agentCommand(
|
||||
}
|
||||
if (
|
||||
deliver &&
|
||||
deliveryProvider &&
|
||||
!isInternalMessageProvider(deliveryProvider)
|
||||
deliveryChannel &&
|
||||
!isInternalMessageChannel(deliveryChannel)
|
||||
) {
|
||||
if (deliveryTarget) {
|
||||
await deliverOutboundPayloads({
|
||||
cfg,
|
||||
provider: deliveryProvider,
|
||||
channel: deliveryChannel,
|
||||
to: deliveryTarget,
|
||||
payloads: deliveryPayloads,
|
||||
bestEffort: bestEffortDeliver,
|
||||
|
||||
@@ -34,9 +34,9 @@ describe("agents helpers", () => {
|
||||
bindings: [
|
||||
{
|
||||
agentId: "work",
|
||||
match: { provider: "whatsapp", accountId: "biz" },
|
||||
match: { channel: "whatsapp", accountId: "biz" },
|
||||
},
|
||||
{ agentId: "main", match: { provider: "telegram" } },
|
||||
{ agentId: "main", match: { channel: "telegram" } },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("agents helpers", () => {
|
||||
bindings: [
|
||||
{
|
||||
agentId: "main",
|
||||
match: { provider: "whatsapp", accountId: "default" },
|
||||
match: { channel: "whatsapp", accountId: "default" },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -94,15 +94,15 @@ describe("agents helpers", () => {
|
||||
const result = applyAgentBindings(cfg, [
|
||||
{
|
||||
agentId: "main",
|
||||
match: { provider: "whatsapp", accountId: "default" },
|
||||
match: { channel: "whatsapp", accountId: "default" },
|
||||
},
|
||||
{
|
||||
agentId: "work",
|
||||
match: { provider: "whatsapp", accountId: "default" },
|
||||
match: { channel: "whatsapp", accountId: "default" },
|
||||
},
|
||||
{
|
||||
agentId: "work",
|
||||
match: { provider: "telegram" },
|
||||
match: { channel: "telegram" },
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -121,8 +121,8 @@ describe("agents helpers", () => {
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "work", match: { provider: "whatsapp" } },
|
||||
{ agentId: "home", match: { provider: "telegram" } },
|
||||
{ agentId: "work", match: { channel: "whatsapp" } },
|
||||
{ agentId: "home", match: { channel: "telegram" } },
|
||||
],
|
||||
tools: {
|
||||
agentToAgent: { enabled: true, allow: ["work", "home"] },
|
||||
|
||||
@@ -7,6 +7,16 @@ import {
|
||||
} from "../agents/agent-scope.js";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { DEFAULT_IDENTITY_FILENAME } from "../agents/workspace.js";
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import {
|
||||
getChannelPlugin,
|
||||
listChannelPlugins,
|
||||
} from "../channels/plugins/index.js";
|
||||
import {
|
||||
type ChatChannelId,
|
||||
getChatChannelMeta,
|
||||
normalizeChatChannelId,
|
||||
} from "../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
CONFIG_PATH_CLAWDBOT,
|
||||
@@ -14,16 +24,7 @@ import {
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js";
|
||||
import { resolveProviderDefaultAccountId } from "../providers/plugins/helpers.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
listProviderPlugins,
|
||||
} from "../providers/plugins/index.js";
|
||||
import {
|
||||
type ChatProviderId,
|
||||
getChatProviderMeta,
|
||||
normalizeChatProviderId,
|
||||
} from "../providers/registry.js";
|
||||
import type { AgentBinding } from "../config/types.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
DEFAULT_AGENT_ID,
|
||||
@@ -36,9 +37,9 @@ import { createClackPrompter } from "../wizard/clack-prompter.js";
|
||||
import { WizardCancelledError } from "../wizard/prompts.js";
|
||||
import { applyAuthChoice, warnIfModelConfigLooksOff } from "./auth-choice.js";
|
||||
import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
|
||||
import { setupChannels } from "./onboard-channels.js";
|
||||
import { ensureWorkspaceAndSessions, moveToTrash } from "./onboard-helpers.js";
|
||||
import { setupProviders } from "./onboard-providers.js";
|
||||
import type { ProviderChoice } from "./onboard-types.js";
|
||||
import type { ChannelChoice } from "./onboard-types.js";
|
||||
|
||||
type AgentsListOptions = {
|
||||
json?: boolean;
|
||||
@@ -77,17 +78,6 @@ export type AgentSummary = {
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
type AgentBinding = {
|
||||
agentId: string;
|
||||
match: {
|
||||
provider: string;
|
||||
accountId?: string;
|
||||
peer?: { kind: "dm" | "group" | "channel"; id: string };
|
||||
guildId?: string;
|
||||
teamId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type AgentEntry = NonNullable<
|
||||
NonNullable<ClawdbotConfig["agents"]>["list"]
|
||||
>[number];
|
||||
@@ -100,7 +90,7 @@ type AgentIdentity = {
|
||||
};
|
||||
|
||||
type ProviderAccountStatus = {
|
||||
provider: ChatProviderId;
|
||||
provider: ChatChannelId;
|
||||
accountId: string;
|
||||
name?: string;
|
||||
state:
|
||||
@@ -276,7 +266,7 @@ export function applyAgentConfig(
|
||||
function bindingMatchKey(match: AgentBinding["match"]) {
|
||||
const accountId = match.accountId?.trim() || DEFAULT_ACCOUNT_ID;
|
||||
return [
|
||||
match.provider,
|
||||
match.channel,
|
||||
accountId,
|
||||
match.peer?.kind ?? "",
|
||||
match.peer?.id ?? "",
|
||||
@@ -438,16 +428,16 @@ function formatSummary(summary: AgentSummary) {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function providerAccountKey(provider: ChatProviderId, accountId?: string) {
|
||||
function providerAccountKey(provider: ChatChannelId, accountId?: string) {
|
||||
return `${provider}:${accountId ?? DEFAULT_ACCOUNT_ID}`;
|
||||
}
|
||||
|
||||
function formatProviderAccountLabel(params: {
|
||||
provider: ChatProviderId;
|
||||
function formatChannelAccountLabel(params: {
|
||||
provider: ChatChannelId;
|
||||
accountId: string;
|
||||
name?: string;
|
||||
}): string {
|
||||
const label = getChatProviderMeta(params.provider).label;
|
||||
const label = getChatChannelMeta(params.provider).label;
|
||||
const account = params.name?.trim()
|
||||
? `${params.accountId} (${params.name.trim()})`
|
||||
: params.accountId;
|
||||
@@ -467,7 +457,7 @@ async function buildProviderStatusIndex(
|
||||
): Promise<Map<string, ProviderAccountStatus>> {
|
||||
const map = new Map<string, ProviderAccountStatus>();
|
||||
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
for (const accountId of accountIds) {
|
||||
const account = plugin.config.resolveAccount(cfg, accountId);
|
||||
@@ -514,18 +504,18 @@ async function buildProviderStatusIndex(
|
||||
|
||||
function resolveDefaultAccountId(
|
||||
cfg: ClawdbotConfig,
|
||||
provider: ChatProviderId,
|
||||
provider: ChatChannelId,
|
||||
): string {
|
||||
const plugin = getProviderPlugin(provider);
|
||||
const plugin = getChannelPlugin(provider);
|
||||
if (!plugin) return DEFAULT_ACCOUNT_ID;
|
||||
return resolveProviderDefaultAccountId({ plugin, cfg });
|
||||
return resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
}
|
||||
|
||||
function shouldShowProviderEntry(
|
||||
entry: ProviderAccountStatus,
|
||||
cfg: ClawdbotConfig,
|
||||
): boolean {
|
||||
const plugin = getProviderPlugin(entry.provider);
|
||||
const plugin = getChannelPlugin(entry.provider);
|
||||
if (!plugin) return Boolean(entry.configured);
|
||||
if (plugin.meta.showConfigured === false) {
|
||||
const providerConfig = (cfg as Record<string, unknown>)[plugin.id];
|
||||
@@ -535,7 +525,7 @@ function shouldShowProviderEntry(
|
||||
}
|
||||
|
||||
function formatProviderEntry(entry: ProviderAccountStatus): string {
|
||||
const label = formatProviderAccountLabel({
|
||||
const label = formatChannelAccountLabel({
|
||||
provider: entry.provider,
|
||||
accountId: entry.accountId,
|
||||
name: entry.name,
|
||||
@@ -550,14 +540,14 @@ function summarizeBindings(
|
||||
if (bindings.length === 0) return [];
|
||||
const seen = new Map<string, string>();
|
||||
for (const binding of bindings) {
|
||||
const provider = normalizeChatProviderId(binding.match.provider);
|
||||
if (!provider) continue;
|
||||
const channel = normalizeChatChannelId(binding.match.channel);
|
||||
if (!channel) continue;
|
||||
const accountId =
|
||||
binding.match.accountId ?? resolveDefaultAccountId(cfg, provider);
|
||||
const key = providerAccountKey(provider, accountId);
|
||||
binding.match.accountId ?? resolveDefaultAccountId(cfg, channel);
|
||||
const key = providerAccountKey(channel, accountId);
|
||||
if (!seen.has(key)) {
|
||||
const label = formatProviderAccountLabel({
|
||||
provider,
|
||||
const label = formatChannelAccountLabel({
|
||||
provider: channel,
|
||||
accountId,
|
||||
});
|
||||
seen.set(key, label);
|
||||
@@ -628,11 +618,11 @@ export async function agentsListCommand(
|
||||
if (bindings.length > 0) {
|
||||
const seen = new Set<string>();
|
||||
for (const binding of bindings) {
|
||||
const provider = normalizeChatProviderId(binding.match.provider);
|
||||
if (!provider) continue;
|
||||
const channel = normalizeChatChannelId(binding.match.channel);
|
||||
if (!channel) continue;
|
||||
const accountId =
|
||||
binding.match.accountId ?? resolveDefaultAccountId(cfg, provider);
|
||||
const key = providerAccountKey(provider, accountId);
|
||||
binding.match.accountId ?? resolveDefaultAccountId(cfg, channel);
|
||||
const key = providerAccountKey(channel, accountId);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const status = providerStatus.get(key);
|
||||
@@ -640,7 +630,7 @@ export async function agentsListCommand(
|
||||
providerLines.push(formatProviderEntry(status));
|
||||
} else {
|
||||
providerLines.push(
|
||||
`${formatProviderAccountLabel({ provider, accountId })}: unknown`,
|
||||
`${formatChannelAccountLabel({ provider: channel, accountId })}: unknown`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -663,17 +653,17 @@ export async function agentsListCommand(
|
||||
|
||||
const lines = ["Agents:", ...summaries.map(formatSummary)];
|
||||
lines.push(
|
||||
"Routing rules map provider/account/peer to an agent. Use --bindings for full rules.",
|
||||
"Routing rules map channel/account/peer to an agent. Use --bindings for full rules.",
|
||||
);
|
||||
lines.push(
|
||||
"Provider status reflects local config/creds. For live health: clawdbot providers status --probe.",
|
||||
"Channel status reflects local config/creds. For live health: clawdbot channels status --probe.",
|
||||
);
|
||||
runtime.log(lines.join("\n"));
|
||||
}
|
||||
|
||||
function describeBinding(binding: AgentBinding) {
|
||||
const match = binding.match;
|
||||
const parts = [match.provider];
|
||||
const parts = [match.channel];
|
||||
if (match.accountId) parts.push(`accountId=${match.accountId}`);
|
||||
if (match.peer) parts.push(`peer=${match.peer.kind}:${match.peer.id}`);
|
||||
if (match.guildId) parts.push(`guild=${match.guildId}`);
|
||||
@@ -681,23 +671,23 @@ function describeBinding(binding: AgentBinding) {
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function buildProviderBindings(params: {
|
||||
function buildChannelBindings(params: {
|
||||
agentId: string;
|
||||
selection: ProviderChoice[];
|
||||
selection: ChannelChoice[];
|
||||
config: ClawdbotConfig;
|
||||
accountIds?: Partial<Record<ProviderChoice, string>>;
|
||||
accountIds?: Partial<Record<ChannelChoice, string>>;
|
||||
}): AgentBinding[] {
|
||||
const bindings: AgentBinding[] = [];
|
||||
const agentId = normalizeAgentId(params.agentId);
|
||||
for (const provider of params.selection) {
|
||||
const match: AgentBinding["match"] = { provider };
|
||||
const accountId = params.accountIds?.[provider]?.trim();
|
||||
for (const channel of params.selection) {
|
||||
const match: AgentBinding["match"] = { channel };
|
||||
const accountId = params.accountIds?.[channel]?.trim();
|
||||
if (accountId) {
|
||||
match.accountId = accountId;
|
||||
} else {
|
||||
const plugin = getProviderPlugin(provider);
|
||||
const plugin = getChannelPlugin(channel);
|
||||
if (plugin?.meta.forceAccountBinding) {
|
||||
match.accountId = resolveDefaultAccountId(params.config, provider);
|
||||
match.accountId = resolveDefaultAccountId(params.config, channel);
|
||||
}
|
||||
}
|
||||
bindings.push({ agentId, match });
|
||||
@@ -717,10 +707,10 @@ function parseBindingSpecs(params: {
|
||||
for (const raw of specs) {
|
||||
const trimmed = raw?.trim();
|
||||
if (!trimmed) continue;
|
||||
const [providerRaw, accountRaw] = trimmed.split(":", 2);
|
||||
const provider = normalizeChatProviderId(providerRaw);
|
||||
if (!provider) {
|
||||
errors.push(`Unknown provider "${providerRaw}".`);
|
||||
const [channelRaw, accountRaw] = trimmed.split(":", 2);
|
||||
const channel = normalizeChatChannelId(channelRaw);
|
||||
if (!channel) {
|
||||
errors.push(`Unknown channel "${channelRaw}".`);
|
||||
continue;
|
||||
}
|
||||
let accountId = accountRaw?.trim();
|
||||
@@ -729,12 +719,12 @@ function parseBindingSpecs(params: {
|
||||
continue;
|
||||
}
|
||||
if (!accountId) {
|
||||
const plugin = getProviderPlugin(provider);
|
||||
const plugin = getChannelPlugin(channel);
|
||||
if (plugin?.meta.forceAccountBinding) {
|
||||
accountId = resolveDefaultAccountId(params.config, provider);
|
||||
accountId = resolveDefaultAccountId(params.config, channel);
|
||||
}
|
||||
}
|
||||
const match: AgentBinding["match"] = { provider };
|
||||
const match: AgentBinding["match"] = { channel };
|
||||
if (accountId) match.accountId = accountId;
|
||||
bindings.push({ agentId, match });
|
||||
}
|
||||
@@ -958,30 +948,30 @@ export async function agentsAddCommand(
|
||||
agentDir,
|
||||
});
|
||||
|
||||
let selection: ProviderChoice[] = [];
|
||||
const providerAccountIds: Partial<Record<ProviderChoice, string>> = {};
|
||||
nextConfig = await setupProviders(nextConfig, runtime, prompter, {
|
||||
let selection: ChannelChoice[] = [];
|
||||
const channelAccountIds: Partial<Record<ChannelChoice, string>> = {};
|
||||
nextConfig = await setupChannels(nextConfig, runtime, prompter, {
|
||||
allowSignalInstall: true,
|
||||
onSelection: (value) => {
|
||||
selection = value;
|
||||
},
|
||||
promptAccountIds: true,
|
||||
onAccountId: (provider, accountId) => {
|
||||
providerAccountIds[provider] = accountId;
|
||||
onAccountId: (channel, accountId) => {
|
||||
channelAccountIds[channel] = accountId;
|
||||
},
|
||||
});
|
||||
|
||||
if (selection.length > 0) {
|
||||
const wantsBindings = await prompter.confirm({
|
||||
message: "Route selected providers to this agent now? (bindings)",
|
||||
message: "Route selected channels to this agent now? (bindings)",
|
||||
initialValue: false,
|
||||
});
|
||||
if (wantsBindings) {
|
||||
const desiredBindings = buildProviderBindings({
|
||||
const desiredBindings = buildChannelBindings({
|
||||
agentId,
|
||||
selection,
|
||||
config: nextConfig,
|
||||
accountIds: providerAccountIds,
|
||||
accountIds: channelAccountIds,
|
||||
});
|
||||
const result = applyAgentBindings(nextConfig, desiredBindings);
|
||||
nextConfig = result.config;
|
||||
|
||||
@@ -30,11 +30,11 @@ vi.mock("../agents/auth-profiles.js", async (importOriginal) => {
|
||||
});
|
||||
|
||||
import {
|
||||
formatGatewayProvidersStatusLines,
|
||||
providersAddCommand,
|
||||
providersListCommand,
|
||||
providersRemoveCommand,
|
||||
} from "./providers.js";
|
||||
channelsAddCommand,
|
||||
channelsListCommand,
|
||||
channelsRemoveCommand,
|
||||
formatGatewayChannelsStatusLines,
|
||||
} from "./channels.js";
|
||||
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
@@ -53,7 +53,7 @@ const baseSnapshot = {
|
||||
legacyIssues: [],
|
||||
};
|
||||
|
||||
describe("providers command", () => {
|
||||
describe("channels command", () => {
|
||||
beforeEach(() => {
|
||||
configMocks.readConfigFileSnapshot.mockReset();
|
||||
configMocks.writeConfigFile.mockClear();
|
||||
@@ -69,28 +69,30 @@ describe("providers command", () => {
|
||||
|
||||
it("adds a non-default telegram account", async () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseSnapshot });
|
||||
await providersAddCommand(
|
||||
{ provider: "telegram", account: "alerts", token: "123:abc" },
|
||||
await channelsAddCommand(
|
||||
{ channel: "telegram", account: "alerts", token: "123:abc" },
|
||||
runtime,
|
||||
{ hasFlags: true },
|
||||
);
|
||||
|
||||
expect(configMocks.writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
const next = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||
telegram?: {
|
||||
enabled?: boolean;
|
||||
accounts?: Record<string, { botToken?: string }>;
|
||||
channels?: {
|
||||
telegram?: {
|
||||
enabled?: boolean;
|
||||
accounts?: Record<string, { botToken?: string }>;
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(next.telegram?.enabled).toBe(true);
|
||||
expect(next.telegram?.accounts?.alerts?.botToken).toBe("123:abc");
|
||||
expect(next.channels?.telegram?.enabled).toBe(true);
|
||||
expect(next.channels?.telegram?.accounts?.alerts?.botToken).toBe("123:abc");
|
||||
});
|
||||
|
||||
it("adds a default slack account with tokens", async () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseSnapshot });
|
||||
await providersAddCommand(
|
||||
await channelsAddCommand(
|
||||
{
|
||||
provider: "slack",
|
||||
channel: "slack",
|
||||
account: "default",
|
||||
botToken: "xoxb-1",
|
||||
appToken: "xapp-1",
|
||||
@@ -101,69 +103,81 @@ describe("providers command", () => {
|
||||
|
||||
expect(configMocks.writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
const next = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||
slack?: { enabled?: boolean; botToken?: string; appToken?: string };
|
||||
channels?: {
|
||||
slack?: { enabled?: boolean; botToken?: string; appToken?: string };
|
||||
};
|
||||
};
|
||||
expect(next.slack?.enabled).toBe(true);
|
||||
expect(next.slack?.botToken).toBe("xoxb-1");
|
||||
expect(next.slack?.appToken).toBe("xapp-1");
|
||||
expect(next.channels?.slack?.enabled).toBe(true);
|
||||
expect(next.channels?.slack?.botToken).toBe("xoxb-1");
|
||||
expect(next.channels?.slack?.appToken).toBe("xapp-1");
|
||||
});
|
||||
|
||||
it("deletes a non-default discord account", async () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseSnapshot,
|
||||
config: {
|
||||
discord: {
|
||||
accounts: {
|
||||
default: { token: "d0" },
|
||||
work: { token: "d1" },
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
default: { token: "d0" },
|
||||
work: { token: "d1" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await providersRemoveCommand(
|
||||
{ provider: "discord", account: "work", delete: true },
|
||||
await channelsRemoveCommand(
|
||||
{ channel: "discord", account: "work", delete: true },
|
||||
runtime,
|
||||
{ hasFlags: true },
|
||||
);
|
||||
|
||||
expect(configMocks.writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
const next = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||
discord?: { accounts?: Record<string, { token?: string }> };
|
||||
channels?: {
|
||||
discord?: { accounts?: Record<string, { token?: string }> };
|
||||
};
|
||||
};
|
||||
expect(next.discord?.accounts?.work).toBeUndefined();
|
||||
expect(next.discord?.accounts?.default?.token).toBe("d0");
|
||||
expect(next.channels?.discord?.accounts?.work).toBeUndefined();
|
||||
expect(next.channels?.discord?.accounts?.default?.token).toBe("d0");
|
||||
});
|
||||
|
||||
it("adds a named WhatsApp account", async () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseSnapshot });
|
||||
await providersAddCommand(
|
||||
{ provider: "whatsapp", account: "family", name: "Family Phone" },
|
||||
await channelsAddCommand(
|
||||
{ channel: "whatsapp", account: "family", name: "Family Phone" },
|
||||
runtime,
|
||||
{ hasFlags: true },
|
||||
);
|
||||
|
||||
const next = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||
whatsapp?: { accounts?: Record<string, { name?: string }> };
|
||||
channels?: {
|
||||
whatsapp?: { accounts?: Record<string, { name?: string }> };
|
||||
};
|
||||
};
|
||||
expect(next.whatsapp?.accounts?.family?.name).toBe("Family Phone");
|
||||
expect(next.channels?.whatsapp?.accounts?.family?.name).toBe(
|
||||
"Family Phone",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds a second signal account with a distinct name", async () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseSnapshot,
|
||||
config: {
|
||||
signal: {
|
||||
accounts: {
|
||||
default: { account: "+15555550111", name: "Primary" },
|
||||
channels: {
|
||||
signal: {
|
||||
accounts: {
|
||||
default: { account: "+15555550111", name: "Primary" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await providersAddCommand(
|
||||
await channelsAddCommand(
|
||||
{
|
||||
provider: "signal",
|
||||
channel: "signal",
|
||||
account: "lab",
|
||||
name: "Lab",
|
||||
signalNumber: "+15555550123",
|
||||
@@ -173,20 +187,22 @@ describe("providers command", () => {
|
||||
);
|
||||
|
||||
const next = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||
signal?: {
|
||||
accounts?: Record<string, { account?: string; name?: string }>;
|
||||
channels?: {
|
||||
signal?: {
|
||||
accounts?: Record<string, { account?: string; name?: string }>;
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(next.signal?.accounts?.lab?.account).toBe("+15555550123");
|
||||
expect(next.signal?.accounts?.lab?.name).toBe("Lab");
|
||||
expect(next.signal?.accounts?.default?.name).toBe("Primary");
|
||||
expect(next.channels?.signal?.accounts?.lab?.account).toBe("+15555550123");
|
||||
expect(next.channels?.signal?.accounts?.lab?.name).toBe("Lab");
|
||||
expect(next.channels?.signal?.accounts?.default?.name).toBe("Primary");
|
||||
});
|
||||
|
||||
it("disables a default provider account when remove has no delete flag", async () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseSnapshot,
|
||||
config: {
|
||||
discord: { token: "d0", enabled: true },
|
||||
channels: { discord: { token: "d0", enabled: true } },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -196,16 +212,16 @@ describe("providers command", () => {
|
||||
.spyOn(prompterModule, "createClackPrompter")
|
||||
.mockReturnValue(prompt as never);
|
||||
|
||||
await providersRemoveCommand(
|
||||
{ provider: "discord", account: "default" },
|
||||
await channelsRemoveCommand(
|
||||
{ channel: "discord", account: "default" },
|
||||
runtime,
|
||||
{ hasFlags: true },
|
||||
);
|
||||
|
||||
const next = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||
discord?: { enabled?: boolean };
|
||||
channels?: { discord?: { enabled?: boolean } };
|
||||
};
|
||||
expect(next.discord?.enabled).toBe(false);
|
||||
expect(next.channels?.discord?.enabled).toBe(false);
|
||||
promptSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -236,7 +252,7 @@ describe("providers command", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await providersListCommand({ json: true, usage: false }, runtime);
|
||||
await channelsListCommand({ json: true, usage: false }, runtime);
|
||||
const payload = JSON.parse(
|
||||
String(runtime.log.mock.calls[0]?.[0] ?? "{}"),
|
||||
) as { auth?: Array<{ id: string }> };
|
||||
@@ -249,18 +265,20 @@ describe("providers command", () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseSnapshot,
|
||||
config: {
|
||||
telegram: {
|
||||
name: "Legacy Name",
|
||||
accounts: {
|
||||
work: { botToken: "t0" },
|
||||
channels: {
|
||||
telegram: {
|
||||
name: "Legacy Name",
|
||||
accounts: {
|
||||
work: { botToken: "t0" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await providersAddCommand(
|
||||
await channelsAddCommand(
|
||||
{
|
||||
provider: "telegram",
|
||||
channel: "telegram",
|
||||
account: "default",
|
||||
token: "123:abc",
|
||||
name: "Primary Bot",
|
||||
@@ -270,46 +288,54 @@ describe("providers command", () => {
|
||||
);
|
||||
|
||||
const next = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||
telegram?: {
|
||||
name?: string;
|
||||
accounts?: Record<string, { botToken?: string; name?: string }>;
|
||||
channels?: {
|
||||
telegram?: {
|
||||
name?: string;
|
||||
accounts?: Record<string, { botToken?: string; name?: string }>;
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(next.telegram?.name).toBeUndefined();
|
||||
expect(next.telegram?.accounts?.default?.name).toBe("Primary Bot");
|
||||
expect(next.channels?.telegram?.name).toBeUndefined();
|
||||
expect(next.channels?.telegram?.accounts?.default?.name).toBe(
|
||||
"Primary Bot",
|
||||
);
|
||||
});
|
||||
|
||||
it("migrates base names when adding non-default accounts", async () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseSnapshot,
|
||||
config: {
|
||||
discord: {
|
||||
name: "Primary Bot",
|
||||
token: "d0",
|
||||
channels: {
|
||||
discord: {
|
||||
name: "Primary Bot",
|
||||
token: "d0",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await providersAddCommand(
|
||||
{ provider: "discord", account: "work", token: "d1" },
|
||||
await channelsAddCommand(
|
||||
{ channel: "discord", account: "work", token: "d1" },
|
||||
runtime,
|
||||
{ hasFlags: true },
|
||||
);
|
||||
|
||||
const next = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||
discord?: {
|
||||
name?: string;
|
||||
accounts?: Record<string, { name?: string; token?: string }>;
|
||||
channels?: {
|
||||
discord?: {
|
||||
name?: string;
|
||||
accounts?: Record<string, { name?: string; token?: string }>;
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(next.discord?.name).toBeUndefined();
|
||||
expect(next.discord?.accounts?.default?.name).toBe("Primary Bot");
|
||||
expect(next.discord?.accounts?.work?.token).toBe("d1");
|
||||
expect(next.channels?.discord?.name).toBeUndefined();
|
||||
expect(next.channels?.discord?.accounts?.default?.name).toBe("Primary Bot");
|
||||
expect(next.channels?.discord?.accounts?.work?.token).toBe("d1");
|
||||
});
|
||||
|
||||
it("formats gateway provider status lines in registry order", () => {
|
||||
const lines = formatGatewayProvidersStatusLines({
|
||||
providerAccounts: {
|
||||
it("formats gateway channel status lines in registry order", () => {
|
||||
const lines = formatGatewayChannelsStatusLines({
|
||||
channelAccounts: {
|
||||
telegram: [{ accountId: "default", configured: true }],
|
||||
whatsapp: [{ accountId: "default", linked: true }],
|
||||
},
|
||||
@@ -326,9 +352,9 @@ describe("providers command", () => {
|
||||
expect(telegramIndex).toBeLessThan(whatsappIndex);
|
||||
});
|
||||
|
||||
it("surfaces Discord privileged intent issues in providers status output", () => {
|
||||
const lines = formatGatewayProvidersStatusLines({
|
||||
providerAccounts: {
|
||||
it("surfaces Discord privileged intent issues in channels status output", () => {
|
||||
const lines = formatGatewayChannelsStatusLines({
|
||||
channelAccounts: {
|
||||
discord: [
|
||||
{
|
||||
accountId: "default",
|
||||
@@ -344,9 +370,9 @@ describe("providers command", () => {
|
||||
expect(lines.join("\n")).toMatch(/Run: clawdbot doctor/);
|
||||
});
|
||||
|
||||
it("surfaces Discord permission audit issues in providers status output", () => {
|
||||
const lines = formatGatewayProvidersStatusLines({
|
||||
providerAccounts: {
|
||||
it("surfaces Discord permission audit issues in channels status output", () => {
|
||||
const lines = formatGatewayChannelsStatusLines({
|
||||
channelAccounts: {
|
||||
discord: [
|
||||
{
|
||||
accountId: "default",
|
||||
@@ -372,8 +398,8 @@ describe("providers command", () => {
|
||||
});
|
||||
|
||||
it("surfaces Telegram privacy-mode hints when allowUnmentionedGroups is enabled", () => {
|
||||
const lines = formatGatewayProvidersStatusLines({
|
||||
providerAccounts: {
|
||||
const lines = formatGatewayChannelsStatusLines({
|
||||
channelAccounts: {
|
||||
telegram: [
|
||||
{
|
||||
accountId: "default",
|
||||
@@ -388,9 +414,9 @@ describe("providers command", () => {
|
||||
expect(lines.join("\n")).toMatch(/Telegram Bot API privacy mode/i);
|
||||
});
|
||||
|
||||
it("surfaces Telegram group membership audit issues in providers status output", () => {
|
||||
const lines = formatGatewayProvidersStatusLines({
|
||||
providerAccounts: {
|
||||
it("surfaces Telegram group membership audit issues in channels status output", () => {
|
||||
const lines = formatGatewayChannelsStatusLines({
|
||||
channelAccounts: {
|
||||
telegram: [
|
||||
{
|
||||
accountId: "default",
|
||||
@@ -418,16 +444,16 @@ describe("providers command", () => {
|
||||
});
|
||||
|
||||
it("surfaces WhatsApp auth/runtime hints when unlinked or disconnected", () => {
|
||||
const unlinked = formatGatewayProvidersStatusLines({
|
||||
providerAccounts: {
|
||||
const unlinked = formatGatewayChannelsStatusLines({
|
||||
channelAccounts: {
|
||||
whatsapp: [{ accountId: "default", enabled: true, linked: false }],
|
||||
},
|
||||
});
|
||||
expect(unlinked.join("\n")).toMatch(/WhatsApp/i);
|
||||
expect(unlinked.join("\n")).toMatch(/Not linked/i);
|
||||
|
||||
const disconnected = formatGatewayProvidersStatusLines({
|
||||
providerAccounts: {
|
||||
const disconnected = formatGatewayChannelsStatusLines({
|
||||
channelAccounts: {
|
||||
whatsapp: [
|
||||
{
|
||||
accountId: "default",
|
||||
@@ -444,9 +470,9 @@ describe("providers command", () => {
|
||||
expect(disconnected.join("\n")).toMatch(/disconnected/i);
|
||||
});
|
||||
|
||||
it("surfaces Signal runtime errors in providers status output", () => {
|
||||
const lines = formatGatewayProvidersStatusLines({
|
||||
providerAccounts: {
|
||||
it("surfaces Signal runtime errors in channels status output", () => {
|
||||
const lines = formatGatewayChannelsStatusLines({
|
||||
channelAccounts: {
|
||||
signal: [
|
||||
{
|
||||
accountId: "default",
|
||||
@@ -460,12 +486,12 @@ describe("providers command", () => {
|
||||
});
|
||||
expect(lines.join("\n")).toMatch(/Warnings:/);
|
||||
expect(lines.join("\n")).toMatch(/signal/i);
|
||||
expect(lines.join("\n")).toMatch(/Provider error/i);
|
||||
expect(lines.join("\n")).toMatch(/Channel error/i);
|
||||
});
|
||||
|
||||
it("surfaces iMessage runtime errors in providers status output", () => {
|
||||
const lines = formatGatewayProvidersStatusLines({
|
||||
providerAccounts: {
|
||||
it("surfaces iMessage runtime errors in channels status output", () => {
|
||||
const lines = formatGatewayChannelsStatusLines({
|
||||
channelAccounts: {
|
||||
imessage: [
|
||||
{
|
||||
accountId: "default",
|
||||
@@ -479,6 +505,6 @@ describe("providers command", () => {
|
||||
});
|
||||
expect(lines.join("\n")).toMatch(/Warnings:/);
|
||||
expect(lines.join("\n")).toMatch(/imessage/i);
|
||||
expect(lines.join("\n")).toMatch(/Provider error/i);
|
||||
expect(lines.join("\n")).toMatch(/Channel error/i);
|
||||
});
|
||||
});
|
||||
13
src/commands/channels.ts
Normal file
13
src/commands/channels.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type { ChannelsAddOptions } from "./channels/add.js";
|
||||
export { channelsAddCommand } from "./channels/add.js";
|
||||
export type { ChannelsListOptions } from "./channels/list.js";
|
||||
export { channelsListCommand } from "./channels/list.js";
|
||||
export type { ChannelsLogsOptions } from "./channels/logs.js";
|
||||
export { channelsLogsCommand } from "./channels/logs.js";
|
||||
export type { ChannelsRemoveOptions } from "./channels/remove.js";
|
||||
export { channelsRemoveCommand } from "./channels/remove.js";
|
||||
export type { ChannelsStatusOptions } from "./channels/status.js";
|
||||
export {
|
||||
channelsStatusCommand,
|
||||
formatGatewayChannelsStatusLines,
|
||||
} from "./channels/status.js";
|
||||
@@ -1,30 +1,30 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { getProviderPlugin } from "../../providers/plugins/index.js";
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import type {
|
||||
ProviderId,
|
||||
ProviderSetupInput,
|
||||
} from "../../providers/plugins/types.js";
|
||||
ChannelId,
|
||||
ChannelSetupInput,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||
|
||||
type ChatProvider = ProviderId;
|
||||
type ChatChannel = ChannelId;
|
||||
|
||||
export function applyAccountName(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
provider: ChatProvider;
|
||||
channel: ChatChannel;
|
||||
accountId: string;
|
||||
name?: string;
|
||||
}): ClawdbotConfig {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const plugin = getProviderPlugin(params.provider);
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
const apply = plugin?.setup?.applyAccountName;
|
||||
return apply
|
||||
? apply({ cfg: params.cfg, accountId, name: params.name })
|
||||
: params.cfg;
|
||||
}
|
||||
|
||||
export function applyProviderAccountConfig(params: {
|
||||
export function applyChannelAccountConfig(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
provider: ChatProvider;
|
||||
channel: ChatChannel;
|
||||
accountId: string;
|
||||
name?: string;
|
||||
token?: string;
|
||||
@@ -43,10 +43,10 @@ export function applyProviderAccountConfig(params: {
|
||||
useEnv?: boolean;
|
||||
}): ClawdbotConfig {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const plugin = getProviderPlugin(params.provider);
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
const apply = plugin?.setup?.applyAccountConfig;
|
||||
if (!apply) return params.cfg;
|
||||
const input: ProviderSetupInput = {
|
||||
const input: ChannelSetupInput = {
|
||||
name: params.name,
|
||||
token: params.token,
|
||||
tokenFile: params.tokenFile,
|
||||
@@ -1,29 +1,22 @@
|
||||
import { writeConfigFile } from "../../config/config.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
normalizeProviderId,
|
||||
} from "../../providers/plugins/index.js";
|
||||
import type { ProviderId } from "../../providers/plugins/types.js";
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import { writeConfigFile } from "../../config/config.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
||||
import { setupProviders } from "../onboard-providers.js";
|
||||
import type { ProviderChoice } from "../onboard-types.js";
|
||||
import {
|
||||
applyAccountName,
|
||||
applyProviderAccountConfig,
|
||||
} from "./add-mutators.js";
|
||||
import {
|
||||
providerLabel,
|
||||
requireValidConfig,
|
||||
shouldUseWizard,
|
||||
} from "./shared.js";
|
||||
import { setupChannels } from "../onboard-channels.js";
|
||||
import type { ChannelChoice } from "../onboard-types.js";
|
||||
import { applyAccountName, applyChannelAccountConfig } from "./add-mutators.js";
|
||||
import { channelLabel, requireValidConfig, shouldUseWizard } from "./shared.js";
|
||||
|
||||
export type ProvidersAddOptions = {
|
||||
provider?: string;
|
||||
export type ChannelsAddOptions = {
|
||||
channel?: string;
|
||||
account?: string;
|
||||
name?: string;
|
||||
token?: string;
|
||||
@@ -42,8 +35,8 @@ export type ProvidersAddOptions = {
|
||||
useEnv?: boolean;
|
||||
};
|
||||
|
||||
export async function providersAddCommand(
|
||||
opts: ProvidersAddOptions,
|
||||
export async function channelsAddCommand(
|
||||
opts: ChannelsAddOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
params?: { hasFlags?: boolean },
|
||||
) {
|
||||
@@ -53,22 +46,22 @@ export async function providersAddCommand(
|
||||
const useWizard = shouldUseWizard(params);
|
||||
if (useWizard) {
|
||||
const prompter = createClackPrompter();
|
||||
let selection: ProviderChoice[] = [];
|
||||
const accountIds: Partial<Record<ProviderChoice, string>> = {};
|
||||
await prompter.intro("Provider setup");
|
||||
let nextConfig = await setupProviders(cfg, runtime, prompter, {
|
||||
let selection: ChannelChoice[] = [];
|
||||
const accountIds: Partial<Record<ChannelChoice, string>> = {};
|
||||
await prompter.intro("Channel setup");
|
||||
let nextConfig = await setupChannels(cfg, runtime, prompter, {
|
||||
allowDisable: false,
|
||||
allowSignalInstall: true,
|
||||
promptAccountIds: true,
|
||||
onSelection: (value) => {
|
||||
selection = value;
|
||||
},
|
||||
onAccountId: (provider, accountId) => {
|
||||
accountIds[provider] = accountId;
|
||||
onAccountId: (channel, accountId) => {
|
||||
accountIds[channel] = accountId;
|
||||
},
|
||||
});
|
||||
if (selection.length === 0) {
|
||||
await prompter.outro("No providers selected.");
|
||||
await prompter.outro("No channels selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,22 +70,22 @@ export async function providersAddCommand(
|
||||
initialValue: false,
|
||||
});
|
||||
if (wantsNames) {
|
||||
for (const provider of selection) {
|
||||
const accountId = accountIds[provider] ?? DEFAULT_ACCOUNT_ID;
|
||||
const plugin = getProviderPlugin(provider as ProviderId);
|
||||
for (const channel of selection) {
|
||||
const accountId = accountIds[channel] ?? DEFAULT_ACCOUNT_ID;
|
||||
const plugin = getChannelPlugin(channel as ChannelId);
|
||||
const account = plugin?.config.resolveAccount(nextConfig, accountId) as
|
||||
| { name?: string }
|
||||
| undefined;
|
||||
const snapshot = plugin?.config.describeAccount?.(account, nextConfig);
|
||||
const existingName = snapshot?.name ?? account?.name;
|
||||
const name = await prompter.text({
|
||||
message: `${provider} account name (${accountId})`,
|
||||
message: `${channel} account name (${accountId})`,
|
||||
initialValue: existingName,
|
||||
});
|
||||
if (name?.trim()) {
|
||||
nextConfig = applyAccountName({
|
||||
cfg: nextConfig,
|
||||
provider,
|
||||
channel,
|
||||
accountId,
|
||||
name,
|
||||
});
|
||||
@@ -101,20 +94,20 @@ export async function providersAddCommand(
|
||||
}
|
||||
|
||||
await writeConfigFile(nextConfig);
|
||||
await prompter.outro("Providers updated.");
|
||||
await prompter.outro("Channels updated.");
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = normalizeProviderId(opts.provider);
|
||||
if (!provider) {
|
||||
runtime.error(`Unknown provider: ${String(opts.provider ?? "")}`);
|
||||
const channel = normalizeChannelId(opts.channel);
|
||||
if (!channel) {
|
||||
runtime.error(`Unknown channel: ${String(opts.channel ?? "")}`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const plugin = getProviderPlugin(provider);
|
||||
const plugin = getChannelPlugin(channel);
|
||||
if (!plugin?.setup?.applyAccountConfig) {
|
||||
runtime.error(`Provider ${provider} does not support add.`);
|
||||
runtime.error(`Channel ${channel} does not support add.`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -149,9 +142,9 @@ export async function providersAddCommand(
|
||||
return;
|
||||
}
|
||||
|
||||
const nextConfig = applyProviderAccountConfig({
|
||||
const nextConfig = applyChannelAccountConfig({
|
||||
cfg,
|
||||
provider,
|
||||
channel,
|
||||
accountId,
|
||||
name: opts.name,
|
||||
token: opts.token,
|
||||
@@ -171,5 +164,5 @@ export async function providersAddCommand(
|
||||
});
|
||||
|
||||
await writeConfigFile(nextConfig);
|
||||
runtime.log(`Added ${providerLabel(provider)} account "${accountId}".`);
|
||||
runtime.log(`Added ${channelLabel(channel)} account "${accountId}".`);
|
||||
}
|
||||
@@ -3,23 +3,23 @@ import {
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
loadAuthProfileStore,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js";
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelPlugin,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import { withProgress } from "../../cli/progress.js";
|
||||
import {
|
||||
formatUsageReportLines,
|
||||
loadProviderUsageSummary,
|
||||
} from "../../infra/provider-usage.js";
|
||||
import { listProviderPlugins } from "../../providers/plugins/index.js";
|
||||
import { buildProviderAccountSnapshot } from "../../providers/plugins/status.js";
|
||||
import type {
|
||||
ProviderAccountSnapshot,
|
||||
ProviderPlugin,
|
||||
} from "../../providers/plugins/types.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { formatProviderAccountLabel, requireValidConfig } from "./shared.js";
|
||||
import { formatChannelAccountLabel, requireValidConfig } from "./shared.js";
|
||||
|
||||
export type ProvidersListOptions = {
|
||||
export type ChannelsListOptions = {
|
||||
json?: boolean;
|
||||
usage?: boolean;
|
||||
};
|
||||
@@ -52,20 +52,20 @@ function formatLinked(value: boolean): string {
|
||||
return value ? theme.success("linked") : theme.warn("not linked");
|
||||
}
|
||||
|
||||
function shouldShowConfigured(provider: ProviderPlugin): boolean {
|
||||
return provider.meta.showConfigured !== false;
|
||||
function shouldShowConfigured(channel: ChannelPlugin): boolean {
|
||||
return channel.meta.showConfigured !== false;
|
||||
}
|
||||
|
||||
function formatAccountLine(params: {
|
||||
provider: ProviderPlugin;
|
||||
snapshot: ProviderAccountSnapshot;
|
||||
channel: ChannelPlugin;
|
||||
snapshot: ChannelAccountSnapshot;
|
||||
}): string {
|
||||
const { provider, snapshot } = params;
|
||||
const label = formatProviderAccountLabel({
|
||||
provider: provider.id,
|
||||
const { channel, snapshot } = params;
|
||||
const label = formatChannelAccountLabel({
|
||||
channel: channel.id,
|
||||
accountId: snapshot.accountId,
|
||||
name: snapshot.name,
|
||||
providerStyle: theme.accent,
|
||||
channelStyle: theme.accent,
|
||||
accountStyle: theme.heading,
|
||||
});
|
||||
const bits: string[] = [];
|
||||
@@ -73,7 +73,7 @@ function formatAccountLine(params: {
|
||||
bits.push(formatLinked(snapshot.linked));
|
||||
}
|
||||
if (
|
||||
shouldShowConfigured(provider) &&
|
||||
shouldShowConfigured(channel) &&
|
||||
typeof snapshot.configured === "boolean"
|
||||
) {
|
||||
bits.push(formatConfigured(snapshot.configured));
|
||||
@@ -109,15 +109,15 @@ async function loadUsageWithProgress(
|
||||
}
|
||||
}
|
||||
|
||||
export async function providersListCommand(
|
||||
opts: ProvidersListOptions,
|
||||
export async function channelsListCommand(
|
||||
opts: ChannelsListOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
) {
|
||||
const cfg = await requireValidConfig(runtime);
|
||||
if (!cfg) return;
|
||||
const includeUsage = opts.usage !== false;
|
||||
|
||||
const plugins = listProviderPlugins();
|
||||
const plugins = listChannelPlugins();
|
||||
|
||||
const authStore = loadAuthProfileStore();
|
||||
const authProfiles = Object.entries(authStore.profiles).map(
|
||||
@@ -142,20 +142,20 @@ export async function providersListCommand(
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(theme.heading("Chat providers:"));
|
||||
lines.push(theme.heading("Chat channels:"));
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const accounts = plugin.config.listAccountIds(cfg);
|
||||
if (!accounts || accounts.length === 0) continue;
|
||||
for (const accountId of accounts) {
|
||||
const snapshot = await buildProviderAccountSnapshot({
|
||||
const snapshot = await buildChannelAccountSnapshot({
|
||||
plugin,
|
||||
cfg,
|
||||
accountId,
|
||||
});
|
||||
lines.push(
|
||||
formatAccountLine({
|
||||
provider: plugin,
|
||||
channel: plugin,
|
||||
snapshot,
|
||||
}),
|
||||
);
|
||||
@@ -1,13 +1,12 @@
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import { parseLogLine } from "../../logging/parse-log-line.js";
|
||||
import { getResolvedLoggerSettings } from "../../logging.js";
|
||||
import { listProviderPlugins } from "../../providers/plugins/index.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
|
||||
export type ProvidersLogsOptions = {
|
||||
provider?: string;
|
||||
export type ChannelsLogsOptions = {
|
||||
channel?: string;
|
||||
lines?: string | number;
|
||||
json?: boolean;
|
||||
};
|
||||
@@ -16,22 +15,22 @@ type LogLine = ReturnType<typeof parseLogLine>;
|
||||
|
||||
const DEFAULT_LIMIT = 200;
|
||||
const MAX_BYTES = 1_000_000;
|
||||
const PROVIDERS = new Set<string>([
|
||||
...listProviderPlugins().map((plugin) => plugin.id),
|
||||
const CHANNELS = new Set<string>([
|
||||
...listChannelPlugins().map((plugin) => plugin.id),
|
||||
"all",
|
||||
]);
|
||||
|
||||
function parseProviderFilter(raw?: string) {
|
||||
function parseChannelFilter(raw?: string) {
|
||||
const trimmed = raw?.trim().toLowerCase();
|
||||
if (!trimmed) return "all";
|
||||
return PROVIDERS.has(trimmed) ? trimmed : "all";
|
||||
return CHANNELS.has(trimmed) ? trimmed : "all";
|
||||
}
|
||||
|
||||
function matchesProvider(line: NonNullable<LogLine>, provider: string) {
|
||||
if (provider === "all") return true;
|
||||
const needle = `gateway/providers/${provider}`;
|
||||
function matchesChannel(line: NonNullable<LogLine>, channel: string) {
|
||||
if (channel === "all") return true;
|
||||
const needle = `gateway/channels/${channel}`;
|
||||
if (line.subsystem?.includes(needle)) return true;
|
||||
if (line.module?.includes(provider)) return true;
|
||||
if (line.module?.includes(channel)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -61,11 +60,11 @@ async function readTailLines(file: string, limit: number): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function providersLogsCommand(
|
||||
opts: ProvidersLogsOptions,
|
||||
export async function channelsLogsCommand(
|
||||
opts: ChannelsLogsOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
) {
|
||||
const provider = parseProviderFilter(opts.provider);
|
||||
const channel = parseChannelFilter(opts.channel);
|
||||
const limitRaw =
|
||||
typeof opts.lines === "string" ? Number(opts.lines) : opts.lines;
|
||||
const limit =
|
||||
@@ -78,17 +77,17 @@ export async function providersLogsCommand(
|
||||
const parsed = rawLines
|
||||
.map(parseLogLine)
|
||||
.filter((line): line is NonNullable<LogLine> => Boolean(line));
|
||||
const filtered = parsed.filter((line) => matchesProvider(line, provider));
|
||||
const filtered = parsed.filter((line) => matchesChannel(line, channel));
|
||||
const lines = filtered.slice(Math.max(0, filtered.length - limit));
|
||||
|
||||
if (opts.json) {
|
||||
runtime.log(JSON.stringify({ file, provider, lines }, null, 2));
|
||||
runtime.log(JSON.stringify({ file, channel, lines }, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.log(theme.info(`Log file: ${file}`));
|
||||
if (provider !== "all") {
|
||||
runtime.log(theme.info(`Provider: ${provider}`));
|
||||
if (channel !== "all") {
|
||||
runtime.log(theme.info(`Channel: ${channel}`));
|
||||
}
|
||||
if (lines.length === 0) {
|
||||
runtime.log(theme.muted("No matching log lines."));
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type ClawdbotConfig, writeConfigFile } from "../../config/config.js";
|
||||
import { resolveProviderDefaultAccountId } from "../../providers/plugins/helpers.js";
|
||||
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
listProviderPlugins,
|
||||
normalizeProviderId,
|
||||
} from "../../providers/plugins/index.js";
|
||||
getChannelPlugin,
|
||||
listChannelPlugins,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import { type ClawdbotConfig, writeConfigFile } from "../../config/config.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
@@ -12,26 +12,26 @@ import {
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
||||
import {
|
||||
type ChatProvider,
|
||||
providerLabel,
|
||||
type ChatChannel,
|
||||
channelLabel,
|
||||
requireValidConfig,
|
||||
shouldUseWizard,
|
||||
} from "./shared.js";
|
||||
|
||||
export type ProvidersRemoveOptions = {
|
||||
provider?: string;
|
||||
export type ChannelsRemoveOptions = {
|
||||
channel?: string;
|
||||
account?: string;
|
||||
delete?: boolean;
|
||||
};
|
||||
|
||||
function listAccountIds(cfg: ClawdbotConfig, provider: ChatProvider): string[] {
|
||||
const plugin = getProviderPlugin(provider);
|
||||
function listAccountIds(cfg: ClawdbotConfig, channel: ChatChannel): string[] {
|
||||
const plugin = getChannelPlugin(channel);
|
||||
if (!plugin) return [];
|
||||
return plugin.config.listAccountIds(cfg);
|
||||
}
|
||||
|
||||
export async function providersRemoveCommand(
|
||||
opts: ProvidersRemoveOptions,
|
||||
export async function channelsRemoveCommand(
|
||||
opts: ChannelsRemoveOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
params?: { hasFlags?: boolean },
|
||||
) {
|
||||
@@ -40,22 +40,23 @@ export async function providersRemoveCommand(
|
||||
|
||||
const useWizard = shouldUseWizard(params);
|
||||
const prompter = useWizard ? createClackPrompter() : null;
|
||||
let provider = normalizeProviderId(opts.provider);
|
||||
let channel: ChatChannel | null = normalizeChannelId(opts.channel);
|
||||
let accountId = normalizeAccountId(opts.account);
|
||||
const deleteConfig = Boolean(opts.delete);
|
||||
|
||||
if (useWizard && prompter) {
|
||||
await prompter.intro("Remove provider account");
|
||||
provider = (await prompter.select({
|
||||
message: "Provider",
|
||||
options: listProviderPlugins().map((plugin) => ({
|
||||
await prompter.intro("Remove channel account");
|
||||
const selectedChannel = (await prompter.select({
|
||||
message: "Channel",
|
||||
options: listChannelPlugins().map((plugin) => ({
|
||||
value: plugin.id,
|
||||
label: plugin.meta.label,
|
||||
})),
|
||||
})) as ChatProvider;
|
||||
})) as ChatChannel;
|
||||
channel = selectedChannel;
|
||||
|
||||
accountId = await (async () => {
|
||||
const ids = listAccountIds(cfg, provider);
|
||||
const ids = listAccountIds(cfg, selectedChannel);
|
||||
const choice = (await prompter.select({
|
||||
message: "Account",
|
||||
options: ids.map((id) => ({
|
||||
@@ -68,7 +69,7 @@ export async function providersRemoveCommand(
|
||||
})();
|
||||
|
||||
const wantsDisable = await prompter.confirm({
|
||||
message: `Disable ${providerLabel(provider)} account "${accountId}"? (keeps config)`,
|
||||
message: `Disable ${channelLabel(selectedChannel)} account "${accountId}"? (keeps config)`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (!wantsDisable) {
|
||||
@@ -76,15 +77,15 @@ export async function providersRemoveCommand(
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!provider) {
|
||||
runtime.error("Provider is required. Use --provider <name>.");
|
||||
if (!channel) {
|
||||
runtime.error("Channel is required. Use --channel <name>.");
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (!deleteConfig) {
|
||||
const confirm = createClackPrompter();
|
||||
const ok = await confirm.confirm({
|
||||
message: `Disable ${providerLabel(provider)} account "${accountId}"? (keeps config)`,
|
||||
message: `Disable ${channelLabel(channel)} account "${accountId}"? (keeps config)`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (!ok) {
|
||||
@@ -93,22 +94,22 @@ export async function providersRemoveCommand(
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = getProviderPlugin(provider);
|
||||
const plugin = getChannelPlugin(channel);
|
||||
if (!plugin) {
|
||||
runtime.error(`Unknown provider: ${provider}`);
|
||||
runtime.error(`Unknown channel: ${channel}`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedAccountId =
|
||||
normalizeAccountId(accountId) ??
|
||||
resolveProviderDefaultAccountId({ plugin, cfg });
|
||||
resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
const accountKey = resolvedAccountId || DEFAULT_ACCOUNT_ID;
|
||||
|
||||
let next = { ...cfg };
|
||||
if (deleteConfig) {
|
||||
if (!plugin.config.deleteAccount) {
|
||||
runtime.error(`Provider ${provider} does not support delete.`);
|
||||
runtime.error(`Channel ${channel} does not support delete.`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -118,7 +119,7 @@ export async function providersRemoveCommand(
|
||||
});
|
||||
} else {
|
||||
if (!plugin.config.setAccountEnabled) {
|
||||
runtime.error(`Provider ${provider} does not support disable.`);
|
||||
runtime.error(`Channel ${channel} does not support disable.`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -133,14 +134,14 @@ export async function providersRemoveCommand(
|
||||
if (useWizard && prompter) {
|
||||
await prompter.outro(
|
||||
deleteConfig
|
||||
? `Deleted ${providerLabel(provider)} account "${accountKey}".`
|
||||
: `Disabled ${providerLabel(provider)} account "${accountKey}".`,
|
||||
? `Deleted ${channelLabel(channel)} account "${accountKey}".`
|
||||
: `Disabled ${channelLabel(channel)} account "${accountKey}".`,
|
||||
);
|
||||
} else {
|
||||
runtime.log(
|
||||
deleteConfig
|
||||
? `Deleted ${providerLabel(provider)} account "${accountKey}".`
|
||||
: `Disabled ${providerLabel(provider)} account "${accountKey}".`,
|
||||
? `Deleted ${channelLabel(channel)} account "${accountKey}".`
|
||||
: `Disabled ${channelLabel(channel)} account "${accountKey}".`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import {
|
||||
type ChannelId,
|
||||
getChannelPlugin,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import {
|
||||
type ClawdbotConfig,
|
||||
readConfigFileSnapshot,
|
||||
} from "../../config/config.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
type ProviderId,
|
||||
} from "../../providers/plugins/index.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
|
||||
export type ChatProvider = ProviderId;
|
||||
export type ChatChannel = ChannelId;
|
||||
|
||||
export async function requireValidConfig(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
@@ -39,30 +39,30 @@ export function formatAccountLabel(params: {
|
||||
return base;
|
||||
}
|
||||
|
||||
export const providerLabel = (provider: ChatProvider) => {
|
||||
const plugin = getProviderPlugin(provider);
|
||||
return plugin?.meta.label ?? provider;
|
||||
export const channelLabel = (channel: ChatChannel) => {
|
||||
const plugin = getChannelPlugin(channel);
|
||||
return plugin?.meta.label ?? channel;
|
||||
};
|
||||
|
||||
export function formatProviderAccountLabel(params: {
|
||||
provider: ChatProvider;
|
||||
export function formatChannelAccountLabel(params: {
|
||||
channel: ChatChannel;
|
||||
accountId: string;
|
||||
name?: string;
|
||||
providerStyle?: (value: string) => string;
|
||||
channelStyle?: (value: string) => string;
|
||||
accountStyle?: (value: string) => string;
|
||||
}): string {
|
||||
const providerText = providerLabel(params.provider);
|
||||
const channelText = channelLabel(params.channel);
|
||||
const accountText = formatAccountLabel({
|
||||
accountId: params.accountId,
|
||||
name: params.name,
|
||||
});
|
||||
const styledProvider = params.providerStyle
|
||||
? params.providerStyle(providerText)
|
||||
: providerText;
|
||||
const styledChannel = params.channelStyle
|
||||
? params.channelStyle(channelText)
|
||||
: channelText;
|
||||
const styledAccount = params.accountStyle
|
||||
? params.accountStyle(accountText)
|
||||
: accountText;
|
||||
return `${styledProvider} ${styledAccount}`;
|
||||
return `${styledChannel} ${styledAccount}`;
|
||||
}
|
||||
|
||||
export function shouldUseWizard(params?: { hasFlags?: boolean }) {
|
||||
@@ -1,36 +1,36 @@
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js";
|
||||
import type { ChannelAccountSnapshot } from "../../channels/plugins/types.js";
|
||||
import { withProgress } from "../../cli/progress.js";
|
||||
import {
|
||||
type ClawdbotConfig,
|
||||
readConfigFileSnapshot,
|
||||
} from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { formatAge } from "../../infra/provider-summary.js";
|
||||
import { collectProvidersStatusIssues } from "../../infra/providers-status-issues.js";
|
||||
import { listProviderPlugins } from "../../providers/plugins/index.js";
|
||||
import { buildProviderAccountSnapshot } from "../../providers/plugins/status.js";
|
||||
import type { ProviderAccountSnapshot } from "../../providers/plugins/types.js";
|
||||
import { formatAge } from "../../infra/channel-summary.js";
|
||||
import { collectChannelStatusIssues } from "../../infra/channels-status-issues.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import {
|
||||
type ChatProvider,
|
||||
formatProviderAccountLabel,
|
||||
type ChatChannel,
|
||||
formatChannelAccountLabel,
|
||||
requireValidConfig,
|
||||
} from "./shared.js";
|
||||
|
||||
export type ProvidersStatusOptions = {
|
||||
export type ChannelsStatusOptions = {
|
||||
json?: boolean;
|
||||
probe?: boolean;
|
||||
timeout?: string;
|
||||
};
|
||||
|
||||
export function formatGatewayProvidersStatusLines(
|
||||
export function formatGatewayChannelsStatusLines(
|
||||
payload: Record<string, unknown>,
|
||||
): string[] {
|
||||
const lines: string[] = [];
|
||||
lines.push(theme.success("Gateway reachable."));
|
||||
const accountLines = (
|
||||
provider: ChatProvider,
|
||||
provider: ChatChannel,
|
||||
accounts: Array<Record<string, unknown>>,
|
||||
) =>
|
||||
accounts.map((account) => {
|
||||
@@ -117,23 +117,23 @@ export function formatGatewayProvidersStatusLines(
|
||||
const accountId =
|
||||
typeof account.accountId === "string" ? account.accountId : "default";
|
||||
const name = typeof account.name === "string" ? account.name.trim() : "";
|
||||
const labelText = formatProviderAccountLabel({
|
||||
provider,
|
||||
const labelText = formatChannelAccountLabel({
|
||||
channel: provider,
|
||||
accountId,
|
||||
name: name || undefined,
|
||||
});
|
||||
return `- ${labelText}: ${bits.join(", ")}`;
|
||||
});
|
||||
|
||||
const plugins = listProviderPlugins();
|
||||
const accountsByProvider = payload.providerAccounts as
|
||||
const plugins = listChannelPlugins();
|
||||
const accountsByChannel = payload.channelAccounts as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const accountPayloads: Partial<
|
||||
Record<string, Array<Record<string, unknown>>>
|
||||
> = {};
|
||||
for (const plugin of plugins) {
|
||||
const raw = accountsByProvider?.[plugin.id];
|
||||
const raw = accountsByChannel?.[plugin.id];
|
||||
if (Array.isArray(raw)) {
|
||||
accountPayloads[plugin.id] = raw as Array<Record<string, unknown>>;
|
||||
}
|
||||
@@ -142,17 +142,17 @@ export function formatGatewayProvidersStatusLines(
|
||||
for (const plugin of plugins) {
|
||||
const accounts = accountPayloads[plugin.id];
|
||||
if (accounts && accounts.length > 0) {
|
||||
lines.push(...accountLines(plugin.id as ChatProvider, accounts));
|
||||
lines.push(...accountLines(plugin.id as ChatChannel, accounts));
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
const issues = collectProvidersStatusIssues(payload);
|
||||
const issues = collectChannelStatusIssues(payload);
|
||||
if (issues.length > 0) {
|
||||
lines.push(theme.warn("Warnings:"));
|
||||
for (const issue of issues) {
|
||||
lines.push(
|
||||
`- ${issue.provider} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`,
|
||||
`- ${issue.channel} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`,
|
||||
);
|
||||
}
|
||||
lines.push(`- Run: clawdbot doctor`);
|
||||
@@ -164,7 +164,7 @@ export function formatGatewayProvidersStatusLines(
|
||||
return lines;
|
||||
}
|
||||
|
||||
async function formatConfigProvidersStatusLines(
|
||||
async function formatConfigChannelsStatusLines(
|
||||
cfg: ClawdbotConfig,
|
||||
meta: { path?: string; mode?: "local" | "remote" },
|
||||
): Promise<string[]> {
|
||||
@@ -179,7 +179,7 @@ async function formatConfigProvidersStatusLines(
|
||||
if (meta.path || meta.mode) lines.push("");
|
||||
|
||||
const accountLines = (
|
||||
provider: ChatProvider,
|
||||
provider: ChatChannel,
|
||||
accounts: Array<Record<string, unknown>>,
|
||||
) =>
|
||||
accounts.map((account) => {
|
||||
@@ -217,21 +217,21 @@ async function formatConfigProvidersStatusLines(
|
||||
const accountId =
|
||||
typeof account.accountId === "string" ? account.accountId : "default";
|
||||
const name = typeof account.name === "string" ? account.name.trim() : "";
|
||||
const labelText = formatProviderAccountLabel({
|
||||
provider,
|
||||
const labelText = formatChannelAccountLabel({
|
||||
channel: provider,
|
||||
accountId,
|
||||
name: name || undefined,
|
||||
});
|
||||
return `- ${labelText}: ${bits.join(", ")}`;
|
||||
});
|
||||
|
||||
const plugins = listProviderPlugins();
|
||||
const plugins = listChannelPlugins();
|
||||
for (const plugin of plugins) {
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
if (!accountIds.length) continue;
|
||||
const snapshots: ProviderAccountSnapshot[] = [];
|
||||
const snapshots: ChannelAccountSnapshot[] = [];
|
||||
for (const accountId of accountIds) {
|
||||
const snapshot = await buildProviderAccountSnapshot({
|
||||
const snapshot = await buildChannelAccountSnapshot({
|
||||
plugin,
|
||||
cfg,
|
||||
accountId,
|
||||
@@ -239,7 +239,7 @@ async function formatConfigProvidersStatusLines(
|
||||
snapshots.push(snapshot);
|
||||
}
|
||||
if (snapshots.length > 0) {
|
||||
lines.push(...accountLines(plugin.id as ChatProvider, snapshots));
|
||||
lines.push(...accountLines(plugin.id as ChatChannel, snapshots));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,14 +250,14 @@ async function formatConfigProvidersStatusLines(
|
||||
return lines;
|
||||
}
|
||||
|
||||
export async function providersStatusCommand(
|
||||
opts: ProvidersStatusOptions,
|
||||
export async function channelsStatusCommand(
|
||||
opts: ChannelsStatusOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
) {
|
||||
const timeoutMs = Number(opts.timeout ?? 10_000);
|
||||
const statusLabel = opts.probe
|
||||
? "Checking provider status (probe)…"
|
||||
: "Checking provider status…";
|
||||
? "Checking channel status (probe)…"
|
||||
: "Checking channel status…";
|
||||
const shouldLogStatus = opts.json !== true && !process.stderr.isTTY;
|
||||
if (shouldLogStatus) runtime.log(statusLabel);
|
||||
try {
|
||||
@@ -269,7 +269,7 @@ export async function providersStatusCommand(
|
||||
},
|
||||
async () =>
|
||||
await callGateway({
|
||||
method: "providers.status",
|
||||
method: "channels.status",
|
||||
params: { probe: Boolean(opts.probe), timeoutMs },
|
||||
timeoutMs,
|
||||
}),
|
||||
@@ -279,9 +279,9 @@ export async function providersStatusCommand(
|
||||
return;
|
||||
}
|
||||
runtime.log(
|
||||
formatGatewayProvidersStatusLines(
|
||||
payload as Record<string, unknown>,
|
||||
).join("\n"),
|
||||
formatGatewayChannelsStatusLines(payload as Record<string, unknown>).join(
|
||||
"\n",
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
runtime.error(`Gateway not reachable: ${String(err)}`);
|
||||
@@ -291,7 +291,7 @@ export async function providersStatusCommand(
|
||||
const mode = cfg.gateway?.mode === "remote" ? "remote" : "local";
|
||||
runtime.log(
|
||||
(
|
||||
await formatConfigProvidersStatusLines(cfg, {
|
||||
await formatConfigChannelsStatusLines(cfg, {
|
||||
path: snapshot.path,
|
||||
mode,
|
||||
})
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
text as clackText,
|
||||
} from "@clack/prompts";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { listChatChannels } from "../channels/registry.js";
|
||||
import type { ClawdbotConfig, GatewayAuthConfig } from "../config/config.js";
|
||||
import {
|
||||
CONFIG_PATH_CLAWDBOT,
|
||||
@@ -26,7 +27,6 @@ import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
||||
import { findTailscaleBinary } from "../infra/tailscale.js";
|
||||
import { listChatProviders } from "../providers/registry.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
import { healthCommand } from "./health.js";
|
||||
import { formatHealthCheckFailure } from "./health-format.js";
|
||||
import { applyPrimaryModel, promptDefaultModel } from "./model-picker.js";
|
||||
import { setupChannels } from "./onboard-channels.js";
|
||||
import {
|
||||
applyWizardMetadata,
|
||||
DEFAULT_WORKSPACE,
|
||||
@@ -65,7 +66,6 @@ import {
|
||||
resolveControlUiLinks,
|
||||
summarizeExistingConfig,
|
||||
} from "./onboard-helpers.js";
|
||||
import { setupProviders } from "./onboard-providers.js";
|
||||
import { promptRemoteGatewayConfig } from "./onboard-remote.js";
|
||||
import { setupSkills } from "./onboard-skills.js";
|
||||
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
||||
@@ -75,14 +75,14 @@ export const CONFIGURE_WIZARD_SECTIONS = [
|
||||
"model",
|
||||
"gateway",
|
||||
"daemon",
|
||||
"providers",
|
||||
"channels",
|
||||
"skills",
|
||||
"health",
|
||||
] as const;
|
||||
|
||||
export type WizardSection = (typeof CONFIGURE_WIZARD_SECTIONS)[number];
|
||||
|
||||
type ProvidersWizardMode = "configure" | "remove";
|
||||
type ChannelsWizardMode = "configure" | "remove";
|
||||
|
||||
type ConfigureWizardParams = {
|
||||
command: "configure" | "update";
|
||||
@@ -140,8 +140,8 @@ const CONFIGURE_SECTION_OPTIONS: {
|
||||
hint: "Install/manage the background service",
|
||||
},
|
||||
{
|
||||
value: "providers",
|
||||
label: "Providers",
|
||||
value: "channels",
|
||||
label: "Channels",
|
||||
hint: "Link WhatsApp/Telegram/etc and defaults",
|
||||
},
|
||||
{
|
||||
@@ -152,7 +152,7 @@ const CONFIGURE_SECTION_OPTIONS: {
|
||||
{
|
||||
value: "health",
|
||||
label: "Health check",
|
||||
hint: "Run gateway + provider checks",
|
||||
hint: "Run gateway + channel checks",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -570,34 +570,31 @@ async function maybeInstallDaemon(params: {
|
||||
}
|
||||
}
|
||||
|
||||
async function removeProviderConfigWizard(
|
||||
async function removeChannelConfigWizard(
|
||||
cfg: ClawdbotConfig,
|
||||
runtime: RuntimeEnv,
|
||||
): Promise<ClawdbotConfig> {
|
||||
let next = { ...cfg };
|
||||
|
||||
const listConfiguredProviders = () =>
|
||||
listChatProviders().filter((meta) => {
|
||||
const value = (next as Record<string, unknown>)[meta.id];
|
||||
return value !== undefined;
|
||||
});
|
||||
const listConfiguredChannels = () =>
|
||||
listChatChannels().filter((meta) => next.channels?.[meta.id] !== undefined);
|
||||
|
||||
while (true) {
|
||||
const configured = listConfiguredProviders();
|
||||
const configured = listConfiguredChannels();
|
||||
if (configured.length === 0) {
|
||||
note(
|
||||
[
|
||||
"No provider config found in clawdbot.json.",
|
||||
"Tip: `clawdbot providers status` shows what is configured and enabled.",
|
||||
"No channel config found in clawdbot.json.",
|
||||
"Tip: `clawdbot channels status` shows what is configured and enabled.",
|
||||
].join("\n"),
|
||||
"Remove provider",
|
||||
"Remove channel",
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
const provider = guardCancel(
|
||||
const channel = guardCancel(
|
||||
await select({
|
||||
message: "Remove which provider config?",
|
||||
message: "Remove which channel config?",
|
||||
options: [
|
||||
...configured.map((meta) => ({
|
||||
value: meta.id,
|
||||
@@ -610,11 +607,10 @@ async function removeProviderConfigWizard(
|
||||
runtime,
|
||||
) as string;
|
||||
|
||||
if (provider === "done") return next;
|
||||
if (channel === "done") return next;
|
||||
|
||||
const label =
|
||||
listChatProviders().find((meta) => meta.id === provider)?.label ??
|
||||
provider;
|
||||
listChatChannels().find((meta) => meta.id === channel)?.label ?? channel;
|
||||
const confirmed = guardCancel(
|
||||
await confirm({
|
||||
message: `Delete ${label} configuration from ${CONFIG_PATH_CLAWDBOT}?`,
|
||||
@@ -624,16 +620,21 @@ async function removeProviderConfigWizard(
|
||||
);
|
||||
if (!confirmed) continue;
|
||||
|
||||
const clone = { ...next } as Record<string, unknown>;
|
||||
delete clone[provider];
|
||||
next = clone as ClawdbotConfig;
|
||||
const nextChannels: Record<string, unknown> = { ...(next.channels ?? {}) };
|
||||
delete nextChannels[channel];
|
||||
next = {
|
||||
...next,
|
||||
channels: Object.keys(nextChannels).length
|
||||
? (nextChannels as ClawdbotConfig["channels"])
|
||||
: undefined,
|
||||
};
|
||||
|
||||
note(
|
||||
[
|
||||
`${label} removed from config.`,
|
||||
"Note: credentials/sessions on disk are unchanged.",
|
||||
].join("\n"),
|
||||
"Provider removed",
|
||||
"Channel removed",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -794,34 +795,34 @@ export async function runConfigureWizard(
|
||||
gatewayToken = gateway.token;
|
||||
}
|
||||
|
||||
if (selected.includes("providers")) {
|
||||
const providerMode = guardCancel(
|
||||
if (selected.includes("channels")) {
|
||||
const channelMode = guardCancel(
|
||||
await select({
|
||||
message: "Providers",
|
||||
message: "Channels",
|
||||
options: [
|
||||
{
|
||||
value: "configure",
|
||||
label: "Configure/link",
|
||||
hint: "Add/update providers; disable unselected accounts",
|
||||
hint: "Add/update channels; disable unselected accounts",
|
||||
},
|
||||
{
|
||||
value: "remove",
|
||||
label: "Remove provider config",
|
||||
hint: "Delete provider tokens/settings from clawdbot.json",
|
||||
label: "Remove channel config",
|
||||
hint: "Delete channel tokens/settings from clawdbot.json",
|
||||
},
|
||||
],
|
||||
initialValue: "configure",
|
||||
}),
|
||||
runtime,
|
||||
) as ProvidersWizardMode;
|
||||
) as ChannelsWizardMode;
|
||||
|
||||
if (providerMode === "configure") {
|
||||
nextConfig = await setupProviders(nextConfig, runtime, prompter, {
|
||||
if (channelMode === "configure") {
|
||||
nextConfig = await setupChannels(nextConfig, runtime, prompter, {
|
||||
allowDisable: true,
|
||||
allowSignalInstall: true,
|
||||
});
|
||||
} else {
|
||||
nextConfig = await removeProviderConfigWizard(nextConfig, runtime);
|
||||
nextConfig = await removeChannelConfigWizard(nextConfig, runtime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -917,34 +918,34 @@ export async function runConfigureWizard(
|
||||
await persistConfig();
|
||||
}
|
||||
|
||||
if (choice === "providers") {
|
||||
const providerMode = guardCancel(
|
||||
if (choice === "channels") {
|
||||
const channelMode = guardCancel(
|
||||
await select({
|
||||
message: "Providers",
|
||||
message: "Channels",
|
||||
options: [
|
||||
{
|
||||
value: "configure",
|
||||
label: "Configure/link",
|
||||
hint: "Add/update providers; disable unselected accounts",
|
||||
hint: "Add/update channels; disable unselected accounts",
|
||||
},
|
||||
{
|
||||
value: "remove",
|
||||
label: "Remove provider config",
|
||||
hint: "Delete provider tokens/settings from clawdbot.json",
|
||||
label: "Remove channel config",
|
||||
hint: "Delete channel tokens/settings from clawdbot.json",
|
||||
},
|
||||
],
|
||||
initialValue: "configure",
|
||||
}),
|
||||
runtime,
|
||||
) as ProvidersWizardMode;
|
||||
) as ChannelsWizardMode;
|
||||
|
||||
if (providerMode === "configure") {
|
||||
nextConfig = await setupProviders(nextConfig, runtime, prompter, {
|
||||
if (channelMode === "configure") {
|
||||
nextConfig = await setupChannels(nextConfig, runtime, prompter, {
|
||||
allowDisable: true,
|
||||
allowSignalInstall: true,
|
||||
});
|
||||
} else {
|
||||
nextConfig = await removeProviderConfigWizard(nextConfig, runtime);
|
||||
nextConfig = await removeChannelConfigWizard(nextConfig, runtime);
|
||||
}
|
||||
await persistConfig();
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
||||
|
||||
const legacyAckReaction = cfg.messages?.ackReaction?.trim();
|
||||
if (legacyAckReaction) {
|
||||
const hasWhatsAppAck = cfg.whatsapp?.ackReaction !== undefined;
|
||||
const hasWhatsAppAck = cfg.channels?.whatsapp?.ackReaction !== undefined;
|
||||
if (!hasWhatsAppAck) {
|
||||
const legacyScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
let direct = true;
|
||||
@@ -268,13 +268,16 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
||||
}
|
||||
next = {
|
||||
...next,
|
||||
whatsapp: {
|
||||
...next.whatsapp,
|
||||
ackReaction: { emoji: legacyAckReaction, direct, group },
|
||||
channels: {
|
||||
...next.channels,
|
||||
whatsapp: {
|
||||
...next.channels?.whatsapp,
|
||||
ackReaction: { emoji: legacyAckReaction, direct, group },
|
||||
},
|
||||
},
|
||||
};
|
||||
changes.push(
|
||||
`Copied messages.ackReaction → whatsapp.ackReaction (scope: ${legacyScope}).`,
|
||||
`Copied messages.ackReaction → channels.whatsapp.ackReaction (scope: ${legacyScope}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { readProviderAllowFromStore } from "../pairing/pairing-store.js";
|
||||
import { resolveProviderDefaultAccountId } from "../providers/plugins/helpers.js";
|
||||
import { listProviderPlugins } from "../providers/plugins/index.js";
|
||||
import type { ProviderId } from "../providers/plugins/types.js";
|
||||
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
|
||||
export async function noteSecurityWarnings(cfg: ClawdbotConfig) {
|
||||
@@ -10,7 +10,7 @@ export async function noteSecurityWarnings(cfg: ClawdbotConfig) {
|
||||
|
||||
const warnDmPolicy = async (params: {
|
||||
label: string;
|
||||
provider: ProviderId;
|
||||
provider: ChannelId;
|
||||
dmPolicy: string;
|
||||
allowFrom?: Array<string | number> | null;
|
||||
policyPath?: string;
|
||||
@@ -24,7 +24,7 @@ export async function noteSecurityWarnings(cfg: ClawdbotConfig) {
|
||||
String(v).trim(),
|
||||
);
|
||||
const hasWildcard = configAllowFrom.includes("*");
|
||||
const storeAllowFrom = await readProviderAllowFromStore(
|
||||
const storeAllowFrom = await readChannelAllowFromStore(
|
||||
params.provider,
|
||||
).catch(() => []);
|
||||
const normalizedCfg = configAllowFrom
|
||||
@@ -68,10 +68,10 @@ export async function noteSecurityWarnings(cfg: ClawdbotConfig) {
|
||||
}
|
||||
};
|
||||
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
if (!plugin.security) continue;
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveProviderDefaultAccountId({
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
|
||||
@@ -59,7 +59,7 @@ beforeEach(() => {
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({
|
||||
config: raw as Record<string, unknown>,
|
||||
changes: ["Moved routing.allowFrom → whatsapp.allowFrom."],
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
}));
|
||||
findLegacyGatewayServices.mockReset().mockResolvedValue([]);
|
||||
uninstallLegacyGatewayServices.mockReset().mockResolvedValue([]);
|
||||
@@ -122,7 +122,7 @@ const runGatewayUpdate = vi.fn().mockResolvedValue({
|
||||
});
|
||||
const migrateLegacyConfig = vi.fn((raw: unknown) => ({
|
||||
config: raw as Record<string, unknown>,
|
||||
changes: ["Moved routing.allowFrom → whatsapp.allowFrom."],
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
}));
|
||||
|
||||
const runExec = vi.fn().mockResolvedValue({ stdout: "", stderr: "" });
|
||||
@@ -253,7 +253,7 @@ vi.mock("../telegram/pairing-store.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../pairing/pairing-store.js", () => ({
|
||||
readProviderAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
vi.mock("../telegram/token.js", () => ({
|
||||
@@ -324,7 +324,7 @@ vi.mock("./doctor-state-migrations.js", () => ({
|
||||
|
||||
describe("doctor", () => {
|
||||
it(
|
||||
"migrates routing.allowFrom to whatsapp.allowFrom",
|
||||
"migrates routing.allowFrom to channels.whatsapp.allowFrom",
|
||||
{ timeout: 30_000 },
|
||||
async () => {
|
||||
readConfigFileSnapshot.mockResolvedValue({
|
||||
@@ -356,8 +356,8 @@ describe("doctor", () => {
|
||||
};
|
||||
|
||||
migrateLegacyConfig.mockReturnValue({
|
||||
config: { whatsapp: { allowFrom: ["+15555550123"] } },
|
||||
changes: ["Moved routing.allowFrom → whatsapp.allowFrom."],
|
||||
config: { channels: { whatsapp: { allowFrom: ["+15555550123"] } } },
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
});
|
||||
|
||||
await doctorCommand(runtime, { nonInteractive: true });
|
||||
@@ -367,9 +367,9 @@ describe("doctor", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect((written.whatsapp as Record<string, unknown>)?.allowFrom).toEqual([
|
||||
"+15555550123",
|
||||
]);
|
||||
expect((written.channels as Record<string, unknown>)?.whatsapp).toEqual({
|
||||
allowFrom: ["+15555550123"],
|
||||
});
|
||||
expect(written.routing).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -33,9 +33,9 @@ import {
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
import { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
|
||||
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
||||
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
|
||||
import { collectProvidersStatusIssues } from "../infra/providers-status-issues.js";
|
||||
import { runGatewayUpdate } from "../infra/update-runner.js";
|
||||
import { loadClawdbotPlugins } from "../plugins/loader.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
@@ -281,7 +281,7 @@ export async function doctorCommand(
|
||||
initialValue: true,
|
||||
});
|
||||
if (migrate) {
|
||||
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into whatsapp.allowFrom.
|
||||
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into channels.whatsapp.allowFrom.
|
||||
const { config: migrated, changes } = migrateLegacyConfig(
|
||||
snapshot.parsed,
|
||||
);
|
||||
@@ -555,20 +555,20 @@ export async function doctorCommand(
|
||||
if (healthOk) {
|
||||
try {
|
||||
const status = await callGateway<Record<string, unknown>>({
|
||||
method: "providers.status",
|
||||
method: "channels.status",
|
||||
params: { probe: true, timeoutMs: 5000 },
|
||||
timeoutMs: 6000,
|
||||
});
|
||||
const issues = collectProvidersStatusIssues(status);
|
||||
const issues = collectChannelStatusIssues(status);
|
||||
if (issues.length > 0) {
|
||||
note(
|
||||
issues
|
||||
.map(
|
||||
(issue) =>
|
||||
`- ${issue.provider} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`,
|
||||
`- ${issue.channel} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`,
|
||||
)
|
||||
.join("\n"),
|
||||
"Provider warnings",
|
||||
"Channel warnings",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("healthCommand (coverage)", () => {
|
||||
ok: true,
|
||||
ts: Date.now(),
|
||||
durationMs: 5,
|
||||
providers: {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
linked: true,
|
||||
authAgeMs: 5 * 60_000,
|
||||
@@ -50,8 +50,8 @@ describe("healthCommand (coverage)", () => {
|
||||
configured: false,
|
||||
},
|
||||
},
|
||||
providerOrder: ["whatsapp", "telegram", "discord"],
|
||||
providerLabels: {
|
||||
channelOrder: ["whatsapp", "telegram", "discord"],
|
||||
channelLabels: {
|
||||
whatsapp: "WhatsApp",
|
||||
telegram: "Telegram",
|
||||
discord: "Discord",
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("getHealthSnapshot", () => {
|
||||
timeoutMs: 10,
|
||||
})) satisfies HealthSummary;
|
||||
expect(snap.ok).toBe(true);
|
||||
const telegram = snap.providers.telegram as {
|
||||
const telegram = snap.channels.telegram as {
|
||||
configured?: boolean;
|
||||
probe?: unknown;
|
||||
};
|
||||
@@ -65,7 +65,7 @@ describe("getHealthSnapshot", () => {
|
||||
});
|
||||
|
||||
it("probes telegram getMe + webhook info when configured", async () => {
|
||||
testConfig = { telegram: { botToken: "t-1" } };
|
||||
testConfig = { channels: { telegram: { botToken: "t-1" } } };
|
||||
testStore = {};
|
||||
vi.stubEnv("DISCORD_BOT_TOKEN", "");
|
||||
|
||||
@@ -106,7 +106,7 @@ describe("getHealthSnapshot", () => {
|
||||
);
|
||||
|
||||
const snap = await getHealthSnapshot({ timeoutMs: 25 });
|
||||
const telegram = snap.providers.telegram as {
|
||||
const telegram = snap.channels.telegram as {
|
||||
configured?: boolean;
|
||||
probe?: {
|
||||
ok?: boolean;
|
||||
@@ -126,7 +126,7 @@ describe("getHealthSnapshot", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-health-"));
|
||||
const tokenFile = path.join(tmpDir, "telegram-token");
|
||||
fs.writeFileSync(tokenFile, "t-file\n", "utf-8");
|
||||
testConfig = { telegram: { tokenFile } };
|
||||
testConfig = { channels: { telegram: { tokenFile } } };
|
||||
testStore = {};
|
||||
vi.stubEnv("TELEGRAM_BOT_TOKEN", "");
|
||||
|
||||
@@ -167,7 +167,7 @@ describe("getHealthSnapshot", () => {
|
||||
);
|
||||
|
||||
const snap = await getHealthSnapshot({ timeoutMs: 25 });
|
||||
const telegram = snap.providers.telegram as {
|
||||
const telegram = snap.channels.telegram as {
|
||||
configured?: boolean;
|
||||
probe?: { ok?: boolean };
|
||||
};
|
||||
@@ -179,7 +179,7 @@ describe("getHealthSnapshot", () => {
|
||||
});
|
||||
|
||||
it("returns a structured telegram probe error when getMe fails", async () => {
|
||||
testConfig = { telegram: { botToken: "bad-token" } };
|
||||
testConfig = { channels: { telegram: { botToken: "bad-token" } } };
|
||||
testStore = {};
|
||||
vi.stubEnv("DISCORD_BOT_TOKEN", "");
|
||||
|
||||
@@ -198,7 +198,7 @@ describe("getHealthSnapshot", () => {
|
||||
);
|
||||
|
||||
const snap = await getHealthSnapshot({ timeoutMs: 25 });
|
||||
const telegram = snap.providers.telegram as {
|
||||
const telegram = snap.channels.telegram as {
|
||||
configured?: boolean;
|
||||
probe?: { ok?: boolean; status?: number; error?: string };
|
||||
};
|
||||
@@ -209,7 +209,7 @@ describe("getHealthSnapshot", () => {
|
||||
});
|
||||
|
||||
it("captures unexpected probe exceptions as errors", async () => {
|
||||
testConfig = { telegram: { botToken: "t-err" } };
|
||||
testConfig = { channels: { telegram: { botToken: "t-err" } } };
|
||||
testStore = {};
|
||||
vi.stubEnv("DISCORD_BOT_TOKEN", "");
|
||||
|
||||
@@ -221,7 +221,7 @@ describe("getHealthSnapshot", () => {
|
||||
);
|
||||
|
||||
const snap = await getHealthSnapshot({ timeoutMs: 25 });
|
||||
const telegram = snap.providers.telegram as {
|
||||
const telegram = snap.channels.telegram as {
|
||||
configured?: boolean;
|
||||
probe?: { ok?: boolean; error?: string };
|
||||
};
|
||||
|
||||
@@ -24,13 +24,13 @@ describe("healthCommand", () => {
|
||||
ok: true,
|
||||
ts: Date.now(),
|
||||
durationMs: 5,
|
||||
providers: {
|
||||
channels: {
|
||||
whatsapp: { linked: true, authAgeMs: 5000 },
|
||||
telegram: { configured: true, probe: { ok: true, elapsedMs: 1 } },
|
||||
discord: { configured: false },
|
||||
},
|
||||
providerOrder: ["whatsapp", "telegram", "discord"],
|
||||
providerLabels: {
|
||||
channelOrder: ["whatsapp", "telegram", "discord"],
|
||||
channelLabels: {
|
||||
whatsapp: "WhatsApp",
|
||||
telegram: "Telegram",
|
||||
discord: "Discord",
|
||||
@@ -49,8 +49,8 @@ describe("healthCommand", () => {
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
const logged = runtime.log.mock.calls[0]?.[0] as string;
|
||||
const parsed = JSON.parse(logged) as HealthSummary;
|
||||
expect(parsed.providers.whatsapp?.linked).toBe(true);
|
||||
expect(parsed.providers.telegram?.configured).toBe(true);
|
||||
expect(parsed.channels.whatsapp?.linked).toBe(true);
|
||||
expect(parsed.channels.telegram?.configured).toBe(true);
|
||||
expect(parsed.sessions.count).toBe(1);
|
||||
});
|
||||
|
||||
@@ -59,13 +59,13 @@ describe("healthCommand", () => {
|
||||
ok: true,
|
||||
ts: Date.now(),
|
||||
durationMs: 5,
|
||||
providers: {
|
||||
channels: {
|
||||
whatsapp: { linked: false, authAgeMs: null },
|
||||
telegram: { configured: false },
|
||||
discord: { configured: false },
|
||||
},
|
||||
providerOrder: ["whatsapp", "telegram", "discord"],
|
||||
providerLabels: {
|
||||
channelOrder: ["whatsapp", "telegram", "discord"],
|
||||
channelLabels: {
|
||||
whatsapp: "WhatsApp",
|
||||
telegram: "Telegram",
|
||||
discord: "Discord",
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import {
|
||||
getChannelPlugin,
|
||||
listChannelPlugins,
|
||||
} from "../channels/plugins/index.js";
|
||||
import type { ChannelAccountSnapshot } from "../channels/plugins/types.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
import { info } from "../globals.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { resolveProviderDefaultAccountId } from "../providers/plugins/helpers.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
listProviderPlugins,
|
||||
} from "../providers/plugins/index.js";
|
||||
import type { ProviderAccountSnapshot } from "../providers/plugins/types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
||||
|
||||
export type ProviderHealthSummary = {
|
||||
export type ChannelHealthSummary = {
|
||||
configured?: boolean;
|
||||
linked?: boolean;
|
||||
authAgeMs?: number | null;
|
||||
@@ -31,9 +31,9 @@ export type HealthSummary = {
|
||||
ok: true;
|
||||
ts: number;
|
||||
durationMs: number;
|
||||
providers: Record<string, ProviderHealthSummary>;
|
||||
providerOrder: string[];
|
||||
providerLabels: Record<string, string>;
|
||||
channels: Record<string, ChannelHealthSummary>;
|
||||
channelOrder: string[];
|
||||
channelLabels: Record<string, string>;
|
||||
heartbeatSeconds: number;
|
||||
sessions: {
|
||||
path: string;
|
||||
@@ -87,29 +87,27 @@ const formatProbeLine = (probe: unknown): string | null => {
|
||||
return label;
|
||||
};
|
||||
|
||||
export const formatHealthProviderLines = (summary: HealthSummary): string[] => {
|
||||
const providers = summary.providers ?? {};
|
||||
const providerOrder =
|
||||
summary.providerOrder?.length > 0
|
||||
? summary.providerOrder
|
||||
: Object.keys(providers);
|
||||
export const formatHealthChannelLines = (summary: HealthSummary): string[] => {
|
||||
const channels = summary.channels ?? {};
|
||||
const channelOrder =
|
||||
summary.channelOrder?.length > 0
|
||||
? summary.channelOrder
|
||||
: Object.keys(channels);
|
||||
|
||||
const lines: string[] = [];
|
||||
for (const providerId of providerOrder) {
|
||||
const providerSummary = providers[providerId];
|
||||
if (!providerSummary) continue;
|
||||
const plugin = getProviderPlugin(providerId as never);
|
||||
for (const channelId of channelOrder) {
|
||||
const channelSummary = channels[channelId];
|
||||
if (!channelSummary) continue;
|
||||
const plugin = getChannelPlugin(channelId as never);
|
||||
const label =
|
||||
summary.providerLabels?.[providerId] ?? plugin?.meta.label ?? providerId;
|
||||
summary.channelLabels?.[channelId] ?? plugin?.meta.label ?? channelId;
|
||||
const linked =
|
||||
typeof providerSummary.linked === "boolean"
|
||||
? providerSummary.linked
|
||||
: null;
|
||||
typeof channelSummary.linked === "boolean" ? channelSummary.linked : null;
|
||||
if (linked !== null) {
|
||||
if (linked) {
|
||||
const authAgeMs =
|
||||
typeof providerSummary.authAgeMs === "number"
|
||||
? providerSummary.authAgeMs
|
||||
typeof channelSummary.authAgeMs === "number"
|
||||
? channelSummary.authAgeMs
|
||||
: null;
|
||||
const authLabel =
|
||||
authAgeMs != null
|
||||
@@ -123,15 +121,15 @@ export const formatHealthProviderLines = (summary: HealthSummary): string[] => {
|
||||
}
|
||||
|
||||
const configured =
|
||||
typeof providerSummary.configured === "boolean"
|
||||
? providerSummary.configured
|
||||
typeof channelSummary.configured === "boolean"
|
||||
? channelSummary.configured
|
||||
: null;
|
||||
if (configured === false) {
|
||||
lines.push(`${label}: not configured`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const probeLine = formatProbeLine(providerSummary.probe);
|
||||
const probeLine = formatProbeLine(channelSummary.probe);
|
||||
if (probeLine) {
|
||||
lines.push(`${label}: ${probeLine}`);
|
||||
continue;
|
||||
@@ -168,14 +166,14 @@ export async function getHealthSnapshot(params?: {
|
||||
const start = Date.now();
|
||||
const cappedTimeout = Math.max(1000, timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
||||
const doProbe = params?.probe !== false;
|
||||
const providers: Record<string, ProviderHealthSummary> = {};
|
||||
const providerOrder = listProviderPlugins().map((plugin) => plugin.id);
|
||||
const providerLabels: Record<string, string> = {};
|
||||
const channels: Record<string, ChannelHealthSummary> = {};
|
||||
const channelOrder = listChannelPlugins().map((plugin) => plugin.id);
|
||||
const channelLabels: Record<string, string> = {};
|
||||
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
providerLabels[plugin.id] = plugin.meta.label ?? plugin.id;
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
channelLabels[plugin.id] = plugin.meta.label ?? plugin.id;
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveProviderDefaultAccountId({
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
@@ -204,7 +202,7 @@ export async function getHealthSnapshot(params?: {
|
||||
}
|
||||
}
|
||||
|
||||
const snapshot: ProviderAccountSnapshot = {
|
||||
const snapshot: ChannelAccountSnapshot = {
|
||||
accountId: defaultAccountId,
|
||||
enabled,
|
||||
configured,
|
||||
@@ -212,8 +210,8 @@ export async function getHealthSnapshot(params?: {
|
||||
if (probe !== undefined) snapshot.probe = probe;
|
||||
if (lastProbeAt) snapshot.lastProbeAt = lastProbeAt;
|
||||
|
||||
const summary = plugin.status?.buildProviderSummary
|
||||
? await plugin.status.buildProviderSummary({
|
||||
const summary = plugin.status?.buildChannelSummary
|
||||
? await plugin.status.buildChannelSummary({
|
||||
account,
|
||||
cfg,
|
||||
defaultAccountId,
|
||||
@@ -222,26 +220,26 @@ export async function getHealthSnapshot(params?: {
|
||||
: undefined;
|
||||
const record =
|
||||
summary && typeof summary === "object"
|
||||
? (summary as ProviderHealthSummary)
|
||||
? (summary as ChannelHealthSummary)
|
||||
: ({
|
||||
configured,
|
||||
probe,
|
||||
lastProbeAt,
|
||||
} satisfies ProviderHealthSummary);
|
||||
} satisfies ChannelHealthSummary);
|
||||
if (record.configured === undefined) record.configured = configured;
|
||||
if (record.lastProbeAt === undefined && lastProbeAt) {
|
||||
record.lastProbeAt = lastProbeAt;
|
||||
}
|
||||
providers[plugin.id] = record;
|
||||
channels[plugin.id] = record;
|
||||
}
|
||||
|
||||
const summary: HealthSummary = {
|
||||
ok: true,
|
||||
ts: Date.now(),
|
||||
durationMs: Date.now() - start,
|
||||
providers,
|
||||
providerOrder,
|
||||
providerLabels,
|
||||
channels,
|
||||
channelOrder,
|
||||
channelLabels,
|
||||
heartbeatSeconds,
|
||||
sessions: {
|
||||
path: storePath,
|
||||
@@ -270,7 +268,7 @@ export async function healthCommand(
|
||||
timeoutMs: opts.timeoutMs,
|
||||
}),
|
||||
);
|
||||
// Gateway reachability defines success; provider issues are reported but not fatal here.
|
||||
// Gateway reachability defines success; channel issues are reported but not fatal here.
|
||||
const fatal = false;
|
||||
|
||||
if (opts.json) {
|
||||
@@ -283,16 +281,16 @@ export async function healthCommand(
|
||||
runtime.log(` ${line}`);
|
||||
}
|
||||
}
|
||||
for (const line of formatHealthProviderLines(summary)) {
|
||||
for (const line of formatHealthChannelLines(summary)) {
|
||||
runtime.log(line);
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
const providerSummary = summary.providers?.[plugin.id];
|
||||
if (!providerSummary || providerSummary.linked !== true) continue;
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
const channelSummary = summary.channels?.[plugin.id];
|
||||
if (!channelSummary || channelSummary.linked !== true) continue;
|
||||
if (!plugin.status?.logSelfId) continue;
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveProviderDefaultAccountId({
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
@@ -302,7 +300,7 @@ export async function healthCommand(
|
||||
account,
|
||||
cfg,
|
||||
runtime,
|
||||
includeProviderPrefix: true,
|
||||
includeChannelPrefix: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||
import type {
|
||||
ChannelId,
|
||||
ChannelMessageActionName,
|
||||
} from "../channels/plugins/types.js";
|
||||
import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
|
||||
import {
|
||||
formatGatewaySummary,
|
||||
formatOutboundDeliverySummary,
|
||||
} from "../infra/outbound/format.js";
|
||||
import type { MessageActionRunResult } from "../infra/outbound/message-action-runner.js";
|
||||
import { getProviderPlugin } from "../providers/plugins/index.js";
|
||||
import type {
|
||||
ProviderId,
|
||||
ProviderMessageActionName,
|
||||
} from "../providers/plugins/types.js";
|
||||
import { renderTable } from "../terminal/table.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
|
||||
@@ -18,8 +18,8 @@ const shortenText = (value: string, maxLen: number) => {
|
||||
return `${chars.slice(0, Math.max(0, maxLen - 1)).join("")}…`;
|
||||
};
|
||||
|
||||
const resolveProviderLabel = (provider: ProviderId) =>
|
||||
getProviderPlugin(provider)?.meta.label ?? provider;
|
||||
const resolveChannelLabel = (channel: ChannelId) =>
|
||||
getChannelPlugin(channel)?.meta.label ?? channel;
|
||||
|
||||
function extractMessageId(payload: unknown): string | null {
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
@@ -34,8 +34,8 @@ function extractMessageId(payload: unknown): string | null {
|
||||
}
|
||||
|
||||
export type MessageCliJsonEnvelope = {
|
||||
action: ProviderMessageActionName;
|
||||
provider: ProviderId;
|
||||
action: ChannelMessageActionName;
|
||||
channel: ChannelId;
|
||||
dryRun: boolean;
|
||||
handledBy: "plugin" | "core" | "dry-run";
|
||||
payload: unknown;
|
||||
@@ -46,7 +46,7 @@ export function buildMessageCliJson(
|
||||
): MessageCliJsonEnvelope {
|
||||
return {
|
||||
action: result.action,
|
||||
provider: result.provider,
|
||||
channel: result.channel,
|
||||
dryRun: result.dryRun,
|
||||
handledBy: result.handledBy,
|
||||
payload: result.payload,
|
||||
@@ -256,7 +256,7 @@ export function formatMessageCliText(result: MessageActionRunResult): string[] {
|
||||
|
||||
if (result.handledBy === "dry-run") {
|
||||
return [
|
||||
muted(`[dry-run] would run ${result.action} via ${result.provider}`),
|
||||
muted(`[dry-run] would run ${result.action} via ${result.channel}`),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -265,20 +265,20 @@ export function formatMessageCliText(result: MessageActionRunResult): string[] {
|
||||
const send = result.sendResult;
|
||||
if (send.via === "direct") {
|
||||
const directResult = send.result as OutboundDeliveryResult | undefined;
|
||||
return [ok(formatOutboundDeliverySummary(send.provider, directResult))];
|
||||
return [ok(formatOutboundDeliverySummary(send.channel, directResult))];
|
||||
}
|
||||
const gatewayResult = send.result as { messageId?: string } | undefined;
|
||||
return [
|
||||
ok(
|
||||
formatGatewaySummary({
|
||||
provider: send.provider,
|
||||
channel: send.channel,
|
||||
messageId: gatewayResult?.messageId ?? null,
|
||||
}),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
const label = resolveProviderLabel(result.provider);
|
||||
const label = resolveChannelLabel(result.channel);
|
||||
const msgId = extractMessageId(result.payload);
|
||||
return [ok(`✅ Sent via ${label}.${msgId ? ` Message ID: ${msgId}` : ""}`)];
|
||||
}
|
||||
@@ -292,7 +292,7 @@ export function formatMessageCliText(result: MessageActionRunResult): string[] {
|
||||
ok(
|
||||
formatGatewaySummary({
|
||||
action: "Poll sent",
|
||||
provider: poll.provider,
|
||||
channel: poll.channel,
|
||||
messageId: msgId,
|
||||
}),
|
||||
),
|
||||
@@ -301,14 +301,14 @@ export function formatMessageCliText(result: MessageActionRunResult): string[] {
|
||||
return lines;
|
||||
}
|
||||
|
||||
const label = resolveProviderLabel(result.provider);
|
||||
const label = resolveChannelLabel(result.channel);
|
||||
const msgId = extractMessageId(result.payload);
|
||||
return [
|
||||
ok(`✅ Poll sent via ${label}.${msgId ? ` Message ID: ${msgId}` : ""}`),
|
||||
];
|
||||
}
|
||||
|
||||
// provider actions (non-send/poll)
|
||||
// channel actions (non-send/poll)
|
||||
const payload = result.payload;
|
||||
const lines: string[] = [];
|
||||
|
||||
@@ -372,7 +372,7 @@ export function formatMessageCliText(result: MessageActionRunResult): string[] {
|
||||
|
||||
// Generic success + compact details table.
|
||||
lines.push(
|
||||
ok(`✅ ${result.action} via ${resolveProviderLabel(result.provider)}.`),
|
||||
ok(`✅ ${result.action} via ${resolveChannelLabel(result.channel)}.`),
|
||||
);
|
||||
const summary = renderObjectSummary(payload, opts);
|
||||
if (summary.length) {
|
||||
|
||||
@@ -83,7 +83,7 @@ const makeDeps = (overrides: Partial<CliDeps> = {}): CliDeps => ({
|
||||
});
|
||||
|
||||
describe("messageCommand", () => {
|
||||
it("defaults provider when only one configured", async () => {
|
||||
it("defaults channel when only one configured", async () => {
|
||||
process.env.TELEGRAM_BOT_TOKEN = "token-abc";
|
||||
const deps = makeDeps();
|
||||
await messageCommand(
|
||||
@@ -97,7 +97,7 @@ describe("messageCommand", () => {
|
||||
expect(handleTelegramAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires provider when multiple configured", async () => {
|
||||
it("requires channel when multiple configured", async () => {
|
||||
process.env.TELEGRAM_BOT_TOKEN = "token-abc";
|
||||
process.env.DISCORD_BOT_TOKEN = "token-discord";
|
||||
const deps = makeDeps();
|
||||
@@ -110,7 +110,7 @@ describe("messageCommand", () => {
|
||||
deps,
|
||||
runtime,
|
||||
),
|
||||
).rejects.toThrow(/Provider is required/);
|
||||
).rejects.toThrow(/Channel is required/);
|
||||
});
|
||||
|
||||
it("sends via gateway for WhatsApp", async () => {
|
||||
@@ -119,7 +119,7 @@ describe("messageCommand", () => {
|
||||
await messageCommand(
|
||||
{
|
||||
action: "send",
|
||||
provider: "whatsapp",
|
||||
channel: "whatsapp",
|
||||
to: "+1",
|
||||
message: "hi",
|
||||
},
|
||||
@@ -134,7 +134,7 @@ describe("messageCommand", () => {
|
||||
await messageCommand(
|
||||
{
|
||||
action: "poll",
|
||||
provider: "discord",
|
||||
channel: "discord",
|
||||
to: "channel:123",
|
||||
pollQuestion: "Snack?",
|
||||
pollOption: ["Pizza", "Sushi"],
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {
|
||||
CHANNEL_MESSAGE_ACTION_NAMES,
|
||||
type ChannelMessageActionName,
|
||||
} from "../channels/plugins/types.js";
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { OutboundSendDeps } from "../infra/outbound/deliver.js";
|
||||
import { runMessageAction } from "../infra/outbound/message-action-runner.js";
|
||||
import {
|
||||
PROVIDER_MESSAGE_ACTION_NAMES,
|
||||
type ProviderMessageActionName,
|
||||
} from "../providers/plugins/types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import { buildMessageCliJson, formatMessageCliText } from "./message-format.js";
|
||||
|
||||
export async function messageCommand(
|
||||
@@ -22,8 +22,8 @@ export async function messageCommand(
|
||||
const cfg = loadConfig();
|
||||
const rawAction =
|
||||
typeof opts.action === "string" ? opts.action.trim().toLowerCase() : "";
|
||||
const action = (rawAction || "send") as ProviderMessageActionName;
|
||||
if (!(PROVIDER_MESSAGE_ACTION_NAMES as readonly string[]).includes(action)) {
|
||||
const action = (rawAction || "send") as ChannelMessageActionName;
|
||||
if (!(CHANNEL_MESSAGE_ACTION_NAMES as readonly string[]).includes(action)) {
|
||||
throw new Error(`Unknown message action: ${action}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { setupProviders } from "./onboard-providers.js";
|
||||
import { setupChannels } from "./onboard-channels.js";
|
||||
|
||||
vi.mock("node:fs/promises", () => ({
|
||||
default: {
|
||||
@@ -13,7 +13,7 @@ vi.mock("node:fs/promises", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../provider-web.js", () => ({
|
||||
vi.mock("../channel-web.js", () => ({
|
||||
loginWeb: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
@@ -21,7 +21,7 @@ vi.mock("./onboard-helpers.js", () => ({
|
||||
detectBinary: vi.fn(async () => false),
|
||||
}));
|
||||
|
||||
describe("setupProviders", () => {
|
||||
describe("setupChannels", () => {
|
||||
it("QuickStart uses single-select (no multiselect) and doesn't prompt for Telegram token when WhatsApp is chosen", async () => {
|
||||
const select = vi.fn(async () => "whatsapp");
|
||||
const multiselect = vi.fn(async () => {
|
||||
@@ -56,14 +56,14 @@ describe("setupProviders", () => {
|
||||
}),
|
||||
};
|
||||
|
||||
await setupProviders({} as ClawdbotConfig, runtime, prompter, {
|
||||
await setupChannels({} as ClawdbotConfig, runtime, prompter, {
|
||||
skipConfirm: true,
|
||||
quickstartDefaults: true,
|
||||
forceAllowFromProviders: ["whatsapp"],
|
||||
forceAllowFromChannels: ["whatsapp"],
|
||||
});
|
||||
|
||||
expect(select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: "Select provider (QuickStart)" }),
|
||||
expect.objectContaining({ message: "Select channel (QuickStart)" }),
|
||||
);
|
||||
expect(multiselect).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1,63 +1,63 @@
|
||||
import {
|
||||
formatChannelPrimerLine,
|
||||
formatChannelSelectionLine,
|
||||
getChatChannelMeta,
|
||||
listChatChannels,
|
||||
} from "../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { DmPolicy } from "../config/types.js";
|
||||
import {
|
||||
formatProviderPrimerLine,
|
||||
formatProviderSelectionLine,
|
||||
getChatProviderMeta,
|
||||
listChatProviders,
|
||||
} from "../providers/registry.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import type { ProviderChoice } from "./onboard-types.js";
|
||||
import type { ChannelChoice } from "./onboard-types.js";
|
||||
import {
|
||||
getProviderOnboardingAdapter,
|
||||
listProviderOnboardingAdapters,
|
||||
getChannelOnboardingAdapter,
|
||||
listChannelOnboardingAdapters,
|
||||
} from "./onboarding/registry.js";
|
||||
import type {
|
||||
ProviderOnboardingDmPolicy,
|
||||
SetupProvidersOptions,
|
||||
ChannelOnboardingDmPolicy,
|
||||
SetupChannelsOptions,
|
||||
} from "./onboarding/types.js";
|
||||
|
||||
async function noteProviderPrimer(prompter: WizardPrompter): Promise<void> {
|
||||
const providerLines = listChatProviders().map((meta) =>
|
||||
formatProviderPrimerLine(meta),
|
||||
async function noteChannelPrimer(prompter: WizardPrompter): Promise<void> {
|
||||
const channelLines = listChatChannels().map((meta) =>
|
||||
formatChannelPrimerLine(meta),
|
||||
);
|
||||
await prompter.note(
|
||||
[
|
||||
"DM security: default is pairing; unknown DMs get a pairing code.",
|
||||
"Approve with: clawdbot pairing approve <provider> <code>",
|
||||
"Approve with: clawdbot pairing approve <channel> <code>",
|
||||
'Public DMs require dmPolicy="open" + allowFrom=["*"].',
|
||||
`Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`,
|
||||
"",
|
||||
...providerLines,
|
||||
...channelLines,
|
||||
].join("\n"),
|
||||
"How providers work",
|
||||
"How channels work",
|
||||
);
|
||||
}
|
||||
|
||||
function resolveQuickstartDefault(
|
||||
statusByProvider: Map<ProviderChoice, { quickstartScore?: number }>,
|
||||
): ProviderChoice | undefined {
|
||||
let best: { provider: ProviderChoice; score: number } | null = null;
|
||||
for (const [provider, status] of statusByProvider) {
|
||||
statusByChannel: Map<ChannelChoice, { quickstartScore?: number }>,
|
||||
): ChannelChoice | undefined {
|
||||
let best: { channel: ChannelChoice; score: number } | null = null;
|
||||
for (const [channel, status] of statusByChannel) {
|
||||
if (status.quickstartScore == null) continue;
|
||||
if (!best || status.quickstartScore > best.score) {
|
||||
best = { provider, score: status.quickstartScore };
|
||||
best = { channel, score: status.quickstartScore };
|
||||
}
|
||||
}
|
||||
return best?.provider;
|
||||
return best?.channel;
|
||||
}
|
||||
|
||||
async function maybeConfigureDmPolicies(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
selection: ProviderChoice[];
|
||||
selection: ChannelChoice[];
|
||||
prompter: WizardPrompter;
|
||||
}): Promise<ClawdbotConfig> {
|
||||
const { selection, prompter } = params;
|
||||
const dmPolicies = selection
|
||||
.map((provider) => getProviderOnboardingAdapter(provider)?.dmPolicy)
|
||||
.filter(Boolean) as ProviderOnboardingDmPolicy[];
|
||||
.map((channel) => getChannelOnboardingAdapter(channel)?.dmPolicy)
|
||||
.filter(Boolean) as ChannelOnboardingDmPolicy[];
|
||||
if (dmPolicies.length === 0) return params.cfg;
|
||||
|
||||
const wants = await prompter.confirm({
|
||||
@@ -67,11 +67,11 @@ async function maybeConfigureDmPolicies(params: {
|
||||
if (!wants) return params.cfg;
|
||||
|
||||
let cfg = params.cfg;
|
||||
const selectPolicy = async (policy: ProviderOnboardingDmPolicy) => {
|
||||
const selectPolicy = async (policy: ChannelOnboardingDmPolicy) => {
|
||||
await prompter.note(
|
||||
[
|
||||
"Default: pairing (unknown DMs get a pairing code).",
|
||||
`Approve: clawdbot pairing approve ${policy.provider} <code>`,
|
||||
`Approve: clawdbot pairing approve ${policy.channel} <code>`,
|
||||
`Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`,
|
||||
`Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`,
|
||||
].join("\n"),
|
||||
@@ -98,18 +98,16 @@ async function maybeConfigureDmPolicies(params: {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Provider-specific prompts moved into onboarding adapters.
|
||||
// Channel-specific prompts moved into onboarding adapters.
|
||||
|
||||
export async function setupProviders(
|
||||
export async function setupChannels(
|
||||
cfg: ClawdbotConfig,
|
||||
runtime: RuntimeEnv,
|
||||
prompter: WizardPrompter,
|
||||
options?: SetupProvidersOptions,
|
||||
options?: SetupChannelsOptions,
|
||||
): Promise<ClawdbotConfig> {
|
||||
const forceAllowFromProviders = new Set(
|
||||
options?.forceAllowFromProviders ?? [],
|
||||
);
|
||||
const accountOverrides: Partial<Record<ProviderChoice, string>> = {
|
||||
const forceAllowFromChannels = new Set(options?.forceAllowFromChannels ?? []);
|
||||
const accountOverrides: Partial<Record<ChannelChoice, string>> = {
|
||||
...options?.accountIds,
|
||||
};
|
||||
if (options?.whatsappAccountId?.trim()) {
|
||||
@@ -117,30 +115,30 @@ export async function setupProviders(
|
||||
}
|
||||
|
||||
const statusEntries = await Promise.all(
|
||||
listProviderOnboardingAdapters().map((adapter) =>
|
||||
listChannelOnboardingAdapters().map((adapter) =>
|
||||
adapter.getStatus({ cfg, options, accountOverrides }),
|
||||
),
|
||||
);
|
||||
const statusByProvider = new Map(
|
||||
statusEntries.map((entry) => [entry.provider, entry]),
|
||||
const statusByChannel = new Map(
|
||||
statusEntries.map((entry) => [entry.channel, entry]),
|
||||
);
|
||||
const statusLines = statusEntries.flatMap((entry) => entry.statusLines);
|
||||
if (statusLines.length > 0) {
|
||||
await prompter.note(statusLines.join("\n"), "Provider status");
|
||||
await prompter.note(statusLines.join("\n"), "Channel status");
|
||||
}
|
||||
|
||||
const shouldConfigure = options?.skipConfirm
|
||||
? true
|
||||
: await prompter.confirm({
|
||||
message: "Configure chat providers now?",
|
||||
message: "Configure chat channels now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!shouldConfigure) return cfg;
|
||||
|
||||
await noteProviderPrimer(prompter);
|
||||
await noteChannelPrimer(prompter);
|
||||
|
||||
const selectionOptions = listChatProviders().map((meta) => {
|
||||
const status = statusByProvider.get(meta.id as ProviderChoice);
|
||||
const selectionOptions = listChatChannels().map((meta) => {
|
||||
const status = statusByChannel.get(meta.id as ChannelChoice);
|
||||
return {
|
||||
value: meta.id,
|
||||
label: meta.selectionLabel,
|
||||
@@ -149,58 +147,57 @@ export async function setupProviders(
|
||||
});
|
||||
|
||||
const quickstartDefault =
|
||||
options?.initialSelection?.[0] ??
|
||||
resolveQuickstartDefault(statusByProvider);
|
||||
options?.initialSelection?.[0] ?? resolveQuickstartDefault(statusByChannel);
|
||||
|
||||
let selection: ProviderChoice[];
|
||||
let selection: ChannelChoice[];
|
||||
if (options?.quickstartDefaults) {
|
||||
const choice = (await prompter.select({
|
||||
message: "Select provider (QuickStart)",
|
||||
message: "Select channel (QuickStart)",
|
||||
options: [
|
||||
...selectionOptions,
|
||||
{
|
||||
value: "__skip__",
|
||||
label: "Skip for now",
|
||||
hint: "You can add providers later via `clawdbot providers add`",
|
||||
hint: "You can add channels later via `clawdbot channels add`",
|
||||
},
|
||||
],
|
||||
initialValue: quickstartDefault,
|
||||
})) as ProviderChoice | "__skip__";
|
||||
})) as ChannelChoice | "__skip__";
|
||||
selection = choice === "__skip__" ? [] : [choice];
|
||||
} else {
|
||||
const initialSelection = options?.initialSelection ?? [];
|
||||
selection = (await prompter.multiselect({
|
||||
message: "Select providers (Space to toggle, Enter to continue)",
|
||||
message: "Select channels (Space to toggle, Enter to continue)",
|
||||
options: selectionOptions,
|
||||
initialValues: initialSelection.length ? initialSelection : undefined,
|
||||
})) as ProviderChoice[];
|
||||
})) as ChannelChoice[];
|
||||
}
|
||||
|
||||
options?.onSelection?.(selection);
|
||||
|
||||
const selectionNotes = new Map(
|
||||
listChatProviders().map((meta) => [
|
||||
listChatChannels().map((meta) => [
|
||||
meta.id,
|
||||
formatProviderSelectionLine(meta, formatDocsLink),
|
||||
formatChannelSelectionLine(meta, formatDocsLink),
|
||||
]),
|
||||
);
|
||||
const selectedLines = selection
|
||||
.map((provider) => selectionNotes.get(provider))
|
||||
.map((channel) => selectionNotes.get(channel))
|
||||
.filter((line): line is string => Boolean(line));
|
||||
if (selectedLines.length > 0) {
|
||||
await prompter.note(selectedLines.join("\n"), "Selected providers");
|
||||
await prompter.note(selectedLines.join("\n"), "Selected channels");
|
||||
}
|
||||
|
||||
const shouldPromptAccountIds = options?.promptAccountIds === true;
|
||||
const recordAccount = (provider: ProviderChoice, accountId: string) => {
|
||||
options?.onAccountId?.(provider, accountId);
|
||||
const adapter = getProviderOnboardingAdapter(provider);
|
||||
const recordAccount = (channel: ChannelChoice, accountId: string) => {
|
||||
options?.onAccountId?.(channel, accountId);
|
||||
const adapter = getChannelOnboardingAdapter(channel);
|
||||
adapter?.onAccountRecorded?.(accountId, options);
|
||||
};
|
||||
|
||||
let next = cfg;
|
||||
for (const provider of selection) {
|
||||
const adapter = getProviderOnboardingAdapter(provider);
|
||||
for (const channel of selection) {
|
||||
const adapter = getChannelOnboardingAdapter(channel);
|
||||
if (!adapter) continue;
|
||||
const result = await adapter.configure({
|
||||
cfg: next,
|
||||
@@ -209,11 +206,11 @@ export async function setupProviders(
|
||||
options,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
forceAllowFrom: forceAllowFromProviders.has(provider),
|
||||
forceAllowFrom: forceAllowFromChannels.has(channel),
|
||||
});
|
||||
next = result.cfg;
|
||||
if (result.accountId) {
|
||||
recordAccount(provider, result.accountId);
|
||||
recordAccount(channel, result.accountId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,14 +219,14 @@ export async function setupProviders(
|
||||
}
|
||||
|
||||
if (options?.allowDisable) {
|
||||
for (const [providerId, status] of statusByProvider) {
|
||||
if (selection.includes(providerId)) continue;
|
||||
for (const [channelId, status] of statusByChannel) {
|
||||
if (selection.includes(channelId)) continue;
|
||||
if (!status.configured) continue;
|
||||
const adapter = getProviderOnboardingAdapter(providerId);
|
||||
const adapter = getChannelOnboardingAdapter(channelId);
|
||||
if (!adapter?.disable) continue;
|
||||
const meta = getChatProviderMeta(providerId);
|
||||
const meta = getChatChannelMeta(channelId);
|
||||
const disable = await prompter.confirm({
|
||||
message: `Disable ${meta.label} provider?`,
|
||||
message: `Disable ${meta.label} channel?`,
|
||||
initialValue: false,
|
||||
});
|
||||
if (disable) {
|
||||
@@ -22,7 +22,7 @@ import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import type {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { rawDataToString } from "../infra/ws.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { rawDataToString } from "../infra/ws.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
|
||||
async function isPortFree(port: number): Promise<boolean> {
|
||||
if (!Number.isFinite(port) || port <= 0 || port > 65535) return false;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChatProviderId } from "../providers/registry.js";
|
||||
import type { ChatChannelId } from "../channels/registry.js";
|
||||
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
||||
|
||||
export type OnboardMode = "local" | "remote";
|
||||
@@ -31,7 +31,9 @@ export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||
export type GatewayBind = "loopback" | "lan" | "auto" | "custom";
|
||||
export type TailscaleMode = "off" | "serve" | "funnel";
|
||||
export type NodeManagerChoice = "npm" | "pnpm" | "bun";
|
||||
export type ProviderChoice = ChatProviderId;
|
||||
export type ChannelChoice = ChatChannelId;
|
||||
// Legacy alias (pre-rename).
|
||||
export type ProviderChoice = ChannelChoice;
|
||||
|
||||
export type OnboardOptions = {
|
||||
mode?: OnboardMode;
|
||||
@@ -66,6 +68,8 @@ export type OnboardOptions = {
|
||||
tailscaleResetOnExit?: boolean;
|
||||
installDaemon?: boolean;
|
||||
daemonRuntime?: GatewayDaemonRuntime;
|
||||
skipChannels?: boolean;
|
||||
/** @deprecated Legacy alias for `skipChannels`. */
|
||||
skipProviders?: boolean;
|
||||
skipSkills?: boolean;
|
||||
skipHealth?: boolean;
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
import { listProviderPlugins } from "../../providers/plugins/index.js";
|
||||
import type { ProviderChoice } from "../onboard-types.js";
|
||||
import type { ProviderOnboardingAdapter } from "./types.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import type { ChannelChoice } from "../onboard-types.js";
|
||||
import type { ChannelOnboardingAdapter } from "./types.js";
|
||||
|
||||
const PROVIDER_ONBOARDING_ADAPTERS = () =>
|
||||
new Map<ProviderChoice, ProviderOnboardingAdapter>(
|
||||
listProviderPlugins()
|
||||
const CHANNEL_ONBOARDING_ADAPTERS = () =>
|
||||
new Map<ChannelChoice, ChannelOnboardingAdapter>(
|
||||
listChannelPlugins()
|
||||
.map((plugin) =>
|
||||
plugin.onboarding
|
||||
? ([plugin.id as ProviderChoice, plugin.onboarding] as const)
|
||||
? ([plugin.id as ChannelChoice, plugin.onboarding] as const)
|
||||
: null,
|
||||
)
|
||||
.filter(
|
||||
(
|
||||
entry,
|
||||
): entry is readonly [ProviderChoice, ProviderOnboardingAdapter] =>
|
||||
(entry): entry is readonly [ChannelChoice, ChannelOnboardingAdapter] =>
|
||||
Boolean(entry),
|
||||
),
|
||||
);
|
||||
|
||||
export function getProviderOnboardingAdapter(
|
||||
provider: ProviderChoice,
|
||||
): ProviderOnboardingAdapter | undefined {
|
||||
return PROVIDER_ONBOARDING_ADAPTERS().get(provider);
|
||||
export function getChannelOnboardingAdapter(
|
||||
channel: ChannelChoice,
|
||||
): ChannelOnboardingAdapter | undefined {
|
||||
return CHANNEL_ONBOARDING_ADAPTERS().get(channel);
|
||||
}
|
||||
|
||||
export function listProviderOnboardingAdapters(): ProviderOnboardingAdapter[] {
|
||||
return Array.from(PROVIDER_ONBOARDING_ADAPTERS().values());
|
||||
export function listChannelOnboardingAdapters(): ChannelOnboardingAdapter[] {
|
||||
return Array.from(CHANNEL_ONBOARDING_ADAPTERS().values());
|
||||
}
|
||||
|
||||
// Legacy aliases (pre-rename).
|
||||
export const getProviderOnboardingAdapter = getChannelOnboardingAdapter;
|
||||
export const listProviderOnboardingAdapters = listChannelOnboardingAdapters;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from "../../providers/plugins/onboarding-types.js";
|
||||
export * from "../../channels/plugins/onboarding-types.js";
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export type { ProvidersAddOptions } from "./providers/add.js";
|
||||
export { providersAddCommand } from "./providers/add.js";
|
||||
export type { ProvidersListOptions } from "./providers/list.js";
|
||||
export { providersListCommand } from "./providers/list.js";
|
||||
export type { ProvidersLogsOptions } from "./providers/logs.js";
|
||||
export { providersLogsCommand } from "./providers/logs.js";
|
||||
export type { ProvidersRemoveOptions } from "./providers/remove.js";
|
||||
export { providersRemoveCommand } from "./providers/remove.js";
|
||||
export type { ProvidersStatusOptions } from "./providers/status.js";
|
||||
export {
|
||||
formatGatewayProvidersStatusLines,
|
||||
providersStatusCommand,
|
||||
} from "./providers/status.js";
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
resolveSandboxConfigForAgent,
|
||||
resolveSandboxToolPolicyForAgent,
|
||||
} from "../agents/sandbox.js";
|
||||
import { normalizeChannelId } from "../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
resolveMainSessionKey,
|
||||
resolveStorePath,
|
||||
} from "../config/sessions.js";
|
||||
import { normalizeProviderId } from "../providers/registry.js";
|
||||
import {
|
||||
buildAgentMainSessionKey,
|
||||
normalizeAgentId,
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
import { INTERNAL_MESSAGE_PROVIDER } from "../utils/message-provider.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
||||
|
||||
type SandboxExplainOptions = {
|
||||
session?: string;
|
||||
@@ -66,11 +66,11 @@ function inferProviderFromSessionKey(params: {
|
||||
if (parts[0] === configuredMainKey) return undefined;
|
||||
const candidate = parts[0]?.trim().toLowerCase();
|
||||
if (!candidate) return undefined;
|
||||
if (candidate === INTERNAL_MESSAGE_PROVIDER) return INTERNAL_MESSAGE_PROVIDER;
|
||||
return normalizeProviderId(candidate) ?? undefined;
|
||||
if (candidate === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL;
|
||||
return normalizeChannelId(candidate) ?? undefined;
|
||||
}
|
||||
|
||||
function resolveActiveProvider(params: {
|
||||
function resolveActiveChannel(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
agentId: string;
|
||||
sessionKey: string;
|
||||
@@ -79,17 +79,26 @@ function resolveActiveProvider(params: {
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = store[params.sessionKey];
|
||||
const entry = store[params.sessionKey] as
|
||||
| {
|
||||
lastChannel?: string;
|
||||
channel?: string;
|
||||
// Legacy keys (pre-rename).
|
||||
lastProvider?: string;
|
||||
provider?: string;
|
||||
}
|
||||
| undefined;
|
||||
const candidate = (
|
||||
entry?.lastChannel ??
|
||||
entry?.channel ??
|
||||
entry?.lastProvider ??
|
||||
entry?.providerOverride ??
|
||||
entry?.provider ??
|
||||
""
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (candidate === INTERNAL_MESSAGE_PROVIDER) return INTERNAL_MESSAGE_PROVIDER;
|
||||
const normalized = normalizeProviderId(candidate);
|
||||
if (candidate === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL;
|
||||
const normalized = normalizeChannelId(candidate);
|
||||
if (normalized) return normalized;
|
||||
return inferProviderFromSessionKey({
|
||||
cfg: params.cfg,
|
||||
@@ -133,7 +142,7 @@ export async function sandboxExplainCommand(
|
||||
? false
|
||||
: sessionKey.trim() !== mainSessionKey.trim();
|
||||
|
||||
const provider = resolveActiveProvider({
|
||||
const channel = resolveActiveChannel({
|
||||
cfg,
|
||||
agentId: resolvedAgentId,
|
||||
sessionKey,
|
||||
@@ -146,12 +155,10 @@ export async function sandboxExplainCommand(
|
||||
const elevatedAgentEnabled = elevatedAgent?.enabled !== false;
|
||||
const elevatedEnabled = elevatedGlobalEnabled && elevatedAgentEnabled;
|
||||
|
||||
const globalAllow = provider
|
||||
? elevatedGlobal?.allowFrom?.[provider]
|
||||
: undefined;
|
||||
const agentAllow = provider
|
||||
? elevatedAgent?.allowFrom?.[provider]
|
||||
const globalAllow = channel
|
||||
? elevatedGlobal?.allowFrom?.[channel]
|
||||
: undefined;
|
||||
const agentAllow = channel ? elevatedAgent?.allowFrom?.[channel] : undefined;
|
||||
|
||||
const allowTokens = (values?: Array<string | number>) =>
|
||||
(values ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||
@@ -160,7 +167,7 @@ export async function sandboxExplainCommand(
|
||||
|
||||
const elevatedAllowedByConfig =
|
||||
elevatedEnabled &&
|
||||
Boolean(provider) &&
|
||||
Boolean(channel) &&
|
||||
globalAllowTokens.length > 0 &&
|
||||
(elevatedAgent?.allowFrom ? agentAllowTokens.length > 0 : true);
|
||||
|
||||
@@ -179,16 +186,16 @@ export async function sandboxExplainCommand(
|
||||
key: "agents.list[].tools.elevated.enabled",
|
||||
});
|
||||
}
|
||||
if (provider && globalAllowTokens.length === 0) {
|
||||
if (channel && globalAllowTokens.length === 0) {
|
||||
elevatedFailures.push({
|
||||
gate: "allowFrom",
|
||||
key: `tools.elevated.allowFrom.${provider}`,
|
||||
key: `tools.elevated.allowFrom.${channel}`,
|
||||
});
|
||||
}
|
||||
if (provider && elevatedAgent?.allowFrom && agentAllowTokens.length === 0) {
|
||||
if (channel && elevatedAgent?.allowFrom && agentAllowTokens.length === 0) {
|
||||
elevatedFailures.push({
|
||||
gate: "allowFrom",
|
||||
key: `agents.list[].tools.elevated.allowFrom.${provider}`,
|
||||
key: `agents.list[].tools.elevated.allowFrom.${channel}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,7 +209,7 @@ export async function sandboxExplainCommand(
|
||||
fixIt.push("agents.list[].tools.sandbox.tools.allow");
|
||||
fixIt.push("agents.list[].tools.sandbox.tools.deny");
|
||||
fixIt.push("tools.elevated.enabled");
|
||||
if (provider) fixIt.push(`tools.elevated.allowFrom.${provider}`);
|
||||
if (channel) fixIt.push(`tools.elevated.allowFrom.${channel}`);
|
||||
|
||||
const payload = {
|
||||
docsUrl: SANDBOX_DOCS_URL,
|
||||
@@ -224,13 +231,13 @@ export async function sandboxExplainCommand(
|
||||
},
|
||||
elevated: {
|
||||
enabled: elevatedEnabled,
|
||||
provider,
|
||||
channel,
|
||||
allowedByConfig: elevatedAllowedByConfig,
|
||||
alwaysAllowedByConfig: elevatedAlwaysAllowedByConfig,
|
||||
allowFrom: {
|
||||
global: provider ? globalAllowTokens : undefined,
|
||||
global: channel ? globalAllowTokens : undefined,
|
||||
agent:
|
||||
elevatedAgent?.allowFrom && provider ? agentAllowTokens : undefined,
|
||||
elevatedAgent?.allowFrom && channel ? agentAllowTokens : undefined,
|
||||
},
|
||||
failures: elevatedFailures,
|
||||
},
|
||||
@@ -287,7 +294,7 @@ export async function sandboxExplainCommand(
|
||||
lines.push(heading("Elevated:"));
|
||||
lines.push(` ${key("enabled:")} ${bool(payload.elevated.enabled)}`);
|
||||
lines.push(
|
||||
` ${key("provider:")} ${value(payload.elevated.provider ?? "(unknown)")}`,
|
||||
` ${key("channel:")} ${value(payload.elevated.channel ?? "(unknown)")}`,
|
||||
);
|
||||
lines.push(
|
||||
` ${key("allowedByConfig:")} ${bool(payload.elevated.allowedByConfig)}`,
|
||||
|
||||
@@ -11,10 +11,10 @@ import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
import { normalizeControlUiBasePath } from "../gateway/control-ui.js";
|
||||
import { probeGateway } from "../gateway/probe.js";
|
||||
import { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
|
||||
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
||||
import { resolveOsSummary } from "../infra/os-summary.js";
|
||||
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
|
||||
import { collectProvidersStatusIssues } from "../infra/providers-status-issues.js";
|
||||
import {
|
||||
readRestartSentinel,
|
||||
summarizeRestartSentinel,
|
||||
@@ -31,6 +31,7 @@ import { isRich, theme } from "../terminal/theme.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { resolveControlUiLinks } from "./onboard-helpers.js";
|
||||
import { getAgentLocalStatuses } from "./status-all/agents.js";
|
||||
import { buildChannelsTable } from "./status-all/channels.js";
|
||||
import {
|
||||
formatAge,
|
||||
formatDuration,
|
||||
@@ -42,7 +43,6 @@ import {
|
||||
readFileTailLines,
|
||||
summarizeLogTail,
|
||||
} from "./status-all/gateway.js";
|
||||
import { buildProvidersTable } from "./status-all/providers.js";
|
||||
|
||||
export async function statusAllCommand(
|
||||
runtime: RuntimeEnv,
|
||||
@@ -192,8 +192,8 @@ export async function statusAllCommand(
|
||||
progress.setLabel("Scanning agents…");
|
||||
const agentStatus = await getAgentLocalStatuses(cfg);
|
||||
progress.tick();
|
||||
progress.setLabel("Summarizing providers…");
|
||||
const providers = await buildProvidersTable(cfg, { showSecrets: false });
|
||||
progress.setLabel("Summarizing channels…");
|
||||
const channels = await buildChannelsTable(cfg, { showSecrets: false });
|
||||
progress.tick();
|
||||
|
||||
const connectionDetailsForReport = (() => {
|
||||
@@ -229,16 +229,16 @@ export async function statusAllCommand(
|
||||
}).catch((err) => ({ error: String(err) }))
|
||||
: { error: gatewayProbe?.error ?? "gateway unreachable" };
|
||||
|
||||
const providersStatus = gatewayReachable
|
||||
const channelsStatus = gatewayReachable
|
||||
? await callGateway<Record<string, unknown>>({
|
||||
method: "providers.status",
|
||||
method: "channels.status",
|
||||
params: { probe: false, timeoutMs: opts?.timeoutMs ?? 10_000 },
|
||||
timeoutMs: Math.min(8000, opts?.timeoutMs ?? 10_000),
|
||||
...callOverrides,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
const providerIssues = providersStatus
|
||||
? collectProvidersStatusIssues(providersStatus)
|
||||
const channelIssues = channelsStatus
|
||||
? collectChannelStatusIssues(channelsStatus)
|
||||
: [];
|
||||
progress.tick();
|
||||
|
||||
@@ -428,9 +428,9 @@ export async function statusAllCommand(
|
||||
rows: overviewRows,
|
||||
});
|
||||
|
||||
const providerRows = providers.rows.map((row) => ({
|
||||
providerId: row.id,
|
||||
Provider: row.provider,
|
||||
const channelRows = channels.rows.map((row) => ({
|
||||
channelId: row.id,
|
||||
Channel: row.label,
|
||||
Enabled: row.enabled ? ok("ON") : muted("OFF"),
|
||||
State:
|
||||
row.state === "ok"
|
||||
@@ -442,18 +442,18 @@ export async function statusAllCommand(
|
||||
: theme.accentDim("SETUP"),
|
||||
Detail: row.detail,
|
||||
}));
|
||||
const providerIssuesByProvider = (() => {
|
||||
const map = new Map<string, typeof providerIssues>();
|
||||
for (const issue of providerIssues) {
|
||||
const key = issue.provider;
|
||||
const channelIssuesByChannel = (() => {
|
||||
const map = new Map<string, typeof channelIssues>();
|
||||
for (const issue of channelIssues) {
|
||||
const key = issue.channel;
|
||||
const list = map.get(key);
|
||||
if (list) list.push(issue);
|
||||
else map.set(key, [issue]);
|
||||
}
|
||||
return map;
|
||||
})();
|
||||
const providerRowsWithIssues = providerRows.map((row) => {
|
||||
const issues = providerIssuesByProvider.get(row.providerId) ?? [];
|
||||
const channelRowsWithIssues = channelRows.map((row) => {
|
||||
const issues = channelIssuesByChannel.get(row.channelId) ?? [];
|
||||
if (issues.length === 0) return row;
|
||||
const issue = issues[0];
|
||||
const suffix = ` · ${warn(`gateway: ${String(issue.message).slice(0, 90)}`)}`;
|
||||
@@ -464,15 +464,15 @@ export async function statusAllCommand(
|
||||
};
|
||||
});
|
||||
|
||||
const providersTable = renderTable({
|
||||
const channelsTable = renderTable({
|
||||
width: tableWidth,
|
||||
columns: [
|
||||
{ key: "Provider", header: "Provider", minWidth: 10 },
|
||||
{ key: "Channel", header: "Channel", minWidth: 10 },
|
||||
{ key: "Enabled", header: "Enabled", minWidth: 7 },
|
||||
{ key: "State", header: "State", minWidth: 8 },
|
||||
{ key: "Detail", header: "Detail", flex: true, minWidth: 28 },
|
||||
],
|
||||
rows: providerRowsWithIssues,
|
||||
rows: channelRowsWithIssues,
|
||||
});
|
||||
|
||||
const agentRows = agentStatus.agents.map((a) => ({
|
||||
@@ -507,9 +507,9 @@ export async function statusAllCommand(
|
||||
lines.push(heading("Overview"));
|
||||
lines.push(overview.trimEnd());
|
||||
lines.push("");
|
||||
lines.push(heading("Providers"));
|
||||
lines.push(providersTable.trimEnd());
|
||||
for (const detail of providers.details) {
|
||||
lines.push(heading("Channels"));
|
||||
lines.push(channelsTable.trimEnd());
|
||||
for (const detail of channels.details) {
|
||||
lines.push("");
|
||||
lines.push(heading(detail.title));
|
||||
lines.push(
|
||||
@@ -690,23 +690,23 @@ export async function statusAllCommand(
|
||||
}
|
||||
progress.tick();
|
||||
|
||||
if (providersStatus) {
|
||||
if (channelsStatus) {
|
||||
emitCheck(
|
||||
`Provider issues (${providerIssues.length || "none"})`,
|
||||
providerIssues.length === 0 ? "ok" : "warn",
|
||||
`Channel issues (${channelIssues.length || "none"})`,
|
||||
channelIssues.length === 0 ? "ok" : "warn",
|
||||
);
|
||||
for (const issue of providerIssues.slice(0, 12)) {
|
||||
for (const issue of channelIssues.slice(0, 12)) {
|
||||
const fixText = issue.fix ? ` · fix: ${issue.fix}` : "";
|
||||
lines.push(
|
||||
` - ${issue.provider}[${issue.accountId}] ${issue.kind}: ${issue.message}${fixText}`,
|
||||
` - ${issue.channel}[${issue.accountId}] ${issue.kind}: ${issue.message}${fixText}`,
|
||||
);
|
||||
}
|
||||
if (providerIssues.length > 12) {
|
||||
lines.push(` ${muted(`… +${providerIssues.length - 12} more`)}`);
|
||||
if (channelIssues.length > 12) {
|
||||
lines.push(` ${muted(`… +${channelIssues.length - 12} more`)}`);
|
||||
}
|
||||
} else {
|
||||
emitCheck(
|
||||
`Provider issues skipped (gateway ${gatewayReachable ? "query failed" : "unreachable"})`,
|
||||
`Channel issues skipped (gateway ${gatewayReachable ? "query failed" : "unreachable"})`,
|
||||
"warn",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { resolveProviderDefaultAccountId } from "../../providers/plugins/helpers.js";
|
||||
import { listProviderPlugins } from "../../providers/plugins/index.js";
|
||||
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import type {
|
||||
ProviderAccountSnapshot,
|
||||
ProviderId,
|
||||
ProviderPlugin,
|
||||
} from "../../providers/plugins/types.js";
|
||||
ChannelAccountSnapshot,
|
||||
ChannelId,
|
||||
ChannelPlugin,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { formatAge } from "./format.js";
|
||||
|
||||
export type ProviderRow = {
|
||||
id: ProviderId;
|
||||
provider: string;
|
||||
export type ChannelRow = {
|
||||
id: ChannelId;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
state: "ok" | "setup" | "warn" | "off";
|
||||
detail: string;
|
||||
};
|
||||
|
||||
type ProviderAccountRow = {
|
||||
type ChannelAccountRow = {
|
||||
accountId: string;
|
||||
account: unknown;
|
||||
enabled: boolean;
|
||||
configured: boolean;
|
||||
snapshot: ProviderAccountSnapshot;
|
||||
snapshot: ChannelAccountSnapshot;
|
||||
};
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||
@@ -81,7 +80,7 @@ const formatAccountLabel = (params: { accountId: string; name?: string }) => {
|
||||
};
|
||||
|
||||
const resolveAccountEnabled = (
|
||||
plugin: ProviderPlugin,
|
||||
plugin: ChannelPlugin,
|
||||
account: unknown,
|
||||
cfg: ClawdbotConfig,
|
||||
): boolean => {
|
||||
@@ -91,7 +90,7 @@ const resolveAccountEnabled = (
|
||||
};
|
||||
|
||||
const resolveAccountConfigured = async (
|
||||
plugin: ProviderPlugin,
|
||||
plugin: ChannelPlugin,
|
||||
account: unknown,
|
||||
cfg: ClawdbotConfig,
|
||||
): Promise<boolean> => {
|
||||
@@ -103,13 +102,13 @@ const resolveAccountConfigured = async (
|
||||
};
|
||||
|
||||
const buildAccountSnapshot = (params: {
|
||||
plugin: ProviderPlugin;
|
||||
plugin: ChannelPlugin;
|
||||
account: unknown;
|
||||
cfg: ClawdbotConfig;
|
||||
accountId: string;
|
||||
enabled: boolean;
|
||||
configured: boolean;
|
||||
}): ProviderAccountSnapshot => {
|
||||
}): ChannelAccountSnapshot => {
|
||||
const described = params.plugin.config.describeAccount?.(
|
||||
params.account,
|
||||
params.cfg,
|
||||
@@ -123,7 +122,7 @@ const buildAccountSnapshot = (params: {
|
||||
};
|
||||
|
||||
const formatAllowFrom = (params: {
|
||||
plugin: ProviderPlugin;
|
||||
plugin: ChannelPlugin;
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
allowFrom: Array<string | number>;
|
||||
@@ -139,9 +138,9 @@ const formatAllowFrom = (params: {
|
||||
};
|
||||
|
||||
const buildAccountNotes = (params: {
|
||||
plugin: ProviderPlugin;
|
||||
plugin: ChannelPlugin;
|
||||
cfg: ClawdbotConfig;
|
||||
entry: ProviderAccountRow;
|
||||
entry: ChannelAccountRow;
|
||||
}) => {
|
||||
const { plugin, cfg, entry } = params;
|
||||
const notes: string[] = [];
|
||||
@@ -192,7 +191,7 @@ function resolveLinkFields(summary: unknown): {
|
||||
return { linked, authAgeMs, selfE164 };
|
||||
}
|
||||
|
||||
function collectMissingPaths(accounts: ProviderAccountRow[]): string[] {
|
||||
function collectMissingPaths(accounts: ChannelAccountRow[]): string[] {
|
||||
const missing: string[] = [];
|
||||
for (const entry of accounts) {
|
||||
const accountRec = asRecord(entry.account);
|
||||
@@ -216,9 +215,9 @@ function collectMissingPaths(accounts: ProviderAccountRow[]): string[] {
|
||||
}
|
||||
|
||||
function summarizeTokenConfig(params: {
|
||||
plugin: ProviderPlugin;
|
||||
plugin: ChannelPlugin;
|
||||
cfg: ClawdbotConfig;
|
||||
accounts: ProviderAccountRow[];
|
||||
accounts: ChannelAccountRow[];
|
||||
showSecrets: boolean;
|
||||
}): { state: "ok" | "setup" | "warn" | null; detail: string | null } {
|
||||
const enabled = params.accounts.filter((a) => a.enabled);
|
||||
@@ -308,13 +307,13 @@ function summarizeTokenConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
// `status --all` providers table.
|
||||
// Keep this generic: provider-specific rules belong in the provider plugin.
|
||||
export async function buildProvidersTable(
|
||||
// `status --all` channels table.
|
||||
// Keep this generic: channel-specific rules belong in the channel plugin.
|
||||
export async function buildChannelsTable(
|
||||
cfg: ClawdbotConfig,
|
||||
opts?: { showSecrets?: boolean },
|
||||
): Promise<{
|
||||
rows: ProviderRow[];
|
||||
rows: ChannelRow[];
|
||||
details: Array<{
|
||||
title: string;
|
||||
columns: string[];
|
||||
@@ -322,16 +321,16 @@ export async function buildProvidersTable(
|
||||
}>;
|
||||
}> {
|
||||
const showSecrets = opts?.showSecrets === true;
|
||||
const rows: ProviderRow[] = [];
|
||||
const rows: ChannelRow[] = [];
|
||||
const details: Array<{
|
||||
title: string;
|
||||
columns: string[];
|
||||
rows: Array<Record<string, string>>;
|
||||
}> = [];
|
||||
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveProviderDefaultAccountId({
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
@@ -339,7 +338,7 @@ export async function buildProvidersTable(
|
||||
const resolvedAccountIds =
|
||||
accountIds.length > 0 ? accountIds : [defaultAccountId];
|
||||
|
||||
const accounts: ProviderAccountRow[] = [];
|
||||
const accounts: ChannelAccountRow[] = [];
|
||||
for (const accountId of resolvedAccountIds) {
|
||||
const account = plugin.config.resolveAccount(cfg, accountId);
|
||||
const enabled = resolveAccountEnabled(plugin, account, cfg);
|
||||
@@ -361,14 +360,14 @@ export async function buildProvidersTable(
|
||||
const defaultEntry =
|
||||
accounts.find((a) => a.accountId === defaultAccountId) ?? accounts[0];
|
||||
|
||||
const summary = plugin.status?.buildProviderSummary
|
||||
? await plugin.status.buildProviderSummary({
|
||||
const summary = plugin.status?.buildChannelSummary
|
||||
? await plugin.status.buildChannelSummary({
|
||||
account: defaultEntry?.account ?? {},
|
||||
cfg,
|
||||
defaultAccountId,
|
||||
snapshot:
|
||||
defaultEntry?.snapshot ??
|
||||
({ accountId: defaultAccountId } as ProviderAccountSnapshot),
|
||||
({ accountId: defaultAccountId } as ChannelAccountSnapshot),
|
||||
})
|
||||
: undefined;
|
||||
|
||||
@@ -440,7 +439,7 @@ export async function buildProvidersTable(
|
||||
|
||||
rows.push({
|
||||
id: plugin.id,
|
||||
provider: label,
|
||||
label,
|
||||
enabled: anyEnabled,
|
||||
state,
|
||||
detail,
|
||||
@@ -39,8 +39,8 @@ vi.mock("../config/sessions.js", () => ({
|
||||
resolveMainSessionKey: mocks.resolveMainSessionKey,
|
||||
resolveStorePath: mocks.resolveStorePath,
|
||||
}));
|
||||
vi.mock("../providers/plugins/index.js", () => ({
|
||||
listProviderPlugins: () =>
|
||||
vi.mock("../channels/plugins/index.js", () => ({
|
||||
listChannelPlugins: () =>
|
||||
[
|
||||
{
|
||||
id: "whatsapp",
|
||||
@@ -56,7 +56,7 @@ vi.mock("../providers/plugins/index.js", () => ({
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
status: {
|
||||
buildProviderSummary: async () => ({ linked: true, authAgeMs: 5000 }),
|
||||
buildChannelSummary: async () => ({ linked: true, authAgeMs: 5000 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -80,12 +80,12 @@ vi.mock("../providers/plugins/index.js", () => ({
|
||||
typeof account.lastError === "string" && account.lastError,
|
||||
)
|
||||
.map((account) => ({
|
||||
provider: "signal",
|
||||
channel: "signal",
|
||||
accountId:
|
||||
typeof account.accountId === "string"
|
||||
? account.accountId
|
||||
: "default",
|
||||
message: `Provider error: ${String(account.lastError)}`,
|
||||
message: `Channel error: ${String(account.lastError)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
@@ -110,12 +110,12 @@ vi.mock("../providers/plugins/index.js", () => ({
|
||||
typeof account.lastError === "string" && account.lastError,
|
||||
)
|
||||
.map((account) => ({
|
||||
provider: "imessage",
|
||||
channel: "imessage",
|
||||
accountId:
|
||||
typeof account.accountId === "string"
|
||||
? account.accountId
|
||||
: "default",
|
||||
message: `Provider error: ${String(account.lastError)}`,
|
||||
message: `Channel error: ${String(account.lastError)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
@@ -210,7 +210,7 @@ describe("statusCommand", () => {
|
||||
it("prints JSON when requested", async () => {
|
||||
await statusCommand({ json: true }, runtime as never);
|
||||
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls[0][0]);
|
||||
expect(payload.linkProvider.linked).toBe(true);
|
||||
expect(payload.linkChannel.linked).toBe(true);
|
||||
expect(payload.sessions.count).toBe(1);
|
||||
expect(payload.sessions.path).toBe("/tmp/sessions.json");
|
||||
expect(payload.sessions.defaults.model).toBeTruthy();
|
||||
@@ -228,7 +228,7 @@ describe("statusCommand", () => {
|
||||
expect(logs.some((l) => l.includes("Overview"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Dashboard"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("macos 14.0 (arm64)"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Providers"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Channels"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("WhatsApp"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Sessions"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("+1000"))).toBe(true);
|
||||
@@ -265,7 +265,7 @@ describe("statusCommand", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("surfaces provider runtime errors from the gateway", async () => {
|
||||
it("surfaces channel runtime errors from the gateway", async () => {
|
||||
mocks.probeGateway.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
url: "ws://127.0.0.1:18789",
|
||||
@@ -278,7 +278,7 @@ describe("statusCommand", () => {
|
||||
configSnapshot: null,
|
||||
});
|
||||
mocks.callGateway.mockResolvedValueOnce({
|
||||
providerAccounts: {
|
||||
channelAccounts: {
|
||||
signal: [
|
||||
{
|
||||
accountId: "default",
|
||||
|
||||
@@ -9,6 +9,13 @@ import {
|
||||
DEFAULT_PROVIDER,
|
||||
} from "../agents/defaults.js";
|
||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelId,
|
||||
ChannelPlugin,
|
||||
} from "../channels/plugins/types.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import {
|
||||
type ClawdbotConfig,
|
||||
@@ -27,14 +34,14 @@ import { normalizeControlUiBasePath } from "../gateway/control-ui.js";
|
||||
import { probeGateway } from "../gateway/probe.js";
|
||||
import { listAgentsForGateway } from "../gateway/session-utils.js";
|
||||
import { info } from "../globals.js";
|
||||
import { buildChannelSummary } from "../infra/channel-summary.js";
|
||||
import { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
|
||||
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
||||
import { resolveOsSummary } from "../infra/os-summary.js";
|
||||
import { buildProviderSummary } from "../infra/provider-summary.js";
|
||||
import {
|
||||
formatUsageReportLines,
|
||||
loadProviderUsageSummary,
|
||||
} from "../infra/provider-usage.js";
|
||||
import { collectProvidersStatusIssues } from "../infra/providers-status-issues.js";
|
||||
import { peekSystemEvents } from "../infra/system-events.js";
|
||||
import { getTailnetHostname } from "../infra/tailscale.js";
|
||||
import {
|
||||
@@ -43,22 +50,15 @@ import {
|
||||
type UpdateCheckResult,
|
||||
} from "../infra/update-check.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
import { resolveProviderDefaultAccountId } from "../providers/plugins/helpers.js";
|
||||
import { listProviderPlugins } from "../providers/plugins/index.js";
|
||||
import type {
|
||||
ProviderAccountSnapshot,
|
||||
ProviderId,
|
||||
ProviderPlugin,
|
||||
} from "../providers/plugins/types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
||||
import { formatHealthProviderLines, type HealthSummary } from "./health.js";
|
||||
import { formatHealthChannelLines, type HealthSummary } from "./health.js";
|
||||
import { resolveControlUiLinks } from "./onboard-helpers.js";
|
||||
import { buildChannelsTable } from "./status-all/channels.js";
|
||||
import { formatGatewayAuthUsed } from "./status-all/format.js";
|
||||
import { buildProvidersTable } from "./status-all/providers.js";
|
||||
import { statusAllCommand } from "./status-all.js";
|
||||
|
||||
export type SessionStatus = {
|
||||
@@ -84,14 +84,14 @@ export type SessionStatus = {
|
||||
};
|
||||
|
||||
export type StatusSummary = {
|
||||
linkProvider?: {
|
||||
id: ProviderId;
|
||||
linkChannel?: {
|
||||
id: ChannelId;
|
||||
label: string;
|
||||
linked: boolean;
|
||||
authAgeMs: number | null;
|
||||
};
|
||||
heartbeatSeconds: number;
|
||||
providerSummary: string[];
|
||||
channelSummary: string[];
|
||||
queuedSystemEvents: string[];
|
||||
sessions: {
|
||||
path: string;
|
||||
@@ -101,20 +101,20 @@ export type StatusSummary = {
|
||||
};
|
||||
};
|
||||
|
||||
type LinkProviderContext = {
|
||||
type LinkChannelContext = {
|
||||
linked: boolean;
|
||||
authAgeMs: number | null;
|
||||
account?: unknown;
|
||||
accountId?: string;
|
||||
plugin: ProviderPlugin;
|
||||
plugin: ChannelPlugin;
|
||||
};
|
||||
|
||||
async function resolveLinkProviderContext(
|
||||
async function resolveLinkChannelContext(
|
||||
cfg: ClawdbotConfig,
|
||||
): Promise<LinkProviderContext | null> {
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
): Promise<LinkChannelContext | null> {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveProviderDefaultAccountId({
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
@@ -132,9 +132,9 @@ async function resolveLinkProviderContext(
|
||||
accountId: defaultAccountId,
|
||||
enabled,
|
||||
configured,
|
||||
} as ProviderAccountSnapshot);
|
||||
const summary = plugin.status?.buildProviderSummary
|
||||
? await plugin.status.buildProviderSummary({
|
||||
} as ChannelAccountSnapshot);
|
||||
const summary = plugin.status?.buildChannelSummary
|
||||
? await plugin.status.buildChannelSummary({
|
||||
account,
|
||||
cfg,
|
||||
defaultAccountId,
|
||||
@@ -158,9 +158,9 @@ async function resolveLinkProviderContext(
|
||||
|
||||
export async function getStatusSummary(): Promise<StatusSummary> {
|
||||
const cfg = loadConfig();
|
||||
const linkContext = await resolveLinkProviderContext(cfg);
|
||||
const linkContext = await resolveLinkChannelContext(cfg);
|
||||
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, undefined);
|
||||
const providerSummary = await buildProviderSummary(cfg, {
|
||||
const channelSummary = await buildChannelSummary(cfg, {
|
||||
colorize: true,
|
||||
includeAllowFrom: true,
|
||||
});
|
||||
@@ -228,16 +228,16 @@ export async function getStatusSummary(): Promise<StatusSummary> {
|
||||
const recent = sessions.slice(0, 5);
|
||||
|
||||
return {
|
||||
linkProvider: linkContext
|
||||
linkChannel: linkContext
|
||||
? {
|
||||
id: linkContext.plugin.id,
|
||||
label: linkContext.plugin.meta.label ?? "Provider",
|
||||
label: linkContext.plugin.meta.label ?? "Channel",
|
||||
linked: linkContext.linked,
|
||||
authAgeMs: linkContext.authAgeMs,
|
||||
}
|
||||
: undefined,
|
||||
heartbeatSeconds,
|
||||
providerSummary,
|
||||
channelSummary,
|
||||
queuedSystemEvents,
|
||||
sessions: {
|
||||
path: storePath,
|
||||
@@ -675,10 +675,10 @@ export async function statusCommand(
|
||||
: null;
|
||||
progress.tick();
|
||||
|
||||
progress.setLabel("Querying provider status…");
|
||||
const providersStatus = gatewayReachable
|
||||
progress.setLabel("Querying channel status…");
|
||||
const channelsStatus = gatewayReachable
|
||||
? await callGateway<Record<string, unknown>>({
|
||||
method: "providers.status",
|
||||
method: "channels.status",
|
||||
params: {
|
||||
probe: false,
|
||||
timeoutMs: Math.min(8000, opts.timeoutMs ?? 10_000),
|
||||
@@ -689,13 +689,13 @@ export async function statusCommand(
|
||||
),
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
const providerIssues = providersStatus
|
||||
? collectProvidersStatusIssues(providersStatus)
|
||||
const channelIssues = channelsStatus
|
||||
? collectChannelStatusIssues(channelsStatus)
|
||||
: [];
|
||||
progress.tick();
|
||||
|
||||
progress.setLabel("Summarizing providers…");
|
||||
const providers = await buildProvidersTable(cfg, {
|
||||
progress.setLabel("Summarizing channels…");
|
||||
const channels = await buildChannelsTable(cfg, {
|
||||
// Show token previews in regular status; keep `status --all` redacted.
|
||||
// Set `CLAWDBOT_SHOW_SECRETS=0` to force redaction.
|
||||
showSecrets: process.env.CLAWDBOT_SHOW_SECRETS?.trim() !== "0",
|
||||
@@ -722,9 +722,9 @@ export async function statusCommand(
|
||||
gatewayProbe,
|
||||
gatewayReachable,
|
||||
gatewaySelf,
|
||||
providerIssues,
|
||||
channelIssues,
|
||||
agentStatus,
|
||||
providers,
|
||||
channels,
|
||||
summary,
|
||||
};
|
||||
},
|
||||
@@ -743,9 +743,9 @@ export async function statusCommand(
|
||||
gatewayProbe,
|
||||
gatewayReachable,
|
||||
gatewaySelf,
|
||||
providerIssues,
|
||||
channelIssues,
|
||||
agentStatus,
|
||||
providers,
|
||||
channels,
|
||||
summary,
|
||||
} = scan;
|
||||
const usage = opts.usage
|
||||
@@ -932,11 +932,11 @@ export async function statusCommand(
|
||||
);
|
||||
|
||||
runtime.log("");
|
||||
runtime.log(theme.heading("Providers"));
|
||||
const providerIssuesByProvider = (() => {
|
||||
const map = new Map<string, typeof providerIssues>();
|
||||
for (const issue of providerIssues) {
|
||||
const key = issue.provider;
|
||||
runtime.log(theme.heading("Channels"));
|
||||
const channelIssuesByChannel = (() => {
|
||||
const map = new Map<string, typeof channelIssues>();
|
||||
for (const issue of channelIssues) {
|
||||
const key = issue.channel;
|
||||
const list = map.get(key);
|
||||
if (list) list.push(issue);
|
||||
else map.set(key, [issue]);
|
||||
@@ -947,13 +947,13 @@ export async function statusCommand(
|
||||
renderTable({
|
||||
width: tableWidth,
|
||||
columns: [
|
||||
{ key: "Provider", header: "Provider", minWidth: 10 },
|
||||
{ key: "Channel", header: "Channel", minWidth: 10 },
|
||||
{ key: "Enabled", header: "Enabled", minWidth: 7 },
|
||||
{ key: "State", header: "State", minWidth: 8 },
|
||||
{ key: "Detail", header: "Detail", flex: true, minWidth: 24 },
|
||||
],
|
||||
rows: providers.rows.map((row) => {
|
||||
const issues = providerIssuesByProvider.get(row.id) ?? [];
|
||||
rows: channels.rows.map((row) => {
|
||||
const issues = channelIssuesByChannel.get(row.id) ?? [];
|
||||
const effectiveState =
|
||||
row.state === "off" ? "off" : issues.length > 0 ? "warn" : row.state;
|
||||
const issueSuffix =
|
||||
@@ -961,7 +961,7 @@ export async function statusCommand(
|
||||
? ` · ${warn(`gateway: ${shortenText(issues[0]?.message ?? "issue", 84)}`)}`
|
||||
: "";
|
||||
return {
|
||||
Provider: row.provider,
|
||||
Channel: row.label,
|
||||
Enabled: row.enabled ? ok("ON") : muted("OFF"),
|
||||
State:
|
||||
effectiveState === "ok"
|
||||
@@ -1032,15 +1032,15 @@ export async function statusCommand(
|
||||
runtime.log(theme.heading("Health"));
|
||||
const rows: Array<Record<string, string>> = [];
|
||||
rows.push({
|
||||
Provider: "Gateway",
|
||||
Item: "Gateway",
|
||||
Status: ok("reachable"),
|
||||
Detail: `${health.durationMs}ms`,
|
||||
});
|
||||
|
||||
for (const line of formatHealthProviderLines(health)) {
|
||||
for (const line of formatHealthChannelLines(health)) {
|
||||
const colon = line.indexOf(":");
|
||||
if (colon === -1) continue;
|
||||
const provider = line.slice(0, colon).trim();
|
||||
const item = line.slice(0, colon).trim();
|
||||
const detail = line.slice(colon + 1).trim();
|
||||
const normalized = detail.toLowerCase();
|
||||
const status = (() => {
|
||||
@@ -1052,14 +1052,14 @@ export async function statusCommand(
|
||||
if (normalized.startsWith("not linked")) return warn("UNLINKED");
|
||||
return warn("WARN");
|
||||
})();
|
||||
rows.push({ Provider: provider, Status: status, Detail: detail });
|
||||
rows.push({ Item: item, Status: status, Detail: detail });
|
||||
}
|
||||
|
||||
runtime.log(
|
||||
renderTable({
|
||||
width: tableWidth,
|
||||
columns: [
|
||||
{ key: "Provider", header: "Provider", minWidth: 10 },
|
||||
{ key: "Item", header: "Item", minWidth: 10 },
|
||||
{ key: "Status", header: "Status", minWidth: 8 },
|
||||
{ key: "Detail", header: "Detail", flex: true, minWidth: 28 },
|
||||
],
|
||||
@@ -1084,7 +1084,7 @@ export async function statusCommand(
|
||||
runtime.log(" Need to share? clawdbot status --all");
|
||||
runtime.log(" Need to debug live? clawdbot logs --follow");
|
||||
if (gatewayReachable) {
|
||||
runtime.log(" Need to test providers? clawdbot status --deep");
|
||||
runtime.log(" Need to test channels? clawdbot status --deep");
|
||||
} else {
|
||||
runtime.log(" Fix reachability first: clawdbot gateway status");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user