mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:51:24 +00:00
chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -79,9 +79,7 @@ describe("agentCliCommand", () => {
|
||||
const store = path.join(dir, "sessions.json");
|
||||
mockConfig(store);
|
||||
|
||||
vi.mocked(callGateway).mockRejectedValue(
|
||||
new Error("gateway not connected"),
|
||||
);
|
||||
vi.mocked(callGateway).mockRejectedValue(new Error("gateway not connected"));
|
||||
vi.mocked(agentCommand).mockImplementationOnce(async (_opts, rt) => {
|
||||
rt.log?.("local");
|
||||
return { payloads: [{ text: "local" }], meta: { stub: true } };
|
||||
|
||||
@@ -2,11 +2,7 @@ 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";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionKey,
|
||||
resolveStorePath,
|
||||
} from "../config/sessions.js";
|
||||
import { loadSessionStore, resolveSessionKey, resolveStorePath } from "../config/sessions.js";
|
||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||
import { normalizeMainKey } from "../routing/session-key.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
@@ -62,27 +58,17 @@ function resolveGatewaySessionKey(opts: {
|
||||
const store = loadSessionStore(storePath);
|
||||
|
||||
const ctx = opts.to?.trim() ? ({ From: opts.to } as { From: string }) : null;
|
||||
let sessionKey: string | undefined = ctx
|
||||
? resolveSessionKey(scope, ctx, mainKey)
|
||||
: undefined;
|
||||
let sessionKey: string | undefined = ctx ? resolveSessionKey(scope, ctx, mainKey) : undefined;
|
||||
|
||||
if (
|
||||
opts.sessionId &&
|
||||
(!sessionKey || store[sessionKey]?.sessionId !== opts.sessionId)
|
||||
) {
|
||||
const foundKey = Object.keys(store).find(
|
||||
(key) => store[key]?.sessionId === opts.sessionId,
|
||||
);
|
||||
if (opts.sessionId && (!sessionKey || store[sessionKey]?.sessionId !== opts.sessionId)) {
|
||||
const foundKey = Object.keys(store).find((key) => store[key]?.sessionId === opts.sessionId);
|
||||
if (foundKey) sessionKey = foundKey;
|
||||
}
|
||||
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
function parseTimeoutSeconds(opts: {
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
timeout?: string;
|
||||
}) {
|
||||
function parseTimeoutSeconds(opts: { cfg: ReturnType<typeof loadConfig>; timeout?: string }) {
|
||||
const raw =
|
||||
opts.timeout !== undefined
|
||||
? Number.parseInt(String(opts.timeout), 10)
|
||||
@@ -109,10 +95,7 @@ function formatPayloadForLog(payload: {
|
||||
return lines.join("\n").trimEnd();
|
||||
}
|
||||
|
||||
export async function agentViaGatewayCommand(
|
||||
opts: AgentCliOpts,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
export async function agentViaGatewayCommand(opts: AgentCliOpts, runtime: RuntimeEnv) {
|
||||
const body = (opts.message ?? "").trim();
|
||||
if (!body) throw new Error("Message (--message) is required");
|
||||
if (!opts.to && !opts.sessionId) {
|
||||
@@ -170,9 +153,7 @@ export async function agentViaGatewayCommand(
|
||||
const payloads = result?.payloads ?? [];
|
||||
|
||||
if (payloads.length === 0) {
|
||||
runtime.log(
|
||||
response?.summary ? String(response.summary) : "No reply from agent.",
|
||||
);
|
||||
runtime.log(response?.summary ? String(response.summary) : "No reply from agent.");
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -184,11 +165,7 @@ export async function agentViaGatewayCommand(
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function agentCliCommand(
|
||||
opts: AgentCliOpts,
|
||||
runtime: RuntimeEnv,
|
||||
deps?: CliDeps,
|
||||
) {
|
||||
export async function agentCliCommand(opts: AgentCliOpts, runtime: RuntimeEnv, deps?: CliDeps) {
|
||||
if (opts.local === true) {
|
||||
return await agentCommand(opts, runtime, deps);
|
||||
}
|
||||
@@ -196,9 +173,7 @@ export async function agentCliCommand(
|
||||
try {
|
||||
return await agentViaGatewayCommand(opts, runtime);
|
||||
} catch (err) {
|
||||
runtime.error?.(
|
||||
`Gateway agent failed; falling back to embedded: ${String(err)}`,
|
||||
);
|
||||
runtime.error?.(`Gateway agent failed; falling back to embedded: ${String(err)}`);
|
||||
return await agentCommand(opts, runtime, deps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type MockInstance,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";
|
||||
|
||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
||||
|
||||
vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
}));
|
||||
vi.mock("../agents/model-catalog.js", () => ({
|
||||
loadModelCatalog: vi.fn(),
|
||||
@@ -46,9 +38,7 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
function mockConfig(
|
||||
home: string,
|
||||
storePath: string,
|
||||
agentOverrides?: Partial<
|
||||
NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
|
||||
>,
|
||||
agentOverrides?: Partial<NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>>,
|
||||
telegramOverrides?: Partial<NonNullable<ClawdbotConfig["telegram"]>>,
|
||||
) {
|
||||
configSpy.mockReturnValue({
|
||||
@@ -99,10 +89,7 @@ describe("agentCommand", () => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store);
|
||||
|
||||
await agentCommand(
|
||||
{ message: "hi", to: "+1222", thinking: "high", verbose: "on" },
|
||||
runtime,
|
||||
);
|
||||
await agentCommand({ message: "hi", to: "+1222", thinking: "high", verbose: "on" }, runtime);
|
||||
|
||||
const saved = JSON.parse(fs.readFileSync(store, "utf-8")) as Record<
|
||||
string,
|
||||
@@ -138,10 +125,7 @@ describe("agentCommand", () => {
|
||||
);
|
||||
mockConfig(home, store);
|
||||
|
||||
await agentCommand(
|
||||
{ message: "resume me", sessionId: "session-123" },
|
||||
runtime,
|
||||
);
|
||||
await agentCommand({ message: "resume me", sessionId: "session-123" }, runtime);
|
||||
|
||||
const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0];
|
||||
expect(callArgs?.sessionId).toBe("session-123");
|
||||
@@ -240,9 +224,7 @@ describe("agentCommand", () => {
|
||||
|
||||
await agentCommand({ message: "hi", to: "+1999", json: true }, runtime);
|
||||
|
||||
const logged = (runtime.log as MockInstance).mock.calls.at(
|
||||
-1,
|
||||
)?.[0] as string;
|
||||
const logged = (runtime.log as MockInstance).mock.calls.at(-1)?.[0] as string;
|
||||
const parsed = JSON.parse(logged) as {
|
||||
payloads: Array<{ text: string; mediaUrl?: string | null }>;
|
||||
meta: { durationMs: number };
|
||||
@@ -271,9 +253,7 @@ describe("agentCommand", () => {
|
||||
mockConfig(home, store, undefined, { botToken: "t-1" });
|
||||
const deps = {
|
||||
sendMessageWhatsApp: vi.fn(),
|
||||
sendMessageTelegram: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ messageId: "t1", chatId: "123" }),
|
||||
sendMessageTelegram: vi.fn().mockResolvedValue({ messageId: "t1", chatId: "123" }),
|
||||
sendMessageDiscord: vi.fn(),
|
||||
sendMessageSignal: vi.fn(),
|
||||
sendMessageIMessage: vi.fn(),
|
||||
|
||||
@@ -38,10 +38,7 @@ import {
|
||||
type SessionEntry,
|
||||
saveSessionStore,
|
||||
} from "../config/sessions.js";
|
||||
import {
|
||||
emitAgentEvent,
|
||||
registerAgentRunContext,
|
||||
} from "../infra/agent-events.js";
|
||||
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { applyVerboseOverride } from "../sessions/level-overrides.js";
|
||||
import { resolveSendPolicy } from "../sessions/send-policy.js";
|
||||
@@ -77,22 +74,15 @@ export async function agentCommand(
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
const thinkingLevelsHint = formatThinkingLevels(
|
||||
configuredModel.provider,
|
||||
configuredModel.model,
|
||||
);
|
||||
const thinkingLevelsHint = formatThinkingLevels(configuredModel.provider, configuredModel.model);
|
||||
|
||||
const thinkOverride = normalizeThinkLevel(opts.thinking);
|
||||
const thinkOnce = normalizeThinkLevel(opts.thinkingOnce);
|
||||
if (opts.thinking && !thinkOverride) {
|
||||
throw new Error(
|
||||
`Invalid thinking level. Use one of: ${thinkingLevelsHint}.`,
|
||||
);
|
||||
throw new Error(`Invalid thinking level. Use one of: ${thinkingLevelsHint}.`);
|
||||
}
|
||||
if (opts.thinkingOnce && !thinkOnce) {
|
||||
throw new Error(
|
||||
`Invalid one-shot thinking level. Use one of: ${thinkingLevelsHint}.`,
|
||||
);
|
||||
throw new Error(`Invalid one-shot thinking level. Use one of: ${thinkingLevelsHint}.`);
|
||||
}
|
||||
|
||||
const verboseOverride = normalizeVerboseLevel(opts.verbose);
|
||||
@@ -101,9 +91,7 @@ export async function agentCommand(
|
||||
}
|
||||
|
||||
const timeoutSecondsRaw =
|
||||
opts.timeout !== undefined
|
||||
? Number.parseInt(String(opts.timeout), 10)
|
||||
: undefined;
|
||||
opts.timeout !== undefined ? Number.parseInt(String(opts.timeout), 10) : undefined;
|
||||
if (
|
||||
timeoutSecondsRaw !== undefined &&
|
||||
(Number.isNaN(timeoutSecondsRaw) || timeoutSecondsRaw <= 0)
|
||||
@@ -154,9 +142,7 @@ export async function agentCommand(
|
||||
persistedThinking ??
|
||||
(agentCfg?.thinkingDefault as ThinkLevel | undefined);
|
||||
const resolvedVerboseLevel =
|
||||
verboseOverride ??
|
||||
persistedVerbose ??
|
||||
(agentCfg?.verboseDefault as VerboseLevel | undefined);
|
||||
verboseOverride ?? persistedVerbose ?? (agentCfg?.verboseDefault as VerboseLevel | undefined);
|
||||
|
||||
if (sessionKey) {
|
||||
registerAgentRunContext(runId, {
|
||||
@@ -188,8 +174,7 @@ export async function agentCommand(
|
||||
|
||||
// Persist explicit /command overrides to the session store when we have a key.
|
||||
if (sessionStore && sessionKey) {
|
||||
const entry = sessionStore[sessionKey] ??
|
||||
sessionEntry ?? { sessionId, updatedAt: Date.now() };
|
||||
const entry = sessionStore[sessionKey] ?? sessionEntry ?? { sessionId, updatedAt: Date.now() };
|
||||
const next: SessionEntry = { ...entry, sessionId, updatedAt: Date.now() };
|
||||
if (thinkOverride) {
|
||||
if (thinkOverride === "off") delete next.thinkingLevel;
|
||||
@@ -219,19 +204,15 @@ export async function agentCommand(
|
||||
}
|
||||
: cfg;
|
||||
|
||||
const { provider: defaultProvider, model: defaultModel } =
|
||||
resolveConfiguredModelRef({
|
||||
cfg: cfgForModelSelection,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
const { provider: defaultProvider, model: defaultModel } = resolveConfiguredModelRef({
|
||||
cfg: cfgForModelSelection,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
let provider = defaultProvider;
|
||||
let model = defaultModel;
|
||||
const hasAllowlist =
|
||||
agentCfg?.models && Object.keys(agentCfg.models).length > 0;
|
||||
const hasStoredOverride = Boolean(
|
||||
sessionEntry?.modelOverride || sessionEntry?.providerOverride,
|
||||
);
|
||||
const hasAllowlist = agentCfg?.models && Object.keys(agentCfg.models).length > 0;
|
||||
const hasStoredOverride = Boolean(sessionEntry?.modelOverride || sessionEntry?.providerOverride);
|
||||
const needsModelCatalog = hasAllowlist || hasStoredOverride;
|
||||
let allowedModelKeys = new Set<string>();
|
||||
let allowedModelCatalog: Awaited<ReturnType<typeof loadModelCatalog>> = [];
|
||||
@@ -250,8 +231,7 @@ export async function agentCommand(
|
||||
}
|
||||
|
||||
if (sessionEntry && sessionStore && sessionKey && hasStoredOverride) {
|
||||
const overrideProvider =
|
||||
sessionEntry.providerOverride?.trim() || defaultProvider;
|
||||
const overrideProvider = sessionEntry.providerOverride?.trim() || defaultProvider;
|
||||
const overrideModel = sessionEntry.modelOverride?.trim();
|
||||
if (overrideModel) {
|
||||
const key = modelKey(overrideProvider, overrideModel);
|
||||
@@ -309,23 +289,13 @@ export async function agentCommand(
|
||||
catalog: catalogForThinking,
|
||||
});
|
||||
}
|
||||
if (
|
||||
resolvedThinkLevel === "xhigh" &&
|
||||
!supportsXHighThinking(provider, model)
|
||||
) {
|
||||
if (resolvedThinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) {
|
||||
const explicitThink = Boolean(thinkOnce || thinkOverride);
|
||||
if (explicitThink) {
|
||||
throw new Error(
|
||||
`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
|
||||
);
|
||||
throw new Error(`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`);
|
||||
}
|
||||
resolvedThinkLevel = "high";
|
||||
if (
|
||||
sessionEntry &&
|
||||
sessionStore &&
|
||||
sessionKey &&
|
||||
sessionEntry.thinkingLevel === "xhigh"
|
||||
) {
|
||||
if (sessionEntry && sessionStore && sessionKey && sessionEntry.thinkingLevel === "xhigh") {
|
||||
sessionEntry.thinkingLevel = "high";
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
@@ -343,18 +313,12 @@ export async function agentCommand(
|
||||
let fallbackProvider = provider;
|
||||
let fallbackModel = model;
|
||||
try {
|
||||
const messageChannel = resolveMessageChannel(
|
||||
opts.messageChannel,
|
||||
opts.channel,
|
||||
);
|
||||
const messageChannel = resolveMessageChannel(opts.messageChannel, opts.channel);
|
||||
const fallbackResult = await runWithModelFallback({
|
||||
cfg,
|
||||
provider,
|
||||
model,
|
||||
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||
cfg,
|
||||
sessionAgentId,
|
||||
),
|
||||
fallbacksOverride: resolveAgentModelFallbacksOverride(cfg, sessionAgentId),
|
||||
run: (providerOverride, modelOverride) => {
|
||||
if (isCliProvider(providerOverride, cfg)) {
|
||||
const cliSessionId = getCliSessionId(sessionEntry, providerOverride);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.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 } from "../../cli/deps.js";
|
||||
@@ -25,7 +22,7 @@ import {
|
||||
import type { AgentCommandOpts } from "./types.js";
|
||||
|
||||
type RunResult = Awaited<
|
||||
ReturnType<typeof import("../../agents/pi-embedded.js")["runEmbeddedPiAgent"]>
|
||||
ReturnType<(typeof import("../../agents/pi-embedded.js"))["runEmbeddedPiAgent"]>
|
||||
>;
|
||||
|
||||
export async function deliverAgentCommandResult(params: {
|
||||
@@ -40,8 +37,7 @@ export async function deliverAgentCommandResult(params: {
|
||||
const { cfg, deps, runtime, opts, sessionEntry, payloads, result } = params;
|
||||
const deliver = opts.deliver === true;
|
||||
const bestEffortDeliver = opts.bestEffortDeliver === true;
|
||||
const deliveryChannel =
|
||||
resolveGatewayMessageChannel(opts.channel) ?? DEFAULT_CHAT_CHANNEL;
|
||||
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)
|
||||
@@ -58,8 +54,7 @@ export async function deliverAgentCommandResult(params: {
|
||||
channel: deliveryChannel,
|
||||
to: opts.to,
|
||||
cfg,
|
||||
accountId:
|
||||
targetMode === "implicit" ? sessionEntry?.lastAccountId : undefined,
|
||||
accountId: targetMode === "implicit" ? sessionEntry?.lastAccountId : undefined,
|
||||
mode: targetMode,
|
||||
})
|
||||
: null;
|
||||
@@ -111,11 +106,7 @@ export async function deliverAgentCommandResult(params: {
|
||||
if (!deliver) {
|
||||
for (const payload of deliveryPayloads) logPayload(payload);
|
||||
}
|
||||
if (
|
||||
deliver &&
|
||||
deliveryChannel &&
|
||||
!isInternalMessageChannel(deliveryChannel)
|
||||
) {
|
||||
if (deliver && deliveryChannel && !isInternalMessageChannel(deliveryChannel)) {
|
||||
if (deliveryTarget) {
|
||||
await deliverOutboundPayloads({
|
||||
cfg,
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
||||
|
||||
type RunResult = Awaited<
|
||||
ReturnType<typeof import("../../agents/pi-embedded.js")["runEmbeddedPiAgent"]>
|
||||
ReturnType<(typeof import("../../agents/pi-embedded.js"))["runEmbeddedPiAgent"]>
|
||||
>;
|
||||
|
||||
export async function updateSessionStoreAfterAgentRun(params: {
|
||||
@@ -37,14 +37,10 @@ export async function updateSessionStoreAfterAgentRun(params: {
|
||||
} = params;
|
||||
|
||||
const usage = result.meta.agentMeta?.usage;
|
||||
const modelUsed =
|
||||
result.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||
const providerUsed =
|
||||
result.meta.agentMeta?.provider ?? fallbackProvider ?? defaultProvider;
|
||||
const modelUsed = result.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||
const providerUsed = result.meta.agentMeta?.provider ?? fallbackProvider ?? defaultProvider;
|
||||
const contextTokens =
|
||||
params.contextTokensOverride ??
|
||||
lookupContextTokens(modelUsed) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
params.contextTokensOverride ?? lookupContextTokens(modelUsed) ?? DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
const entry = sessionStore[sessionKey] ?? {
|
||||
sessionId,
|
||||
@@ -66,8 +62,7 @@ export async function updateSessionStoreAfterAgentRun(params: {
|
||||
if (hasNonzeroUsage(usage)) {
|
||||
const input = usage.input ?? 0;
|
||||
const output = usage.output ?? 0;
|
||||
const promptTokens =
|
||||
input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
||||
const promptTokens = input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
||||
next.inputTokens = input;
|
||||
next.outputTokens = output;
|
||||
next.totalTokens = promptTokens > 0 ? promptTokens : (usage.total ?? input);
|
||||
|
||||
@@ -38,10 +38,7 @@ export function resolveSession(opts: {
|
||||
const sessionCfg = opts.cfg.session;
|
||||
const scope = sessionCfg?.scope ?? "per-sender";
|
||||
const mainKey = normalizeMainKey(sessionCfg?.mainKey);
|
||||
const idleMinutes = Math.max(
|
||||
sessionCfg?.idleMinutes ?? DEFAULT_IDLE_MINUTES,
|
||||
1,
|
||||
);
|
||||
const idleMinutes = Math.max(sessionCfg?.idleMinutes ?? DEFAULT_IDLE_MINUTES, 1);
|
||||
const idleMs = idleMinutes * 60_000;
|
||||
const explicitSessionKey = opts.sessionKey?.trim();
|
||||
const storeAgentId = resolveAgentIdFromSessionKey(explicitSessionKey);
|
||||
@@ -51,12 +48,9 @@ export function resolveSession(opts: {
|
||||
const sessionStore = loadSessionStore(storePath);
|
||||
const now = Date.now();
|
||||
|
||||
const ctx: MsgContext | undefined = opts.to?.trim()
|
||||
? { From: opts.to }
|
||||
: undefined;
|
||||
const ctx: MsgContext | undefined = opts.to?.trim() ? { From: opts.to } : undefined;
|
||||
let sessionKey: string | undefined =
|
||||
explicitSessionKey ??
|
||||
(ctx ? resolveSessionKey(scope, ctx, mainKey) : undefined);
|
||||
explicitSessionKey ?? (ctx ? resolveSessionKey(scope, ctx, mainKey) : undefined);
|
||||
let sessionEntry = sessionKey ? sessionStore[sessionKey] : undefined;
|
||||
|
||||
// If a session id was provided, prefer to re-use its entry (by id) even when no key was derived.
|
||||
@@ -76,9 +70,7 @@ export function resolveSession(opts: {
|
||||
|
||||
const fresh = sessionEntry && sessionEntry.updatedAt >= now - idleMs;
|
||||
const sessionId =
|
||||
opts.sessionId?.trim() ||
|
||||
(fresh ? sessionEntry?.sessionId : undefined) ||
|
||||
crypto.randomUUID();
|
||||
opts.sessionId?.trim() || (fresh ? sessionEntry?.sessionId : undefined) || crypto.randomUUID();
|
||||
const isNewSession = !fresh && !opts.sessionId;
|
||||
|
||||
const persistedThinking =
|
||||
|
||||
@@ -49,9 +49,7 @@ describe("agents add command", () => {
|
||||
|
||||
await agentsAddCommand({ name: "Work" }, runtime, { hasFlags: true });
|
||||
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("--workspace"),
|
||||
);
|
||||
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("--workspace"));
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
expect(configMocks.writeConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -63,9 +61,7 @@ describe("agents add command", () => {
|
||||
hasFlags: false,
|
||||
});
|
||||
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("--workspace"),
|
||||
);
|
||||
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("--workspace"));
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
expect(configMocks.writeConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -4,10 +4,7 @@ import type { ChatChannelId } from "../channels/registry.js";
|
||||
import { normalizeChatChannelId } from "../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { AgentBinding } from "../config/types.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAgentId,
|
||||
} from "../routing/session-key.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAgentId } from "../routing/session-key.js";
|
||||
import type { ChannelChoice } from "./onboard-types.js";
|
||||
|
||||
function bindingMatchKey(match: AgentBinding["match"]) {
|
||||
@@ -52,8 +49,7 @@ export function applyAgentBindings(
|
||||
|
||||
const added: AgentBinding[] = [];
|
||||
const skipped: AgentBinding[] = [];
|
||||
const conflicts: Array<{ binding: AgentBinding; existingAgentId: string }> =
|
||||
[];
|
||||
const conflicts: Array<{ binding: AgentBinding; existingAgentId: string }> = [];
|
||||
|
||||
for (const binding of bindings) {
|
||||
const agentId = normalizeAgentId(binding.agentId);
|
||||
@@ -86,10 +82,7 @@ export function applyAgentBindings(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDefaultAccountId(
|
||||
cfg: ClawdbotConfig,
|
||||
provider: ChatChannelId,
|
||||
): string {
|
||||
function resolveDefaultAccountId(cfg: ClawdbotConfig, provider: ChatChannelId): string {
|
||||
const plugin = getChannelPlugin(provider);
|
||||
if (!plugin) return DEFAULT_ACCOUNT_ID;
|
||||
return resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
|
||||
@@ -6,16 +6,12 @@ export function createQuietRuntime(runtime: RuntimeEnv): RuntimeEnv {
|
||||
return { ...runtime, log: () => {} };
|
||||
}
|
||||
|
||||
export async function requireValidConfig(
|
||||
runtime: RuntimeEnv,
|
||||
): Promise<ClawdbotConfig | null> {
|
||||
export async function requireValidConfig(runtime: RuntimeEnv): Promise<ClawdbotConfig | null> {
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (snapshot.exists && !snapshot.valid) {
|
||||
const issues =
|
||||
snapshot.issues.length > 0
|
||||
? snapshot.issues
|
||||
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
||||
.join("\n")
|
||||
? snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n")
|
||||
: "Unknown validation issue.";
|
||||
runtime.error(`Config invalid:\n${issues}`);
|
||||
runtime.error("Fix the config or run clawdbot doctor.");
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { CONFIG_PATH_CLAWDBOT, writeConfigFile } from "../config/config.js";
|
||||
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
|
||||
@@ -16,15 +13,8 @@ import {
|
||||
describeBinding,
|
||||
parseBindingSpecs,
|
||||
} from "./agents.bindings.js";
|
||||
import {
|
||||
createQuietRuntime,
|
||||
requireValidConfig,
|
||||
} from "./agents.command-shared.js";
|
||||
import {
|
||||
applyAgentConfig,
|
||||
findAgentEntryIndex,
|
||||
listAgentEntries,
|
||||
} from "./agents.config.js";
|
||||
import { createQuietRuntime, requireValidConfig } from "./agents.command-shared.js";
|
||||
import { applyAgentConfig, findAgentEntryIndex, listAgentEntries } from "./agents.config.js";
|
||||
import { applyAuthChoice, warnIfModelConfigLooksOff } from "./auth-choice.js";
|
||||
import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
|
||||
import { setupChannels } from "./onboard-channels.js";
|
||||
@@ -122,9 +112,7 @@ export async function agentsAddCommand(
|
||||
if (!opts.json) runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
const quietRuntime = opts.json ? createQuietRuntime(runtime) : runtime;
|
||||
await ensureWorkspaceAndSessions(workspaceDir, quietRuntime, {
|
||||
skipBootstrap: Boolean(
|
||||
bindingResult.config.agents?.defaults?.skipBootstrap,
|
||||
),
|
||||
skipBootstrap: Boolean(bindingResult.config.agents?.defaults?.skipBootstrap),
|
||||
agentId,
|
||||
});
|
||||
|
||||
@@ -138,8 +126,7 @@ export async function agentsAddCommand(
|
||||
added: bindingResult.added.map(describeBinding),
|
||||
skipped: bindingResult.skipped.map(describeBinding),
|
||||
conflicts: bindingResult.conflicts.map(
|
||||
(conflict) =>
|
||||
`${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`,
|
||||
(conflict) => `${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`,
|
||||
),
|
||||
},
|
||||
};
|
||||
@@ -208,9 +195,7 @@ export async function agentsAddCommand(
|
||||
initialValue: workspaceDefault,
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
});
|
||||
const workspaceDir = resolveUserPath(
|
||||
String(workspaceInput).trim() || workspaceDefault,
|
||||
);
|
||||
const workspaceDir = resolveUserPath(String(workspaceInput).trim() || workspaceDefault);
|
||||
const agentDir = resolveAgentDir(cfg, agentId);
|
||||
|
||||
let nextConfig = applyAgentConfig(cfg, {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import { CONFIG_PATH_CLAWDBOT, writeConfigFile } from "../config/config.js";
|
||||
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js";
|
||||
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
|
||||
@@ -9,15 +6,8 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
||||
|
||||
import {
|
||||
createQuietRuntime,
|
||||
requireValidConfig,
|
||||
} from "./agents.command-shared.js";
|
||||
import {
|
||||
findAgentEntryIndex,
|
||||
listAgentEntries,
|
||||
pruneAgentConfig,
|
||||
} from "./agents.config.js";
|
||||
import { createQuietRuntime, requireValidConfig } from "./agents.command-shared.js";
|
||||
import { findAgentEntryIndex, listAgentEntries, pruneAgentConfig } from "./agents.config.js";
|
||||
import { moveToTrash } from "./onboard-helpers.js";
|
||||
|
||||
type AgentsDeleteOptions = {
|
||||
|
||||
@@ -27,8 +27,7 @@ function formatSummary(summary: AgentSummary) {
|
||||
const identityParts = [];
|
||||
if (summary.identityEmoji) identityParts.push(summary.identityEmoji);
|
||||
if (summary.identityName) identityParts.push(summary.identityName);
|
||||
const identityLine =
|
||||
identityParts.length > 0 ? identityParts.join(" ") : null;
|
||||
const identityLine = identityParts.length > 0 ? identityParts.join(" ") : null;
|
||||
const identitySource =
|
||||
summary.identitySource === "identity"
|
||||
? "IDENTITY.md"
|
||||
@@ -38,9 +37,7 @@ function formatSummary(summary: AgentSummary) {
|
||||
|
||||
const lines = [`- ${header}`];
|
||||
if (identityLine) {
|
||||
lines.push(
|
||||
` Identity: ${identityLine}${identitySource ? ` (${identitySource})` : ""}`,
|
||||
);
|
||||
lines.push(` Identity: ${identityLine}${identitySource ? ` (${identitySource})` : ""}`);
|
||||
}
|
||||
lines.push(` Workspace: ${summary.workspace}`);
|
||||
lines.push(` Agent dir: ${summary.agentDir}`);
|
||||
@@ -86,9 +83,7 @@ export async function agentsListCommand(
|
||||
for (const summary of summaries) {
|
||||
const bindings = bindingMap.get(summary.id) ?? [];
|
||||
if (bindings.length > 0) {
|
||||
summary.bindingDetails = bindings.map((binding) =>
|
||||
describeBinding(binding),
|
||||
);
|
||||
summary.bindingDetails = bindings.map((binding) => describeBinding(binding));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,9 +114,7 @@ export async function agentsListCommand(
|
||||
}
|
||||
|
||||
const lines = ["Agents:", ...summaries.map(formatSummary)];
|
||||
lines.push(
|
||||
"Routing rules map channel/account/peer to an agent. Use --bindings for full rules.",
|
||||
);
|
||||
lines.push("Routing rules map channel/account/peer to an agent. Use --bindings for full rules.");
|
||||
lines.push(
|
||||
"Channel status reflects local config/creds. For live health: clawdbot channels status --probe.",
|
||||
);
|
||||
|
||||
@@ -26,9 +26,7 @@ export type AgentSummary = {
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
type AgentEntry = NonNullable<
|
||||
NonNullable<ClawdbotConfig["agents"]>["list"]
|
||||
>[number];
|
||||
type AgentEntry = NonNullable<NonNullable<ClawdbotConfig["agents"]>["list"]>[number];
|
||||
|
||||
type AgentIdentity = {
|
||||
name?: string;
|
||||
@@ -40,15 +38,10 @@ type AgentIdentity = {
|
||||
export function listAgentEntries(cfg: ClawdbotConfig): AgentEntry[] {
|
||||
const list = cfg.agents?.list;
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list.filter((entry): entry is AgentEntry =>
|
||||
Boolean(entry && typeof entry === "object"),
|
||||
);
|
||||
return list.filter((entry): entry is AgentEntry => Boolean(entry && typeof entry === "object"));
|
||||
}
|
||||
|
||||
export function findAgentEntryIndex(
|
||||
list: AgentEntry[],
|
||||
agentId: string,
|
||||
): number {
|
||||
export function findAgentEntryIndex(list: AgentEntry[], agentId: string): number {
|
||||
const id = normalizeAgentId(agentId);
|
||||
return list.findIndex((entry) => normalizeAgentId(entry.id) === id);
|
||||
}
|
||||
@@ -120,9 +113,7 @@ export function buildAgentSummaries(cfg: ClawdbotConfig): AgentSummary[] {
|
||||
bindingCounts.set(agentId, (bindingCounts.get(agentId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const ordered = orderedIds.filter(
|
||||
(id, index) => orderedIds.indexOf(id) === index,
|
||||
);
|
||||
const ordered = orderedIds.filter((id, index) => orderedIds.indexOf(id) === index);
|
||||
|
||||
return ordered.map((id) => {
|
||||
const workspace = resolveAgentWorkspaceDir(cfg, id);
|
||||
@@ -178,10 +169,7 @@ export function applyAgentConfig(
|
||||
if (index >= 0) {
|
||||
nextList[index] = nextEntry;
|
||||
} else {
|
||||
if (
|
||||
nextList.length === 0 &&
|
||||
agentId !== normalizeAgentId(resolveDefaultAgentId(cfg))
|
||||
) {
|
||||
if (nextList.length === 0 && agentId !== normalizeAgentId(resolveDefaultAgentId(cfg))) {
|
||||
nextList.push({ id: resolveDefaultAgentId(cfg) });
|
||||
}
|
||||
nextList.push(nextEntry);
|
||||
@@ -205,15 +193,11 @@ export function pruneAgentConfig(
|
||||
} {
|
||||
const id = normalizeAgentId(agentId);
|
||||
const agents = listAgentEntries(cfg);
|
||||
const nextAgentsList = agents.filter(
|
||||
(entry) => normalizeAgentId(entry.id) !== id,
|
||||
);
|
||||
const nextAgentsList = agents.filter((entry) => normalizeAgentId(entry.id) !== id);
|
||||
const nextAgents = nextAgentsList.length > 0 ? nextAgentsList : undefined;
|
||||
|
||||
const bindings = cfg.bindings ?? [];
|
||||
const filteredBindings = bindings.filter(
|
||||
(binding) => normalizeAgentId(binding.agentId) !== id,
|
||||
);
|
||||
const filteredBindings = bindings.filter((binding) => normalizeAgentId(binding.agentId) !== id);
|
||||
|
||||
const allow = cfg.tools?.agentToAgent?.allow ?? [];
|
||||
const filteredAllow = allow.filter((entry) => entry !== id);
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import {
|
||||
getChannelPlugin,
|
||||
listChannelPlugins,
|
||||
} from "../channels/plugins/index.js";
|
||||
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChatChannelId } from "../channels/registry.js";
|
||||
import {
|
||||
getChatChannelMeta,
|
||||
normalizeChatChannelId,
|
||||
} from "../channels/registry.js";
|
||||
import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { AgentBinding } from "../config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||
@@ -16,13 +10,7 @@ type ProviderAccountStatus = {
|
||||
provider: ChatChannelId;
|
||||
accountId: string;
|
||||
name?: string;
|
||||
state:
|
||||
| "linked"
|
||||
| "not linked"
|
||||
| "configured"
|
||||
| "not configured"
|
||||
| "enabled"
|
||||
| "disabled";
|
||||
state: "linked" | "not linked" | "configured" | "not configured" | "enabled" | "disabled";
|
||||
enabled?: boolean;
|
||||
configured?: boolean;
|
||||
};
|
||||
@@ -70,8 +58,7 @@ export async function buildProviderStatusIndex(
|
||||
? await plugin.config.isConfigured(account, cfg)
|
||||
: snapshot?.configured;
|
||||
const resolvedEnabled = typeof enabled === "boolean" ? enabled : true;
|
||||
const resolvedConfigured =
|
||||
typeof configured === "boolean" ? configured : true;
|
||||
const resolvedConfigured = typeof configured === "boolean" ? configured : true;
|
||||
const state =
|
||||
plugin.status?.resolveAccountState?.({
|
||||
account,
|
||||
@@ -101,19 +88,13 @@ export async function buildProviderStatusIndex(
|
||||
return map;
|
||||
}
|
||||
|
||||
function resolveDefaultAccountId(
|
||||
cfg: ClawdbotConfig,
|
||||
provider: ChatChannelId,
|
||||
): string {
|
||||
function resolveDefaultAccountId(cfg: ClawdbotConfig, provider: ChatChannelId): string {
|
||||
const plugin = getChannelPlugin(provider);
|
||||
if (!plugin) return DEFAULT_ACCOUNT_ID;
|
||||
return resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
}
|
||||
|
||||
function shouldShowProviderEntry(
|
||||
entry: ProviderAccountStatus,
|
||||
cfg: ClawdbotConfig,
|
||||
): boolean {
|
||||
function shouldShowProviderEntry(entry: ProviderAccountStatus, cfg: ClawdbotConfig): boolean {
|
||||
const plugin = getChannelPlugin(entry.provider);
|
||||
if (!plugin) return Boolean(entry.configured);
|
||||
if (plugin.meta.showConfigured === false) {
|
||||
@@ -132,17 +113,13 @@ function formatProviderEntry(entry: ProviderAccountStatus): string {
|
||||
return `${label}: ${formatProviderState(entry)}`;
|
||||
}
|
||||
|
||||
export function summarizeBindings(
|
||||
cfg: ClawdbotConfig,
|
||||
bindings: AgentBinding[],
|
||||
): string[] {
|
||||
export function summarizeBindings(cfg: ClawdbotConfig, bindings: AgentBinding[]): string[] {
|
||||
if (bindings.length === 0) return [];
|
||||
const seen = new Map<string, string>();
|
||||
for (const binding of bindings) {
|
||||
const channel = normalizeChatChannelId(binding.match.channel);
|
||||
if (!channel) continue;
|
||||
const accountId =
|
||||
binding.match.accountId ?? resolveDefaultAccountId(cfg, channel);
|
||||
const accountId = binding.match.accountId ?? resolveDefaultAccountId(cfg, channel);
|
||||
const key = providerAccountKey(channel, accountId);
|
||||
if (!seen.has(key)) {
|
||||
const label = formatChannelAccountLabel({
|
||||
@@ -168,8 +145,7 @@ export function listProvidersForAgent(params: {
|
||||
for (const binding of params.bindings) {
|
||||
const channel = normalizeChatChannelId(binding.match.channel);
|
||||
if (!channel) continue;
|
||||
const accountId =
|
||||
binding.match.accountId ?? resolveDefaultAccountId(params.cfg, channel);
|
||||
const accountId = binding.match.accountId ?? resolveDefaultAccountId(params.cfg, channel);
|
||||
const key = providerAccountKey(channel, accountId);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
|
||||
@@ -48,9 +48,7 @@ describe("agents helpers", () => {
|
||||
expect(main?.workspace).toBe(path.join(os.homedir(), "clawd-main"));
|
||||
expect(main?.bindings).toBe(1);
|
||||
expect(main?.model).toBe("anthropic/claude");
|
||||
expect(main?.agentDir.endsWith(path.join("agents", "main", "agent"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(main?.agentDir.endsWith(path.join("agents", "main", "agent"))).toBe(true);
|
||||
|
||||
expect(work).toBeTruthy();
|
||||
expect(work?.name).toBe("Work");
|
||||
@@ -130,12 +128,8 @@ describe("agents helpers", () => {
|
||||
};
|
||||
|
||||
const result = pruneAgentConfig(cfg, "work");
|
||||
expect(
|
||||
result.config.agents?.list?.some((agent) => agent.id === "work"),
|
||||
).toBe(false);
|
||||
expect(
|
||||
result.config.agents?.list?.some((agent) => agent.id === "home"),
|
||||
).toBe(true);
|
||||
expect(result.config.agents?.list?.some((agent) => agent.id === "work")).toBe(false);
|
||||
expect(result.config.agents?.list?.some((agent) => agent.id === "home")).toBe(true);
|
||||
expect(result.config.bindings).toHaveLength(1);
|
||||
expect(result.config.bindings?.[0]?.agentId).toBe("home");
|
||||
expect(result.config.tools?.agentToAgent?.allow).toEqual(["home"]);
|
||||
|
||||
@@ -16,9 +16,7 @@ const decode = (s: string) => Buffer.from(s, "base64").toString();
|
||||
const CLIENT_ID = decode(
|
||||
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
|
||||
);
|
||||
const CLIENT_SECRET = decode(
|
||||
"R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=",
|
||||
);
|
||||
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
||||
const REDIRECT_URI = "http://localhost:51121/oauth-callback";
|
||||
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
||||
@@ -64,11 +62,7 @@ function isWSL2(): boolean {
|
||||
*/
|
||||
export function isRemoteEnvironment(): boolean {
|
||||
// SSH session indicators
|
||||
if (
|
||||
process.env.SSH_CLIENT ||
|
||||
process.env.SSH_TTY ||
|
||||
process.env.SSH_CONNECTION
|
||||
) {
|
||||
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -162,10 +156,7 @@ function parseCallbackInput(
|
||||
/**
|
||||
* Exchange authorization code for tokens.
|
||||
*/
|
||||
async function exchangeCodeForTokens(
|
||||
code: string,
|
||||
verifier: string,
|
||||
): Promise<OAuthCredentials> {
|
||||
async function exchangeCodeForTokens(code: string, verifier: string): Promise<OAuthCredentials> {
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -219,14 +210,11 @@ async function exchangeCodeForTokens(
|
||||
*/
|
||||
async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as { email?: string };
|
||||
return data.email;
|
||||
@@ -372,9 +360,7 @@ export async function loginAntigravityManual(
|
||||
console.log("=".repeat(60));
|
||||
console.log("\n1. Open the URL above in your LOCAL browser");
|
||||
console.log("2. Complete the Google sign-in");
|
||||
console.log(
|
||||
"3. Your browser will redirect to a localhost URL that won't load",
|
||||
);
|
||||
console.log("3. Your browser will redirect to a localhost URL that won't load");
|
||||
console.log("4. Copy the ENTIRE URL from your browser's address bar");
|
||||
console.log("5. Paste it below\n");
|
||||
console.log("The URL will look like:");
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
type AuthProfileStore,
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { type AuthProfileStore, CLAUDE_CLI_PROFILE_ID } from "../agents/auth-profiles.js";
|
||||
import { buildAuthChoiceOptions } from "./auth-choice-options.js";
|
||||
|
||||
describe("buildAuthChoiceOptions", () => {
|
||||
@@ -90,9 +87,7 @@ describe("buildAuthChoiceOptions", () => {
|
||||
});
|
||||
|
||||
expect(options.some((opt) => opt.value === "minimax-api")).toBe(true);
|
||||
expect(options.some((opt) => opt.value === "minimax-api-lightning")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(options.some((opt) => opt.value === "minimax-api-lightning")).toBe(true);
|
||||
});
|
||||
|
||||
it("includes Moonshot auth choice", () => {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { CLAUDE_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID } from "../agents/auth-profiles.js";
|
||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
import type { AuthChoice } from "./onboard-types.js";
|
||||
|
||||
@@ -92,10 +89,7 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||
},
|
||||
];
|
||||
|
||||
function formatOAuthHint(
|
||||
expires?: number,
|
||||
opts?: { allowStale?: boolean },
|
||||
): string {
|
||||
function formatOAuthHint(expires?: number, opts?: { allowStale?: boolean }): string {
|
||||
const rich = isRich();
|
||||
if (!expires) {
|
||||
return colorize(rich, theme.muted, "token unavailable");
|
||||
|
||||
@@ -34,9 +34,7 @@ export async function promptAuthChoiceGrouped(params: {
|
||||
return "skip";
|
||||
}
|
||||
|
||||
const group = availableGroups.find(
|
||||
(candidate) => candidate.value === providerSelection,
|
||||
);
|
||||
const group = availableGroups.find((candidate) => candidate.value === providerSelection);
|
||||
|
||||
if (!group || group.options.length === 0) {
|
||||
await params.prompter.note(
|
||||
|
||||
@@ -5,9 +5,7 @@ export function normalizeApiKeyInput(raw: string): string {
|
||||
if (!trimmed) return "";
|
||||
|
||||
// Handle shell-style assignments: export KEY="value" or KEY=value
|
||||
const assignmentMatch = trimmed.match(
|
||||
/^(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.+)$/,
|
||||
);
|
||||
const assignmentMatch = trimmed.match(/^(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.+)$/);
|
||||
const valuePart = assignmentMatch ? assignmentMatch[1].trim() : trimmed;
|
||||
|
||||
const unquoted =
|
||||
@@ -18,9 +16,7 @@ export function normalizeApiKeyInput(raw: string): string {
|
||||
? valuePart.slice(1, -1)
|
||||
: valuePart;
|
||||
|
||||
const withoutSemicolon = unquoted.endsWith(";")
|
||||
? unquoted.slice(0, -1)
|
||||
: unquoted;
|
||||
const withoutSemicolon = unquoted.endsWith(";") ? unquoted.slice(0, -1) : unquoted;
|
||||
|
||||
return withoutSemicolon.trim();
|
||||
}
|
||||
|
||||
@@ -8,14 +8,8 @@ import {
|
||||
normalizeApiKeyInput,
|
||||
validateApiKeyInput,
|
||||
} from "./auth-choice.api-key.js";
|
||||
import type {
|
||||
ApplyAuthChoiceParams,
|
||||
ApplyAuthChoiceResult,
|
||||
} from "./auth-choice.apply.js";
|
||||
import {
|
||||
buildTokenProfileId,
|
||||
validateAnthropicSetupToken,
|
||||
} from "./auth-token.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { buildTokenProfileId, validateAnthropicSetupToken } from "./auth-token.js";
|
||||
import { applyAuthProfileConfig, setAnthropicApiKey } from "./onboard-auth.js";
|
||||
|
||||
export async function applyAuthChoiceAnthropic(
|
||||
@@ -164,10 +158,9 @@ export async function applyAuthChoiceAnthropic(
|
||||
options: [{ value: "anthropic", label: "Anthropic (only supported)" }],
|
||||
})) as "anthropic";
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Run `claude setup-token` in your terminal.",
|
||||
"Then paste the generated token below.",
|
||||
].join("\n"),
|
||||
["Run `claude setup-token` in your terminal.", "Then paste the generated token below."].join(
|
||||
"\n",
|
||||
),
|
||||
"Anthropic token",
|
||||
);
|
||||
|
||||
@@ -223,10 +216,7 @@ export async function applyAuthChoiceAnthropic(
|
||||
message: "Enter Anthropic API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setAnthropicApiKey(
|
||||
normalizeApiKeyInput(String(key)),
|
||||
params.agentDir,
|
||||
);
|
||||
await setAnthropicApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "anthropic:default",
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
resolveAuthProfileOrder,
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../agents/auth-profiles.js";
|
||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import {
|
||||
formatApiKeyPreview,
|
||||
normalizeApiKeyInput,
|
||||
validateApiKeyInput,
|
||||
} from "./auth-choice.api-key.js";
|
||||
import type {
|
||||
ApplyAuthChoiceParams,
|
||||
ApplyAuthChoiceResult,
|
||||
} from "./auth-choice.apply.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||
import {
|
||||
applyGoogleGeminiModelDefault,
|
||||
@@ -63,12 +57,8 @@ export async function applyAuthChoiceApiProviders(
|
||||
store,
|
||||
provider: "openrouter",
|
||||
});
|
||||
const existingProfileId = profileOrder.find((profileId) =>
|
||||
Boolean(store.profiles[profileId]),
|
||||
);
|
||||
const existingCred = existingProfileId
|
||||
? store.profiles[existingProfileId]
|
||||
: undefined;
|
||||
const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId]));
|
||||
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
|
||||
let profileId = "openrouter:default";
|
||||
let mode: "api_key" | "oauth" | "token" = "api_key";
|
||||
let hasCredential = false;
|
||||
@@ -103,10 +93,7 @@ export async function applyAuthChoiceApiProviders(
|
||||
message: "Enter OpenRouter API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setOpenrouterApiKey(
|
||||
normalizeApiKeyInput(String(key)),
|
||||
params.agentDir,
|
||||
);
|
||||
await setOpenrouterApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
@@ -152,10 +139,7 @@ export async function applyAuthChoiceApiProviders(
|
||||
message: "Enter Moonshot API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setMoonshotApiKey(
|
||||
normalizeApiKeyInput(String(key)),
|
||||
params.agentDir,
|
||||
);
|
||||
await setMoonshotApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "moonshot:default",
|
||||
@@ -260,9 +244,7 @@ export async function applyAuthChoiceApiProviders(
|
||||
...config.agents?.defaults?.models,
|
||||
[ZAI_DEFAULT_MODEL_REF]: {
|
||||
...config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF],
|
||||
alias:
|
||||
config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF]
|
||||
?.alias ?? "GLM",
|
||||
alias: config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF]?.alias ?? "GLM",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -332,10 +314,7 @@ export async function applyAuthChoiceApiProviders(
|
||||
message: "Enter OpenCode Zen API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setOpencodeZenApiKey(
|
||||
normalizeApiKeyInput(String(key)),
|
||||
params.agentDir,
|
||||
);
|
||||
await setOpencodeZenApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "opencode:default",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js";
|
||||
import type {
|
||||
ApplyAuthChoiceParams,
|
||||
ApplyAuthChoiceResult,
|
||||
} from "./auth-choice.apply.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { applyAuthProfileConfig } from "./onboard-auth.js";
|
||||
|
||||
export async function applyAuthChoiceGitHubCopilot(
|
||||
@@ -31,10 +28,7 @@ export async function applyAuthChoiceGitHubCopilot(
|
||||
try {
|
||||
await githubCopilotLoginCommand({ yes: true }, params.runtime);
|
||||
} catch (err) {
|
||||
await params.prompter.note(
|
||||
`GitHub Copilot login failed: ${String(err)}`,
|
||||
"GitHub Copilot",
|
||||
);
|
||||
await params.prompter.note(`GitHub Copilot login failed: ${String(err)}`, "GitHub Copilot");
|
||||
return { config: nextConfig };
|
||||
}
|
||||
|
||||
@@ -61,10 +55,7 @@ export async function applyAuthChoiceGitHubCopilot(
|
||||
},
|
||||
},
|
||||
};
|
||||
await params.prompter.note(
|
||||
`Default model set to ${model}`,
|
||||
"Model configured",
|
||||
);
|
||||
await params.prompter.note(`Default model set to ${model}`, "Model configured");
|
||||
}
|
||||
|
||||
return { config: nextConfig };
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
normalizeApiKeyInput,
|
||||
validateApiKeyInput,
|
||||
} from "./auth-choice.api-key.js";
|
||||
import type {
|
||||
ApplyAuthChoiceParams,
|
||||
ApplyAuthChoiceResult,
|
||||
} from "./auth-choice.apply.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
@@ -37,9 +34,7 @@ export async function applyAuthChoiceMiniMax(
|
||||
params.authChoice === "minimax-api-lightning"
|
||||
) {
|
||||
const modelId =
|
||||
params.authChoice === "minimax-api-lightning"
|
||||
? "MiniMax-M2.1-lightning"
|
||||
: "MiniMax-M2.1";
|
||||
params.authChoice === "minimax-api-lightning" ? "MiniMax-M2.1-lightning" : "MiniMax-M2.1";
|
||||
let hasCredential = false;
|
||||
const envKey = resolveEnvApiKey("minimax");
|
||||
if (envKey) {
|
||||
@@ -57,10 +52,7 @@ export async function applyAuthChoiceMiniMax(
|
||||
message: "Enter MiniMax API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setMinimaxApiKey(
|
||||
normalizeApiKeyInput(String(key)),
|
||||
params.agentDir,
|
||||
);
|
||||
await setMinimaxApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "minimax:default",
|
||||
@@ -74,8 +66,7 @@ export async function applyAuthChoiceMiniMax(
|
||||
setDefaultModel: params.setDefaultModel,
|
||||
defaultModel: modelRef,
|
||||
applyDefaultConfig: (config) => applyMinimaxApiConfig(config, modelId),
|
||||
applyProviderConfig: (config) =>
|
||||
applyMinimaxApiProviderConfig(config, modelId),
|
||||
applyProviderConfig: (config) => applyMinimaxApiProviderConfig(config, modelId),
|
||||
noteAgentModel,
|
||||
prompter: params.prompter,
|
||||
});
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
isRemoteEnvironment,
|
||||
loginAntigravityVpsAware,
|
||||
} from "./antigravity-oauth.js";
|
||||
import type {
|
||||
ApplyAuthChoiceParams,
|
||||
ApplyAuthChoiceResult,
|
||||
} from "./auth-choice.apply.js";
|
||||
import { isRemoteEnvironment, loginAntigravityVpsAware } from "./antigravity-oauth.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { loginChutes } from "./chutes-oauth.js";
|
||||
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
writeOAuthCredentials,
|
||||
} from "./onboard-auth.js";
|
||||
import { applyAuthProfileConfig, writeOAuthCredentials } from "./onboard-auth.js";
|
||||
import { openUrl } from "./onboard-helpers.js";
|
||||
|
||||
export async function applyAuthChoiceOAuth(
|
||||
@@ -22,10 +13,8 @@ export async function applyAuthChoiceOAuth(
|
||||
let nextConfig = params.config;
|
||||
const isRemote = isRemoteEnvironment();
|
||||
const redirectUri =
|
||||
process.env.CHUTES_OAUTH_REDIRECT_URI?.trim() ||
|
||||
"http://127.0.0.1:1456/oauth-callback";
|
||||
const scopes =
|
||||
process.env.CHUTES_OAUTH_SCOPES?.trim() || "openid profile chutes:invoke";
|
||||
process.env.CHUTES_OAUTH_REDIRECT_URI?.trim() || "http://127.0.0.1:1456/oauth-callback";
|
||||
const scopes = process.env.CHUTES_OAUTH_SCOPES?.trim() || "openid profile chutes:invoke";
|
||||
const clientId =
|
||||
process.env.CHUTES_CLIENT_ID?.trim() ||
|
||||
String(
|
||||
@@ -138,9 +127,7 @@ export async function applyAuthChoiceOAuth(
|
||||
async (url) => {
|
||||
if (isRemote) {
|
||||
spin.stop("OAuth URL ready");
|
||||
params.runtime.log(
|
||||
`\nOpen this URL in your LOCAL browser:\n\n${url}\n`,
|
||||
);
|
||||
params.runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
|
||||
} else {
|
||||
spin.update("Complete sign-in in browser…");
|
||||
await openUrl(url);
|
||||
@@ -151,11 +138,7 @@ export async function applyAuthChoiceOAuth(
|
||||
);
|
||||
spin.stop("Antigravity OAuth complete");
|
||||
if (oauthCreds) {
|
||||
await writeOAuthCredentials(
|
||||
"google-antigravity",
|
||||
oauthCreds,
|
||||
params.agentDir,
|
||||
);
|
||||
await writeOAuthCredentials("google-antigravity", oauthCreds, params.agentDir);
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: `google-antigravity:${oauthCreds.email ?? "default"}`,
|
||||
provider: "google-antigravity",
|
||||
@@ -170,8 +153,7 @@ export async function applyAuthChoiceOAuth(
|
||||
...nextConfig.agents?.defaults,
|
||||
models: {
|
||||
...nextConfig.agents?.defaults?.models,
|
||||
[modelKey]:
|
||||
nextConfig.agents?.defaults?.models?.[modelKey] ?? {},
|
||||
[modelKey]: nextConfig.agents?.defaults?.models?.[modelKey] ?? {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -185,11 +167,9 @@ export async function applyAuthChoiceOAuth(
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel &&
|
||||
"fallbacks" in (existingModel as Record<string, unknown>)
|
||||
...(existingModel && "fallbacks" in (existingModel as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: modelKey,
|
||||
@@ -197,10 +177,7 @@ export async function applyAuthChoiceOAuth(
|
||||
},
|
||||
},
|
||||
};
|
||||
await params.prompter.note(
|
||||
`Default model set to ${modelKey}`,
|
||||
"Model configured",
|
||||
);
|
||||
await params.prompter.note(`Default model set to ${modelKey}`, "Model configured");
|
||||
} else {
|
||||
agentModelOverride = modelKey;
|
||||
await noteAgentModel(modelKey);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { loginOpenAICodex } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
ensureAuthProfileStore,
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import { upsertSharedEnvVar } from "../infra/env-file.js";
|
||||
import { isRemoteEnvironment } from "./antigravity-oauth.js";
|
||||
@@ -11,15 +8,9 @@ import {
|
||||
normalizeApiKeyInput,
|
||||
validateApiKeyInput,
|
||||
} from "./auth-choice.api-key.js";
|
||||
import type {
|
||||
ApplyAuthChoiceParams,
|
||||
ApplyAuthChoiceResult,
|
||||
} from "./auth-choice.apply.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
writeOAuthCredentials,
|
||||
} from "./onboard-auth.js";
|
||||
import { applyAuthProfileConfig, writeOAuthCredentials } from "./onboard-auth.js";
|
||||
import { openUrl } from "./onboard-helpers.js";
|
||||
import {
|
||||
applyOpenAICodexModelDefault,
|
||||
|
||||
@@ -27,9 +27,7 @@ export type ApplyAuthChoiceResult = {
|
||||
export async function applyAuthChoice(
|
||||
params: ApplyAuthChoiceParams,
|
||||
): Promise<ApplyAuthChoiceResult> {
|
||||
const handlers: Array<
|
||||
(p: ApplyAuthChoiceParams) => Promise<ApplyAuthChoiceResult | null>
|
||||
> = [
|
||||
const handlers: Array<(p: ApplyAuthChoiceParams) => Promise<ApplyAuthChoiceResult | null>> = [
|
||||
applyAuthChoiceAnthropic,
|
||||
applyAuthChoiceOpenAI,
|
||||
applyAuthChoiceOAuth,
|
||||
|
||||
@@ -14,10 +14,7 @@ export async function applyDefaultModelChoice(params: {
|
||||
if (params.setDefaultModel) {
|
||||
const next = params.applyDefaultConfig(params.config);
|
||||
if (params.noteDefault) {
|
||||
await params.prompter.note(
|
||||
`Default model set to ${params.noteDefault}`,
|
||||
"Model configured",
|
||||
);
|
||||
await params.prompter.note(`Default model set to ${params.noteDefault}`, "Model configured");
|
||||
}
|
||||
return { config: next };
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { resolveAgentModelPrimary } from "../agents/agent-scope.js";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { ensureAuthProfileStore, listProfilesForProvider } from "../agents/auth-profiles.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import {
|
||||
getCustomProviderApiKey,
|
||||
resolveEnvApiKey,
|
||||
} from "../agents/model-auth.js";
|
||||
import { getCustomProviderApiKey, resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
|
||||
@@ -24,8 +24,6 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
||||
"opencode-zen": "opencode",
|
||||
};
|
||||
|
||||
export function resolvePreferredProviderForAuthChoice(
|
||||
choice: AuthChoice,
|
||||
): string | undefined {
|
||||
export function resolvePreferredProviderForAuthChoice(choice: AuthChoice): string | undefined {
|
||||
return PREFERRED_PROVIDER_BY_AUTH_CHOICE[choice];
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import {
|
||||
applyAuthChoice,
|
||||
resolvePreferredProviderForAuthChoice,
|
||||
} from "./auth-choice.js";
|
||||
import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js";
|
||||
import type { AuthChoice } from "./onboard-types.js";
|
||||
|
||||
vi.mock("../providers/github-copilot-auth.js", () => ({
|
||||
@@ -18,8 +15,7 @@ vi.mock("../providers/github-copilot-auth.js", () => ({
|
||||
|
||||
const noopAsync = async () => {};
|
||||
const noop = () => {};
|
||||
const authProfilePathFor = (agentDir: string) =>
|
||||
path.join(agentDir, "auth-profiles.json");
|
||||
const authProfilePathFor = (agentDir: string) => path.join(agentDir, "auth-profiles.json");
|
||||
const requireAgentDir = () => {
|
||||
const agentDir = process.env.CLAWDBOT_AGENT_DIR;
|
||||
if (!agentDir) throw new Error("CLAWDBOT_AGENT_DIR not set");
|
||||
@@ -176,9 +172,7 @@ describe("applyAuthChoice", () => {
|
||||
const parsed = JSON.parse(raw) as {
|
||||
profiles?: Record<string, { key?: string }>;
|
||||
};
|
||||
expect(parsed.profiles?.["synthetic:default"]?.key).toBe(
|
||||
"sk-synthetic-test",
|
||||
);
|
||||
expect(parsed.profiles?.["synthetic:default"]?.key).toBe("sk-synthetic-test");
|
||||
});
|
||||
|
||||
it("sets default model when selecting github-copilot", async () => {
|
||||
@@ -218,9 +212,7 @@ describe("applyAuthChoice", () => {
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe(
|
||||
"github-copilot/gpt-4o",
|
||||
);
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe("github-copilot/gpt-4o");
|
||||
} finally {
|
||||
stdin.isTTY = previousTty;
|
||||
}
|
||||
@@ -272,9 +264,7 @@ describe("applyAuthChoice", () => {
|
||||
expect(text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: "Enter OpenCode Zen API key" }),
|
||||
);
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe(
|
||||
"anthropic/claude-opus-4-5",
|
||||
);
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe("anthropic/claude-opus-4-5");
|
||||
expect(result.config.models?.providers?.["opencode-zen"]).toBeUndefined();
|
||||
expect(result.agentModelOverride).toBe("opencode/claude-opus-4-5");
|
||||
});
|
||||
@@ -328,18 +318,14 @@ describe("applyAuthChoice", () => {
|
||||
provider: "openrouter",
|
||||
mode: "api_key",
|
||||
});
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe(
|
||||
"openrouter/auto",
|
||||
);
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe("openrouter/auto");
|
||||
|
||||
const authProfilePath = authProfilePathFor(requireAgentDir());
|
||||
const raw = await fs.readFile(authProfilePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
profiles?: Record<string, { key?: string }>;
|
||||
};
|
||||
expect(parsed.profiles?.["openrouter:default"]?.key).toBe(
|
||||
"sk-openrouter-test",
|
||||
);
|
||||
expect(parsed.profiles?.["openrouter:default"]?.key).toBe("sk-openrouter-test");
|
||||
|
||||
delete process.env.OPENROUTER_API_KEY;
|
||||
});
|
||||
@@ -434,14 +420,10 @@ describe("applyAuthChoice", () => {
|
||||
|
||||
describe("resolvePreferredProviderForAuthChoice", () => {
|
||||
it("maps github-copilot to the provider", () => {
|
||||
expect(resolvePreferredProviderForAuthChoice("github-copilot")).toBe(
|
||||
"github-copilot",
|
||||
);
|
||||
expect(resolvePreferredProviderForAuthChoice("github-copilot")).toBe("github-copilot");
|
||||
});
|
||||
|
||||
it("returns undefined for unknown choices", () => {
|
||||
expect(
|
||||
resolvePreferredProviderForAuthChoice("unknown" as AuthChoice),
|
||||
).toBeUndefined();
|
||||
expect(resolvePreferredProviderForAuthChoice("unknown" as AuthChoice)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,10 +15,7 @@ export function normalizeTokenProfileName(raw: string): string {
|
||||
return slug || DEFAULT_TOKEN_PROFILE_NAME;
|
||||
}
|
||||
|
||||
export function buildTokenProfileId(params: {
|
||||
provider: string;
|
||||
name: string;
|
||||
}): string {
|
||||
export function buildTokenProfileId(params: { provider: string; name: string }): string {
|
||||
const provider = normalizeProviderId(params.provider);
|
||||
const name = normalizeTokenProfileName(params.name);
|
||||
return `${provider}:${name}`;
|
||||
|
||||
@@ -21,8 +21,7 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
});
|
||||
|
||||
vi.mock("../agents/auth-profiles.js", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("../agents/auth-profiles.js")>();
|
||||
const actual = await importOriginal<typeof import("../agents/auth-profiles.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadAuthProfileStore: authMocks.loadAuthProfileStore,
|
||||
@@ -127,11 +126,9 @@ describe("channels command", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await channelsRemoveCommand(
|
||||
{ channel: "discord", account: "work", delete: true },
|
||||
runtime,
|
||||
{ hasFlags: 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 {
|
||||
@@ -156,9 +153,7 @@ describe("channels command", () => {
|
||||
whatsapp?: { accounts?: Record<string, { name?: string }> };
|
||||
};
|
||||
};
|
||||
expect(next.channels?.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 () => {
|
||||
@@ -212,11 +207,9 @@ describe("channels command", () => {
|
||||
.spyOn(prompterModule, "createClackPrompter")
|
||||
.mockReturnValue(prompt as never);
|
||||
|
||||
await channelsRemoveCommand(
|
||||
{ channel: "discord", account: "default" },
|
||||
runtime,
|
||||
{ hasFlags: true },
|
||||
);
|
||||
await channelsRemoveCommand({ channel: "discord", account: "default" }, runtime, {
|
||||
hasFlags: true,
|
||||
});
|
||||
|
||||
const next = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||
channels?: { discord?: { enabled?: boolean } };
|
||||
@@ -253,9 +246,9 @@ describe("channels command", () => {
|
||||
});
|
||||
|
||||
await channelsListCommand({ json: true, usage: false }, runtime);
|
||||
const payload = JSON.parse(
|
||||
String(runtime.log.mock.calls[0]?.[0] ?? "{}"),
|
||||
) as { auth?: Array<{ id: string }> };
|
||||
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0] ?? "{}")) as {
|
||||
auth?: Array<{ id: string }>;
|
||||
};
|
||||
const ids = payload.auth?.map((entry) => entry.id) ?? [];
|
||||
expect(ids).toContain("anthropic:claude-cli");
|
||||
expect(ids).toContain("openai-codex:codex-cli");
|
||||
@@ -296,9 +289,7 @@ describe("channels command", () => {
|
||||
};
|
||||
};
|
||||
expect(next.channels?.telegram?.name).toBeUndefined();
|
||||
expect(next.channels?.telegram?.accounts?.default?.name).toBe(
|
||||
"Primary Bot",
|
||||
);
|
||||
expect(next.channels?.telegram?.accounts?.default?.name).toBe("Primary Bot");
|
||||
});
|
||||
|
||||
it("migrates base names when adding non-default accounts", async () => {
|
||||
@@ -314,11 +305,9 @@ describe("channels command", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await channelsAddCommand(
|
||||
{ channel: "discord", account: "work", token: "d1" },
|
||||
runtime,
|
||||
{ hasFlags: true },
|
||||
);
|
||||
await channelsAddCommand({ channel: "discord", account: "work", token: "d1" }, runtime, {
|
||||
hasFlags: true,
|
||||
});
|
||||
|
||||
const next = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||
channels?: {
|
||||
@@ -341,12 +330,8 @@ describe("channels command", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const telegramIndex = lines.findIndex((line) =>
|
||||
line.includes("Telegram default"),
|
||||
);
|
||||
const whatsappIndex = lines.findIndex((line) =>
|
||||
line.includes("WhatsApp default"),
|
||||
);
|
||||
const telegramIndex = lines.findIndex((line) => line.includes("Telegram default"));
|
||||
const whatsappIndex = lines.findIndex((line) => line.includes("WhatsApp default"));
|
||||
expect(telegramIndex).toBeGreaterThan(-1);
|
||||
expect(whatsappIndex).toBeGreaterThan(-1);
|
||||
expect(telegramIndex).toBeLessThan(whatsappIndex);
|
||||
|
||||
@@ -21,8 +21,7 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
});
|
||||
|
||||
vi.mock("../agents/auth-profiles.js", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("../agents/auth-profiles.js")>();
|
||||
const actual = await importOriginal<typeof import("../agents/auth-profiles.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadAuthProfileStore: authMocks.loadAuthProfileStore,
|
||||
|
||||
@@ -7,7 +7,4 @@ 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";
|
||||
export { channelsStatusCommand, formatGatewayChannelsStatusLines } from "./channels/status.js";
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import type {
|
||||
ChannelId,
|
||||
ChannelSetupInput,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { ChannelId, ChannelSetupInput } from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||
|
||||
@@ -17,9 +14,7 @@ export function applyAccountName(params: {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
const apply = plugin?.setup?.applyAccountName;
|
||||
return apply
|
||||
? apply({ cfg: params.cfg, accountId, name: params.name })
|
||||
: params.cfg;
|
||||
return apply ? apply({ cfg: params.cfg, accountId, name: params.name }) : params.cfg;
|
||||
}
|
||||
|
||||
export function applyChannelAccountConfig(params: {
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import {
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import { 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 { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
||||
import { setupChannels } from "../onboard-channels.js";
|
||||
|
||||
@@ -5,15 +5,9 @@ import {
|
||||
} 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 type { ChannelAccountSnapshot, ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import { withProgress } from "../../cli/progress.js";
|
||||
import {
|
||||
formatUsageReportLines,
|
||||
loadProviderUsageSummary,
|
||||
} from "../../infra/provider-usage.js";
|
||||
import { formatUsageReportLines, loadProviderUsageSummary } from "../../infra/provider-usage.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
@@ -72,10 +66,7 @@ function formatAccountLine(params: {
|
||||
if (snapshot.linked !== undefined) {
|
||||
bits.push(formatLinked(snapshot.linked));
|
||||
}
|
||||
if (
|
||||
shouldShowConfigured(channel) &&
|
||||
typeof snapshot.configured === "boolean"
|
||||
) {
|
||||
if (shouldShowConfigured(channel) && typeof snapshot.configured === "boolean") {
|
||||
bits.push(formatConfigured(snapshot.configured));
|
||||
}
|
||||
if (snapshot.tokenSource) {
|
||||
@@ -120,16 +111,12 @@ export async function channelsListCommand(
|
||||
const plugins = listChannelPlugins();
|
||||
|
||||
const authStore = loadAuthProfileStore();
|
||||
const authProfiles = Object.entries(authStore.profiles).map(
|
||||
([profileId, profile]) => ({
|
||||
id: profileId,
|
||||
provider: profile.provider,
|
||||
type: profile.type,
|
||||
isExternal:
|
||||
profileId === CLAUDE_CLI_PROFILE_ID ||
|
||||
profileId === CODEX_CLI_PROFILE_ID,
|
||||
}),
|
||||
);
|
||||
const authProfiles = Object.entries(authStore.profiles).map(([profileId, profile]) => ({
|
||||
id: profileId,
|
||||
provider: profile.provider,
|
||||
type: profile.type,
|
||||
isExternal: profileId === CLAUDE_CLI_PROFILE_ID || profileId === CODEX_CLI_PROFILE_ID,
|
||||
}));
|
||||
if (opts.json) {
|
||||
const usage = includeUsage ? await loadProviderUsageSummary() : undefined;
|
||||
const chat: Record<string, string[]> = {};
|
||||
@@ -169,9 +156,7 @@ export async function channelsListCommand(
|
||||
} else {
|
||||
for (const profile of authProfiles) {
|
||||
const external = profile.isExternal ? theme.muted(" (synced)") : "";
|
||||
lines.push(
|
||||
`- ${theme.accent(profile.id)} (${theme.success(profile.type)}${external})`,
|
||||
);
|
||||
lines.push(`- ${theme.accent(profile.id)} (${theme.success(profile.type)}${external})`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +175,5 @@ export async function channelsListCommand(
|
||||
}
|
||||
|
||||
runtime.log("");
|
||||
runtime.log(
|
||||
`Docs: ${formatDocsLink("/gateway/configuration", "gateway/configuration")}`,
|
||||
);
|
||||
runtime.log(`Docs: ${formatDocsLink("/gateway/configuration", "gateway/configuration")}`);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,7 @@ type LogLine = ReturnType<typeof parseLogLine>;
|
||||
|
||||
const DEFAULT_LIMIT = 200;
|
||||
const MAX_BYTES = 1_000_000;
|
||||
const CHANNELS = new Set<string>([
|
||||
...listChannelPlugins().map((plugin) => plugin.id),
|
||||
"all",
|
||||
]);
|
||||
const CHANNELS = new Set<string>([...listChannelPlugins().map((plugin) => plugin.id), "all"]);
|
||||
|
||||
function parseChannelFilter(raw?: string) {
|
||||
const trimmed = raw?.trim().toLowerCase();
|
||||
@@ -65,8 +62,7 @@ export async function channelsLogsCommand(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
) {
|
||||
const channel = parseChannelFilter(opts.channel);
|
||||
const limitRaw =
|
||||
typeof opts.lines === "string" ? Number(opts.lines) : opts.lines;
|
||||
const limitRaw = typeof opts.lines === "string" ? Number(opts.lines) : opts.lines;
|
||||
const limit =
|
||||
typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0
|
||||
? Math.floor(limitRaw)
|
||||
|
||||
@@ -5,18 +5,10 @@ import {
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import { type ClawdbotConfig, writeConfigFile } from "../../config/config.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../../routing/session-key.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 {
|
||||
type ChatChannel,
|
||||
channelLabel,
|
||||
requireValidConfig,
|
||||
shouldUseWizard,
|
||||
} from "./shared.js";
|
||||
import { type ChatChannel, channelLabel, requireValidConfig, shouldUseWizard } from "./shared.js";
|
||||
|
||||
export type ChannelsRemoveOptions = {
|
||||
channel?: string;
|
||||
@@ -102,8 +94,7 @@ export async function channelsRemoveCommand(
|
||||
}
|
||||
|
||||
const resolvedAccountId =
|
||||
normalizeAccountId(accountId) ??
|
||||
resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
normalizeAccountId(accountId) ?? resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
const accountKey = resolvedAccountId || DEFAULT_ACCOUNT_ID;
|
||||
|
||||
let next = { ...cfg };
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
type ChannelId,
|
||||
getChannelPlugin,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import {
|
||||
type ClawdbotConfig,
|
||||
readConfigFileSnapshot,
|
||||
} from "../../config/config.js";
|
||||
import { type ChannelId, getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import { type ClawdbotConfig, readConfigFileSnapshot } from "../../config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
|
||||
@@ -18,9 +12,7 @@ export async function requireValidConfig(
|
||||
if (snapshot.exists && !snapshot.valid) {
|
||||
const issues =
|
||||
snapshot.issues.length > 0
|
||||
? snapshot.issues
|
||||
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
||||
.join("\n")
|
||||
? snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n")
|
||||
: "Unknown validation issue.";
|
||||
runtime.error(`Config invalid:\n${issues}`);
|
||||
runtime.error("Fix the config or run clawdbot doctor.");
|
||||
@@ -30,10 +22,7 @@ export async function requireValidConfig(
|
||||
return snapshot.config;
|
||||
}
|
||||
|
||||
export function formatAccountLabel(params: {
|
||||
accountId: string;
|
||||
name?: string;
|
||||
}) {
|
||||
export function formatAccountLabel(params: { accountId: string; name?: string }) {
|
||||
const base = params.accountId || DEFAULT_ACCOUNT_ID;
|
||||
if (params.name?.trim()) return `${base} (${params.name.trim()})`;
|
||||
return base;
|
||||
@@ -56,12 +45,8 @@ export function formatChannelAccountLabel(params: {
|
||||
accountId: params.accountId,
|
||||
name: params.name,
|
||||
});
|
||||
const styledChannel = params.channelStyle
|
||||
? params.channelStyle(channelText)
|
||||
: channelText;
|
||||
const styledAccount = params.accountStyle
|
||||
? params.accountStyle(accountText)
|
||||
: accountText;
|
||||
const styledChannel = params.channelStyle ? params.channelStyle(channelText) : channelText;
|
||||
const styledAccount = params.accountStyle ? params.accountStyle(accountText) : accountText;
|
||||
return `${styledChannel} ${styledAccount}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,14 @@ 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 { type ClawdbotConfig, readConfigFileSnapshot } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.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 ChatChannel,
|
||||
formatChannelAccountLabel,
|
||||
requireValidConfig,
|
||||
} from "./shared.js";
|
||||
import { type ChatChannel, formatChannelAccountLabel, requireValidConfig } from "./shared.js";
|
||||
|
||||
export type ChannelsStatusOptions = {
|
||||
json?: boolean;
|
||||
@@ -24,15 +17,10 @@ export type ChannelsStatusOptions = {
|
||||
timeout?: string;
|
||||
};
|
||||
|
||||
export function formatGatewayChannelsStatusLines(
|
||||
payload: Record<string, unknown>,
|
||||
): string[] {
|
||||
export function formatGatewayChannelsStatusLines(payload: Record<string, unknown>): string[] {
|
||||
const lines: string[] = [];
|
||||
lines.push(theme.success("Gateway reachable."));
|
||||
const accountLines = (
|
||||
provider: ChatChannel,
|
||||
accounts: Array<Record<string, unknown>>,
|
||||
) =>
|
||||
const accountLines = (provider: ChatChannel, accounts: Array<Record<string, unknown>>) =>
|
||||
accounts.map((account) => {
|
||||
const bits: string[] = [];
|
||||
if (typeof account.enabled === "boolean") {
|
||||
@@ -51,13 +39,11 @@ export function formatGatewayChannelsStatusLines(
|
||||
bits.push(account.connected ? "connected" : "disconnected");
|
||||
}
|
||||
const inboundAt =
|
||||
typeof account.lastInboundAt === "number" &&
|
||||
Number.isFinite(account.lastInboundAt)
|
||||
typeof account.lastInboundAt === "number" && Number.isFinite(account.lastInboundAt)
|
||||
? account.lastInboundAt
|
||||
: null;
|
||||
const outboundAt =
|
||||
typeof account.lastOutboundAt === "number" &&
|
||||
Number.isFinite(account.lastOutboundAt)
|
||||
typeof account.lastOutboundAt === "number" && Number.isFinite(account.lastOutboundAt)
|
||||
? account.lastOutboundAt
|
||||
: null;
|
||||
if (inboundAt) bits.push(`in:${formatAge(Date.now() - inboundAt)}`);
|
||||
@@ -74,16 +60,10 @@ export function formatGatewayChannelsStatusLines(
|
||||
if (typeof account.tokenSource === "string" && account.tokenSource) {
|
||||
bits.push(`token:${account.tokenSource}`);
|
||||
}
|
||||
if (
|
||||
typeof account.botTokenSource === "string" &&
|
||||
account.botTokenSource
|
||||
) {
|
||||
if (typeof account.botTokenSource === "string" && account.botTokenSource) {
|
||||
bits.push(`bot:${account.botTokenSource}`);
|
||||
}
|
||||
if (
|
||||
typeof account.appTokenSource === "string" &&
|
||||
account.appTokenSource
|
||||
) {
|
||||
if (typeof account.appTokenSource === "string" && account.appTokenSource) {
|
||||
bits.push(`app:${account.appTokenSource}`);
|
||||
}
|
||||
const application = account.application as
|
||||
@@ -114,8 +94,7 @@ export function formatGatewayChannelsStatusLines(
|
||||
if (typeof account.lastError === "string" && account.lastError) {
|
||||
bits.push(`error:${account.lastError}`);
|
||||
}
|
||||
const accountId =
|
||||
typeof account.accountId === "string" ? account.accountId : "default";
|
||||
const accountId = typeof account.accountId === "string" ? account.accountId : "default";
|
||||
const name = typeof account.name === "string" ? account.name.trim() : "";
|
||||
const labelText = formatChannelAccountLabel({
|
||||
channel: provider,
|
||||
@@ -126,12 +105,8 @@ export function formatGatewayChannelsStatusLines(
|
||||
});
|
||||
|
||||
const plugins = listChannelPlugins();
|
||||
const accountsByChannel = payload.channelAccounts as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const accountPayloads: Partial<
|
||||
Record<string, Array<Record<string, unknown>>>
|
||||
> = {};
|
||||
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 = accountsByChannel?.[plugin.id];
|
||||
if (Array.isArray(raw)) {
|
||||
@@ -178,10 +153,7 @@ async function formatConfigChannelsStatusLines(
|
||||
}
|
||||
if (meta.path || meta.mode) lines.push("");
|
||||
|
||||
const accountLines = (
|
||||
provider: ChatChannel,
|
||||
accounts: Array<Record<string, unknown>>,
|
||||
) =>
|
||||
const accountLines = (provider: ChatChannel, accounts: Array<Record<string, unknown>>) =>
|
||||
accounts.map((account) => {
|
||||
const bits: string[] = [];
|
||||
if (typeof account.enabled === "boolean") {
|
||||
@@ -199,23 +171,16 @@ async function formatConfigChannelsStatusLines(
|
||||
if (typeof account.tokenSource === "string" && account.tokenSource) {
|
||||
bits.push(`token:${account.tokenSource}`);
|
||||
}
|
||||
if (
|
||||
typeof account.botTokenSource === "string" &&
|
||||
account.botTokenSource
|
||||
) {
|
||||
if (typeof account.botTokenSource === "string" && account.botTokenSource) {
|
||||
bits.push(`bot:${account.botTokenSource}`);
|
||||
}
|
||||
if (
|
||||
typeof account.appTokenSource === "string" &&
|
||||
account.appTokenSource
|
||||
) {
|
||||
if (typeof account.appTokenSource === "string" && account.appTokenSource) {
|
||||
bits.push(`app:${account.appTokenSource}`);
|
||||
}
|
||||
if (typeof account.baseUrl === "string" && account.baseUrl) {
|
||||
bits.push(`url:${account.baseUrl}`);
|
||||
}
|
||||
const accountId =
|
||||
typeof account.accountId === "string" ? account.accountId : "default";
|
||||
const accountId = typeof account.accountId === "string" ? account.accountId : "default";
|
||||
const name = typeof account.name === "string" ? account.name.trim() : "";
|
||||
const labelText = formatChannelAccountLabel({
|
||||
channel: provider,
|
||||
@@ -255,9 +220,7 @@ export async function channelsStatusCommand(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
) {
|
||||
const timeoutMs = Number(opts.timeout ?? 10_000);
|
||||
const statusLabel = opts.probe
|
||||
? "Checking channel status (probe)…"
|
||||
: "Checking channel status…";
|
||||
const statusLabel = opts.probe ? "Checking channel status (probe)…" : "Checking channel status…";
|
||||
const shouldLogStatus = opts.json !== true && !process.stderr.isTTY;
|
||||
if (shouldLogStatus) runtime.log(statusLabel);
|
||||
try {
|
||||
@@ -278,11 +241,7 @@ export async function channelsStatusCommand(
|
||||
runtime.log(JSON.stringify(payload, null, 2));
|
||||
return;
|
||||
}
|
||||
runtime.log(
|
||||
formatGatewayChannelsStatusLines(payload as Record<string, unknown>).join(
|
||||
"\n",
|
||||
),
|
||||
);
|
||||
runtime.log(formatGatewayChannelsStatusLines(payload as Record<string, unknown>).join("\n"));
|
||||
} catch (err) {
|
||||
runtime.error(`Gateway not reachable: ${String(err)}`);
|
||||
const cfg = await requireValidConfig(runtime);
|
||||
|
||||
@@ -2,10 +2,7 @@ import net from "node:net";
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
CHUTES_TOKEN_ENDPOINT,
|
||||
CHUTES_USERINFO_ENDPOINT,
|
||||
} from "../agents/chutes-oauth.js";
|
||||
import { CHUTES_TOKEN_ENDPOINT, CHUTES_USERINFO_ENDPOINT } from "../agents/chutes-oauth.js";
|
||||
import { loginChutes } from "./chutes-oauth.js";
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
@@ -160,8 +157,7 @@ describe("loginChutes", () => {
|
||||
});
|
||||
|
||||
it("rejects pasted redirect URLs missing state", async () => {
|
||||
const fetchFn: typeof fetch = async () =>
|
||||
new Response("not found", { status: 404 });
|
||||
const fetchFn: typeof fetch = async () => new Response("not found", { status: 404 });
|
||||
|
||||
await expect(
|
||||
loginChutes({
|
||||
@@ -174,8 +170,7 @@ describe("loginChutes", () => {
|
||||
createPkce: () => ({ verifier: "verifier_123", challenge: "chal_123" }),
|
||||
createState: () => "state_456",
|
||||
onAuth: async () => {},
|
||||
onPrompt: async () =>
|
||||
"http://127.0.0.1:1456/oauth-callback?code=code_only",
|
||||
onPrompt: async () => "http://127.0.0.1:1456/oauth-callback?code=code_only",
|
||||
fetchFn,
|
||||
}),
|
||||
).rejects.toThrow("Missing 'state' parameter");
|
||||
|
||||
@@ -43,82 +43,76 @@ async function waitForLocalCallback(params: {
|
||||
}): Promise<{ code: string; state: string }> {
|
||||
const redirectUrl = new URL(params.redirectUri);
|
||||
if (redirectUrl.protocol !== "http:") {
|
||||
throw new Error(
|
||||
`Chutes OAuth redirect URI must be http:// (got ${params.redirectUri})`,
|
||||
);
|
||||
throw new Error(`Chutes OAuth redirect URI must be http:// (got ${params.redirectUri})`);
|
||||
}
|
||||
const hostname = redirectUrl.hostname || "127.0.0.1";
|
||||
const port = redirectUrl.port ? Number.parseInt(redirectUrl.port, 10) : 80;
|
||||
const expectedPath = redirectUrl.pathname || "/";
|
||||
|
||||
return await new Promise<{ code: string; state: string }>(
|
||||
(resolve, reject) => {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
const server = createServer((req, res) => {
|
||||
try {
|
||||
const requestUrl = new URL(req.url ?? "/", redirectUrl.origin);
|
||||
if (requestUrl.pathname !== expectedPath) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("Not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const code = requestUrl.searchParams.get("code")?.trim();
|
||||
const state = requestUrl.searchParams.get("state")?.trim();
|
||||
|
||||
if (!code) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("Missing code");
|
||||
return;
|
||||
}
|
||||
if (!state || state !== params.expectedState) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("Invalid state");
|
||||
return;
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.end(
|
||||
[
|
||||
"<!doctype html>",
|
||||
"<html><head><meta charset='utf-8' /></head>",
|
||||
"<body><h2>Chutes OAuth complete</h2>",
|
||||
"<p>You can close this window and return to clawdbot.</p></body></html>",
|
||||
].join(""),
|
||||
);
|
||||
if (timeout) clearTimeout(timeout);
|
||||
server.close();
|
||||
resolve({ code, state });
|
||||
} catch (err) {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
server.close();
|
||||
reject(err);
|
||||
return await new Promise<{ code: string; state: string }>((resolve, reject) => {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
const server = createServer((req, res) => {
|
||||
try {
|
||||
const requestUrl = new URL(req.url ?? "/", redirectUrl.origin);
|
||||
if (requestUrl.pathname !== expectedPath) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("Not found");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
server.once("error", (err) => {
|
||||
const code = requestUrl.searchParams.get("code")?.trim();
|
||||
const state = requestUrl.searchParams.get("state")?.trim();
|
||||
|
||||
if (!code) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("Missing code");
|
||||
return;
|
||||
}
|
||||
if (!state || state !== params.expectedState) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("Invalid state");
|
||||
return;
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.end(
|
||||
[
|
||||
"<!doctype html>",
|
||||
"<html><head><meta charset='utf-8' /></head>",
|
||||
"<body><h2>Chutes OAuth complete</h2>",
|
||||
"<p>You can close this window and return to clawdbot.</p></body></html>",
|
||||
].join(""),
|
||||
);
|
||||
if (timeout) clearTimeout(timeout);
|
||||
server.close();
|
||||
resolve({ code, state });
|
||||
} catch (err) {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
server.close();
|
||||
reject(err);
|
||||
});
|
||||
server.listen(port, hostname, () => {
|
||||
params.onProgress?.(
|
||||
`Waiting for OAuth callback on ${redirectUrl.origin}${expectedPath}…`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
try {
|
||||
server.close();
|
||||
} catch {}
|
||||
reject(new Error("OAuth callback timeout"));
|
||||
}, params.timeoutMs);
|
||||
},
|
||||
);
|
||||
server.once("error", (err) => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
server.close();
|
||||
reject(err);
|
||||
});
|
||||
server.listen(port, hostname, () => {
|
||||
params.onProgress?.(`Waiting for OAuth callback on ${redirectUrl.origin}${expectedPath}…`);
|
||||
});
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
try {
|
||||
server.close();
|
||||
} catch {}
|
||||
reject(new Error("OAuth callback timeout"));
|
||||
}, params.timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
export async function loginChutes(params: {
|
||||
@@ -133,8 +127,7 @@ export async function loginChutes(params: {
|
||||
fetchFn?: typeof fetch;
|
||||
}): Promise<OAuthCredentials> {
|
||||
const createPkce = params.createPkce ?? generateChutesPkce;
|
||||
const createState =
|
||||
params.createState ?? (() => randomBytes(16).toString("hex"));
|
||||
const createState = params.createState ?? (() => randomBytes(16).toString("hex"));
|
||||
|
||||
const { verifier, challenge } = createPkce();
|
||||
const state = createState();
|
||||
|
||||
@@ -11,9 +11,7 @@ export type RemovalResult = {
|
||||
skipped?: boolean;
|
||||
};
|
||||
|
||||
export function collectWorkspaceDirs(
|
||||
cfg: ClawdbotConfig | undefined,
|
||||
): string[] {
|
||||
export function collectWorkspaceDirs(cfg: ClawdbotConfig | undefined): string[] {
|
||||
const dirs = new Set<string>();
|
||||
const defaults = cfg?.agents?.defaults;
|
||||
if (typeof defaults?.workspace === "string" && defaults.workspace.trim()) {
|
||||
@@ -34,10 +32,7 @@ export function collectWorkspaceDirs(
|
||||
|
||||
export function isPathWithin(child: string, parent: string): boolean {
|
||||
const relative = path.relative(parent, child);
|
||||
return (
|
||||
relative === "" ||
|
||||
(!relative.startsWith("..") && !path.isAbsolute(relative))
|
||||
);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function isUnsafeRemovalTarget(target: string): boolean {
|
||||
@@ -76,9 +71,7 @@ export async function removePath(
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAgentSessionDirs(
|
||||
stateDir: string,
|
||||
): Promise<string[]> {
|
||||
export async function listAgentSessionDirs(stateDir: string): Promise<string[]> {
|
||||
const root = path.join(stateDir, "agents");
|
||||
try {
|
||||
const entries = await fs.readdir(root, { withFileTypes: true });
|
||||
|
||||
@@ -45,8 +45,7 @@ export async function removeChannelConfigWizard(
|
||||
|
||||
if (channel === "done") return next;
|
||||
|
||||
const label =
|
||||
listChatChannels().find((meta) => meta.id === channel)?.label ?? channel;
|
||||
const label = listChatChannels().find((meta) => meta.id === channel)?.label ?? channel;
|
||||
const confirmed = guardCancel(
|
||||
await confirm({
|
||||
message: `Delete ${label} configuration from ${CONFIG_PATH_CLAWDBOT}?`,
|
||||
@@ -66,10 +65,9 @@ export async function removeChannelConfigWizard(
|
||||
};
|
||||
|
||||
note(
|
||||
[
|
||||
`${label} removed from config.`,
|
||||
"Note: credentials/sessions on disk are unchanged.",
|
||||
].join("\n"),
|
||||
[`${label} removed from config.`, "Note: credentials/sessions on disk are unchanged."].join(
|
||||
"\n",
|
||||
),
|
||||
"Channel removed",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,19 +72,17 @@ export async function maybeInstallDaemon(params: {
|
||||
) as GatewayDaemonRuntime;
|
||||
}
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts");
|
||||
const nodePath = await resolvePreferredNodePath({
|
||||
env: process.env,
|
||||
runtime: daemonRuntime,
|
||||
});
|
||||
const { programArguments, workingDirectory } =
|
||||
await resolveGatewayProgramArguments({
|
||||
port: params.port,
|
||||
dev: devMode,
|
||||
runtime: daemonRuntime,
|
||||
nodePath,
|
||||
});
|
||||
const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({
|
||||
port: params.port,
|
||||
dev: devMode,
|
||||
runtime: daemonRuntime,
|
||||
nodePath,
|
||||
});
|
||||
if (daemonRuntime === "node") {
|
||||
const systemNode = await resolveSystemNodeInfo({ env: process.env });
|
||||
const warning = renderSystemNodeWarning(systemNode, programArguments[0]);
|
||||
@@ -113,8 +111,7 @@ export async function maybeInstallDaemon(params: {
|
||||
await ensureSystemdUserLingerInteractive({
|
||||
runtime: params.runtime,
|
||||
prompter: {
|
||||
confirm: async (p) =>
|
||||
guardCancel(await confirm(p), params.runtime) === true,
|
||||
confirm: async (p) => guardCancel(await confirm(p), params.runtime) === true,
|
||||
note,
|
||||
},
|
||||
reason:
|
||||
|
||||
@@ -2,10 +2,7 @@ import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import type { ClawdbotConfig, GatewayAuthConfig } from "../config/config.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import {
|
||||
applyAuthChoice,
|
||||
resolvePreferredProviderForAuthChoice,
|
||||
} from "./auth-choice.js";
|
||||
import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js";
|
||||
import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
|
||||
import { applyPrimaryModel, promptDefaultModel } from "./model-picker.js";
|
||||
|
||||
|
||||
@@ -21,8 +21,7 @@ export async function promptGatewayConfig(
|
||||
await text({
|
||||
message: "Gateway port",
|
||||
initialValue: String(resolveGatewayPort(cfg)),
|
||||
validate: (value) =>
|
||||
Number.isFinite(Number(value)) ? undefined : "Invalid port",
|
||||
validate: (value) => (Number.isFinite(Number(value)) ? undefined : "Invalid port"),
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
@@ -67,14 +66,11 @@ export async function promptGatewayConfig(
|
||||
if (!value) return "IP address is required for custom bind mode";
|
||||
const trimmed = value.trim();
|
||||
const parts = trimmed.split(".");
|
||||
if (parts.length !== 4)
|
||||
return "Invalid IPv4 address (e.g., 192.168.1.100)";
|
||||
if (parts.length !== 4) return "Invalid IPv4 address (e.g., 192.168.1.100)";
|
||||
if (
|
||||
parts.every((part) => {
|
||||
const n = parseInt(part, 10);
|
||||
return (
|
||||
!Number.isNaN(n) && n >= 0 && n <= 255 && part === String(n)
|
||||
);
|
||||
return !Number.isNaN(n) && n >= 0 && n <= 255 && part === String(n);
|
||||
})
|
||||
)
|
||||
return undefined;
|
||||
@@ -143,11 +139,9 @@ export async function promptGatewayConfig(
|
||||
let tailscaleResetOnExit = false;
|
||||
if (tailscaleMode !== "off") {
|
||||
note(
|
||||
[
|
||||
"Docs:",
|
||||
"https://docs.clawd.bot/gateway/tailscale",
|
||||
"https://docs.clawd.bot/web",
|
||||
].join("\n"),
|
||||
["Docs:", "https://docs.clawd.bot/gateway/tailscale", "https://docs.clawd.bot/web"].join(
|
||||
"\n",
|
||||
),
|
||||
"Tailscale",
|
||||
);
|
||||
tailscaleResetOnExit = Boolean(
|
||||
@@ -162,10 +156,7 @@ export async function promptGatewayConfig(
|
||||
}
|
||||
|
||||
if (tailscaleMode !== "off" && bind !== "loopback") {
|
||||
note(
|
||||
"Tailscale requires bind=loopback. Adjusting bind to loopback.",
|
||||
"Note",
|
||||
);
|
||||
note("Tailscale requires bind=loopback. Adjusting bind to loopback.", "Note");
|
||||
bind = "loopback";
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,7 @@ import {
|
||||
text as clackText,
|
||||
} from "@clack/prompts";
|
||||
|
||||
import {
|
||||
stylePromptHint,
|
||||
stylePromptMessage,
|
||||
stylePromptTitle,
|
||||
} from "../terminal/prompt-style.js";
|
||||
import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
|
||||
export const CONFIGURE_WIZARD_SECTIONS = [
|
||||
"workspace",
|
||||
@@ -57,10 +53,8 @@ export const CONFIGURE_SECTION_OPTIONS: Array<{
|
||||
},
|
||||
];
|
||||
|
||||
export const intro = (message: string) =>
|
||||
clackIntro(stylePromptTitle(message) ?? message);
|
||||
export const outro = (message: string) =>
|
||||
clackOutro(stylePromptTitle(message) ?? message);
|
||||
export const intro = (message: string) => clackIntro(stylePromptTitle(message) ?? message);
|
||||
export const outro = (message: string) => clackOutro(stylePromptTitle(message) ?? message);
|
||||
export const text = (params: Parameters<typeof clackText>[0]) =>
|
||||
clackText({
|
||||
...params,
|
||||
@@ -76,8 +70,6 @@ export const select = <T>(params: Parameters<typeof clackSelect<T>>[0]) =>
|
||||
...params,
|
||||
message: stylePromptMessage(params.message),
|
||||
options: params.options.map((opt) =>
|
||||
opt.hint === undefined
|
||||
? opt
|
||||
: { ...opt, hint: stylePromptHint(opt.hint) },
|
||||
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
export {
|
||||
configureCommand,
|
||||
configureCommandWithSections,
|
||||
} from "./configure.commands.js";
|
||||
export { configureCommand, configureCommandWithSections } from "./configure.commands.js";
|
||||
export { buildGatewayAuthConfig } from "./configure.gateway-auth.js";
|
||||
export {
|
||||
CONFIGURE_WIZARD_SECTIONS,
|
||||
type WizardSection,
|
||||
} from "./configure.shared.js";
|
||||
export { CONFIGURE_WIZARD_SECTIONS, type WizardSection } from "./configure.shared.js";
|
||||
export { runConfigureWizard } from "./configure.wizard.js";
|
||||
|
||||
@@ -21,13 +21,7 @@ import type {
|
||||
ConfigureWizardParams,
|
||||
WizardSection,
|
||||
} from "./configure.shared.js";
|
||||
import {
|
||||
CONFIGURE_SECTION_OPTIONS,
|
||||
intro,
|
||||
outro,
|
||||
select,
|
||||
text,
|
||||
} from "./configure.shared.js";
|
||||
import { CONFIGURE_SECTION_OPTIONS, intro, outro, select, text } from "./configure.shared.js";
|
||||
import { healthCommand } from "./health.js";
|
||||
import { formatHealthCheckFailure } from "./health-format.js";
|
||||
import { setupChannels } from "./onboard-channels.js";
|
||||
@@ -67,9 +61,7 @@ async function promptConfigureSection(
|
||||
);
|
||||
}
|
||||
|
||||
async function promptChannelMode(
|
||||
runtime: RuntimeEnv,
|
||||
): Promise<ChannelsWizardMode> {
|
||||
async function promptChannelMode(runtime: RuntimeEnv): Promise<ChannelsWizardMode> {
|
||||
return guardCancel(
|
||||
await select({
|
||||
message: "Channels",
|
||||
@@ -97,20 +89,14 @@ export async function runConfigureWizard(
|
||||
) {
|
||||
try {
|
||||
printWizardHeader(runtime);
|
||||
intro(
|
||||
opts.command === "update"
|
||||
? "Clawdbot update wizard"
|
||||
: "Clawdbot configure",
|
||||
);
|
||||
intro(opts.command === "update" ? "Clawdbot update wizard" : "Clawdbot configure");
|
||||
const prompter = createClackPrompter();
|
||||
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const baseConfig: ClawdbotConfig = snapshot.valid ? snapshot.config : {};
|
||||
|
||||
if (snapshot.exists) {
|
||||
const title = snapshot.valid
|
||||
? "Existing config detected"
|
||||
: "Invalid config";
|
||||
const title = snapshot.valid ? "Existing config detected" : "Invalid config";
|
||||
note(summarizeExistingConfig(baseConfig), title);
|
||||
if (!snapshot.valid && snapshot.issues.length > 0) {
|
||||
note(
|
||||
@@ -123,9 +109,7 @@ export async function runConfigureWizard(
|
||||
);
|
||||
}
|
||||
if (!snapshot.valid) {
|
||||
outro(
|
||||
"Config invalid. Run `clawdbot doctor` to repair it, then re-run configure.",
|
||||
);
|
||||
outro("Config invalid. Run `clawdbot doctor` to repair it, then re-run configure.");
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -134,11 +118,8 @@ export async function runConfigureWizard(
|
||||
const localUrl = "ws://127.0.0.1:18789";
|
||||
const localProbe = await probeGatewayReachable({
|
||||
url: localUrl,
|
||||
token:
|
||||
baseConfig.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
password:
|
||||
baseConfig.gateway?.auth?.password ??
|
||||
process.env.CLAWDBOT_GATEWAY_PASSWORD,
|
||||
token: baseConfig.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
password: baseConfig.gateway?.auth?.password ?? process.env.CLAWDBOT_GATEWAY_PASSWORD,
|
||||
});
|
||||
const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? "";
|
||||
const remoteProbe = remoteUrl
|
||||
@@ -220,9 +201,7 @@ export async function runConfigureWizard(
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
workspaceDir = resolveUserPath(
|
||||
String(workspaceInput ?? "").trim() || DEFAULT_WORKSPACE,
|
||||
);
|
||||
workspaceDir = resolveUserPath(String(workspaceInput ?? "").trim() || DEFAULT_WORKSPACE);
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
@@ -272,8 +251,7 @@ export async function runConfigureWizard(
|
||||
await text({
|
||||
message: "Gateway port for daemon install",
|
||||
initialValue: String(gatewayPort),
|
||||
validate: (value) =>
|
||||
Number.isFinite(Number(value)) ? undefined : "Invalid port",
|
||||
validate: (value) => (Number.isFinite(Number(value)) ? undefined : "Invalid port"),
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
@@ -316,9 +294,7 @@ export async function runConfigureWizard(
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
workspaceDir = resolveUserPath(
|
||||
String(workspaceInput ?? "").trim() || DEFAULT_WORKSPACE,
|
||||
);
|
||||
workspaceDir = resolveUserPath(String(workspaceInput ?? "").trim() || DEFAULT_WORKSPACE);
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
@@ -372,8 +348,7 @@ export async function runConfigureWizard(
|
||||
await text({
|
||||
message: "Gateway port for daemon install",
|
||||
initialValue: String(gatewayPort),
|
||||
validate: (value) =>
|
||||
Number.isFinite(Number(value)) ? undefined : "Invalid port",
|
||||
validate: (value) => (Number.isFinite(Number(value)) ? undefined : "Invalid port"),
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
@@ -423,14 +398,9 @@ export async function runConfigureWizard(
|
||||
basePath: nextConfig.gateway?.controlUi?.basePath,
|
||||
});
|
||||
// Try both new and old passwords since gateway may still have old config.
|
||||
const newPassword =
|
||||
nextConfig.gateway?.auth?.password ??
|
||||
process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||
const oldPassword =
|
||||
baseConfig.gateway?.auth?.password ??
|
||||
process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||
const token =
|
||||
nextConfig.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
const newPassword = nextConfig.gateway?.auth?.password ?? process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||
const oldPassword = baseConfig.gateway?.auth?.password ?? process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||
const token = nextConfig.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
|
||||
let gatewayProbe = await probeGatewayReachable({
|
||||
url: links.wsUrl,
|
||||
|
||||
@@ -14,8 +14,6 @@ export const GATEWAY_DAEMON_RUNTIME_OPTIONS: Array<{
|
||||
},
|
||||
];
|
||||
|
||||
export function isGatewayDaemonRuntime(
|
||||
value: string | undefined,
|
||||
): value is GatewayDaemonRuntime {
|
||||
export function isGatewayDaemonRuntime(value: string | undefined): value is GatewayDaemonRuntime {
|
||||
return value === "node" || value === "bun";
|
||||
}
|
||||
|
||||
@@ -81,12 +81,8 @@ describe("dashboardCommand", () => {
|
||||
customBindHost: undefined,
|
||||
basePath: undefined,
|
||||
});
|
||||
expect(mocks.copyToClipboard).toHaveBeenCalledWith(
|
||||
"http://127.0.0.1:18789/?token=abc123",
|
||||
);
|
||||
expect(mocks.openUrl).toHaveBeenCalledWith(
|
||||
"http://127.0.0.1:18789/?token=abc123",
|
||||
);
|
||||
expect(mocks.copyToClipboard).toHaveBeenCalledWith("http://127.0.0.1:18789/?token=abc123");
|
||||
expect(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/?token=abc123");
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
"Opened in your browser. Keep that tab to control Clawdbot.",
|
||||
);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
readConfigFileSnapshot,
|
||||
resolveGatewayPort,
|
||||
} from "../config/config.js";
|
||||
import { readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import {
|
||||
@@ -26,8 +23,7 @@ export async function dashboardCommand(
|
||||
const bind = cfg.gateway?.bind ?? "loopback";
|
||||
const basePath = cfg.gateway?.controlUi?.basePath;
|
||||
const customBindHost = cfg.gateway?.customBindHost;
|
||||
const token =
|
||||
cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN ?? "";
|
||||
const token = cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN ?? "";
|
||||
|
||||
const links = resolveControlUiLinks({
|
||||
port,
|
||||
@@ -35,16 +31,12 @@ export async function dashboardCommand(
|
||||
customBindHost,
|
||||
basePath,
|
||||
});
|
||||
const authedUrl = token
|
||||
? `${links.httpUrl}?token=${encodeURIComponent(token)}`
|
||||
: links.httpUrl;
|
||||
const authedUrl = token ? `${links.httpUrl}?token=${encodeURIComponent(token)}` : links.httpUrl;
|
||||
|
||||
runtime.log(`Dashboard URL: ${authedUrl}`);
|
||||
|
||||
const copied = await copyToClipboard(authedUrl).catch(() => false);
|
||||
runtime.log(
|
||||
copied ? "Copied to clipboard." : "Copy to clipboard unavailable.",
|
||||
);
|
||||
runtime.log(copied ? "Copied to clipboard." : "Copy to clipboard unavailable.");
|
||||
|
||||
let opened = false;
|
||||
let hint: string | undefined;
|
||||
|
||||
@@ -30,11 +30,7 @@ function resolveNodeRunner(): NodeRunner {
|
||||
throw new Error("Missing pnpm or npx; install a Node package runner.");
|
||||
}
|
||||
|
||||
async function runNodeTool(
|
||||
tool: string,
|
||||
toolArgs: string[],
|
||||
options: ToolRunOptions = {},
|
||||
) {
|
||||
async function runNodeTool(tool: string, toolArgs: string[], options: ToolRunOptions = {}) {
|
||||
const runner = resolveNodeRunner();
|
||||
const argv = [runner.cmd, ...runner.args, tool, ...toolArgs];
|
||||
return await runCommandWithTimeout(argv, {
|
||||
@@ -43,11 +39,7 @@ async function runNodeTool(
|
||||
});
|
||||
}
|
||||
|
||||
async function runTool(
|
||||
tool: string,
|
||||
toolArgs: string[],
|
||||
options: ToolRunOptions = {},
|
||||
) {
|
||||
async function runTool(tool: string, toolArgs: string[], options: ToolRunOptions = {}) {
|
||||
if (hasBinary(tool)) {
|
||||
return await runCommandWithTimeout([tool, ...toolArgs], {
|
||||
timeoutMs: options.timeoutMs ?? SEARCH_TIMEOUT_MS,
|
||||
@@ -130,11 +122,7 @@ function formatLinkLabel(link: string): string {
|
||||
return link.replace(/^https?:\/\//i, "");
|
||||
}
|
||||
|
||||
function renderRichResults(
|
||||
query: string,
|
||||
results: DocResult[],
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
function renderRichResults(query: string, results: DocResult[], runtime: RuntimeEnv) {
|
||||
runtime.log(`${theme.heading("Docs search:")} ${theme.info(query)}`);
|
||||
if (results.length === 0) {
|
||||
runtime.log(theme.muted("No results."));
|
||||
@@ -156,10 +144,7 @@ async function renderMarkdown(markdown: string, runtime: RuntimeEnv) {
|
||||
runtime.log(markdown.trimEnd());
|
||||
}
|
||||
|
||||
export async function docsSearchCommand(
|
||||
queryParts: string[],
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
export async function docsSearchCommand(queryParts: string[], runtime: RuntimeEnv) {
|
||||
const query = queryParts.join(" ").trim();
|
||||
if (!query) {
|
||||
const docs = formatDocsLink("/", "docs.clawd.bot");
|
||||
|
||||
@@ -45,16 +45,10 @@ type AuthIssue = {
|
||||
};
|
||||
|
||||
function formatAuthIssueHint(issue: AuthIssue): string | null {
|
||||
if (
|
||||
issue.provider === "anthropic" &&
|
||||
issue.profileId === CLAUDE_CLI_PROFILE_ID
|
||||
) {
|
||||
if (issue.provider === "anthropic" && issue.profileId === CLAUDE_CLI_PROFILE_ID) {
|
||||
return "Run `claude setup-token` on the gateway host.";
|
||||
}
|
||||
if (
|
||||
issue.provider === "openai-codex" &&
|
||||
issue.profileId === CODEX_CLI_PROFILE_ID
|
||||
) {
|
||||
if (issue.provider === "openai-codex" && issue.profileId === CODEX_CLI_PROFILE_ID) {
|
||||
return "Run `codex login` (or `clawdbot configure` → OpenAI Codex OAuth).";
|
||||
}
|
||||
return "Re-auth via `clawdbot configure` or `clawdbot onboard`.";
|
||||
@@ -62,9 +56,7 @@ function formatAuthIssueHint(issue: AuthIssue): string | null {
|
||||
|
||||
function formatAuthIssueLine(issue: AuthIssue): string {
|
||||
const remaining =
|
||||
issue.remainingMs !== undefined
|
||||
? ` (${formatRemainingShort(issue.remainingMs)})`
|
||||
: "";
|
||||
issue.remainingMs !== undefined ? ` (${formatRemainingShort(issue.remainingMs)})` : "";
|
||||
const hint = formatAuthIssueHint(issue);
|
||||
return `- ${issue.profileId}: ${issue.status}${remaining}${hint ? ` — ${hint}` : ""}`;
|
||||
}
|
||||
@@ -92,9 +84,7 @@ export async function noteAuthProfileHealth(params: {
|
||||
const hint = kind.startsWith("disabled:billing")
|
||||
? "Top up credits (provider billing) or switch provider."
|
||||
: "Wait for cooldown or switch provider.";
|
||||
out.push(
|
||||
`- ${profileId}: ${kind} (${remaining})${hint ? ` — ${hint}` : ""}`,
|
||||
);
|
||||
out.push(`- ${profileId}: ${kind} (${remaining})${hint ? ` — ${hint}` : ""}`);
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
@@ -129,8 +119,7 @@ export async function noteAuthProfileHealth(params: {
|
||||
if (shouldRefresh) {
|
||||
const refreshTargets = issues.filter(
|
||||
(issue) =>
|
||||
issue.type === "oauth" &&
|
||||
["expired", "expiring", "missing"].includes(issue.status),
|
||||
issue.type === "oauth" && ["expired", "expiring", "missing"].includes(issue.status),
|
||||
);
|
||||
const errors: string[] = [];
|
||||
for (const profile of refreshTargets) {
|
||||
@@ -141,9 +130,7 @@ export async function noteAuthProfileHealth(params: {
|
||||
profileId: profile.profileId,
|
||||
});
|
||||
} catch (err) {
|
||||
errors.push(
|
||||
`- ${profile.profileId}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
errors.push(`- ${profile.profileId}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
|
||||
@@ -47,19 +47,13 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
}) {
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
let cfg: ClawdbotConfig = snapshot.valid ? snapshot.config : {};
|
||||
if (
|
||||
snapshot.exists &&
|
||||
!snapshot.valid &&
|
||||
snapshot.legacyIssues.length === 0
|
||||
) {
|
||||
if (snapshot.exists && !snapshot.valid && snapshot.legacyIssues.length === 0) {
|
||||
note("Config invalid; doctor will run with defaults.", "Config");
|
||||
}
|
||||
|
||||
if (snapshot.legacyIssues.length > 0) {
|
||||
note(
|
||||
snapshot.legacyIssues
|
||||
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
||||
.join("\n"),
|
||||
snapshot.legacyIssues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n"),
|
||||
"Legacy config keys detected",
|
||||
);
|
||||
const migrate =
|
||||
@@ -71,9 +65,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
});
|
||||
if (migrate) {
|
||||
// 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,
|
||||
);
|
||||
const { config: migrated, changes } = migrateLegacyConfig(snapshot.parsed);
|
||||
if (changes.length > 0) note(changes.join("\n"), "Doctor changes");
|
||||
if (migrated) cfg = migrated;
|
||||
}
|
||||
|
||||
@@ -67,9 +67,7 @@ export function buildGatewayRuntimeHints(
|
||||
return hints;
|
||||
}
|
||||
if (runtime.status === "stopped") {
|
||||
hints.push(
|
||||
"Service is loaded but not running (likely exited immediately).",
|
||||
);
|
||||
hints.push("Service is loaded but not running (likely exited immediately).");
|
||||
if (fileLog) hints.push(`File logs: ${fileLog}`);
|
||||
if (platform === "darwin") {
|
||||
const logs = resolveGatewayLogPaths(env);
|
||||
@@ -77,9 +75,7 @@ export function buildGatewayRuntimeHints(
|
||||
hints.push(`Launchd stderr (if installed): ${logs.stderrPath}`);
|
||||
} else if (platform === "linux") {
|
||||
const unit = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
hints.push(
|
||||
`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`,
|
||||
);
|
||||
hints.push(`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`);
|
||||
} else if (platform === "win32") {
|
||||
const task = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
hints.push(`Logs: schtasks /Query /TN "${task}" /V /FO LIST`);
|
||||
|
||||
@@ -21,10 +21,7 @@ import {
|
||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
type GatewayDaemonRuntime,
|
||||
} from "./daemon-runtime.js";
|
||||
import {
|
||||
buildGatewayRuntimeHints,
|
||||
formatGatewayRuntimeSummary,
|
||||
} from "./doctor-format.js";
|
||||
import { buildGatewayRuntimeHints, formatGatewayRuntimeSummary } from "./doctor-format.js";
|
||||
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
|
||||
import { healthCommand } from "./health.js";
|
||||
import { formatHealthCheckFailure } from "./health-format.js";
|
||||
@@ -44,13 +41,9 @@ export async function maybeRepairGatewayDaemon(params: {
|
||||
env: process.env,
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
});
|
||||
let serviceRuntime:
|
||||
| Awaited<ReturnType<typeof service.readRuntime>>
|
||||
| undefined;
|
||||
let serviceRuntime: Awaited<ReturnType<typeof service.readRuntime>> | undefined;
|
||||
if (loaded) {
|
||||
serviceRuntime = await service
|
||||
.readRuntime(process.env)
|
||||
.catch(() => undefined);
|
||||
serviceRuntime = await service.readRuntime(process.env).catch(() => undefined);
|
||||
}
|
||||
|
||||
if (params.cfg.gateway?.mode !== "remote") {
|
||||
@@ -72,15 +65,14 @@ export async function maybeRepairGatewayDaemon(params: {
|
||||
initialValue: true,
|
||||
});
|
||||
if (install) {
|
||||
const daemonRuntime =
|
||||
await params.prompter.select<GatewayDaemonRuntime>(
|
||||
{
|
||||
message: "Gateway daemon runtime",
|
||||
options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
},
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
);
|
||||
const daemonRuntime = await params.prompter.select<GatewayDaemonRuntime>(
|
||||
{
|
||||
message: "Gateway daemon runtime",
|
||||
options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
},
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
);
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
@@ -89,27 +81,21 @@ export async function maybeRepairGatewayDaemon(params: {
|
||||
env: process.env,
|
||||
runtime: daemonRuntime,
|
||||
});
|
||||
const { programArguments, workingDirectory } =
|
||||
await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: daemonRuntime,
|
||||
nodePath,
|
||||
});
|
||||
const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: daemonRuntime,
|
||||
nodePath,
|
||||
});
|
||||
if (daemonRuntime === "node") {
|
||||
const systemNode = await resolveSystemNodeInfo({ env: process.env });
|
||||
const warning = renderSystemNodeWarning(
|
||||
systemNode,
|
||||
programArguments[0],
|
||||
);
|
||||
const warning = renderSystemNodeWarning(systemNode, programArguments[0]);
|
||||
if (warning) note(warning, "Gateway runtime");
|
||||
}
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
port,
|
||||
token:
|
||||
params.cfg.gateway?.auth?.token ??
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
token: params.cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
launchdLabel:
|
||||
process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE)
|
||||
|
||||
@@ -6,10 +6,7 @@ import { note } from "../terminal/note.js";
|
||||
import { healthCommand } from "./health.js";
|
||||
import { formatHealthCheckFailure } from "./health-format.js";
|
||||
|
||||
export async function checkGatewayHealth(params: {
|
||||
runtime: RuntimeEnv;
|
||||
cfg: ClawdbotConfig;
|
||||
}) {
|
||||
export async function checkGatewayHealth(params: { runtime: RuntimeEnv; cfg: ClawdbotConfig }) {
|
||||
const gatewayDetails = buildGatewayConnectionDetails({ config: params.cfg });
|
||||
let healthOk = false;
|
||||
try {
|
||||
|
||||
@@ -3,14 +3,8 @@ import path from "node:path";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import {
|
||||
findExtraGatewayServices,
|
||||
renderGatewayServiceCleanupHints,
|
||||
} from "../daemon/inspect.js";
|
||||
import {
|
||||
findLegacyGatewayServices,
|
||||
uninstallLegacyGatewayServices,
|
||||
} from "../daemon/legacy.js";
|
||||
import { findExtraGatewayServices, renderGatewayServiceCleanupHints } from "../daemon/inspect.js";
|
||||
import { findLegacyGatewayServices, uninstallLegacyGatewayServices } from "../daemon/legacy.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import {
|
||||
renderSystemNodeWarning,
|
||||
@@ -33,9 +27,7 @@ import {
|
||||
} from "./daemon-runtime.js";
|
||||
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
|
||||
|
||||
function detectGatewayRuntime(
|
||||
programArguments: string[] | undefined,
|
||||
): GatewayDaemonRuntime {
|
||||
function detectGatewayRuntime(programArguments: string[] | undefined): GatewayDaemonRuntime {
|
||||
const first = programArguments?.[0];
|
||||
if (first) {
|
||||
const base = path.basename(first).toLowerCase();
|
||||
@@ -66,9 +58,7 @@ export async function maybeMigrateLegacyGatewayService(
|
||||
if (legacyServices.length === 0) return;
|
||||
|
||||
note(
|
||||
legacyServices
|
||||
.map((svc) => `- ${svc.label} (${svc.platform}, ${svc.detail})`)
|
||||
.join("\n"),
|
||||
legacyServices.map((svc) => `- ${svc.label} (${svc.platform}, ${svc.detail})`).join("\n"),
|
||||
"Legacy Clawdis services detected",
|
||||
);
|
||||
|
||||
@@ -123,20 +113,18 @@ export async function maybeMigrateLegacyGatewayService(
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
);
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts");
|
||||
const port = resolveGatewayPort(cfg, process.env);
|
||||
const nodePath = await resolvePreferredNodePath({
|
||||
env: process.env,
|
||||
runtime: daemonRuntime,
|
||||
});
|
||||
const { programArguments, workingDirectory } =
|
||||
await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: daemonRuntime,
|
||||
nodePath,
|
||||
});
|
||||
const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: daemonRuntime,
|
||||
nodePath,
|
||||
});
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
port,
|
||||
@@ -199,24 +187,21 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
}
|
||||
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts");
|
||||
const port = resolveGatewayPort(cfg, process.env);
|
||||
const runtimeChoice = detectGatewayRuntime(command.programArguments);
|
||||
const { programArguments, workingDirectory } =
|
||||
await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
|
||||
nodePath: systemNodePath ?? undefined,
|
||||
});
|
||||
const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
|
||||
nodePath: systemNodePath ?? undefined,
|
||||
});
|
||||
const expectedEntrypoint = findGatewayEntrypoint(programArguments);
|
||||
const currentEntrypoint = findGatewayEntrypoint(command.programArguments);
|
||||
if (
|
||||
expectedEntrypoint &&
|
||||
currentEntrypoint &&
|
||||
normalizeExecutablePath(expectedEntrypoint) !==
|
||||
normalizeExecutablePath(currentEntrypoint)
|
||||
normalizeExecutablePath(expectedEntrypoint) !== normalizeExecutablePath(currentEntrypoint)
|
||||
) {
|
||||
audit.issues.push({
|
||||
code: SERVICE_AUDIT_CODES.gatewayEntrypointMismatch,
|
||||
@@ -231,17 +216,13 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
note(
|
||||
audit.issues
|
||||
.map((issue) =>
|
||||
issue.detail
|
||||
? `- ${issue.message} (${issue.detail})`
|
||||
: `- ${issue.message}`,
|
||||
issue.detail ? `- ${issue.message} (${issue.detail})` : `- ${issue.message}`,
|
||||
)
|
||||
.join("\n"),
|
||||
"Gateway service config",
|
||||
);
|
||||
|
||||
const aggressiveIssues = audit.issues.filter(
|
||||
(issue) => issue.level === "aggressive",
|
||||
);
|
||||
const aggressiveIssues = audit.issues.filter((issue) => issue.level === "aggressive");
|
||||
const needsAggressive = aggressiveIssues.length > 0;
|
||||
|
||||
if (needsAggressive && !prompter.shouldForce) {
|
||||
@@ -257,8 +238,7 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
initialValue: Boolean(prompter.shouldForce),
|
||||
})
|
||||
: await prompter.confirmRepair({
|
||||
message:
|
||||
"Update gateway service config to the recommended defaults now?",
|
||||
message: "Update gateway service config to the recommended defaults now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!repair) return;
|
||||
@@ -292,9 +272,7 @@ export async function maybeScanExtraGatewayServices(options: DoctorOptions) {
|
||||
if (extraServices.length === 0) return;
|
||||
|
||||
note(
|
||||
extraServices
|
||||
.map((svc) => `- ${svc.label} (${svc.scope}, ${svc.detail})`)
|
||||
.join("\n"),
|
||||
extraServices.map((svc) => `- ${svc.label} (${svc.scope}, ${svc.detail})`).join("\n"),
|
||||
"Other gateway-like services detected",
|
||||
);
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@ function resolveLegacyConfigPath(env: NodeJS.ProcessEnv): string {
|
||||
return path.join(os.homedir(), ".clawdis", "clawdis.json");
|
||||
}
|
||||
|
||||
function normalizeDefaultWorkspacePath(
|
||||
value: string | undefined,
|
||||
): string | undefined {
|
||||
function normalizeDefaultWorkspacePath(value: string | undefined): string | undefined {
|
||||
if (!value) return value;
|
||||
|
||||
const resolved = resolveUserPath(value);
|
||||
@@ -43,17 +41,13 @@ function normalizeDefaultWorkspacePath(
|
||||
return next === resolved ? value : next;
|
||||
}
|
||||
|
||||
export function replaceLegacyName(
|
||||
value: string | undefined,
|
||||
): string | undefined {
|
||||
export function replaceLegacyName(value: string | undefined): string | undefined {
|
||||
if (!value) return value;
|
||||
const replacedClawdis = value.replace(/clawdis/g, "clawdbot");
|
||||
return replacedClawdis.replace(/clawd(?!bot)/g, "clawdbot");
|
||||
}
|
||||
|
||||
export function replaceModernName(
|
||||
value: string | undefined,
|
||||
): string | undefined {
|
||||
export function replaceModernName(value: string | undefined): string | undefined {
|
||||
if (!value) return value;
|
||||
if (!value.includes("clawdbot")) return value;
|
||||
return value.replace(/clawdbot/g, "clawdis");
|
||||
@@ -83,21 +77,14 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
||||
let updatedSandbox = sandbox;
|
||||
let sandboxChanged = false;
|
||||
|
||||
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(
|
||||
sandbox.workspaceRoot,
|
||||
);
|
||||
if (
|
||||
updatedWorkspaceRoot &&
|
||||
updatedWorkspaceRoot !== sandbox.workspaceRoot
|
||||
) {
|
||||
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(sandbox.workspaceRoot);
|
||||
if (updatedWorkspaceRoot && updatedWorkspaceRoot !== sandbox.workspaceRoot) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
workspaceRoot: updatedWorkspaceRoot,
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.workspaceRoot → ${updatedWorkspaceRoot}`,
|
||||
);
|
||||
changes.push(`Updated agents.defaults.sandbox.workspaceRoot → ${updatedWorkspaceRoot}`);
|
||||
}
|
||||
|
||||
const dockerImage = sandbox.docker?.image;
|
||||
@@ -111,17 +98,12 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
||||
},
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.docker.image → ${updatedDockerImage}`,
|
||||
);
|
||||
changes.push(`Updated agents.defaults.sandbox.docker.image → ${updatedDockerImage}`);
|
||||
}
|
||||
|
||||
const containerPrefix = sandbox.docker?.containerPrefix;
|
||||
const updatedContainerPrefix = replaceLegacyName(containerPrefix);
|
||||
if (
|
||||
updatedContainerPrefix &&
|
||||
updatedContainerPrefix !== containerPrefix
|
||||
) {
|
||||
if (updatedContainerPrefix && updatedContainerPrefix !== containerPrefix) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
docker: {
|
||||
@@ -163,9 +145,7 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
||||
if (updatedWorkspace && updatedWorkspace !== agent.workspace) {
|
||||
updatedAgent = { ...updatedAgent, workspace: updatedWorkspace };
|
||||
agentChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.list (id "${agent.id}") workspace → ${updatedWorkspace}`,
|
||||
);
|
||||
changes.push(`Updated agents.list (id "${agent.id}") workspace → ${updatedWorkspace}`);
|
||||
}
|
||||
|
||||
const sandbox = agent.sandbox;
|
||||
@@ -173,13 +153,8 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
||||
let updatedSandbox = sandbox;
|
||||
let sandboxChanged = false;
|
||||
|
||||
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(
|
||||
sandbox.workspaceRoot,
|
||||
);
|
||||
if (
|
||||
updatedWorkspaceRoot &&
|
||||
updatedWorkspaceRoot !== sandbox.workspaceRoot
|
||||
) {
|
||||
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(sandbox.workspaceRoot);
|
||||
if (updatedWorkspaceRoot && updatedWorkspaceRoot !== sandbox.workspaceRoot) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
workspaceRoot: updatedWorkspaceRoot,
|
||||
@@ -208,10 +183,7 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
||||
|
||||
const containerPrefix = sandbox.docker?.containerPrefix;
|
||||
const updatedContainerPrefix = replaceLegacyName(containerPrefix);
|
||||
if (
|
||||
updatedContainerPrefix &&
|
||||
updatedContainerPrefix !== containerPrefix
|
||||
) {
|
||||
if (updatedContainerPrefix && updatedContainerPrefix !== containerPrefix) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
docker: {
|
||||
@@ -324,13 +296,9 @@ export async function maybeMigrateLegacyConfigFile(runtime: RuntimeEnv) {
|
||||
? (parsed.agent as Record<string, unknown>)
|
||||
: undefined;
|
||||
const defaultWorkspace =
|
||||
typeof parsedDefaults?.workspace === "string"
|
||||
? parsedDefaults.workspace
|
||||
: undefined;
|
||||
typeof parsedDefaults?.workspace === "string" ? parsedDefaults.workspace : undefined;
|
||||
const legacyWorkspace =
|
||||
typeof parsedLegacyAgent?.workspace === "string"
|
||||
? parsedLegacyAgent.workspace
|
||||
: undefined;
|
||||
typeof parsedLegacyAgent?.workspace === "string" ? parsedLegacyAgent.workspace : undefined;
|
||||
const agentWorkspace = defaultWorkspace ?? legacyWorkspace;
|
||||
const workspaceLabel = defaultWorkspace
|
||||
? "agents.defaults.workspace"
|
||||
@@ -351,16 +319,11 @@ export async function maybeMigrateLegacyConfigFile(runtime: RuntimeEnv) {
|
||||
);
|
||||
|
||||
let nextConfig = legacySnapshot.valid ? legacySnapshot.config : null;
|
||||
const { config: migratedConfig, changes } = migrateLegacyConfig(
|
||||
legacySnapshot.parsed,
|
||||
);
|
||||
const { config: migratedConfig, changes } = migrateLegacyConfig(legacySnapshot.parsed);
|
||||
if (migratedConfig) {
|
||||
nextConfig = migratedConfig;
|
||||
} else if (!nextConfig) {
|
||||
note(
|
||||
`Legacy config at ${legacyConfigPath} is invalid; skipping migration.`,
|
||||
"Legacy config",
|
||||
);
|
||||
note(`Legacy config at ${legacyConfigPath} is invalid; skipping migration.`, "Legacy config");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,7 @@ function resolveHomeDir(): string {
|
||||
|
||||
export async function noteMacLaunchAgentOverrides() {
|
||||
if (process.platform !== "darwin") return;
|
||||
const markerPath = path.join(
|
||||
resolveHomeDir(),
|
||||
".clawdbot",
|
||||
"disable-launchagent",
|
||||
);
|
||||
const markerPath = path.join(resolveHomeDir(), ".clawdbot", "disable-launchagent");
|
||||
const hasMarker = fs.existsSync(markerPath);
|
||||
if (!hasMarker) return;
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { confirm, select } from "@clack/prompts";
|
||||
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
stylePromptHint,
|
||||
stylePromptMessage,
|
||||
} from "../terminal/prompt-style.js";
|
||||
import { stylePromptHint, stylePromptMessage } from "../terminal/prompt-style.js";
|
||||
import { guardCancel } from "./onboard-helpers.js";
|
||||
|
||||
export type DoctorOptions = {
|
||||
@@ -20,12 +17,8 @@ export type DoctorOptions = {
|
||||
export type DoctorPrompter = {
|
||||
confirm: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmRepair: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmAggressive: (
|
||||
params: Parameters<typeof confirm>[0],
|
||||
) => Promise<boolean>;
|
||||
confirmSkipInNonInteractive: (
|
||||
params: Parameters<typeof confirm>[0],
|
||||
) => Promise<boolean>;
|
||||
confirmAggressive: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmSkipInNonInteractive: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
select: <T>(params: Parameters<typeof select>[0], fallback: T) => Promise<T>;
|
||||
shouldRepair: boolean;
|
||||
shouldForce: boolean;
|
||||
@@ -91,9 +84,7 @@ export function createDoctorPrompter(params: {
|
||||
...p,
|
||||
message: stylePromptMessage(p.message),
|
||||
options: p.options.map((opt) =>
|
||||
opt.hint === undefined
|
||||
? opt
|
||||
: { ...opt, hint: stylePromptHint(opt.hint) },
|
||||
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
|
||||
),
|
||||
}),
|
||||
params.runtime,
|
||||
|
||||
@@ -39,16 +39,10 @@ function resolveSandboxScript(scriptRel: string): SandboxScriptInfo | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function runSandboxScript(
|
||||
scriptRel: string,
|
||||
runtime: RuntimeEnv,
|
||||
): Promise<boolean> {
|
||||
async function runSandboxScript(scriptRel: string, runtime: RuntimeEnv): Promise<boolean> {
|
||||
const script = resolveSandboxScript(scriptRel);
|
||||
if (!script) {
|
||||
note(
|
||||
`Unable to locate ${scriptRel}. Run it from the repo root.`,
|
||||
"Sandbox",
|
||||
);
|
||||
note(`Unable to locate ${scriptRel}. Run it from the repo root.`, "Sandbox");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -100,10 +94,7 @@ function resolveSandboxBrowserImage(cfg: ClawdbotConfig): string {
|
||||
return image ? image : DEFAULT_SANDBOX_BROWSER_IMAGE;
|
||||
}
|
||||
|
||||
function updateSandboxDockerImage(
|
||||
cfg: ClawdbotConfig,
|
||||
image: string,
|
||||
): ClawdbotConfig {
|
||||
function updateSandboxDockerImage(cfg: ClawdbotConfig, image: string): ClawdbotConfig {
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
@@ -122,10 +113,7 @@ function updateSandboxDockerImage(
|
||||
};
|
||||
}
|
||||
|
||||
function updateSandboxBrowserImage(
|
||||
cfg: ClawdbotConfig,
|
||||
image: string,
|
||||
): ClawdbotConfig {
|
||||
function updateSandboxBrowserImage(cfg: ClawdbotConfig, image: string): ClawdbotConfig {
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
@@ -162,10 +150,7 @@ async function handleMissingSandboxImage(
|
||||
const buildHint = params.buildScript
|
||||
? `Build it with ${params.buildScript}.`
|
||||
: "Build or pull it first.";
|
||||
note(
|
||||
`Sandbox ${params.label} image missing: ${params.image}. ${buildHint}`,
|
||||
"Sandbox",
|
||||
);
|
||||
note(`Sandbox ${params.label} image missing: ${params.image}. ${buildHint}`, "Sandbox");
|
||||
|
||||
let built = false;
|
||||
if (params.buildScript) {
|
||||
@@ -240,9 +225,7 @@ export async function maybeRepairSandboxImages(
|
||||
buildScript: "scripts/sandbox-browser-setup.sh",
|
||||
updateConfig: (image) => {
|
||||
next = updateSandboxBrowserImage(next, image);
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.browser.image → ${image}`,
|
||||
);
|
||||
changes.push(`Updated agents.defaults.sandbox.browser.image → ${image}`);
|
||||
},
|
||||
},
|
||||
runtime,
|
||||
@@ -289,9 +272,7 @@ export function noteSandboxScopeWarnings(cfg: ClawdbotConfig) {
|
||||
|
||||
warnings.push(
|
||||
[
|
||||
`- agents.list (id "${agentId}") sandbox ${overrides.join(
|
||||
"/",
|
||||
)} overrides ignored.`,
|
||||
`- agents.list (id "${agentId}") sandbox ${overrides.join("/")} overrides ignored.`,
|
||||
` scope resolves to "shared".`,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
@@ -20,13 +20,9 @@ export async function noteSecurityWarnings(cfg: ClawdbotConfig) {
|
||||
}) => {
|
||||
const dmPolicy = params.dmPolicy;
|
||||
const policyPath = params.policyPath ?? `${params.allowFromPath}policy`;
|
||||
const configAllowFrom = (params.allowFrom ?? []).map((v) =>
|
||||
String(v).trim(),
|
||||
);
|
||||
const configAllowFrom = (params.allowFrom ?? []).map((v) => String(v).trim());
|
||||
const hasWildcard = configAllowFrom.includes("*");
|
||||
const storeAllowFrom = await readChannelAllowFromStore(
|
||||
params.provider,
|
||||
).catch(() => []);
|
||||
const storeAllowFrom = await readChannelAllowFromStore(params.provider).catch(() => []);
|
||||
const normalizedCfg = configAllowFrom
|
||||
.filter((v) => v !== "*")
|
||||
.map((v) => (params.normalizeEntry ? params.normalizeEntry(v) : v))
|
||||
@@ -36,15 +32,11 @@ export async function noteSecurityWarnings(cfg: ClawdbotConfig) {
|
||||
.map((v) => (params.normalizeEntry ? params.normalizeEntry(v) : v))
|
||||
.map((v) => v.trim())
|
||||
.filter(Boolean);
|
||||
const allowCount = Array.from(
|
||||
new Set([...normalizedCfg, ...normalizedStore]),
|
||||
).length;
|
||||
const allowCount = Array.from(new Set([...normalizedCfg, ...normalizedStore])).length;
|
||||
|
||||
if (dmPolicy === "open") {
|
||||
const allowFromPath = `${params.allowFromPath}allowFrom`;
|
||||
warnings.push(
|
||||
`- ${params.label} DMs: OPEN (${policyPath}="open"). Anyone can DM it.`,
|
||||
);
|
||||
warnings.push(`- ${params.label} DMs: OPEN (${policyPath}="open"). Anyone can DM it.`);
|
||||
if (!hasWildcard) {
|
||||
warnings.push(
|
||||
`- ${params.label} DMs: config invalid — "open" requires ${allowFromPath} to include "*".`,
|
||||
@@ -54,9 +46,7 @@ export async function noteSecurityWarnings(cfg: ClawdbotConfig) {
|
||||
}
|
||||
|
||||
if (dmPolicy === "disabled") {
|
||||
warnings.push(
|
||||
`- ${params.label} DMs: disabled (${policyPath}="disabled").`,
|
||||
);
|
||||
warnings.push(`- ${params.label} DMs: disabled (${policyPath}="disabled").`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,9 +67,7 @@ export async function noteSecurityWarnings(cfg: ClawdbotConfig) {
|
||||
accountIds,
|
||||
});
|
||||
const account = plugin.config.resolveAccount(cfg, defaultAccountId);
|
||||
const enabled = plugin.config.isEnabled
|
||||
? plugin.config.isEnabled(account, cfg)
|
||||
: true;
|
||||
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : true;
|
||||
if (!enabled) continue;
|
||||
const configured = plugin.config.isConfigured
|
||||
? await plugin.config.isConfigured(account, cfg)
|
||||
|
||||
@@ -95,11 +95,7 @@ function countJsonlLines(filePath: string): number {
|
||||
function findOtherStateDirs(stateDir: string): string[] {
|
||||
const resolvedState = path.resolve(stateDir);
|
||||
const roots =
|
||||
process.platform === "darwin"
|
||||
? ["/Users"]
|
||||
: process.platform === "linux"
|
||||
? ["/home"]
|
||||
: [];
|
||||
process.platform === "darwin" ? ["/Users"] : process.platform === "linux" ? ["/home"] : [];
|
||||
const found: string[] = [];
|
||||
for (const root of roots) {
|
||||
let entries: fs.Dirent[] = [];
|
||||
@@ -132,11 +128,7 @@ export async function noteStateIntegrity(
|
||||
const defaultStateDir = path.join(homedir(), ".clawdbot");
|
||||
const oauthDir = resolveOAuthDir(env, stateDir);
|
||||
const agentId = resolveDefaultAgentId(cfg);
|
||||
const sessionsDir = resolveSessionTranscriptsDirForAgent(
|
||||
agentId,
|
||||
env,
|
||||
homedir,
|
||||
);
|
||||
const sessionsDir = resolveSessionTranscriptsDirForAgent(agentId, env, homedir);
|
||||
const storePath = resolveStorePath(cfg.session?.store, { agentId });
|
||||
const storeDir = path.dirname(storePath);
|
||||
|
||||
@@ -222,9 +214,7 @@ export async function noteStateIntegrity(
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
warnings.push(
|
||||
`- Failed to read config permissions (${configPath}): ${String(err)}`,
|
||||
);
|
||||
warnings.push(`- Failed to read config permissions (${configPath}): ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,17 +281,13 @@ export async function noteStateIntegrity(
|
||||
}
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
const entries = Object.entries(store).filter(
|
||||
([, entry]) => entry && typeof entry === "object",
|
||||
);
|
||||
const entries = Object.entries(store).filter(([, entry]) => entry && typeof entry === "object");
|
||||
if (entries.length > 0) {
|
||||
const recent = entries
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const aUpdated =
|
||||
typeof a[1].updatedAt === "number" ? a[1].updatedAt : 0;
|
||||
const bUpdated =
|
||||
typeof b[1].updatedAt === "number" ? b[1].updatedAt : 0;
|
||||
const aUpdated = typeof a[1].updatedAt === "number" ? a[1].updatedAt : 0;
|
||||
const bUpdated = typeof b[1].updatedAt === "number" ? b[1].updatedAt : 0;
|
||||
return bUpdated - aUpdated;
|
||||
})
|
||||
.slice(0, 5);
|
||||
@@ -322,11 +308,7 @@ export async function noteStateIntegrity(
|
||||
const mainKey = resolveMainSessionKey(cfg);
|
||||
const mainEntry = store[mainKey];
|
||||
if (mainEntry?.sessionId) {
|
||||
const transcriptPath = resolveSessionFilePath(
|
||||
mainEntry.sessionId,
|
||||
mainEntry,
|
||||
{ agentId },
|
||||
);
|
||||
const transcriptPath = resolveSessionFilePath(mainEntry.sessionId, mainEntry, { agentId });
|
||||
if (!existsFile(transcriptPath)) {
|
||||
warnings.push(
|
||||
`- Main session transcript missing (${transcriptPath}). History will appear to reset.`,
|
||||
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
let tempRoot: string | null = null;
|
||||
|
||||
async function makeTempRoot() {
|
||||
const root = await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-doctor-"),
|
||||
);
|
||||
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "clawdbot-doctor-"));
|
||||
tempRoot = root;
|
||||
return root;
|
||||
}
|
||||
@@ -94,9 +92,7 @@ describe("doctor legacy state migrations", () => {
|
||||
});
|
||||
await runLegacyStateMigrations({ detected, now: () => 123 });
|
||||
|
||||
expect(fs.readFileSync(path.join(targetAgentDir, "baz.txt"), "utf-8")).toBe(
|
||||
"legacy2",
|
||||
);
|
||||
expect(fs.readFileSync(path.join(targetAgentDir, "baz.txt"), "utf-8")).toBe("legacy2");
|
||||
const backupDir = path.join(root, "agents", "main", "agent.legacy-123");
|
||||
expect(fs.existsSync(path.join(backupDir, "foo.txt"))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -28,10 +28,7 @@ export async function maybeRepairUiProtocolFreshness(
|
||||
]);
|
||||
|
||||
if (schemaStats && !uiStats) {
|
||||
note(
|
||||
["- Control UI assets are missing.", "- Run: pnpm ui:build"].join("\n"),
|
||||
"UI",
|
||||
);
|
||||
note(["- Control UI assets are missing.", "- Run: pnpm ui:build"].join("\n"), "UI");
|
||||
|
||||
// In slim/docker environments we may not have the UI source tree. Trying
|
||||
// to build would fail (and spam logs), so skip the interactive repair.
|
||||
@@ -50,14 +47,11 @@ export async function maybeRepairUiProtocolFreshness(
|
||||
if (shouldRepair) {
|
||||
note("Building Control UI assets... (this may take a moment)", "UI");
|
||||
const uiScriptPath = path.join(root, "scripts/ui.js");
|
||||
const buildResult = await runCommandWithTimeout(
|
||||
[process.execPath, uiScriptPath, "build"],
|
||||
{
|
||||
cwd: root,
|
||||
timeoutMs: 120_000,
|
||||
env: { ...process.env, FORCE_COLOR: "1" },
|
||||
},
|
||||
);
|
||||
const buildResult = await runCommandWithTimeout([process.execPath, uiScriptPath, "build"], {
|
||||
cwd: root,
|
||||
timeoutMs: 120_000,
|
||||
env: { ...process.env, FORCE_COLOR: "1" },
|
||||
});
|
||||
if (buildResult.code === 0) {
|
||||
note("UI build complete.", "UI");
|
||||
} else {
|
||||
@@ -102,8 +96,7 @@ export async function maybeRepairUiProtocolFreshness(
|
||||
);
|
||||
|
||||
const shouldRepair = await prompter.confirmAggressive({
|
||||
message:
|
||||
"Rebuild UI now? (Detected protocol mismatch requiring update)",
|
||||
message: "Rebuild UI now? (Detected protocol mismatch requiring update)",
|
||||
initialValue: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -4,13 +4,10 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import type { DoctorOptions } from "./doctor-prompter.js";
|
||||
|
||||
async function detectClawdbotGitCheckout(
|
||||
root: string,
|
||||
): Promise<"git" | "not-git" | "unknown"> {
|
||||
const res = await runCommandWithTimeout(
|
||||
["git", "-C", root, "rev-parse", "--show-toplevel"],
|
||||
{ timeoutMs: 5000 },
|
||||
).catch(() => null);
|
||||
async function detectClawdbotGitCheckout(root: string): Promise<"git" | "not-git" | "unknown"> {
|
||||
const res = await runCommandWithTimeout(["git", "-C", root, "rev-parse", "--show-toplevel"], {
|
||||
timeoutMs: 5000,
|
||||
}).catch(() => null);
|
||||
if (!res) return "unknown";
|
||||
if (res.code !== 0) {
|
||||
// Avoid noisy "Update via package manager" notes when git is missing/broken,
|
||||
@@ -63,9 +60,7 @@ export async function maybeOfferUpdateBeforeDoctor(params: {
|
||||
"Update result",
|
||||
);
|
||||
if (result.status === "ok") {
|
||||
params.outro(
|
||||
"Update completed (doctor already ran as part of the update).",
|
||||
);
|
||||
params.outro("Update completed (doctor already ran as part of the update).");
|
||||
return { updated: true, handled: true };
|
||||
}
|
||||
return { updated: true, handled: false };
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { loadClawdbotPlugins } from "../plugins/loader.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import {
|
||||
detectLegacyWorkspaceDirs,
|
||||
formatLegacyWorkspaceWarning,
|
||||
} from "./doctor-workspace.js";
|
||||
import { detectLegacyWorkspaceDirs, formatLegacyWorkspaceWarning } from "./doctor-workspace.js";
|
||||
|
||||
export function noteWorkspaceStatus(cfg: ClawdbotConfig) {
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
cfg,
|
||||
resolveDefaultAgentId(cfg),
|
||||
);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
|
||||
const legacyWorkspace = detectLegacyWorkspaceDirs({ workspaceDir });
|
||||
if (legacyWorkspace.legacyDirs.length > 0) {
|
||||
note(formatLegacyWorkspaceWarning(legacyWorkspace), "Legacy workspace");
|
||||
@@ -26,13 +17,10 @@ export function noteWorkspaceStatus(cfg: ClawdbotConfig) {
|
||||
[
|
||||
`Eligible: ${skillsReport.skills.filter((s) => s.eligible).length}`,
|
||||
`Missing requirements: ${
|
||||
skillsReport.skills.filter(
|
||||
(s) => !s.eligible && !s.disabled && !s.blockedByAllowlist,
|
||||
).length
|
||||
}`,
|
||||
`Blocked by allowlist: ${
|
||||
skillsReport.skills.filter((s) => s.blockedByAllowlist).length
|
||||
skillsReport.skills.filter((s) => !s.eligible && !s.disabled && !s.blockedByAllowlist)
|
||||
.length
|
||||
}`,
|
||||
`Blocked by allowlist: ${skillsReport.skills.filter((s) => s.blockedByAllowlist).length}`,
|
||||
].join("\n"),
|
||||
"Skills status",
|
||||
);
|
||||
@@ -49,9 +37,7 @@ export function noteWorkspaceStatus(cfg: ClawdbotConfig) {
|
||||
});
|
||||
if (pluginRegistry.plugins.length > 0) {
|
||||
const loaded = pluginRegistry.plugins.filter((p) => p.status === "loaded");
|
||||
const disabled = pluginRegistry.plugins.filter(
|
||||
(p) => p.status === "disabled",
|
||||
);
|
||||
const disabled = pluginRegistry.plugins.filter((p) => p.status === "disabled");
|
||||
const errored = pluginRegistry.plugins.filter((p) => p.status === "error");
|
||||
|
||||
const lines = [
|
||||
|
||||
@@ -18,13 +18,8 @@ export const MEMORY_SYSTEM_PROMPT = [
|
||||
"https://github.com/clawdbot/clawdbot/commit/7d1fee70e76f2f634f1b41fca927ee663914183a",
|
||||
].join("\n");
|
||||
|
||||
export async function shouldSuggestMemorySystem(
|
||||
workspaceDir: string,
|
||||
): Promise<boolean> {
|
||||
const memoryPaths = [
|
||||
path.join(workspaceDir, "MEMORY.md"),
|
||||
path.join(workspaceDir, "memory.md"),
|
||||
];
|
||||
export async function shouldSuggestMemorySystem(workspaceDir: string): Promise<boolean> {
|
||||
const memoryPaths = [path.join(workspaceDir, "MEMORY.md"), path.join(workspaceDir, "memory.md")];
|
||||
|
||||
for (const memoryPath of memoryPaths) {
|
||||
try {
|
||||
@@ -51,10 +46,7 @@ export type LegacyWorkspaceDetection = {
|
||||
legacyDirs: string[];
|
||||
};
|
||||
|
||||
function looksLikeWorkspaceDir(
|
||||
dir: string,
|
||||
exists: (value: string) => boolean,
|
||||
) {
|
||||
function looksLikeWorkspaceDir(dir: string, exists: (value: string) => boolean) {
|
||||
const markers = [
|
||||
DEFAULT_AGENTS_FILENAME,
|
||||
DEFAULT_SOUL_FILENAME,
|
||||
@@ -85,9 +77,7 @@ export function detectLegacyWorkspaceDirs(params: {
|
||||
return { activeWorkspace, legacyDirs };
|
||||
}
|
||||
|
||||
export function formatLegacyWorkspaceWarning(
|
||||
detection: LegacyWorkspaceDetection,
|
||||
): string {
|
||||
export function formatLegacyWorkspaceWarning(detection: LegacyWorkspaceDetection): string {
|
||||
return [
|
||||
"Legacy workspace directories detected (may contain old agent files):",
|
||||
...detection.legacyDirs.map((dir) => `- ${dir}`),
|
||||
|
||||
@@ -54,9 +54,7 @@ beforeEach(() => {
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
ensureAuthProfileStore
|
||||
.mockReset()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} });
|
||||
migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({
|
||||
config: raw as Record<string, unknown>,
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
@@ -80,9 +78,7 @@ beforeEach(() => {
|
||||
originalStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
originalUpdateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS;
|
||||
process.env.CLAWDBOT_UPDATE_IN_PROGRESS = "1";
|
||||
tempStateDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clawdbot-doctor-state-"),
|
||||
);
|
||||
tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-doctor-state-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), {
|
||||
recursive: true,
|
||||
@@ -134,9 +130,7 @@ const runCommandWithTimeout = vi.fn().mockResolvedValue({
|
||||
killed: false,
|
||||
});
|
||||
|
||||
const ensureAuthProfileStore = vi
|
||||
.fn()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} });
|
||||
|
||||
const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({
|
||||
path: "/tmp/clawdis.json",
|
||||
@@ -390,10 +384,7 @@ describe("doctor command", () => {
|
||||
|
||||
await doctorCommand(runtime);
|
||||
|
||||
const written = writeConfigFile.mock.calls.at(-1)?.[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const written = writeConfigFile.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
const agents = written.agents as Record<string, unknown>;
|
||||
const defaults = agents.defaults as Record<string, unknown>;
|
||||
const sandbox = defaults.sandbox as Record<string, unknown>;
|
||||
|
||||
@@ -54,9 +54,7 @@ beforeEach(() => {
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
ensureAuthProfileStore
|
||||
.mockReset()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} });
|
||||
migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({
|
||||
config: raw as Record<string, unknown>,
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
@@ -80,9 +78,7 @@ beforeEach(() => {
|
||||
originalStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
originalUpdateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS;
|
||||
process.env.CLAWDBOT_UPDATE_IN_PROGRESS = "1";
|
||||
tempStateDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clawdbot-doctor-state-"),
|
||||
);
|
||||
tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-doctor-state-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), {
|
||||
recursive: true,
|
||||
@@ -134,9 +130,7 @@ const runCommandWithTimeout = vi.fn().mockResolvedValue({
|
||||
killed: false,
|
||||
});
|
||||
|
||||
const ensureAuthProfileStore = vi
|
||||
.fn()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} });
|
||||
|
||||
const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({
|
||||
path: "/tmp/clawdis.json",
|
||||
@@ -437,10 +431,7 @@ describe("doctor command", () => {
|
||||
|
||||
await doctorCommand(runtime);
|
||||
|
||||
const written = writeConfigFile.mock.calls.at(-1)?.[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const written = writeConfigFile.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
const agents = written.agents as Record<string, unknown>;
|
||||
const defaults = agents.defaults as Record<string, unknown>;
|
||||
const sandbox = defaults.sandbox as Record<string, unknown>;
|
||||
|
||||
@@ -54,9 +54,7 @@ beforeEach(() => {
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
ensureAuthProfileStore
|
||||
.mockReset()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} });
|
||||
migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({
|
||||
config: raw as Record<string, unknown>,
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
@@ -80,9 +78,7 @@ beforeEach(() => {
|
||||
originalStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
originalUpdateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS;
|
||||
process.env.CLAWDBOT_UPDATE_IN_PROGRESS = "1";
|
||||
tempStateDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clawdbot-doctor-state-"),
|
||||
);
|
||||
tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-doctor-state-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), {
|
||||
recursive: true,
|
||||
@@ -134,9 +130,7 @@ const runCommandWithTimeout = vi.fn().mockResolvedValue({
|
||||
killed: false,
|
||||
});
|
||||
|
||||
const ensureAuthProfileStore = vi
|
||||
.fn()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} });
|
||||
|
||||
const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({
|
||||
path: "/tmp/clawdis.json",
|
||||
@@ -323,56 +317,49 @@ vi.mock("./doctor-state-migrations.js", () => ({
|
||||
}));
|
||||
|
||||
describe("doctor command", () => {
|
||||
it(
|
||||
"migrates routing.allowFrom to channels.whatsapp.allowFrom",
|
||||
{ timeout: 30_000 },
|
||||
async () => {
|
||||
readConfigFileSnapshot.mockResolvedValue({
|
||||
path: "/tmp/clawdbot.json",
|
||||
exists: true,
|
||||
raw: "{}",
|
||||
parsed: { routing: { allowFrom: ["+15555550123"] } },
|
||||
valid: false,
|
||||
config: {},
|
||||
issues: [
|
||||
{
|
||||
path: "routing.allowFrom",
|
||||
message: "legacy",
|
||||
},
|
||||
],
|
||||
legacyIssues: [
|
||||
{
|
||||
path: "routing.allowFrom",
|
||||
message: "legacy",
|
||||
},
|
||||
],
|
||||
});
|
||||
it("migrates routing.allowFrom to channels.whatsapp.allowFrom", { timeout: 30_000 }, async () => {
|
||||
readConfigFileSnapshot.mockResolvedValue({
|
||||
path: "/tmp/clawdbot.json",
|
||||
exists: true,
|
||||
raw: "{}",
|
||||
parsed: { routing: { allowFrom: ["+15555550123"] } },
|
||||
valid: false,
|
||||
config: {},
|
||||
issues: [
|
||||
{
|
||||
path: "routing.allowFrom",
|
||||
message: "legacy",
|
||||
},
|
||||
],
|
||||
legacyIssues: [
|
||||
{
|
||||
path: "routing.allowFrom",
|
||||
message: "legacy",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { doctorCommand } = await import("./doctor.js");
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
const { doctorCommand } = await import("./doctor.js");
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
migrateLegacyConfig.mockReturnValue({
|
||||
config: { channels: { whatsapp: { allowFrom: ["+15555550123"] } } },
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
});
|
||||
migrateLegacyConfig.mockReturnValue({
|
||||
config: { channels: { whatsapp: { allowFrom: ["+15555550123"] } } },
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
});
|
||||
|
||||
await doctorCommand(runtime, { nonInteractive: true });
|
||||
await doctorCommand(runtime, { nonInteractive: true });
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect((written.channels as Record<string, unknown>)?.whatsapp).toEqual({
|
||||
allowFrom: ["+15555550123"],
|
||||
});
|
||||
expect(written.routing).toBeUndefined();
|
||||
},
|
||||
);
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect((written.channels as Record<string, unknown>)?.whatsapp).toEqual({
|
||||
allowFrom: ["+15555550123"],
|
||||
});
|
||||
expect(written.routing).toBeUndefined();
|
||||
});
|
||||
|
||||
it("migrates legacy Clawdis services", async () => {
|
||||
readConfigFileSnapshot.mockResolvedValue({
|
||||
@@ -449,14 +436,10 @@ describe("doctor command", () => {
|
||||
|
||||
await doctorCommand(runtime);
|
||||
|
||||
expect(runGatewayUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ cwd: root }),
|
||||
);
|
||||
expect(runGatewayUpdate).toHaveBeenCalledWith(expect.objectContaining({ cwd: root }));
|
||||
expect(readConfigFileSnapshot).not.toHaveBeenCalled();
|
||||
expect(
|
||||
note.mock.calls.some(
|
||||
([, title]) => typeof title === "string" && title === "Update result",
|
||||
),
|
||||
note.mock.calls.some(([, title]) => typeof title === "string" && title === "Update result"),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,9 +54,7 @@ beforeEach(() => {
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
ensureAuthProfileStore
|
||||
.mockReset()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} });
|
||||
migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({
|
||||
config: raw as Record<string, unknown>,
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
@@ -80,9 +78,7 @@ beforeEach(() => {
|
||||
originalStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
originalUpdateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS;
|
||||
process.env.CLAWDBOT_UPDATE_IN_PROGRESS = "1";
|
||||
tempStateDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clawdbot-doctor-state-"),
|
||||
);
|
||||
tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-doctor-state-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), {
|
||||
recursive: true,
|
||||
@@ -134,9 +130,7 @@ const runCommandWithTimeout = vi.fn().mockResolvedValue({
|
||||
killed: false,
|
||||
});
|
||||
|
||||
const ensureAuthProfileStore = vi
|
||||
.fn()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} });
|
||||
|
||||
const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({
|
||||
path: "/tmp/clawdis.json",
|
||||
@@ -447,17 +441,10 @@ describe("doctor command", () => {
|
||||
});
|
||||
|
||||
const { doctorCommand } = await import("./doctor.js");
|
||||
await doctorCommand(
|
||||
{ log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
{ yes: true },
|
||||
);
|
||||
await doctorCommand({ log: vi.fn(), error: vi.fn(), exit: vi.fn() }, { yes: true });
|
||||
|
||||
const written = writeConfigFile.mock.calls.at(-1)?.[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const profiles = (written.auth as { profiles: Record<string, unknown> })
|
||||
.profiles;
|
||||
const written = writeConfigFile.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
const profiles = (written.auth as { profiles: Record<string, unknown> }).profiles;
|
||||
expect(profiles["anthropic:me@example.com"]).toBeTruthy();
|
||||
expect(profiles["anthropic:default"]).toBeUndefined();
|
||||
}, 20_000);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { intro as clackIntro, outro as clackOutro } from "@clack/prompts";
|
||||
import {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import {
|
||||
@@ -19,10 +16,7 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
import {
|
||||
maybeRepairAnthropicOAuthProfileId,
|
||||
noteAuthProfileHealth,
|
||||
} from "./doctor-auth.js";
|
||||
import { maybeRepairAnthropicOAuthProfileId, noteAuthProfileHealth } from "./doctor-auth.js";
|
||||
import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
|
||||
import { maybeRepairGatewayDaemon } from "./doctor-gateway-daemon-flow.js";
|
||||
import { checkGatewayHealth } from "./doctor-gateway-health.js";
|
||||
@@ -35,37 +29,22 @@ import { noteSourceInstallIssues } from "./doctor-install.js";
|
||||
import { maybeMigrateLegacyConfigFile } from "./doctor-legacy-config.js";
|
||||
import { noteMacLaunchAgentOverrides } from "./doctor-platform-notes.js";
|
||||
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
|
||||
import {
|
||||
maybeRepairSandboxImages,
|
||||
noteSandboxScopeWarnings,
|
||||
} from "./doctor-sandbox.js";
|
||||
import { maybeRepairSandboxImages, noteSandboxScopeWarnings } from "./doctor-sandbox.js";
|
||||
import { noteSecurityWarnings } from "./doctor-security.js";
|
||||
import {
|
||||
noteStateIntegrity,
|
||||
noteWorkspaceBackupTip,
|
||||
} from "./doctor-state-integrity.js";
|
||||
import { noteStateIntegrity, noteWorkspaceBackupTip } from "./doctor-state-integrity.js";
|
||||
import {
|
||||
detectLegacyStateMigrations,
|
||||
runLegacyStateMigrations,
|
||||
} from "./doctor-state-migrations.js";
|
||||
import { maybeRepairUiProtocolFreshness } from "./doctor-ui.js";
|
||||
import { maybeOfferUpdateBeforeDoctor } from "./doctor-update.js";
|
||||
import {
|
||||
MEMORY_SYSTEM_PROMPT,
|
||||
shouldSuggestMemorySystem,
|
||||
} from "./doctor-workspace.js";
|
||||
import { MEMORY_SYSTEM_PROMPT, shouldSuggestMemorySystem } from "./doctor-workspace.js";
|
||||
import { noteWorkspaceStatus } from "./doctor-workspace-status.js";
|
||||
import {
|
||||
applyWizardMetadata,
|
||||
printWizardHeader,
|
||||
randomToken,
|
||||
} from "./onboard-helpers.js";
|
||||
import { applyWizardMetadata, printWizardHeader, randomToken } from "./onboard-helpers.js";
|
||||
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
||||
|
||||
const intro = (message: string) =>
|
||||
clackIntro(stylePromptTitle(message) ?? message);
|
||||
const outro = (message: string) =>
|
||||
clackOutro(stylePromptTitle(message) ?? message);
|
||||
const intro = (message: string) => clackIntro(stylePromptTitle(message) ?? message);
|
||||
const outro = (message: string) => clackOutro(stylePromptTitle(message) ?? message);
|
||||
|
||||
function resolveMode(cfg: ClawdbotConfig): "local" | "remote" {
|
||||
return cfg.gateway?.mode === "remote" ? "remote" : "local";
|
||||
@@ -109,8 +88,7 @@ export async function doctorCommand(
|
||||
await noteAuthProfileHealth({
|
||||
cfg,
|
||||
prompter,
|
||||
allowKeychainPrompt:
|
||||
options.nonInteractive !== true && Boolean(process.stdin.isTTY),
|
||||
allowKeychainPrompt: options.nonInteractive !== true && Boolean(process.stdin.isTTY),
|
||||
});
|
||||
const gatewayDetails = buildGatewayConnectionDetails({ config: cfg });
|
||||
if (gatewayDetails.remoteFallbackNote) {
|
||||
@@ -119,11 +97,8 @@ export async function doctorCommand(
|
||||
if (resolveMode(cfg) === "local") {
|
||||
const authMode = cfg.gateway?.auth?.mode;
|
||||
const token =
|
||||
typeof cfg.gateway?.auth?.token === "string"
|
||||
? cfg.gateway?.auth?.token.trim()
|
||||
: "";
|
||||
const needsToken =
|
||||
authMode !== "password" && (authMode !== "token" || !token);
|
||||
typeof cfg.gateway?.auth?.token === "string" ? cfg.gateway?.auth?.token.trim() : "";
|
||||
const needsToken = authMode !== "password" && (authMode !== "token" || !token);
|
||||
if (needsToken) {
|
||||
note(
|
||||
"Gateway auth is off or missing a token. Token auth is now the recommended default (including loopback).",
|
||||
@@ -179,28 +154,14 @@ export async function doctorCommand(
|
||||
}
|
||||
}
|
||||
|
||||
await noteStateIntegrity(
|
||||
cfg,
|
||||
prompter,
|
||||
configResult.path ?? CONFIG_PATH_CLAWDBOT,
|
||||
);
|
||||
await noteStateIntegrity(cfg, prompter, configResult.path ?? CONFIG_PATH_CLAWDBOT);
|
||||
|
||||
cfg = await maybeRepairSandboxImages(cfg, runtime, prompter);
|
||||
noteSandboxScopeWarnings(cfg);
|
||||
|
||||
await maybeMigrateLegacyGatewayService(
|
||||
cfg,
|
||||
resolveMode(cfg),
|
||||
runtime,
|
||||
prompter,
|
||||
);
|
||||
await maybeMigrateLegacyGatewayService(cfg, resolveMode(cfg), runtime, prompter);
|
||||
await maybeScanExtraGatewayServices(options);
|
||||
await maybeRepairGatewayServiceConfig(
|
||||
cfg,
|
||||
resolveMode(cfg),
|
||||
runtime,
|
||||
prompter,
|
||||
);
|
||||
await maybeRepairGatewayServiceConfig(cfg, resolveMode(cfg), runtime, prompter);
|
||||
await noteMacLaunchAgentOverrides();
|
||||
|
||||
await noteSecurityWarnings(cfg);
|
||||
@@ -211,17 +172,13 @@ export async function doctorCommand(
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
if (!hooksModelRef) {
|
||||
note(
|
||||
`- hooks.gmail.model "${cfg.hooks.gmail.model}" could not be resolved`,
|
||||
"Hooks",
|
||||
);
|
||||
note(`- hooks.gmail.model "${cfg.hooks.gmail.model}" could not be resolved`, "Hooks");
|
||||
} else {
|
||||
const { provider: defaultProvider, model: defaultModel } =
|
||||
resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
const { provider: defaultProvider, model: defaultModel } = resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
const catalog = await loadModelCatalog({ config: cfg });
|
||||
const status = getModelRefStatus({
|
||||
cfg,
|
||||
@@ -293,10 +250,7 @@ export async function doctorCommand(
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
|
||||
if (options.workspaceSuggestions !== false) {
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
cfg,
|
||||
resolveDefaultAgentId(cfg),
|
||||
);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
|
||||
noteWorkspaceBackupTip(workspaceDir);
|
||||
if (await shouldSuggestMemorySystem(workspaceDir)) {
|
||||
note(MEMORY_SYSTEM_PROMPT, "Workspace");
|
||||
|
||||
@@ -54,9 +54,7 @@ beforeEach(() => {
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
ensureAuthProfileStore
|
||||
.mockReset()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} });
|
||||
migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({
|
||||
config: raw as Record<string, unknown>,
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
@@ -80,9 +78,7 @@ beforeEach(() => {
|
||||
originalStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
originalUpdateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS;
|
||||
process.env.CLAWDBOT_UPDATE_IN_PROGRESS = "1";
|
||||
tempStateDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clawdbot-doctor-state-"),
|
||||
);
|
||||
tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-doctor-state-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), {
|
||||
recursive: true,
|
||||
@@ -134,9 +130,7 @@ const runCommandWithTimeout = vi.fn().mockResolvedValue({
|
||||
killed: false,
|
||||
});
|
||||
|
||||
const ensureAuthProfileStore = vi
|
||||
.fn()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} });
|
||||
|
||||
const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({
|
||||
path: "/tmp/clawdis.json",
|
||||
@@ -395,18 +389,12 @@ describe("doctor command", () => {
|
||||
});
|
||||
|
||||
note.mockClear();
|
||||
const homedirSpy = vi
|
||||
.spyOn(os, "homedir")
|
||||
.mockReturnValue("/Users/steipete");
|
||||
const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue("/Users/steipete");
|
||||
const realExists = fs.existsSync;
|
||||
const legacyPath = path.join("/Users/steipete", "clawdis");
|
||||
const legacyAgentsPath = path.join(legacyPath, "AGENTS.md");
|
||||
const existsSpy = vi.spyOn(fs, "existsSync").mockImplementation((value) => {
|
||||
if (
|
||||
value === "/Users/steipete/clawdis" ||
|
||||
value === legacyPath ||
|
||||
value === legacyAgentsPath
|
||||
)
|
||||
if (value === "/Users/steipete/clawdis" || value === legacyPath || value === legacyAgentsPath)
|
||||
return true;
|
||||
return realExists(value as never);
|
||||
});
|
||||
|
||||
@@ -54,9 +54,7 @@ beforeEach(() => {
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
ensureAuthProfileStore
|
||||
.mockReset()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} });
|
||||
migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({
|
||||
config: raw as Record<string, unknown>,
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
@@ -80,9 +78,7 @@ beforeEach(() => {
|
||||
originalStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
originalUpdateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS;
|
||||
process.env.CLAWDBOT_UPDATE_IN_PROGRESS = "1";
|
||||
tempStateDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clawdbot-doctor-state-"),
|
||||
);
|
||||
tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-doctor-state-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), {
|
||||
recursive: true,
|
||||
@@ -134,9 +130,7 @@ const runCommandWithTimeout = vi.fn().mockResolvedValue({
|
||||
killed: false,
|
||||
});
|
||||
|
||||
const ensureAuthProfileStore = vi
|
||||
.fn()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} });
|
||||
|
||||
const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({
|
||||
path: "/tmp/clawdis.json",
|
||||
@@ -335,9 +329,7 @@ describe("doctor command", () => {
|
||||
legacyIssues: [],
|
||||
});
|
||||
|
||||
const missingDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clawdbot-missing-state-"),
|
||||
);
|
||||
const missingDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-missing-state-"));
|
||||
fs.rmSync(missingDir, { recursive: true, force: true });
|
||||
process.env.CLAWDBOT_STATE_DIR = missingDir;
|
||||
note.mockClear();
|
||||
@@ -348,9 +340,7 @@ describe("doctor command", () => {
|
||||
{ nonInteractive: true, workspaceSuggestions: false },
|
||||
);
|
||||
|
||||
const stateNote = note.mock.calls.find(
|
||||
(call) => call[1] === "State integrity",
|
||||
);
|
||||
const stateNote = note.mock.calls.find((call) => call[1] === "State integrity");
|
||||
expect(stateNote).toBeTruthy();
|
||||
expect(String(stateNote?.[0])).toContain("CRITICAL");
|
||||
}, 20_000);
|
||||
@@ -384,8 +374,7 @@ describe("doctor command", () => {
|
||||
|
||||
const warned = note.mock.calls.some(
|
||||
([message, title]) =>
|
||||
title === "OpenCode Zen" &&
|
||||
String(message).includes("models.providers.opencode"),
|
||||
title === "OpenCode Zen" && String(message).includes("models.providers.opencode"),
|
||||
);
|
||||
expect(warned).toBe(true);
|
||||
});
|
||||
|
||||
@@ -37,9 +37,7 @@ const probeGateway = vi.fn(async ({ url }: { url: string }) => {
|
||||
},
|
||||
sessions: { count: 0 },
|
||||
},
|
||||
presence: [
|
||||
{ mode: "gateway", reason: "self", host: "local", ip: "127.0.0.1" },
|
||||
],
|
||||
presence: [{ mode: "gateway", reason: "self", host: "local", ip: "127.0.0.1" }],
|
||||
configSnapshot: {
|
||||
path: "/tmp/cfg.json",
|
||||
exists: true,
|
||||
@@ -69,9 +67,7 @@ const probeGateway = vi.fn(async ({ url }: { url: string }) => {
|
||||
},
|
||||
sessions: { count: 2 },
|
||||
},
|
||||
presence: [
|
||||
{ mode: "gateway", reason: "self", host: "remote", ip: "100.64.0.2" },
|
||||
],
|
||||
presence: [{ mode: "gateway", reason: "self", host: "remote", ip: "100.64.0.2" }],
|
||||
configSnapshot: {
|
||||
path: "/tmp/remote.json",
|
||||
exists: true,
|
||||
@@ -146,10 +142,7 @@ describe("gateway-status command", () => {
|
||||
);
|
||||
|
||||
expect(runtimeErrors).toHaveLength(0);
|
||||
const parsed = JSON.parse(runtimeLogs.join("\n")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const parsed = JSON.parse(runtimeLogs.join("\n")) as Record<string, unknown>;
|
||||
expect(parsed.ok).toBe(true);
|
||||
expect(parsed.targets).toBeTruthy();
|
||||
const targets = parsed.targets as Array<Record<string, unknown>>;
|
||||
@@ -182,10 +175,7 @@ describe("gateway-status command", () => {
|
||||
expect(probeGateway).toHaveBeenCalled();
|
||||
expect(sshStop).toHaveBeenCalledTimes(1);
|
||||
|
||||
const parsed = JSON.parse(runtimeLogs.join("\n")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const parsed = JSON.parse(runtimeLogs.join("\n")) as Record<string, unknown>;
|
||||
const targets = parsed.targets as Array<Record<string, unknown>>;
|
||||
expect(targets.some((t) => t.kind === "sshTunnel")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -46,12 +46,9 @@ export async function gatewayStatusCommand(
|
||||
timeoutMs: discoveryTimeoutMs,
|
||||
});
|
||||
|
||||
let sshTarget =
|
||||
sanitizeSshTarget(opts.ssh) ??
|
||||
sanitizeSshTarget(cfg.gateway?.remote?.sshTarget);
|
||||
let sshTarget = sanitizeSshTarget(opts.ssh) ?? sanitizeSshTarget(cfg.gateway?.remote?.sshTarget);
|
||||
const sshIdentity =
|
||||
sanitizeSshTarget(opts.sshIdentity) ??
|
||||
sanitizeSshTarget(cfg.gateway?.remote?.sshIdentity);
|
||||
sanitizeSshTarget(opts.sshIdentity) ?? sanitizeSshTarget(cfg.gateway?.remote?.sshIdentity);
|
||||
const remotePort = resolveGatewayPort(cfg);
|
||||
|
||||
let sshTunnelError: string | null = null;
|
||||
@@ -85,10 +82,7 @@ export async function gatewayStatusCommand(
|
||||
const discoveryTask = discoveryPromise.catch(() => []);
|
||||
const tunnelTask = sshTarget ? tryStartTunnel() : Promise.resolve(null);
|
||||
|
||||
const [discovery, tunnelFirst] = await Promise.all([
|
||||
discoveryTask,
|
||||
tunnelTask,
|
||||
]);
|
||||
const [discovery, tunnelFirst] = await Promise.all([discoveryTask, tunnelTask]);
|
||||
|
||||
if (!sshTarget && opts.sshAuto) {
|
||||
const user = process.env.USER?.trim() || "";
|
||||
@@ -96,8 +90,7 @@ export async function gatewayStatusCommand(
|
||||
.map((b) => {
|
||||
const host = b.tailnetDns || b.lanHost || b.host;
|
||||
if (!host?.trim()) return null;
|
||||
const sshPort =
|
||||
typeof b.sshPort === "number" && b.sshPort > 0 ? b.sshPort : 22;
|
||||
const sshPort = typeof b.sshPort === "number" && b.sshPort > 0 ? b.sshPort : 22;
|
||||
const base = user ? `${user}@${host.trim()}` : host.trim();
|
||||
return sshPort !== 22 ? `${base}:${sshPort}` : base;
|
||||
})
|
||||
@@ -107,9 +100,7 @@ export async function gatewayStatusCommand(
|
||||
|
||||
const tunnel =
|
||||
tunnelFirst ||
|
||||
(sshTarget && !sshTunnelStarted && !sshTunnelError
|
||||
? await tryStartTunnel()
|
||||
: null);
|
||||
(sshTarget && !sshTunnelStarted && !sshTunnelError ? await tryStartTunnel() : null);
|
||||
|
||||
const tunnelTarget: GatewayStatusTarget | null = tunnel
|
||||
? {
|
||||
@@ -128,10 +119,7 @@ export async function gatewayStatusCommand(
|
||||
: null;
|
||||
|
||||
const targets: GatewayStatusTarget[] = tunnelTarget
|
||||
? [
|
||||
tunnelTarget,
|
||||
...baseTargets.filter((t) => t.url !== tunnelTarget.url),
|
||||
]
|
||||
? [tunnelTarget, ...baseTargets.filter((t) => t.url !== tunnelTarget.url)]
|
||||
: baseTargets;
|
||||
|
||||
try {
|
||||
@@ -139,13 +127,9 @@ export async function gatewayStatusCommand(
|
||||
targets.map(async (target) => {
|
||||
const auth = resolveAuthForTarget(cfg, target, {
|
||||
token: typeof opts.token === "string" ? opts.token : undefined,
|
||||
password:
|
||||
typeof opts.password === "string" ? opts.password : undefined,
|
||||
password: typeof opts.password === "string" ? opts.password : undefined,
|
||||
});
|
||||
const timeoutMs = resolveProbeBudgetMs(
|
||||
overallTimeoutMs,
|
||||
target.kind,
|
||||
);
|
||||
const timeoutMs = resolveProbeBudgetMs(overallTimeoutMs, target.kind);
|
||||
const probe = await probeGateway({
|
||||
url: target.url,
|
||||
auth,
|
||||
@@ -268,9 +252,7 @@ export async function gatewayStatusCommand(
|
||||
? `${colorize(rich, theme.success, "Reachable")}: yes`
|
||||
: `${colorize(rich, theme.error, "Reachable")}: no`,
|
||||
);
|
||||
runtime.log(
|
||||
colorize(rich, theme.muted, `Probe budget: ${overallTimeoutMs}ms`),
|
||||
);
|
||||
runtime.log(colorize(rich, theme.muted, `Probe budget: ${overallTimeoutMs}ms`));
|
||||
|
||||
if (warnings.length > 0) {
|
||||
runtime.log("");
|
||||
@@ -310,18 +292,12 @@ export async function gatewayStatusCommand(
|
||||
const ip = p.self.ip ? ` (${p.self.ip})` : "";
|
||||
const platform = p.self.platform ? ` · ${p.self.platform}` : "";
|
||||
const version = p.self.version ? ` · app ${p.self.version}` : "";
|
||||
runtime.log(
|
||||
` ${colorize(rich, theme.info, "Gateway")}: ${host}${ip}${platform}${version}`,
|
||||
);
|
||||
runtime.log(` ${colorize(rich, theme.info, "Gateway")}: ${host}${ip}${platform}${version}`);
|
||||
}
|
||||
if (p.configSummary) {
|
||||
const c = p.configSummary;
|
||||
const bridge =
|
||||
c.bridge.enabled === false
|
||||
? "disabled"
|
||||
: c.bridge.enabled === true
|
||||
? "enabled"
|
||||
: "unknown";
|
||||
c.bridge.enabled === false ? "disabled" : c.bridge.enabled === true ? "enabled" : "unknown";
|
||||
const wideArea =
|
||||
c.discovery.wideAreaEnabled === true
|
||||
? "enabled"
|
||||
@@ -331,9 +307,7 @@ export async function gatewayStatusCommand(
|
||||
runtime.log(
|
||||
` ${colorize(rich, theme.info, "Bridge")}: ${bridge}${c.bridge.bind ? ` · bind ${c.bridge.bind}` : ""}${c.bridge.port ? ` · port ${c.bridge.port}` : ""}`,
|
||||
);
|
||||
runtime.log(
|
||||
` ${colorize(rich, theme.info, "Wide-area discovery")}: ${wideArea}`,
|
||||
);
|
||||
runtime.log(` ${colorize(rich, theme.info, "Wide-area discovery")}: ${wideArea}`);
|
||||
}
|
||||
runtime.log("");
|
||||
}
|
||||
|
||||
@@ -80,29 +80,21 @@ export function parseTimeoutMs(raw: unknown, fallbackMs: number): number {
|
||||
function normalizeWsUrl(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed.startsWith("ws://") && !trimmed.startsWith("wss://"))
|
||||
return null;
|
||||
if (!trimmed.startsWith("ws://") && !trimmed.startsWith("wss://")) return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function resolveTargets(
|
||||
cfg: ClawdbotConfig,
|
||||
explicitUrl?: string,
|
||||
): GatewayStatusTarget[] {
|
||||
export function resolveTargets(cfg: ClawdbotConfig, explicitUrl?: string): GatewayStatusTarget[] {
|
||||
const targets: GatewayStatusTarget[] = [];
|
||||
const add = (t: GatewayStatusTarget) => {
|
||||
if (!targets.some((x) => x.url === t.url)) targets.push(t);
|
||||
};
|
||||
|
||||
const explicit =
|
||||
typeof explicitUrl === "string" ? normalizeWsUrl(explicitUrl) : null;
|
||||
if (explicit)
|
||||
add({ id: "explicit", kind: "explicit", url: explicit, active: true });
|
||||
const explicit = typeof explicitUrl === "string" ? normalizeWsUrl(explicitUrl) : null;
|
||||
if (explicit) add({ id: "explicit", kind: "explicit", url: explicit, active: true });
|
||||
|
||||
const remoteUrl =
|
||||
typeof cfg.gateway?.remote?.url === "string"
|
||||
? normalizeWsUrl(cfg.gateway.remote.url)
|
||||
: null;
|
||||
typeof cfg.gateway?.remote?.url === "string" ? normalizeWsUrl(cfg.gateway.remote.url) : null;
|
||||
if (remoteUrl) {
|
||||
add({
|
||||
id: "configRemote",
|
||||
@@ -123,10 +115,7 @@ export function resolveTargets(
|
||||
return targets;
|
||||
}
|
||||
|
||||
export function resolveProbeBudgetMs(
|
||||
overallMs: number,
|
||||
kind: TargetKind,
|
||||
): number {
|
||||
export function resolveProbeBudgetMs(overallMs: number, kind: TargetKind): number {
|
||||
if (kind === "localLoopback") return Math.min(800, overallMs);
|
||||
if (kind === "sshTunnel") return Math.min(2000, overallMs);
|
||||
return Math.min(1500, overallMs);
|
||||
@@ -144,26 +133,17 @@ export function resolveAuthForTarget(
|
||||
target: GatewayStatusTarget,
|
||||
overrides: { token?: string; password?: string },
|
||||
): { token?: string; password?: string } {
|
||||
const tokenOverride = overrides.token?.trim()
|
||||
? overrides.token.trim()
|
||||
: undefined;
|
||||
const passwordOverride = overrides.password?.trim()
|
||||
? overrides.password.trim()
|
||||
: undefined;
|
||||
const tokenOverride = overrides.token?.trim() ? overrides.token.trim() : undefined;
|
||||
const passwordOverride = overrides.password?.trim() ? overrides.password.trim() : undefined;
|
||||
if (tokenOverride || passwordOverride) {
|
||||
return { token: tokenOverride, password: passwordOverride };
|
||||
}
|
||||
|
||||
if (target.kind === "configRemote") {
|
||||
const token =
|
||||
typeof cfg.gateway?.remote?.token === "string"
|
||||
? cfg.gateway.remote.token.trim()
|
||||
: "";
|
||||
const remotePassword = (
|
||||
cfg.gateway?.remote as { password?: unknown } | undefined
|
||||
)?.password;
|
||||
const password =
|
||||
typeof remotePassword === "string" ? remotePassword.trim() : "";
|
||||
typeof cfg.gateway?.remote?.token === "string" ? cfg.gateway.remote.token.trim() : "";
|
||||
const remotePassword = (cfg.gateway?.remote as { password?: unknown } | undefined)?.password;
|
||||
const password = typeof remotePassword === "string" ? remotePassword.trim() : "";
|
||||
return {
|
||||
token: token.length > 0 ? token : undefined,
|
||||
password: password.length > 0 ? password : undefined,
|
||||
@@ -173,13 +153,9 @@ export function resolveAuthForTarget(
|
||||
const envToken = process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() || "";
|
||||
const envPassword = process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || "";
|
||||
const cfgToken =
|
||||
typeof cfg.gateway?.auth?.token === "string"
|
||||
? cfg.gateway.auth.token.trim()
|
||||
: "";
|
||||
typeof cfg.gateway?.auth?.token === "string" ? cfg.gateway.auth.token.trim() : "";
|
||||
const cfgPassword =
|
||||
typeof cfg.gateway?.auth?.password === "string"
|
||||
? cfg.gateway.auth.password.trim()
|
||||
: "";
|
||||
typeof cfg.gateway?.auth?.password === "string" ? cfg.gateway.auth.password.trim() : "";
|
||||
|
||||
return {
|
||||
token: envToken || cfgToken || undefined,
|
||||
@@ -194,10 +170,7 @@ export function pickGatewaySelfPresence(
|
||||
const entries = presence as Array<Record<string, unknown>>;
|
||||
const self =
|
||||
entries.find((e) => e.mode === "gateway" && e.reason === "self") ??
|
||||
entries.find(
|
||||
(e) =>
|
||||
typeof e.text === "string" && String(e.text).startsWith("Gateway:"),
|
||||
) ??
|
||||
entries.find((e) => typeof e.text === "string" && String(e.text).startsWith("Gateway:")) ??
|
||||
null;
|
||||
if (!self) return null;
|
||||
return {
|
||||
@@ -208,9 +181,7 @@ export function pickGatewaySelfPresence(
|
||||
};
|
||||
}
|
||||
|
||||
export function extractConfigSummary(
|
||||
snapshotUnknown: unknown,
|
||||
): GatewayConfigSummary {
|
||||
export function extractConfigSummary(snapshotUnknown: unknown): GatewayConfigSummary {
|
||||
const snap = snapshotUnknown as Partial<ConfigFileSnapshot> | null;
|
||||
const path = typeof snap?.path === "string" ? snap.path : null;
|
||||
const exists = Boolean(snap?.exists);
|
||||
@@ -230,27 +201,21 @@ export function extractConfigSummary(
|
||||
const tailscale = (gateway.tailscale ?? {}) as Record<string, unknown>;
|
||||
|
||||
const authMode = typeof auth.mode === "string" ? auth.mode : null;
|
||||
const authTokenConfigured =
|
||||
typeof auth.token === "string" ? auth.token.trim().length > 0 : false;
|
||||
const authTokenConfigured = typeof auth.token === "string" ? auth.token.trim().length > 0 : false;
|
||||
const authPasswordConfigured =
|
||||
typeof auth.password === "string" ? auth.password.trim().length > 0 : false;
|
||||
|
||||
const remoteUrl =
|
||||
typeof remote.url === "string" ? normalizeWsUrl(remote.url) : null;
|
||||
const remoteUrl = typeof remote.url === "string" ? normalizeWsUrl(remote.url) : null;
|
||||
const remoteTokenConfigured =
|
||||
typeof remote.token === "string" ? remote.token.trim().length > 0 : false;
|
||||
const remotePasswordConfigured =
|
||||
typeof remote.password === "string"
|
||||
? String(remote.password).trim().length > 0
|
||||
: false;
|
||||
typeof remote.password === "string" ? String(remote.password).trim().length > 0 : false;
|
||||
|
||||
const bridgeEnabled =
|
||||
typeof bridge.enabled === "boolean" ? bridge.enabled : null;
|
||||
const bridgeEnabled = typeof bridge.enabled === "boolean" ? bridge.enabled : null;
|
||||
const bridgeBind = typeof bridge.bind === "string" ? bridge.bind : null;
|
||||
const bridgePort = parseIntOrNull(bridge.port);
|
||||
|
||||
const wideAreaEnabled =
|
||||
typeof wideArea.enabled === "boolean" ? wideArea.enabled : null;
|
||||
const wideAreaEnabled = typeof wideArea.enabled === "boolean" ? wideArea.enabled : null;
|
||||
|
||||
return {
|
||||
path,
|
||||
@@ -258,26 +223,20 @@ export function extractConfigSummary(
|
||||
valid,
|
||||
issues: issuesRaw
|
||||
.filter((i): i is { path: string; message: string } =>
|
||||
Boolean(
|
||||
i && typeof i.path === "string" && typeof i.message === "string",
|
||||
),
|
||||
Boolean(i && typeof i.path === "string" && typeof i.message === "string"),
|
||||
)
|
||||
.map((i) => ({ path: i.path, message: i.message })),
|
||||
legacyIssues: legacyRaw
|
||||
.filter((i): i is { path: string; message: string } =>
|
||||
Boolean(
|
||||
i && typeof i.path === "string" && typeof i.message === "string",
|
||||
),
|
||||
Boolean(i && typeof i.path === "string" && typeof i.message === "string"),
|
||||
)
|
||||
.map((i) => ({ path: i.path, message: i.message })),
|
||||
gateway: {
|
||||
mode: typeof gateway.mode === "string" ? gateway.mode : null,
|
||||
bind: typeof gateway.bind === "string" ? gateway.bind : null,
|
||||
port: parseIntOrNull(gateway.port),
|
||||
controlUiEnabled:
|
||||
typeof controlUi.enabled === "boolean" ? controlUi.enabled : null,
|
||||
controlUiBasePath:
|
||||
typeof controlUi.basePath === "string" ? controlUi.basePath : null,
|
||||
controlUiEnabled: typeof controlUi.enabled === "boolean" ? controlUi.enabled : null,
|
||||
controlUiBasePath: typeof controlUi.basePath === "string" ? controlUi.basePath : null,
|
||||
authMode,
|
||||
authTokenConfigured,
|
||||
authPasswordConfigured,
|
||||
@@ -315,24 +274,17 @@ export function renderTargetHeader(target: GatewayStatusTarget, rich: boolean) {
|
||||
return `${colorize(rich, theme.heading, kindLabel)} ${colorize(rich, theme.muted, target.url)}`;
|
||||
}
|
||||
|
||||
export function renderProbeSummaryLine(
|
||||
probe: GatewayProbeResult,
|
||||
rich: boolean,
|
||||
) {
|
||||
export function renderProbeSummaryLine(probe: GatewayProbeResult, rich: boolean) {
|
||||
if (probe.ok) {
|
||||
const latency =
|
||||
typeof probe.connectLatencyMs === "number"
|
||||
? `${probe.connectLatencyMs}ms`
|
||||
: "unknown";
|
||||
typeof probe.connectLatencyMs === "number" ? `${probe.connectLatencyMs}ms` : "unknown";
|
||||
return `${colorize(rich, theme.success, "Connect: ok")} (${latency}) · ${colorize(rich, theme.success, "RPC: ok")}`;
|
||||
}
|
||||
|
||||
const detail = probe.error ? ` - ${probe.error}` : "";
|
||||
if (probe.connectLatencyMs != null) {
|
||||
const latency =
|
||||
typeof probe.connectLatencyMs === "number"
|
||||
? `${probe.connectLatencyMs}ms`
|
||||
: "unknown";
|
||||
typeof probe.connectLatencyMs === "number" ? `${probe.connectLatencyMs}ms` : "unknown";
|
||||
return `${colorize(rich, theme.success, "Connect: ok")} (${latency}) · ${colorize(rich, theme.error, "RPC: failed")}${detail}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ import type { AgentModelListConfig } from "../config/types.js";
|
||||
|
||||
export const GOOGLE_GEMINI_DEFAULT_MODEL = "google/gemini-3-pro-preview";
|
||||
|
||||
function resolvePrimaryModel(
|
||||
model?: AgentModelListConfig | string,
|
||||
): string | undefined {
|
||||
function resolvePrimaryModel(model?: AgentModelListConfig | string): string | undefined {
|
||||
if (typeof model === "string") return model;
|
||||
if (model && typeof model === "object" && typeof model.primary === "string") {
|
||||
return model.primary;
|
||||
@@ -30,8 +28,7 @@ export function applyGoogleGeminiModelDefault(cfg: ClawdbotConfig): {
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model:
|
||||
cfg.agents?.defaults?.model &&
|
||||
typeof cfg.agents.defaults.model === "object"
|
||||
cfg.agents?.defaults?.model && typeof cfg.agents.defaults.model === "object"
|
||||
? {
|
||||
...cfg.agents.defaults.model,
|
||||
primary: GOOGLE_GEMINI_DEFAULT_MODEL,
|
||||
|
||||
@@ -8,9 +8,7 @@ const stripAnsi = (input: string) => input.replace(ansiRegex, "");
|
||||
|
||||
describe("formatHealthCheckFailure", () => {
|
||||
it("keeps non-rich output stable", () => {
|
||||
const err = new Error(
|
||||
"gateway closed (1006 abnormal closure): no close reason",
|
||||
);
|
||||
const err = new Error("gateway closed (1006 abnormal closure): no close reason");
|
||||
expect(formatHealthCheckFailure(err, { rich: false })).toBe(
|
||||
`Health check failed: ${String(err)}`,
|
||||
);
|
||||
|
||||
@@ -16,10 +16,7 @@ const formatKv = (line: string, rich: boolean) => {
|
||||
return `${colorize(rich, theme.muted, `${key}:`)} ${colorize(rich, valueColor, value)}`;
|
||||
};
|
||||
|
||||
export function formatHealthCheckFailure(
|
||||
err: unknown,
|
||||
opts: { rich?: boolean } = {},
|
||||
): string {
|
||||
export function formatHealthCheckFailure(err: unknown, opts: { rich?: boolean } = {}): string {
|
||||
const rich = opts.rich ?? isRich();
|
||||
const raw = String(err);
|
||||
const message = err instanceof Error ? err.message : raw;
|
||||
|
||||
@@ -70,9 +70,7 @@ describe("healthCommand (coverage)", () => {
|
||||
await healthCommand({ json: false, timeoutMs: 1000 }, runtime as never);
|
||||
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
expect(runtime.log.mock.calls.map((c) => String(c[0])).join("\n")).toMatch(
|
||||
/WhatsApp: linked/i,
|
||||
);
|
||||
expect(runtime.log.mock.calls.map((c) => String(c[0])).join("\n")).toMatch(/WhatsApp: linked/i);
|
||||
expect(logWebSelfIdMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import {
|
||||
getChannelPlugin,
|
||||
listChannelPlugins,
|
||||
} from "../channels/plugins/index.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";
|
||||
@@ -55,25 +52,20 @@ const isAccountEnabled = (account: unknown): boolean => {
|
||||
};
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> | null =>
|
||||
value && typeof value === "object"
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
||||
|
||||
const formatProbeLine = (probe: unknown): string | null => {
|
||||
const record = asRecord(probe);
|
||||
if (!record) return null;
|
||||
const ok = typeof record.ok === "boolean" ? record.ok : undefined;
|
||||
if (ok === undefined) return null;
|
||||
const elapsedMs =
|
||||
typeof record.elapsedMs === "number" ? record.elapsedMs : null;
|
||||
const elapsedMs = typeof record.elapsedMs === "number" ? record.elapsedMs : null;
|
||||
const status = typeof record.status === "number" ? record.status : null;
|
||||
const error = typeof record.error === "string" ? record.error : null;
|
||||
const bot = asRecord(record.bot);
|
||||
const botUsername =
|
||||
bot && typeof bot.username === "string" ? bot.username : null;
|
||||
const botUsername = bot && typeof bot.username === "string" ? bot.username : null;
|
||||
const webhook = asRecord(record.webhook);
|
||||
const webhookUrl =
|
||||
webhook && typeof webhook.url === "string" ? webhook.url : null;
|
||||
const webhookUrl = webhook && typeof webhook.url === "string" ? webhook.url : null;
|
||||
|
||||
if (ok) {
|
||||
let label = "ok";
|
||||
@@ -90,29 +82,20 @@ const formatProbeLine = (probe: unknown): string | null => {
|
||||
export const formatHealthChannelLines = (summary: HealthSummary): string[] => {
|
||||
const channels = summary.channels ?? {};
|
||||
const channelOrder =
|
||||
summary.channelOrder?.length > 0
|
||||
? summary.channelOrder
|
||||
: Object.keys(channels);
|
||||
summary.channelOrder?.length > 0 ? summary.channelOrder : Object.keys(channels);
|
||||
|
||||
const lines: string[] = [];
|
||||
for (const channelId of channelOrder) {
|
||||
const channelSummary = channels[channelId];
|
||||
if (!channelSummary) continue;
|
||||
const plugin = getChannelPlugin(channelId as never);
|
||||
const label =
|
||||
summary.channelLabels?.[channelId] ?? plugin?.meta.label ?? channelId;
|
||||
const linked =
|
||||
typeof channelSummary.linked === "boolean" ? channelSummary.linked : null;
|
||||
const label = summary.channelLabels?.[channelId] ?? plugin?.meta.label ?? channelId;
|
||||
const linked = typeof channelSummary.linked === "boolean" ? channelSummary.linked : null;
|
||||
if (linked !== null) {
|
||||
if (linked) {
|
||||
const authAgeMs =
|
||||
typeof channelSummary.authAgeMs === "number"
|
||||
? channelSummary.authAgeMs
|
||||
: null;
|
||||
const authLabel =
|
||||
authAgeMs != null
|
||||
? ` (auth age ${Math.round(authAgeMs / 60000)}m)`
|
||||
: "";
|
||||
typeof channelSummary.authAgeMs === "number" ? channelSummary.authAgeMs : null;
|
||||
const authLabel = authAgeMs != null ? ` (auth age ${Math.round(authAgeMs / 60000)}m)` : "";
|
||||
lines.push(`${label}: linked${authLabel}`);
|
||||
} else {
|
||||
lines.push(`${label}: not linked`);
|
||||
@@ -121,9 +104,7 @@ export const formatHealthChannelLines = (summary: HealthSummary): string[] => {
|
||||
}
|
||||
|
||||
const configured =
|
||||
typeof channelSummary.configured === "boolean"
|
||||
? channelSummary.configured
|
||||
: null;
|
||||
typeof channelSummary.configured === "boolean" ? channelSummary.configured : null;
|
||||
if (configured === false) {
|
||||
lines.push(`${label}: not configured`);
|
||||
continue;
|
||||
@@ -306,9 +287,7 @@ export async function healthCommand(
|
||||
|
||||
runtime.log(info(`Heartbeat interval: ${summary.heartbeatSeconds}s`));
|
||||
runtime.log(
|
||||
info(
|
||||
`Session store: ${summary.sessions.path} (${summary.sessions.count} entries)`,
|
||||
),
|
||||
info(`Session store: ${summary.sessions.path} (${summary.sessions.count} entries)`),
|
||||
);
|
||||
if (summary.sessions.recent.length > 0) {
|
||||
runtime.log("Recent sessions:");
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||
import type {
|
||||
ChannelId,
|
||||
ChannelMessageActionName,
|
||||
} from "../channels/plugins/types.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 { formatGatewaySummary, formatOutboundDeliverySummary } from "../infra/outbound/format.js";
|
||||
import type { MessageActionRunResult } from "../infra/outbound/message-action-runner.js";
|
||||
import { renderTable } from "../terminal/table.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
@@ -41,9 +35,7 @@ export type MessageCliJsonEnvelope = {
|
||||
payload: unknown;
|
||||
};
|
||||
|
||||
export function buildMessageCliJson(
|
||||
result: MessageActionRunResult,
|
||||
): MessageCliJsonEnvelope {
|
||||
export function buildMessageCliJson(result: MessageActionRunResult): MessageCliJsonEnvelope {
|
||||
return {
|
||||
action: result.action,
|
||||
channel: result.channel,
|
||||
@@ -103,11 +95,7 @@ function renderObjectSummary(payload: unknown, opts: FormatOpts): string[] {
|
||||
];
|
||||
}
|
||||
|
||||
function renderMessageList(
|
||||
messages: unknown[],
|
||||
opts: FormatOpts,
|
||||
emptyLabel: string,
|
||||
): string[] {
|
||||
function renderMessageList(messages: unknown[], opts: FormatOpts, emptyLabel: string): string[] {
|
||||
const rows = messages.slice(0, 25).map((m) => {
|
||||
const msg = m as Record<string, unknown>;
|
||||
const id =
|
||||
@@ -155,29 +143,21 @@ function renderMessageList(
|
||||
];
|
||||
}
|
||||
|
||||
function renderMessagesFromPayload(
|
||||
payload: unknown,
|
||||
opts: FormatOpts,
|
||||
): string[] | null {
|
||||
function renderMessagesFromPayload(payload: unknown, opts: FormatOpts): string[] | null {
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
const messages = (payload as { messages?: unknown }).messages;
|
||||
if (!Array.isArray(messages)) return null;
|
||||
return renderMessageList(messages, opts, "No messages.");
|
||||
}
|
||||
|
||||
function renderPinsFromPayload(
|
||||
payload: unknown,
|
||||
opts: FormatOpts,
|
||||
): string[] | null {
|
||||
function renderPinsFromPayload(payload: unknown, opts: FormatOpts): string[] | null {
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
const pins = (payload as { pins?: unknown }).pins;
|
||||
if (!Array.isArray(pins)) return null;
|
||||
return renderMessageList(pins, opts, "No pins.");
|
||||
}
|
||||
|
||||
function extractDiscordSearchResultsMessages(
|
||||
results: unknown,
|
||||
): unknown[] | null {
|
||||
function extractDiscordSearchResultsMessages(results: unknown): unknown[] | null {
|
||||
if (!results || typeof results !== "object") return null;
|
||||
const raw = (results as { messages?: unknown }).messages;
|
||||
if (!Array.isArray(raw)) return null;
|
||||
@@ -255,9 +235,7 @@ export function formatMessageCliText(result: MessageActionRunResult): string[] {
|
||||
const opts: FormatOpts = { width };
|
||||
|
||||
if (result.handledBy === "dry-run") {
|
||||
return [
|
||||
muted(`[dry-run] would run ${result.action} via ${result.channel}`),
|
||||
];
|
||||
return [muted(`[dry-run] would run ${result.action} via ${result.channel}`)];
|
||||
}
|
||||
|
||||
if (result.kind === "send") {
|
||||
@@ -303,9 +281,7 @@ export function formatMessageCliText(result: MessageActionRunResult): string[] {
|
||||
|
||||
const label = resolveChannelLabel(result.channel);
|
||||
const msgId = extractMessageId(result.payload);
|
||||
return [
|
||||
ok(`✅ Poll sent via ${label}.${msgId ? ` Message ID: ${msgId}` : ""}`),
|
||||
];
|
||||
return [ok(`✅ Poll sent via ${label}.${msgId ? ` Message ID: ${msgId}` : ""}`)];
|
||||
}
|
||||
|
||||
// channel actions (non-send/poll)
|
||||
@@ -371,9 +347,7 @@ export function formatMessageCliText(result: MessageActionRunResult): string[] {
|
||||
}
|
||||
|
||||
// Generic success + compact details table.
|
||||
lines.push(
|
||||
ok(`✅ ${result.action} via ${resolveChannelLabel(result.channel)}.`),
|
||||
);
|
||||
lines.push(ok(`✅ ${result.action} via ${resolveChannelLabel(result.channel)}.`));
|
||||
const summary = renderObjectSummary(payload, opts);
|
||||
if (summary.length) {
|
||||
lines.push("");
|
||||
|
||||
@@ -8,10 +8,7 @@ import { loadConfig } from "../config/config.js";
|
||||
import type { OutboundSendDeps } from "../infra/outbound/deliver.js";
|
||||
import { runMessageAction } from "../infra/outbound/message-action-runner.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-channel.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
import { buildMessageCliJson, formatMessageCliText } from "./message-format.js";
|
||||
|
||||
export async function messageCommand(
|
||||
@@ -20,8 +17,7 @@ export async function messageCommand(
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const cfg = loadConfig();
|
||||
const rawAction =
|
||||
typeof opts.action === "string" ? opts.action.trim().toLowerCase() : "";
|
||||
const rawAction = typeof opts.action === "string" ? opts.action.trim().toLowerCase() : "";
|
||||
const action = (rawAction || "send") as ChannelMessageActionName;
|
||||
if (!(CHANNEL_MESSAGE_ACTION_NAMES as readonly string[]).includes(action)) {
|
||||
throw new Error(`Unknown message action: ${action}`);
|
||||
@@ -52,8 +48,7 @@ export async function messageCommand(
|
||||
|
||||
const json = opts.json === true;
|
||||
const dryRun = opts.dryRun === true;
|
||||
const needsSpinner =
|
||||
!json && !dryRun && (action === "send" || action === "poll");
|
||||
const needsSpinner = !json && !dryRun && (action === "send" || action === "poll");
|
||||
|
||||
const result = needsSpinner
|
||||
? await withProgress(
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { ensureAuthProfileStore, listProfilesForProvider } from "../agents/auth-profiles.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import {
|
||||
getCustomProviderApiKey,
|
||||
resolveEnvApiKey,
|
||||
} from "../agents/model-auth.js";
|
||||
import { getCustomProviderApiKey, resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import {
|
||||
buildAllowedModelSet,
|
||||
@@ -48,10 +42,7 @@ function hasAuthForProvider(
|
||||
}
|
||||
|
||||
function resolveConfiguredModelRaw(cfg: ClawdbotConfig): string {
|
||||
const raw = cfg.agents?.defaults?.model as
|
||||
| { primary?: string }
|
||||
| string
|
||||
| undefined;
|
||||
const raw = cfg.agents?.defaults?.model as { primary?: string } | string | undefined;
|
||||
if (typeof raw === "string") return raw.trim();
|
||||
return raw?.primary?.trim() ?? "";
|
||||
}
|
||||
@@ -62,14 +53,10 @@ async function promptManualModel(params: {
|
||||
initialValue?: string;
|
||||
}): Promise<PromptDefaultModelResult> {
|
||||
const modelInput = await params.prompter.text({
|
||||
message: params.allowBlank
|
||||
? "Default model (blank to keep)"
|
||||
: "Default model",
|
||||
message: params.allowBlank ? "Default model (blank to keep)" : "Default model",
|
||||
initialValue: params.initialValue,
|
||||
placeholder: "provider/model",
|
||||
validate: params.allowBlank
|
||||
? undefined
|
||||
: (value) => (value?.trim() ? undefined : "Required"),
|
||||
validate: params.allowBlank ? undefined : (value) => (value?.trim() ? undefined : "Required"),
|
||||
});
|
||||
const model = String(modelInput ?? "").trim();
|
||||
if (!model) return {};
|
||||
@@ -128,26 +115,20 @@ export async function promptDefaultModel(
|
||||
});
|
||||
}
|
||||
|
||||
const providers = Array.from(
|
||||
new Set(models.map((entry) => entry.provider)),
|
||||
).sort((a, b) => a.localeCompare(b));
|
||||
const providers = Array.from(new Set(models.map((entry) => entry.provider))).sort((a, b) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
|
||||
const hasPreferredProvider = preferredProvider
|
||||
? providers.includes(preferredProvider)
|
||||
: false;
|
||||
const hasPreferredProvider = preferredProvider ? providers.includes(preferredProvider) : false;
|
||||
const shouldPromptProvider =
|
||||
!hasPreferredProvider &&
|
||||
providers.length > 1 &&
|
||||
models.length > PROVIDER_FILTER_THRESHOLD;
|
||||
!hasPreferredProvider && providers.length > 1 && models.length > PROVIDER_FILTER_THRESHOLD;
|
||||
if (shouldPromptProvider) {
|
||||
const selection = await params.prompter.select({
|
||||
message: "Filter models by provider",
|
||||
options: [
|
||||
{ value: "*", label: "All providers" },
|
||||
...providers.map((provider) => {
|
||||
const count = models.filter(
|
||||
(entry) => entry.provider === provider,
|
||||
).length;
|
||||
const count = models.filter((entry) => entry.provider === provider).length;
|
||||
return {
|
||||
value: provider,
|
||||
label: provider,
|
||||
@@ -185,9 +166,7 @@ export async function promptDefaultModel(
|
||||
? `Keep current (${configuredRaw})`
|
||||
: `Keep current (default: ${resolvedKey})`,
|
||||
hint:
|
||||
configuredRaw && configuredRaw !== resolvedKey
|
||||
? `resolves to ${resolvedKey}`
|
||||
: undefined,
|
||||
configuredRaw && configuredRaw !== resolvedKey ? `resolves to ${resolvedKey}` : undefined,
|
||||
});
|
||||
}
|
||||
if (includeManual) {
|
||||
@@ -206,8 +185,7 @@ export async function promptDefaultModel(
|
||||
if (seen.has(key)) return;
|
||||
const hints: string[] = [];
|
||||
if (entry.name && entry.name !== entry.id) hints.push(entry.name);
|
||||
if (entry.contextWindow)
|
||||
hints.push(`ctx ${formatTokenK(entry.contextWindow)}`);
|
||||
if (entry.contextWindow) hints.push(`ctx ${formatTokenK(entry.contextWindow)}`);
|
||||
if (entry.reasoning) hints.push("reasoning");
|
||||
const aliases = aliasIndex.byKey.get(key);
|
||||
if (aliases?.length) hints.push(`alias: ${aliases.join(", ")}`);
|
||||
@@ -230,9 +208,7 @@ export async function promptDefaultModel(
|
||||
});
|
||||
}
|
||||
|
||||
let initialValue: string | undefined = allowKeep
|
||||
? KEEP_VALUE
|
||||
: configuredKey || undefined;
|
||||
let initialValue: string | undefined = allowKeep ? KEEP_VALUE : configuredKey || undefined;
|
||||
if (
|
||||
allowKeep &&
|
||||
hasPreferredProvider &&
|
||||
@@ -262,17 +238,12 @@ export async function promptDefaultModel(
|
||||
return { model: String(selection) };
|
||||
}
|
||||
|
||||
export function applyPrimaryModel(
|
||||
cfg: ClawdbotConfig,
|
||||
model: string,
|
||||
): ClawdbotConfig {
|
||||
export function applyPrimaryModel(cfg: ClawdbotConfig, model: string): ClawdbotConfig {
|
||||
const defaults = cfg.agents?.defaults;
|
||||
const existingModel = defaults?.model;
|
||||
const existingModels = defaults?.models;
|
||||
const fallbacks =
|
||||
typeof existingModel === "object" &&
|
||||
existingModel !== null &&
|
||||
"fallbacks" in existingModel
|
||||
typeof existingModel === "object" && existingModel !== null && "fallbacks" in existingModel
|
||||
? (existingModel as { fallbacks?: string[] }).fallbacks
|
||||
: undefined;
|
||||
return {
|
||||
|
||||
@@ -3,13 +3,9 @@ import { describe, expect, it, vi } from "vitest";
|
||||
const loadConfig = vi.fn();
|
||||
const ensureClawdbotModelsJson = vi.fn().mockResolvedValue(undefined);
|
||||
const resolveClawdbotAgentDir = vi.fn().mockReturnValue("/tmp/clawdbot-agent");
|
||||
const ensureAuthProfileStore = vi
|
||||
.fn()
|
||||
.mockReturnValue({ version: 1, profiles: {} });
|
||||
const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} });
|
||||
const listProfilesForProvider = vi.fn().mockReturnValue([]);
|
||||
const resolveAuthProfileDisplayLabel = vi.fn(
|
||||
({ profileId }: { profileId: string }) => profileId,
|
||||
);
|
||||
const resolveAuthProfileDisplayLabel = vi.fn(({ profileId }: { profileId: string }) => profileId);
|
||||
const resolveAuthStorePathForDisplay = vi
|
||||
.fn()
|
||||
.mockReturnValue("/tmp/clawdbot-agent/auth-profiles.json");
|
||||
@@ -171,10 +167,7 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
const { modelsListCommand } = await import("./models/list.js");
|
||||
await modelsListCommand(
|
||||
{ all: true, provider: "z.ai", json: true },
|
||||
runtime,
|
||||
);
|
||||
await modelsListCommand({ all: true, provider: "z.ai", json: true }, runtime);
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0]));
|
||||
@@ -213,10 +206,7 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
const { modelsListCommand } = await import("./models/list.js");
|
||||
await modelsListCommand(
|
||||
{ all: true, provider: "Z.AI", json: true },
|
||||
runtime,
|
||||
);
|
||||
await modelsListCommand({ all: true, provider: "Z.AI", json: true }, runtime);
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0]));
|
||||
@@ -255,10 +245,7 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
const { modelsListCommand } = await import("./models/list.js");
|
||||
await modelsListCommand(
|
||||
{ all: true, provider: "z-ai", json: true },
|
||||
runtime,
|
||||
);
|
||||
await modelsListCommand({ all: true, provider: "z-ai", json: true }, runtime);
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0]));
|
||||
|
||||
@@ -35,10 +35,7 @@ describe("models set + fallbacks", () => {
|
||||
await modelsSetCommand("z.ai/glm-4.7", runtime);
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(written.agents).toEqual({
|
||||
defaults: {
|
||||
model: { primary: "zai/glm-4.7" },
|
||||
@@ -65,10 +62,7 @@ describe("models set + fallbacks", () => {
|
||||
await modelsFallbacksAddCommand("z-ai/glm-4.7", runtime);
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(written.agents).toEqual({
|
||||
defaults: {
|
||||
model: { fallbacks: ["zai/glm-4.7"] },
|
||||
@@ -95,10 +89,7 @@ describe("models set + fallbacks", () => {
|
||||
await modelsSetCommand("Z.AI/glm-4.7", runtime);
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(written.agents).toEqual({
|
||||
defaults: {
|
||||
model: { primary: "zai/glm-4.7" },
|
||||
|
||||
@@ -78,10 +78,7 @@ export async function modelsAliasesAddCommand(
|
||||
runtime.log(`Alias ${alias} -> ${resolved.provider}/${resolved.model}`);
|
||||
}
|
||||
|
||||
export async function modelsAliasesRemoveCommand(
|
||||
aliasRaw: string,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
export async function modelsAliasesRemoveCommand(aliasRaw: string, runtime: RuntimeEnv) {
|
||||
const alias = normalizeAlias(aliasRaw);
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
@@ -111,9 +108,7 @@ export async function modelsAliasesRemoveCommand(
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
if (
|
||||
!updated.agents?.defaults?.models ||
|
||||
Object.values(updated.agents.defaults.models).every(
|
||||
(entry) => !entry?.alias?.trim(),
|
||||
)
|
||||
Object.values(updated.agents.defaults.models).every((entry) => !entry?.alias?.trim())
|
||||
) {
|
||||
runtime.log("No aliases configured.");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { resolveAgentDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import {
|
||||
type AuthProfileStore,
|
||||
ensureAuthProfileStore,
|
||||
@@ -20,9 +17,7 @@ function resolveTargetAgent(
|
||||
agentId: string;
|
||||
agentDir: string;
|
||||
} {
|
||||
const agentId = raw?.trim()
|
||||
? normalizeAgentId(raw.trim())
|
||||
: resolveDefaultAgentId(cfg);
|
||||
const agentId = raw?.trim() ? normalizeAgentId(raw.trim()) : resolveDefaultAgentId(cfg);
|
||||
const agentDir = resolveAgentDir(cfg, agentId);
|
||||
return { agentId, agentDir };
|
||||
}
|
||||
@@ -67,14 +62,8 @@ export async function modelsAuthOrderGetCommand(
|
||||
|
||||
runtime.log(`Agent: ${agentId}`);
|
||||
runtime.log(`Provider: ${provider}`);
|
||||
runtime.log(
|
||||
`Auth file: ${shortenHomePath(`${agentDir}/auth-profiles.json`)}`,
|
||||
);
|
||||
runtime.log(
|
||||
order.length > 0
|
||||
? `Order override: ${order.join(", ")}`
|
||||
: "Order override: (none)",
|
||||
);
|
||||
runtime.log(`Auth file: ${shortenHomePath(`${agentDir}/auth-profiles.json`)}`);
|
||||
runtime.log(order.length > 0 ? `Order override: ${order.join(", ")}` : "Order override: (none)");
|
||||
}
|
||||
|
||||
export async function modelsAuthOrderClearCommand(
|
||||
@@ -92,8 +81,7 @@ export async function modelsAuthOrderClearCommand(
|
||||
provider,
|
||||
order: null,
|
||||
});
|
||||
if (!updated)
|
||||
throw new Error("Failed to update auth-profiles.json (lock busy?).");
|
||||
if (!updated) throw new Error("Failed to update auth-profiles.json (lock busy?).");
|
||||
|
||||
runtime.log(`Agent: ${agentId}`);
|
||||
runtime.log(`Provider: ${provider}`);
|
||||
@@ -115,9 +103,7 @@ export async function modelsAuthOrderSetCommand(
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const providerKey = normalizeProviderId(provider);
|
||||
const requested = (opts.order ?? [])
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter(Boolean);
|
||||
const requested = (opts.order ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
||||
if (requested.length === 0) {
|
||||
throw new Error("Missing profile ids. Provide one or more profile ids.");
|
||||
}
|
||||
@@ -128,9 +114,7 @@ export async function modelsAuthOrderSetCommand(
|
||||
throw new Error(`Auth profile "${profileId}" not found in ${agentDir}.`);
|
||||
}
|
||||
if (normalizeProviderId(cred.provider) !== providerKey) {
|
||||
throw new Error(
|
||||
`Auth profile "${profileId}" is for ${cred.provider}, not ${provider}.`,
|
||||
);
|
||||
throw new Error(`Auth profile "${profileId}" is for ${cred.provider}, not ${provider}.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +123,7 @@ export async function modelsAuthOrderSetCommand(
|
||||
provider,
|
||||
order: requested,
|
||||
});
|
||||
if (!updated)
|
||||
throw new Error("Failed to update auth-profiles.json (lock busy?).");
|
||||
if (!updated) throw new Error("Failed to update auth-profiles.json (lock busy?).");
|
||||
|
||||
runtime.log(`Agent: ${agentId}`);
|
||||
runtime.log(`Provider: ${provider}`);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
import {
|
||||
confirm as clackConfirm,
|
||||
select as clackSelect,
|
||||
text as clackText,
|
||||
} from "@clack/prompts";
|
||||
import { confirm as clackConfirm, select as clackSelect, text as clackText } from "@clack/prompts";
|
||||
|
||||
import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
@@ -15,10 +11,7 @@ import { normalizeProviderId } from "../../agents/model-selection.js";
|
||||
import { parseDurationMs } from "../../cli/parse-duration.js";
|
||||
import { CONFIG_PATH_CLAWDBOT } from "../../config/config.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import {
|
||||
stylePromptHint,
|
||||
stylePromptMessage,
|
||||
} from "../../terminal/prompt-style.js";
|
||||
import { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js";
|
||||
import { applyAuthProfileConfig } from "../onboard-auth.js";
|
||||
import { updateConfig } from "./shared.js";
|
||||
|
||||
@@ -37,9 +30,7 @@ const select = <T>(params: Parameters<typeof clackSelect<T>>[0]) =>
|
||||
...params,
|
||||
message: stylePromptMessage(params.message),
|
||||
options: params.options.map((opt) =>
|
||||
opt.hint === undefined
|
||||
? opt
|
||||
: { ...opt, hint: stylePromptHint(opt.hint) },
|
||||
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
|
||||
),
|
||||
});
|
||||
|
||||
@@ -121,8 +112,7 @@ export async function modelsAuthPasteTokenCommand(
|
||||
throw new Error("Missing --provider.");
|
||||
}
|
||||
const provider = normalizeProviderId(rawProvider);
|
||||
const profileId =
|
||||
opts.profileId?.trim() || resolveDefaultTokenProfileId(provider);
|
||||
const profileId = opts.profileId?.trim() || resolveDefaultTokenProfileId(provider);
|
||||
|
||||
const tokenInput = await text({
|
||||
message: `Paste token for ${provider}`,
|
||||
@@ -132,8 +122,7 @@ export async function modelsAuthPasteTokenCommand(
|
||||
|
||||
const expires =
|
||||
opts.expiresIn?.trim() && opts.expiresIn.trim().length > 0
|
||||
? Date.now() +
|
||||
parseDurationMs(String(opts.expiresIn).trim(), { defaultUnit: "d" })
|
||||
? Date.now() + parseDurationMs(String(opts.expiresIn).trim(), { defaultUnit: "d" })
|
||||
: undefined;
|
||||
|
||||
upsertAuthProfile({
|
||||
@@ -146,18 +135,13 @@ export async function modelsAuthPasteTokenCommand(
|
||||
},
|
||||
});
|
||||
|
||||
await updateConfig((cfg) =>
|
||||
applyAuthProfileConfig(cfg, { profileId, provider, mode: "token" }),
|
||||
);
|
||||
await updateConfig((cfg) => applyAuthProfileConfig(cfg, { profileId, provider, mode: "token" }));
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(`Auth profile: ${profileId} (${provider}/token)`);
|
||||
}
|
||||
|
||||
export async function modelsAuthAddCommand(
|
||||
_opts: Record<string, never>,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
export async function modelsAuthAddCommand(_opts: Record<string, never>, runtime: RuntimeEnv) {
|
||||
const provider = (await select({
|
||||
message: "Token provider",
|
||||
options: [
|
||||
@@ -229,8 +213,5 @@ export async function modelsAuthAddCommand(
|
||||
).trim()
|
||||
: undefined;
|
||||
|
||||
await modelsAuthPasteTokenCommand(
|
||||
{ provider: providerId, profileId, expiresIn },
|
||||
runtime,
|
||||
);
|
||||
await modelsAuthPasteTokenCommand({ provider: providerId, profileId, expiresIn }, runtime);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import { buildModelAliasIndex, resolveModelRefFromString } from "../../agents/model-selection.js";
|
||||
import { CONFIG_PATH_CLAWDBOT, loadConfig } from "../../config/config.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import {
|
||||
@@ -37,10 +34,7 @@ export async function modelsFallbacksListCommand(
|
||||
for (const entry of fallbacks) runtime.log(`- ${entry}`);
|
||||
}
|
||||
|
||||
export async function modelsFallbacksAddCommand(
|
||||
modelRaw: string,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
export async function modelsFallbacksAddCommand(modelRaw: string, runtime: RuntimeEnv) {
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const targetKey = modelKey(resolved.provider, resolved.model);
|
||||
@@ -75,9 +69,7 @@ export async function modelsFallbacksAddCommand(
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
|
||||
fallbacks: [...existing, targetKey],
|
||||
},
|
||||
models: nextModels,
|
||||
@@ -87,15 +79,10 @@ export async function modelsFallbacksAddCommand(
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(
|
||||
`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`,
|
||||
);
|
||||
runtime.log(`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`);
|
||||
}
|
||||
|
||||
export async function modelsFallbacksRemoveCommand(
|
||||
modelRaw: string,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
export async function modelsFallbacksRemoveCommand(modelRaw: string, runtime: RuntimeEnv) {
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const targetKey = modelKey(resolved.provider, resolved.model);
|
||||
@@ -111,10 +98,7 @@ export async function modelsFallbacksRemoveCommand(
|
||||
aliasIndex,
|
||||
});
|
||||
if (!resolvedEntry) return true;
|
||||
return (
|
||||
modelKey(resolvedEntry.ref.provider, resolvedEntry.ref.model) !==
|
||||
targetKey
|
||||
);
|
||||
return modelKey(resolvedEntry.ref.provider, resolvedEntry.ref.model) !== targetKey;
|
||||
});
|
||||
|
||||
if (filtered.length === existing.length) {
|
||||
@@ -132,9 +116,7 @@ export async function modelsFallbacksRemoveCommand(
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
|
||||
fallbacks: filtered,
|
||||
},
|
||||
},
|
||||
@@ -143,9 +125,7 @@ export async function modelsFallbacksRemoveCommand(
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(
|
||||
`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`,
|
||||
);
|
||||
runtime.log(`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`);
|
||||
}
|
||||
|
||||
export async function modelsFallbacksClearCommand(runtime: RuntimeEnv) {
|
||||
@@ -160,9 +140,7 @@ export async function modelsFallbacksClearCommand(runtime: RuntimeEnv) {
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import { buildModelAliasIndex, resolveModelRefFromString } from "../../agents/model-selection.js";
|
||||
import { CONFIG_PATH_CLAWDBOT, loadConfig } from "../../config/config.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import {
|
||||
@@ -37,10 +34,7 @@ export async function modelsImageFallbacksListCommand(
|
||||
for (const entry of fallbacks) runtime.log(`- ${entry}`);
|
||||
}
|
||||
|
||||
export async function modelsImageFallbacksAddCommand(
|
||||
modelRaw: string,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
export async function modelsImageFallbacksAddCommand(modelRaw: string, runtime: RuntimeEnv) {
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const targetKey = modelKey(resolved.provider, resolved.model);
|
||||
@@ -75,9 +69,7 @@ export async function modelsImageFallbacksAddCommand(
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
|
||||
fallbacks: [...existing, targetKey],
|
||||
},
|
||||
models: nextModels,
|
||||
@@ -92,10 +84,7 @@ export async function modelsImageFallbacksAddCommand(
|
||||
);
|
||||
}
|
||||
|
||||
export async function modelsImageFallbacksRemoveCommand(
|
||||
modelRaw: string,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
export async function modelsImageFallbacksRemoveCommand(modelRaw: string, runtime: RuntimeEnv) {
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const targetKey = modelKey(resolved.provider, resolved.model);
|
||||
@@ -111,10 +100,7 @@ export async function modelsImageFallbacksRemoveCommand(
|
||||
aliasIndex,
|
||||
});
|
||||
if (!resolvedEntry) return true;
|
||||
return (
|
||||
modelKey(resolvedEntry.ref.provider, resolvedEntry.ref.model) !==
|
||||
targetKey
|
||||
);
|
||||
return modelKey(resolvedEntry.ref.provider, resolvedEntry.ref.model) !== targetKey;
|
||||
});
|
||||
|
||||
if (filtered.length === existing.length) {
|
||||
@@ -132,9 +118,7 @@ export async function modelsImageFallbacksRemoveCommand(
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
|
||||
fallbacks: filtered,
|
||||
},
|
||||
},
|
||||
@@ -160,9 +144,7 @@ export async function modelsImageFallbacksClearCommand(runtime: RuntimeEnv) {
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,10 +6,7 @@ import {
|
||||
resolveAuthStorePathForDisplay,
|
||||
resolveProfileUnusableUntilForDisplay,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import {
|
||||
getCustomProviderApiKey,
|
||||
resolveEnvApiKey,
|
||||
} from "../../agents/model-auth.js";
|
||||
import { getCustomProviderApiKey, resolveEnvApiKey } from "../../agents/model-auth.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { shortenHomePath } from "../../utils.js";
|
||||
import { maskApiKey } from "./list.format.js";
|
||||
@@ -25,10 +22,7 @@ export function resolveProviderAuthOverview(params: {
|
||||
const now = Date.now();
|
||||
const profiles = listProfilesForProvider(store, provider);
|
||||
const withUnusableSuffix = (base: string, profileId: string) => {
|
||||
const unusableUntil = resolveProfileUnusableUntilForDisplay(
|
||||
store,
|
||||
profileId,
|
||||
);
|
||||
const unusableUntil = resolveProfileUnusableUntilForDisplay(store, profileId);
|
||||
if (!unusableUntil || now >= unusableUntil) return base;
|
||||
const stats = store.usageStats?.[profileId];
|
||||
const kind =
|
||||
@@ -42,16 +36,10 @@ export function resolveProviderAuthOverview(params: {
|
||||
const profile = store.profiles[profileId];
|
||||
if (!profile) return `${profileId}=missing`;
|
||||
if (profile.type === "api_key") {
|
||||
return withUnusableSuffix(
|
||||
`${profileId}=${maskApiKey(profile.key)}`,
|
||||
profileId,
|
||||
);
|
||||
return withUnusableSuffix(`${profileId}=${maskApiKey(profile.key)}`, profileId);
|
||||
}
|
||||
if (profile.type === "token") {
|
||||
return withUnusableSuffix(
|
||||
`${profileId}=token:${maskApiKey(profile.token)}`,
|
||||
profileId,
|
||||
);
|
||||
return withUnusableSuffix(`${profileId}=token:${maskApiKey(profile.token)}`, profileId);
|
||||
}
|
||||
const display = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
|
||||
const suffix =
|
||||
@@ -63,15 +51,9 @@ export function resolveProviderAuthOverview(params: {
|
||||
const base = `${profileId}=OAuth${suffix ? ` ${suffix}` : ""}`;
|
||||
return withUnusableSuffix(base, profileId);
|
||||
});
|
||||
const oauthCount = profiles.filter(
|
||||
(id) => store.profiles[id]?.type === "oauth",
|
||||
).length;
|
||||
const tokenCount = profiles.filter(
|
||||
(id) => store.profiles[id]?.type === "token",
|
||||
).length;
|
||||
const apiKeyCount = profiles.filter(
|
||||
(id) => store.profiles[id]?.type === "api_key",
|
||||
).length;
|
||||
const oauthCount = profiles.filter((id) => store.profiles[id]?.type === "oauth").length;
|
||||
const tokenCount = profiles.filter((id) => store.profiles[id]?.type === "token").length;
|
||||
const apiKeyCount = profiles.filter((id) => store.profiles[id]?.type === "api_key").length;
|
||||
|
||||
const envKey = resolveEnvApiKey(provider);
|
||||
const customKey = getCustomProviderApiKey(cfg, provider);
|
||||
@@ -85,8 +67,7 @@ export function resolveProviderAuthOverview(params: {
|
||||
}
|
||||
if (envKey) {
|
||||
const isOAuthEnv =
|
||||
envKey.source.includes("OAUTH_TOKEN") ||
|
||||
envKey.source.toLowerCase().includes("oauth");
|
||||
envKey.source.includes("OAUTH_TOKEN") || envKey.source.toLowerCase().includes("oauth");
|
||||
return {
|
||||
kind: "env",
|
||||
detail: isOAuthEnv ? "OAuth (env)" : maskApiKey(envKey.apiKey),
|
||||
@@ -112,8 +93,7 @@ export function resolveProviderAuthOverview(params: {
|
||||
? {
|
||||
env: {
|
||||
value:
|
||||
envKey.source.includes("OAUTH_TOKEN") ||
|
||||
envKey.source.toLowerCase().includes("oauth")
|
||||
envKey.source.includes("OAUTH_TOKEN") || envKey.source.toLowerCase().includes("oauth")
|
||||
? "OAuth (env)"
|
||||
: maskApiKey(envKey.apiKey),
|
||||
source: envKey.source,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user