feat: wire multi-agent config and routing
Co-authored-by: Mark Pors <1078320+pors@users.noreply.github.com>
This commit is contained in:
@@ -29,9 +29,11 @@ const configSpy = vi.spyOn(configModule, "loadConfig");
|
||||
|
||||
function mockConfig(storePath: string, overrides?: Partial<ClawdbotConfig>) {
|
||||
configSpy.mockReturnValue({
|
||||
agent: {
|
||||
timeoutSeconds: 600,
|
||||
...overrides?.agent,
|
||||
agents: {
|
||||
defaults: {
|
||||
timeoutSeconds: 600,
|
||||
...overrides?.agents?.defaults,
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: storePath,
|
||||
|
||||
@@ -80,7 +80,7 @@ function parseTimeoutSeconds(opts: {
|
||||
const raw =
|
||||
opts.timeout !== undefined
|
||||
? Number.parseInt(String(opts.timeout), 10)
|
||||
: (opts.cfg.agent?.timeoutSeconds ?? 600);
|
||||
: (opts.cfg.agents?.defaults?.timeoutSeconds ?? 600);
|
||||
if (Number.isNaN(raw) || raw <= 0) {
|
||||
throw new Error("--timeout must be a positive integer (seconds)");
|
||||
}
|
||||
|
||||
@@ -53,19 +53,21 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
function mockConfig(
|
||||
home: string,
|
||||
storePath: string,
|
||||
routingOverrides?: Partial<NonNullable<ClawdbotConfig["routing"]>>,
|
||||
agentOverrides?: Partial<NonNullable<ClawdbotConfig["agent"]>>,
|
||||
agentOverrides?: Partial<
|
||||
NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
|
||||
>,
|
||||
telegramOverrides?: Partial<NonNullable<ClawdbotConfig["telegram"]>>,
|
||||
) {
|
||||
configSpy.mockReturnValue({
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
models: { "anthropic/claude-opus-4-5": {} },
|
||||
workspace: path.join(home, "clawd"),
|
||||
...agentOverrides,
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
models: { "anthropic/claude-opus-4-5": {} },
|
||||
workspace: path.join(home, "clawd"),
|
||||
...agentOverrides,
|
||||
},
|
||||
},
|
||||
session: { store: storePath, mainKey: "main" },
|
||||
routing: routingOverrides ? { ...routingOverrides } : undefined,
|
||||
telegram: telegramOverrides ? { ...telegramOverrides } : undefined,
|
||||
});
|
||||
}
|
||||
@@ -153,11 +155,15 @@ describe("agentCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses provider/model from agent.model", async () => {
|
||||
it("uses provider/model from agents.defaults.model.primary", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store, undefined, {
|
||||
model: "openai/gpt-4.1-mini",
|
||||
mockConfig(home, store, {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
});
|
||||
|
||||
await agentCommand({ message: "hi", to: "+1555" }, runtime);
|
||||
@@ -269,7 +275,7 @@ describe("agentCommand", () => {
|
||||
it("passes through telegram accountId when delivering", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store, undefined, undefined, { botToken: "t-1" });
|
||||
mockConfig(home, store, undefined, { botToken: "t-1" });
|
||||
const deps = {
|
||||
sendMessageWhatsApp: vi.fn(),
|
||||
sendMessageTelegram: vi
|
||||
|
||||
@@ -181,13 +181,13 @@ export async function agentCommand(
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const agentCfg = cfg.agent;
|
||||
const agentCfg = cfg.agents?.defaults;
|
||||
const sessionAgentId = resolveAgentIdFromSessionKey(opts.sessionKey?.trim());
|
||||
const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, sessionAgentId);
|
||||
const agentDir = resolveAgentDir(cfg, sessionAgentId);
|
||||
const workspace = await ensureAgentWorkspace({
|
||||
dir: workspaceDirRaw,
|
||||
ensureBootstrapFiles: !cfg.agent?.skipBootstrap,
|
||||
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
|
||||
});
|
||||
const workspaceDir = workspace.dir;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
|
||||
import {
|
||||
applyAgentBindings,
|
||||
applyAgentConfig,
|
||||
@@ -12,27 +12,32 @@ import {
|
||||
} from "./agents.js";
|
||||
|
||||
describe("agents helpers", () => {
|
||||
it("buildAgentSummaries includes default + routing agents", () => {
|
||||
it("buildAgentSummaries includes default + configured agents", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: { workspace: "/main-ws", model: { primary: "anthropic/claude" } },
|
||||
routing: {
|
||||
defaultAgentId: "work",
|
||||
agents: {
|
||||
work: {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/main-ws",
|
||||
model: { primary: "anthropic/claude" },
|
||||
},
|
||||
list: [
|
||||
{ id: "main" },
|
||||
{
|
||||
id: "work",
|
||||
default: true,
|
||||
name: "Work",
|
||||
workspace: "/work-ws",
|
||||
agentDir: "/state/agents/work/agent",
|
||||
model: "openai/gpt-4.1",
|
||||
},
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "work",
|
||||
match: { provider: "whatsapp", accountId: "biz" },
|
||||
},
|
||||
{ agentId: "main", match: { provider: "telegram" } },
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "work",
|
||||
match: { provider: "whatsapp", accountId: "biz" },
|
||||
},
|
||||
{ agentId: "main", match: { provider: "telegram" } },
|
||||
],
|
||||
};
|
||||
|
||||
const summaries = buildAgentSummaries(cfg);
|
||||
@@ -40,7 +45,7 @@ describe("agents helpers", () => {
|
||||
const work = summaries.find((summary) => summary.id === "work");
|
||||
|
||||
expect(main).toBeTruthy();
|
||||
expect(main?.workspace).toBe(path.resolve("/main-ws"));
|
||||
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(
|
||||
@@ -57,10 +62,8 @@ describe("agents helpers", () => {
|
||||
|
||||
it("applyAgentConfig merges updates", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
agents: {
|
||||
work: { workspace: "/old-ws", model: "anthropic/claude" },
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "work", workspace: "/old-ws", model: "anthropic/claude" }],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -71,7 +74,7 @@ describe("agents helpers", () => {
|
||||
agentDir: "/state/work/agent",
|
||||
});
|
||||
|
||||
const work = next.routing?.agents?.work;
|
||||
const work = next.agents?.list?.find((agent) => agent.id === "work");
|
||||
expect(work?.name).toBe("Work");
|
||||
expect(work?.workspace).toBe("/new-ws");
|
||||
expect(work?.agentDir).toBe("/state/work/agent");
|
||||
@@ -80,14 +83,12 @@ describe("agents helpers", () => {
|
||||
|
||||
it("applyAgentBindings skips duplicates and reports conflicts", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
bindings: [
|
||||
{
|
||||
agentId: "main",
|
||||
match: { provider: "whatsapp", accountId: "default" },
|
||||
},
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "main",
|
||||
match: { provider: "whatsapp", accountId: "default" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = applyAgentBindings(cfg, [
|
||||
@@ -108,32 +109,36 @@ describe("agents helpers", () => {
|
||||
expect(result.added).toHaveLength(1);
|
||||
expect(result.skipped).toHaveLength(1);
|
||||
expect(result.conflicts).toHaveLength(1);
|
||||
expect(result.config.routing?.bindings).toHaveLength(2);
|
||||
expect(result.config.bindings).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("pruneAgentConfig removes agent, bindings, and allowlist entries", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
defaultAgentId: "work",
|
||||
agents: {
|
||||
work: { workspace: "/work-ws" },
|
||||
home: { workspace: "/home-ws" },
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "work", match: { provider: "whatsapp" } },
|
||||
{ agentId: "home", match: { provider: "telegram" } },
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "work", default: true, workspace: "/work-ws" },
|
||||
{ id: "home", workspace: "/home-ws" },
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "work", match: { provider: "whatsapp" } },
|
||||
{ agentId: "home", match: { provider: "telegram" } },
|
||||
],
|
||||
tools: {
|
||||
agentToAgent: { enabled: true, allow: ["work", "home"] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = pruneAgentConfig(cfg, "work");
|
||||
expect(result.config.routing?.agents?.work).toBeUndefined();
|
||||
expect(result.config.routing?.agents?.home).toBeTruthy();
|
||||
expect(result.config.routing?.bindings).toHaveLength(1);
|
||||
expect(result.config.routing?.bindings?.[0]?.agentId).toBe("home");
|
||||
expect(result.config.routing?.agentToAgent?.allow).toEqual(["home"]);
|
||||
expect(result.config.routing?.defaultAgentId).toBe(DEFAULT_AGENT_ID);
|
||||
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"]);
|
||||
expect(result.removedBindings).toBe(1);
|
||||
expect(result.removedAllow).toBe(1);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { DEFAULT_IDENTITY_FILENAME } from "../agents/workspace.js";
|
||||
@@ -114,6 +114,10 @@ type AgentBinding = {
|
||||
};
|
||||
};
|
||||
|
||||
type AgentEntry = NonNullable<
|
||||
NonNullable<ClawdbotConfig["agents"]>["list"]
|
||||
>[number];
|
||||
|
||||
type AgentIdentity = {
|
||||
name?: string;
|
||||
emoji?: string;
|
||||
@@ -140,15 +144,32 @@ function createQuietRuntime(runtime: RuntimeEnv): RuntimeEnv {
|
||||
return { ...runtime, log: () => {} };
|
||||
}
|
||||
|
||||
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"),
|
||||
);
|
||||
}
|
||||
|
||||
function findAgentEntryIndex(list: AgentEntry[], agentId: string): number {
|
||||
const id = normalizeAgentId(agentId);
|
||||
return list.findIndex((entry) => normalizeAgentId(entry.id) === id);
|
||||
}
|
||||
|
||||
function resolveAgentName(cfg: ClawdbotConfig, agentId: string) {
|
||||
return cfg.routing?.agents?.[agentId]?.name?.trim() || undefined;
|
||||
const entry = listAgentEntries(cfg).find(
|
||||
(agent) => normalizeAgentId(agent.id) === normalizeAgentId(agentId),
|
||||
);
|
||||
return entry?.name?.trim() || undefined;
|
||||
}
|
||||
|
||||
function resolveAgentModel(cfg: ClawdbotConfig, agentId: string) {
|
||||
if (agentId !== DEFAULT_AGENT_ID) {
|
||||
return cfg.routing?.agents?.[agentId]?.model?.trim() || undefined;
|
||||
}
|
||||
const raw = cfg.agent?.model;
|
||||
const entry = listAgentEntries(cfg).find(
|
||||
(agent) => normalizeAgentId(agent.id) === normalizeAgentId(agentId),
|
||||
);
|
||||
if (entry?.model?.trim()) return entry.model.trim();
|
||||
const raw = cfg.agents?.defaults?.model;
|
||||
if (typeof raw === "string") return raw;
|
||||
return raw?.primary?.trim() || undefined;
|
||||
}
|
||||
@@ -183,37 +204,33 @@ function loadAgentIdentity(workspace: string): AgentIdentity | null {
|
||||
}
|
||||
|
||||
export function buildAgentSummaries(cfg: ClawdbotConfig): AgentSummary[] {
|
||||
const defaultAgentId = normalizeAgentId(
|
||||
cfg.routing?.defaultAgentId ?? DEFAULT_AGENT_ID,
|
||||
);
|
||||
const agentIds = new Set<string>([
|
||||
DEFAULT_AGENT_ID,
|
||||
defaultAgentId,
|
||||
...Object.keys(cfg.routing?.agents ?? {}),
|
||||
]);
|
||||
|
||||
const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg));
|
||||
const configuredAgents = listAgentEntries(cfg);
|
||||
const orderedIds =
|
||||
configuredAgents.length > 0
|
||||
? configuredAgents.map((agent) => normalizeAgentId(agent.id))
|
||||
: [defaultAgentId];
|
||||
const bindingCounts = new Map<string, number>();
|
||||
for (const binding of cfg.routing?.bindings ?? []) {
|
||||
for (const binding of cfg.bindings ?? []) {
|
||||
const agentId = normalizeAgentId(binding.agentId);
|
||||
bindingCounts.set(agentId, (bindingCounts.get(agentId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const ordered = [
|
||||
DEFAULT_AGENT_ID,
|
||||
...[...agentIds]
|
||||
.filter((id) => id !== DEFAULT_AGENT_ID)
|
||||
.sort((a, b) => a.localeCompare(b)),
|
||||
];
|
||||
const ordered = orderedIds.filter(
|
||||
(id, index) => orderedIds.indexOf(id) === index,
|
||||
);
|
||||
|
||||
return ordered.map((id) => {
|
||||
const workspace = resolveAgentWorkspaceDir(cfg, id);
|
||||
const identity = loadAgentIdentity(workspace);
|
||||
const fallbackIdentity = id === defaultAgentId ? cfg.identity : undefined;
|
||||
const identityName = identity?.name ?? fallbackIdentity?.name?.trim();
|
||||
const identityEmoji = identity?.emoji ?? fallbackIdentity?.emoji?.trim();
|
||||
const configIdentity = configuredAgents.find(
|
||||
(agent) => normalizeAgentId(agent.id) === id,
|
||||
)?.identity;
|
||||
const identityName = identity?.name ?? configIdentity?.name?.trim();
|
||||
const identityEmoji = identity?.emoji ?? configIdentity?.emoji?.trim();
|
||||
const identitySource = identity
|
||||
? "identity"
|
||||
: fallbackIdentity && (identityName || identityEmoji)
|
||||
: configIdentity && (identityName || identityEmoji)
|
||||
? "config"
|
||||
: undefined;
|
||||
return {
|
||||
@@ -242,22 +259,34 @@ export function applyAgentConfig(
|
||||
},
|
||||
): ClawdbotConfig {
|
||||
const agentId = normalizeAgentId(params.agentId);
|
||||
const existing = cfg.routing?.agents?.[agentId] ?? {};
|
||||
const name = params.name?.trim();
|
||||
const list = listAgentEntries(cfg);
|
||||
const index = findAgentEntryIndex(list, agentId);
|
||||
const base = index >= 0 ? list[index] : { id: agentId };
|
||||
const nextEntry: AgentEntry = {
|
||||
...base,
|
||||
...(name ? { name } : {}),
|
||||
...(params.workspace ? { workspace: params.workspace } : {}),
|
||||
...(params.agentDir ? { agentDir: params.agentDir } : {}),
|
||||
...(params.model ? { model: params.model } : {}),
|
||||
};
|
||||
const nextList = [...list];
|
||||
if (index >= 0) {
|
||||
nextList[index] = nextEntry;
|
||||
} else {
|
||||
if (
|
||||
nextList.length === 0 &&
|
||||
agentId !== normalizeAgentId(resolveDefaultAgentId(cfg))
|
||||
) {
|
||||
nextList.push({ id: resolveDefaultAgentId(cfg) });
|
||||
}
|
||||
nextList.push(nextEntry);
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
routing: {
|
||||
...cfg.routing,
|
||||
agents: {
|
||||
...cfg.routing?.agents,
|
||||
[agentId]: {
|
||||
...existing,
|
||||
...(name ? { name } : {}),
|
||||
...(params.workspace ? { workspace: params.workspace } : {}),
|
||||
...(params.agentDir ? { agentDir: params.agentDir } : {}),
|
||||
...(params.model ? { model: params.model } : {}),
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
list: nextList,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -283,7 +312,7 @@ export function applyAgentBindings(
|
||||
skipped: AgentBinding[];
|
||||
conflicts: Array<{ binding: AgentBinding; existingAgentId: string }>;
|
||||
} {
|
||||
const existing = cfg.routing?.bindings ?? [];
|
||||
const existing = cfg.bindings ?? [];
|
||||
const existingMatchMap = new Map<string, string>();
|
||||
for (const binding of existing) {
|
||||
const key = bindingMatchKey(binding.match);
|
||||
@@ -320,10 +349,7 @@ export function applyAgentBindings(
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
routing: {
|
||||
...cfg.routing,
|
||||
bindings: [...existing, ...added],
|
||||
},
|
||||
bindings: [...existing, ...added],
|
||||
},
|
||||
added,
|
||||
skipped,
|
||||
@@ -340,39 +366,41 @@ export function pruneAgentConfig(
|
||||
removedAllow: number;
|
||||
} {
|
||||
const id = normalizeAgentId(agentId);
|
||||
const agents = { ...cfg.routing?.agents };
|
||||
delete agents[id];
|
||||
const nextAgents = Object.keys(agents).length > 0 ? agents : undefined;
|
||||
const agents = listAgentEntries(cfg);
|
||||
const nextAgentsList = agents.filter(
|
||||
(entry) => normalizeAgentId(entry.id) !== id,
|
||||
);
|
||||
const nextAgents = nextAgentsList.length > 0 ? nextAgentsList : undefined;
|
||||
|
||||
const bindings = cfg.routing?.bindings ?? [];
|
||||
const bindings = cfg.bindings ?? [];
|
||||
const filteredBindings = bindings.filter(
|
||||
(binding) => normalizeAgentId(binding.agentId) !== id,
|
||||
);
|
||||
|
||||
const allow = cfg.routing?.agentToAgent?.allow ?? [];
|
||||
const allow = cfg.tools?.agentToAgent?.allow ?? [];
|
||||
const filteredAllow = allow.filter((entry) => entry !== id);
|
||||
|
||||
const nextRouting = {
|
||||
...cfg.routing,
|
||||
...(nextAgents ? { agents: nextAgents } : {}),
|
||||
...(nextAgents ? {} : { agents: undefined }),
|
||||
bindings: filteredBindings.length > 0 ? filteredBindings : undefined,
|
||||
agentToAgent: cfg.routing?.agentToAgent
|
||||
? {
|
||||
...cfg.routing.agentToAgent,
|
||||
const nextAgentsConfig = cfg.agents
|
||||
? { ...cfg.agents, list: nextAgents }
|
||||
: nextAgents
|
||||
? { list: nextAgents }
|
||||
: undefined;
|
||||
const nextTools = cfg.tools?.agentToAgent
|
||||
? {
|
||||
...cfg.tools,
|
||||
agentToAgent: {
|
||||
...cfg.tools.agentToAgent,
|
||||
allow: filteredAllow.length > 0 ? filteredAllow : undefined,
|
||||
}
|
||||
: undefined,
|
||||
defaultAgentId:
|
||||
normalizeAgentId(cfg.routing?.defaultAgentId ?? DEFAULT_AGENT_ID) === id
|
||||
? DEFAULT_AGENT_ID
|
||||
: cfg.routing?.defaultAgentId,
|
||||
};
|
||||
},
|
||||
}
|
||||
: cfg.tools;
|
||||
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
routing: nextRouting,
|
||||
agents: nextAgentsConfig,
|
||||
bindings: filteredBindings.length > 0 ? filteredBindings : undefined,
|
||||
tools: nextTools,
|
||||
},
|
||||
removedBindings: bindings.length - filteredBindings.length,
|
||||
removedAllow: allow.length - filteredAllow.length,
|
||||
@@ -632,7 +660,7 @@ export async function agentsListCommand(
|
||||
|
||||
const summaries = buildAgentSummaries(cfg);
|
||||
const bindingMap = new Map<string, AgentBinding[]>();
|
||||
for (const binding of cfg.routing?.bindings ?? []) {
|
||||
for (const binding of cfg.bindings ?? []) {
|
||||
const agentId = normalizeAgentId(binding.agentId);
|
||||
const list = bindingMap.get(agentId) ?? [];
|
||||
list.push(binding as AgentBinding);
|
||||
@@ -818,7 +846,7 @@ export async function agentsAddCommand(
|
||||
if (agentId !== nameInput) {
|
||||
runtime.log(`Normalized agent id to "${agentId}".`);
|
||||
}
|
||||
if (cfg.routing?.agents?.[agentId]) {
|
||||
if (findAgentEntryIndex(listAgentEntries(cfg), agentId) >= 0) {
|
||||
runtime.error(`Agent "${agentId}" already exists.`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
@@ -856,7 +884,9 @@ 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.agent?.skipBootstrap),
|
||||
skipBootstrap: Boolean(
|
||||
bindingResult.config.agents?.defaults?.skipBootstrap,
|
||||
),
|
||||
agentId,
|
||||
});
|
||||
|
||||
@@ -920,7 +950,9 @@ export async function agentsAddCommand(
|
||||
await prompter.note(`Normalized id to "${agentId}".`, "Agent id");
|
||||
}
|
||||
|
||||
const existingAgent = cfg.routing?.agents?.[agentId];
|
||||
const existingAgent = listAgentEntries(cfg).find(
|
||||
(agent) => normalizeAgentId(agent.id) === agentId,
|
||||
);
|
||||
if (existingAgent) {
|
||||
const shouldUpdate = await prompter.confirm({
|
||||
message: `Agent "${agentId}" already exists. Update it?`,
|
||||
@@ -1005,8 +1037,7 @@ export async function agentsAddCommand(
|
||||
|
||||
if (selection.length > 0) {
|
||||
const wantsBindings = await prompter.confirm({
|
||||
message:
|
||||
"Route selected providers to this agent now? (routing.bindings)",
|
||||
message: "Route selected providers to this agent now? (bindings)",
|
||||
initialValue: false,
|
||||
});
|
||||
if (wantsBindings) {
|
||||
@@ -1033,7 +1064,7 @@ export async function agentsAddCommand(
|
||||
} else {
|
||||
await prompter.note(
|
||||
[
|
||||
"Routing unchanged. Add routing.bindings when you're ready.",
|
||||
"Routing unchanged. Add bindings when you're ready.",
|
||||
"Docs: https://docs.clawd.bot/concepts/multi-agent",
|
||||
].join("\n"),
|
||||
"Routing",
|
||||
@@ -1044,7 +1075,7 @@ export async function agentsAddCommand(
|
||||
await writeConfigFile(nextConfig);
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap),
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
agentId,
|
||||
});
|
||||
|
||||
@@ -1091,7 +1122,7 @@ export async function agentsDeleteCommand(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cfg.routing?.agents?.[agentId]) {
|
||||
if (findAgentEntryIndex(listAgentEntries(cfg), agentId) < 0) {
|
||||
runtime.error(`Agent "${agentId}" not found.`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
|
||||
@@ -65,13 +65,16 @@ export async function warnIfModelConfigLooksOff(
|
||||
agentModelOverride && agentModelOverride.length > 0
|
||||
? {
|
||||
...config,
|
||||
agent: {
|
||||
...config.agent,
|
||||
model: {
|
||||
...(typeof config.agent?.model === "object"
|
||||
? config.agent.model
|
||||
: undefined),
|
||||
primary: agentModelOverride,
|
||||
agents: {
|
||||
...config.agents,
|
||||
defaults: {
|
||||
...config.agents?.defaults,
|
||||
model: {
|
||||
...(typeof config.agents?.defaults?.model === "object"
|
||||
? config.agents.defaults.model
|
||||
: undefined),
|
||||
primary: agentModelOverride,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -92,7 +95,7 @@ export async function warnIfModelConfigLooksOff(
|
||||
);
|
||||
if (!known) {
|
||||
warnings.push(
|
||||
`Model not found: ${ref.provider}/${ref.model}. Update agent.model or run /models list.`,
|
||||
`Model not found: ${ref.provider}/${ref.model}. Update agents.defaults.model or run /models list.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -111,7 +114,7 @@ export async function warnIfModelConfigLooksOff(
|
||||
const hasCodex = listProfilesForProvider(store, "openai-codex").length > 0;
|
||||
if (hasCodex) {
|
||||
warnings.push(
|
||||
`Detected OpenAI Codex OAuth. Consider setting agent.model to ${OPENAI_CODEX_DEFAULT_MODEL}.`,
|
||||
`Detected OpenAI Codex OAuth. Consider setting agents.defaults.model to ${OPENAI_CODEX_DEFAULT_MODEL}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -454,30 +457,36 @@ export async function applyAuthChoice(params: {
|
||||
const modelKey = "google-antigravity/claude-opus-4-5-thinking";
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agent: {
|
||||
...nextConfig.agent,
|
||||
models: {
|
||||
...nextConfig.agent?.models,
|
||||
[modelKey]: nextConfig.agent?.models?.[modelKey] ?? {},
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
models: {
|
||||
...nextConfig.agents?.defaults?.models,
|
||||
[modelKey]:
|
||||
nextConfig.agents?.defaults?.models?.[modelKey] ?? {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (params.setDefaultModel) {
|
||||
const existingModel = nextConfig.agents?.defaults?.model;
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agent: {
|
||||
...nextConfig.agent,
|
||||
model: {
|
||||
...(nextConfig.agent?.model &&
|
||||
"fallbacks" in
|
||||
(nextConfig.agent.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (
|
||||
nextConfig.agent.model as { fallbacks?: string[] }
|
||||
).fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: modelKey,
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel &&
|
||||
"fallbacks" in (existingModel as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: modelKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -625,26 +625,32 @@ async function promptAuthConfig(
|
||||
mode: "oauth",
|
||||
});
|
||||
// Set default model to Claude Opus 4.5 via Antigravity
|
||||
const existingDefaults = next.agents?.defaults;
|
||||
const existingModel = existingDefaults?.model;
|
||||
const existingModels = existingDefaults?.models;
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
model: {
|
||||
...(next.agent?.model &&
|
||||
"fallbacks" in (next.agent.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (next.agent.model as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: "google-antigravity/claude-opus-4-5-thinking",
|
||||
},
|
||||
models: {
|
||||
...next.agent?.models,
|
||||
"google-antigravity/claude-opus-4-5-thinking":
|
||||
next.agent?.models?.[
|
||||
"google-antigravity/claude-opus-4-5-thinking"
|
||||
] ?? {},
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...existingDefaults,
|
||||
model: {
|
||||
...(existingModel &&
|
||||
"fallbacks" in (existingModel as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: "google-antigravity/claude-opus-4-5-thinking",
|
||||
},
|
||||
models: {
|
||||
...existingModels,
|
||||
"google-antigravity/claude-opus-4-5-thinking":
|
||||
existingModels?.[
|
||||
"google-antigravity/claude-opus-4-5-thinking"
|
||||
] ?? {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -714,9 +720,9 @@ async function promptAuthConfig(
|
||||
}
|
||||
|
||||
const currentModel =
|
||||
typeof next.agent?.model === "string"
|
||||
? next.agent?.model
|
||||
: (next.agent?.model?.primary ?? "");
|
||||
typeof next.agents?.defaults?.model === "string"
|
||||
? next.agents?.defaults?.model
|
||||
: (next.agents?.defaults?.model?.primary ?? "");
|
||||
const preferAnthropic =
|
||||
authChoice === "claude-cli" ||
|
||||
authChoice === "token" ||
|
||||
@@ -736,23 +742,29 @@ async function promptAuthConfig(
|
||||
);
|
||||
const model = String(modelInput ?? "").trim();
|
||||
if (model) {
|
||||
const existingDefaults = next.agents?.defaults;
|
||||
const existingModel = existingDefaults?.model;
|
||||
const existingModels = existingDefaults?.models;
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
model: {
|
||||
...(next.agent?.model &&
|
||||
"fallbacks" in (next.agent.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (next.agent.model as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: model,
|
||||
},
|
||||
models: {
|
||||
...next.agent?.models,
|
||||
[model]: next.agent?.models?.[model] ?? {},
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...existingDefaults,
|
||||
model: {
|
||||
...(existingModel &&
|
||||
"fallbacks" in (existingModel as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: model,
|
||||
},
|
||||
models: {
|
||||
...existingModels,
|
||||
[model]: existingModels?.[model] ?? {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -955,7 +967,7 @@ export async function runConfigureWizard(
|
||||
{
|
||||
value: "workspace",
|
||||
label: "Workspace",
|
||||
hint: "Set agent workspace + ensure sessions",
|
||||
hint: "Set default workspace + ensure sessions",
|
||||
},
|
||||
{
|
||||
value: "model",
|
||||
@@ -999,8 +1011,8 @@ export async function runConfigureWizard(
|
||||
|
||||
let nextConfig = { ...baseConfig };
|
||||
let workspaceDir =
|
||||
nextConfig.agent?.workspace ??
|
||||
baseConfig.agent?.workspace ??
|
||||
nextConfig.agents?.defaults?.workspace ??
|
||||
baseConfig.agents?.defaults?.workspace ??
|
||||
DEFAULT_WORKSPACE;
|
||||
let gatewayPort = resolveGatewayPort(baseConfig);
|
||||
let gatewayToken: string | undefined;
|
||||
@@ -1018,9 +1030,12 @@ export async function runConfigureWizard(
|
||||
);
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agent: {
|
||||
...nextConfig.agent,
|
||||
workspace: workspaceDir,
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
},
|
||||
};
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime);
|
||||
|
||||
@@ -71,75 +71,184 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
||||
const changes: string[] = [];
|
||||
let next: ClawdbotConfig = cfg;
|
||||
|
||||
const workspace = cfg.agent?.workspace;
|
||||
const updatedWorkspace = normalizeDefaultWorkspacePath(workspace);
|
||||
if (updatedWorkspace && updatedWorkspace !== workspace) {
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
workspace: updatedWorkspace,
|
||||
},
|
||||
};
|
||||
changes.push(`Updated agent.workspace → ${updatedWorkspace}`);
|
||||
}
|
||||
const defaults = cfg.agents?.defaults;
|
||||
if (defaults) {
|
||||
let updatedDefaults = defaults;
|
||||
let defaultsChanged = false;
|
||||
|
||||
const workspaceRoot = cfg.agent?.sandbox?.workspaceRoot;
|
||||
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(workspaceRoot);
|
||||
if (updatedWorkspaceRoot && updatedWorkspaceRoot !== workspaceRoot) {
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
sandbox: {
|
||||
...next.agent?.sandbox,
|
||||
const updatedWorkspace = normalizeDefaultWorkspacePath(defaults.workspace);
|
||||
if (updatedWorkspace && updatedWorkspace !== defaults.workspace) {
|
||||
updatedDefaults = { ...updatedDefaults, workspace: updatedWorkspace };
|
||||
defaultsChanged = true;
|
||||
changes.push(`Updated agents.defaults.workspace → ${updatedWorkspace}`);
|
||||
}
|
||||
|
||||
const sandbox = defaults.sandbox;
|
||||
if (sandbox) {
|
||||
let updatedSandbox = sandbox;
|
||||
let sandboxChanged = false;
|
||||
|
||||
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(
|
||||
sandbox.workspaceRoot,
|
||||
);
|
||||
if (
|
||||
updatedWorkspaceRoot &&
|
||||
updatedWorkspaceRoot !== sandbox.workspaceRoot
|
||||
) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
workspaceRoot: updatedWorkspaceRoot,
|
||||
},
|
||||
},
|
||||
};
|
||||
changes.push(
|
||||
`Updated agent.sandbox.workspaceRoot → ${updatedWorkspaceRoot}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.workspaceRoot → ${updatedWorkspaceRoot}`,
|
||||
);
|
||||
}
|
||||
|
||||
const dockerImage = cfg.agent?.sandbox?.docker?.image;
|
||||
const updatedDockerImage = replaceLegacyName(dockerImage);
|
||||
if (updatedDockerImage && updatedDockerImage !== dockerImage) {
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
sandbox: {
|
||||
...next.agent?.sandbox,
|
||||
const dockerImage = sandbox.docker?.image;
|
||||
const updatedDockerImage = replaceLegacyName(dockerImage);
|
||||
if (updatedDockerImage && updatedDockerImage !== dockerImage) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
docker: {
|
||||
...next.agent?.sandbox?.docker,
|
||||
...updatedSandbox.docker,
|
||||
image: updatedDockerImage,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
changes.push(`Updated agent.sandbox.docker.image → ${updatedDockerImage}`);
|
||||
}
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.docker.image → ${updatedDockerImage}`,
|
||||
);
|
||||
}
|
||||
|
||||
const containerPrefix = cfg.agent?.sandbox?.docker?.containerPrefix;
|
||||
const updatedContainerPrefix = replaceLegacyName(containerPrefix);
|
||||
if (updatedContainerPrefix && updatedContainerPrefix !== containerPrefix) {
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
sandbox: {
|
||||
...next.agent?.sandbox,
|
||||
const containerPrefix = sandbox.docker?.containerPrefix;
|
||||
const updatedContainerPrefix = replaceLegacyName(containerPrefix);
|
||||
if (
|
||||
updatedContainerPrefix &&
|
||||
updatedContainerPrefix !== containerPrefix
|
||||
) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
docker: {
|
||||
...next.agent?.sandbox?.docker,
|
||||
...updatedSandbox.docker,
|
||||
containerPrefix: updatedContainerPrefix,
|
||||
},
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.docker.containerPrefix → ${updatedContainerPrefix}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (sandboxChanged) {
|
||||
updatedDefaults = { ...updatedDefaults, sandbox: updatedSandbox };
|
||||
defaultsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultsChanged) {
|
||||
next = {
|
||||
...next,
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: updatedDefaults,
|
||||
},
|
||||
},
|
||||
};
|
||||
changes.push(
|
||||
`Updated agent.sandbox.docker.containerPrefix → ${updatedContainerPrefix}`,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const list = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
||||
if (list.length > 0) {
|
||||
let listChanged = false;
|
||||
const nextList = list.map((agent) => {
|
||||
let updatedAgent = agent;
|
||||
let agentChanged = false;
|
||||
|
||||
const updatedWorkspace = normalizeDefaultWorkspacePath(agent.workspace);
|
||||
if (updatedWorkspace && updatedWorkspace !== agent.workspace) {
|
||||
updatedAgent = { ...updatedAgent, workspace: updatedWorkspace };
|
||||
agentChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.list (id "${agent.id}") workspace → ${updatedWorkspace}`,
|
||||
);
|
||||
}
|
||||
|
||||
const sandbox = agent.sandbox;
|
||||
if (sandbox) {
|
||||
let updatedSandbox = sandbox;
|
||||
let sandboxChanged = false;
|
||||
|
||||
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(
|
||||
sandbox.workspaceRoot,
|
||||
);
|
||||
if (
|
||||
updatedWorkspaceRoot &&
|
||||
updatedWorkspaceRoot !== sandbox.workspaceRoot
|
||||
) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
workspaceRoot: updatedWorkspaceRoot,
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.list (id "${agent.id}") sandbox.workspaceRoot → ${updatedWorkspaceRoot}`,
|
||||
);
|
||||
}
|
||||
|
||||
const dockerImage = sandbox.docker?.image;
|
||||
const updatedDockerImage = replaceLegacyName(dockerImage);
|
||||
if (updatedDockerImage && updatedDockerImage !== dockerImage) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
docker: {
|
||||
...updatedSandbox.docker,
|
||||
image: updatedDockerImage,
|
||||
},
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.list (id "${agent.id}") sandbox.docker.image → ${updatedDockerImage}`,
|
||||
);
|
||||
}
|
||||
|
||||
const containerPrefix = sandbox.docker?.containerPrefix;
|
||||
const updatedContainerPrefix = replaceLegacyName(containerPrefix);
|
||||
if (
|
||||
updatedContainerPrefix &&
|
||||
updatedContainerPrefix !== containerPrefix
|
||||
) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
docker: {
|
||||
...updatedSandbox.docker,
|
||||
containerPrefix: updatedContainerPrefix,
|
||||
},
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.list (id "${agent.id}") sandbox.docker.containerPrefix → ${updatedContainerPrefix}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (sandboxChanged) {
|
||||
updatedAgent = { ...updatedAgent, sandbox: updatedSandbox };
|
||||
agentChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (agentChanged) listChanged = true;
|
||||
return agentChanged ? updatedAgent : agent;
|
||||
});
|
||||
|
||||
if (listChanged) {
|
||||
next = {
|
||||
...next,
|
||||
agents: {
|
||||
...next.agents,
|
||||
list: nextList,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { config: next, changes };
|
||||
@@ -170,18 +279,40 @@ export async function maybeMigrateLegacyConfigFile(runtime: RuntimeEnv) {
|
||||
typeof (legacySnapshot.parsed as ClawdbotConfig)?.gateway?.bind === "string"
|
||||
? (legacySnapshot.parsed as ClawdbotConfig).gateway?.bind
|
||||
: undefined;
|
||||
const agentWorkspace =
|
||||
typeof (legacySnapshot.parsed as ClawdbotConfig)?.agent?.workspace ===
|
||||
"string"
|
||||
? (legacySnapshot.parsed as ClawdbotConfig).agent?.workspace
|
||||
const parsed = legacySnapshot.parsed as Record<string, unknown>;
|
||||
const parsedAgents =
|
||||
parsed.agents && typeof parsed.agents === "object"
|
||||
? (parsed.agents as Record<string, unknown>)
|
||||
: undefined;
|
||||
const parsedDefaults =
|
||||
parsedAgents?.defaults && typeof parsedAgents.defaults === "object"
|
||||
? (parsedAgents.defaults as Record<string, unknown>)
|
||||
: undefined;
|
||||
const parsedLegacyAgent =
|
||||
parsed.agent && typeof parsed.agent === "object"
|
||||
? (parsed.agent as Record<string, unknown>)
|
||||
: undefined;
|
||||
const defaultWorkspace =
|
||||
typeof parsedDefaults?.workspace === "string"
|
||||
? parsedDefaults.workspace
|
||||
: undefined;
|
||||
const legacyWorkspace =
|
||||
typeof parsedLegacyAgent?.workspace === "string"
|
||||
? parsedLegacyAgent.workspace
|
||||
: undefined;
|
||||
const agentWorkspace = defaultWorkspace ?? legacyWorkspace;
|
||||
const workspaceLabel = defaultWorkspace
|
||||
? "agents.defaults.workspace"
|
||||
: legacyWorkspace
|
||||
? "agent.workspace"
|
||||
: "agents.defaults.workspace";
|
||||
|
||||
note(
|
||||
[
|
||||
`- File exists at ${legacyConfigPath}`,
|
||||
gatewayMode ? `- gateway.mode: ${gatewayMode}` : undefined,
|
||||
gatewayBind ? `- gateway.bind: ${gatewayBind}` : undefined,
|
||||
agentWorkspace ? `- agent.workspace: ${agentWorkspace}` : undefined,
|
||||
agentWorkspace ? `- ${workspaceLabel}: ${agentWorkspace}` : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
|
||||
@@ -96,12 +96,12 @@ async function dockerImageExists(image: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
function resolveSandboxDockerImage(cfg: ClawdbotConfig): string {
|
||||
const image = cfg.agent?.sandbox?.docker?.image?.trim();
|
||||
const image = cfg.agents?.defaults?.sandbox?.docker?.image?.trim();
|
||||
return image ? image : DEFAULT_SANDBOX_IMAGE;
|
||||
}
|
||||
|
||||
function resolveSandboxBrowserImage(cfg: ClawdbotConfig): string {
|
||||
const image = cfg.agent?.sandbox?.browser?.image?.trim();
|
||||
const image = cfg.agents?.defaults?.sandbox?.browser?.image?.trim();
|
||||
return image ? image : DEFAULT_SANDBOX_BROWSER_IMAGE;
|
||||
}
|
||||
|
||||
@@ -111,13 +111,16 @@ function updateSandboxDockerImage(
|
||||
): ClawdbotConfig {
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
sandbox: {
|
||||
...cfg.agent?.sandbox,
|
||||
docker: {
|
||||
...cfg.agent?.sandbox?.docker,
|
||||
image,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
sandbox: {
|
||||
...cfg.agents?.defaults?.sandbox,
|
||||
docker: {
|
||||
...cfg.agents?.defaults?.sandbox?.docker,
|
||||
image,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -130,13 +133,16 @@ function updateSandboxBrowserImage(
|
||||
): ClawdbotConfig {
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
sandbox: {
|
||||
...cfg.agent?.sandbox,
|
||||
browser: {
|
||||
...cfg.agent?.sandbox?.browser,
|
||||
image,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
sandbox: {
|
||||
...cfg.agents?.defaults?.sandbox,
|
||||
browser: {
|
||||
...cfg.agents?.defaults?.sandbox?.browser,
|
||||
image,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -198,7 +204,7 @@ export async function maybeRepairSandboxImages(
|
||||
runtime: RuntimeEnv,
|
||||
prompter: DoctorPrompter,
|
||||
): Promise<ClawdbotConfig> {
|
||||
const sandbox = cfg.agent?.sandbox;
|
||||
const sandbox = cfg.agents?.defaults?.sandbox;
|
||||
const mode = sandbox?.mode ?? "off";
|
||||
if (!sandbox || mode === "off") return cfg;
|
||||
|
||||
@@ -224,7 +230,7 @@ export async function maybeRepairSandboxImages(
|
||||
: undefined,
|
||||
updateConfig: (image) => {
|
||||
next = updateSandboxDockerImage(next, image);
|
||||
changes.push(`Updated agent.sandbox.docker.image → ${image}`);
|
||||
changes.push(`Updated agents.defaults.sandbox.docker.image → ${image}`);
|
||||
},
|
||||
},
|
||||
runtime,
|
||||
@@ -239,7 +245,9 @@ export async function maybeRepairSandboxImages(
|
||||
buildScript: "scripts/sandbox-browser-setup.sh",
|
||||
updateConfig: (image) => {
|
||||
next = updateSandboxBrowserImage(next, image);
|
||||
changes.push(`Updated agent.sandbox.browser.image → ${image}`);
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.browser.image → ${image}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
runtime,
|
||||
@@ -255,11 +263,12 @@ export async function maybeRepairSandboxImages(
|
||||
}
|
||||
|
||||
export function noteSandboxScopeWarnings(cfg: ClawdbotConfig) {
|
||||
const globalSandbox = cfg.agent?.sandbox;
|
||||
const agents = cfg.routing?.agents ?? {};
|
||||
const globalSandbox = cfg.agents?.defaults?.sandbox;
|
||||
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
for (const [agentId, agent] of Object.entries(agents)) {
|
||||
for (const agent of agents) {
|
||||
const agentId = agent.id;
|
||||
const agentSandbox = agent.sandbox;
|
||||
if (!agentSandbox) continue;
|
||||
|
||||
@@ -284,7 +293,7 @@ export function noteSandboxScopeWarnings(cfg: ClawdbotConfig) {
|
||||
if (overrides.length === 0) continue;
|
||||
|
||||
warnings.push(
|
||||
`- routing.agents.${agentId}.sandbox: ${overrides.join(
|
||||
`- agents.list (id "${agentId}") sandbox ${overrides.join(
|
||||
"/",
|
||||
)} overrides ignored (scope resolves to "shared").`,
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
|
||||
import { note as clackNote } from "@clack/prompts";
|
||||
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
|
||||
import {
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
resolveSessionTranscriptsDirForAgent,
|
||||
resolveStorePath,
|
||||
} from "../config/sessions.js";
|
||||
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
|
||||
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
|
||||
const note = (message: string, title?: string) =>
|
||||
@@ -136,9 +136,7 @@ export async function noteStateIntegrity(
|
||||
const stateDir = resolveStateDir(env, homedir);
|
||||
const defaultStateDir = path.join(homedir(), ".clawdbot");
|
||||
const oauthDir = resolveOAuthDir(env, stateDir);
|
||||
const agentId = normalizeAgentId(
|
||||
cfg.routing?.defaultAgentId ?? DEFAULT_AGENT_ID,
|
||||
);
|
||||
const agentId = resolveDefaultAgentId(cfg);
|
||||
const sessionsDir = resolveSessionTranscriptsDirForAgent(
|
||||
agentId,
|
||||
env,
|
||||
|
||||
@@ -186,9 +186,11 @@ describe("doctor legacy state migrations", () => {
|
||||
expect(result.changes).toEqual([]);
|
||||
});
|
||||
|
||||
it("routes legacy state to routing.defaultAgentId", async () => {
|
||||
it("routes legacy state to the default agent entry", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cfg: ClawdbotConfig = { routing: { defaultAgentId: "alpha" } };
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { list: [{ id: "alpha", default: true }] },
|
||||
};
|
||||
const legacySessionsDir = path.join(root, "sessions");
|
||||
fs.mkdirSync(legacySessionsDir, { recursive: true });
|
||||
writeJson5(path.join(legacySessionsDir, "sessions.json"), {
|
||||
|
||||
@@ -344,13 +344,15 @@ describe("doctor", () => {
|
||||
raw: "{}",
|
||||
parsed: {
|
||||
gateway: { mode: "local", bind: "loopback" },
|
||||
agent: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox",
|
||||
containerPrefix: "clawdbot-sbx",
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox",
|
||||
containerPrefix: "clawdbot-sbx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -358,13 +360,15 @@ describe("doctor", () => {
|
||||
valid: true,
|
||||
config: {
|
||||
gateway: { mode: "local", bind: "loopback" },
|
||||
agent: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox",
|
||||
containerPrefix: "clawdbot-sbx",
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox",
|
||||
containerPrefix: "clawdbot-sbx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -411,13 +415,15 @@ describe("doctor", () => {
|
||||
migrateLegacyConfig.mockReturnValueOnce({
|
||||
config: {
|
||||
gateway: { mode: "local", bind: "loopback" },
|
||||
agent: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdis-sandbox",
|
||||
containerPrefix: "clawdis-sbx",
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdis-sandbox",
|
||||
containerPrefix: "clawdis-sbx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -438,11 +444,12 @@ describe("doctor", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const agent = written.agent as Record<string, unknown>;
|
||||
const sandbox = agent.sandbox 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>;
|
||||
const docker = sandbox.docker as Record<string, unknown>;
|
||||
|
||||
expect(agent.workspace).toBe("/Users/steipete/clawd");
|
||||
expect(defaults.workspace).toBe("/Users/steipete/clawd");
|
||||
expect(sandbox.workspaceRoot).toBe("/Users/steipete/clawd/sandboxes");
|
||||
expect(docker.image).toBe("clawdbot-sandbox");
|
||||
expect(docker.containerPrefix).toBe("clawdbot-sbx");
|
||||
@@ -456,15 +463,16 @@ describe("doctor", () => {
|
||||
parsed: {},
|
||||
valid: true,
|
||||
config: {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "shared",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "shared",
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
work: {
|
||||
list: [
|
||||
{
|
||||
id: "work",
|
||||
workspace: "~/clawd-work",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
@@ -474,7 +482,7 @@ describe("doctor", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
issues: [],
|
||||
@@ -497,7 +505,7 @@ describe("doctor", () => {
|
||||
([message, title]) =>
|
||||
title === "Sandbox" &&
|
||||
typeof message === "string" &&
|
||||
message.includes("routing.agents.work.sandbox") &&
|
||||
message.includes('agents.list (id "work") sandbox docker') &&
|
||||
message.includes('scope resolves to "shared"'),
|
||||
),
|
||||
).toBe(true);
|
||||
@@ -511,7 +519,7 @@ describe("doctor", () => {
|
||||
parsed: {},
|
||||
valid: true,
|
||||
config: {
|
||||
agent: { workspace: "/Users/steipete/clawd" },
|
||||
agents: { defaults: { workspace: "/Users/steipete/clawd" } },
|
||||
},
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
@@ -556,22 +564,26 @@ describe("doctor", () => {
|
||||
exists: true,
|
||||
raw: "{}",
|
||||
parsed: {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox-common:bookworm-slim",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox-common:bookworm-slim",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
config: {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox-common:bookworm-slim",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox-common:bookworm-slim",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -614,8 +626,9 @@ describe("doctor", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const agent = written.agent as Record<string, unknown>;
|
||||
const sandbox = agent.sandbox 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>;
|
||||
const docker = sandbox.docker as Record<string, unknown>;
|
||||
|
||||
expect(docker.image).toBe("clawdis-sandbox-common:bookworm-slim");
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
note as clackNote,
|
||||
outro as clackOutro,
|
||||
} from "@clack/prompts";
|
||||
import {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
@@ -25,7 +29,7 @@ import { collectProvidersStatusIssues } from "../infra/providers-status-issues.j
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
import { resolveUserPath, sleep } from "../utils.js";
|
||||
import { sleep } from "../utils.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
@@ -69,11 +73,7 @@ import {
|
||||
shouldSuggestMemorySystem,
|
||||
} from "./doctor-workspace.js";
|
||||
import { healthCommand } from "./health.js";
|
||||
import {
|
||||
applyWizardMetadata,
|
||||
DEFAULT_WORKSPACE,
|
||||
printWizardHeader,
|
||||
} from "./onboard-helpers.js";
|
||||
import { applyWizardMetadata, printWizardHeader } from "./onboard-helpers.js";
|
||||
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
||||
|
||||
const intro = (message: string) =>
|
||||
@@ -224,8 +224,9 @@ export async function doctorCommand(
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceDir = resolveUserPath(
|
||||
cfg.agent?.workspace ?? DEFAULT_WORKSPACE,
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
cfg,
|
||||
resolveDefaultAgentId(cfg),
|
||||
);
|
||||
const legacyWorkspace = detectLegacyWorkspaceDirs({ workspaceDir });
|
||||
if (legacyWorkspace.legacyDirs.length > 0) {
|
||||
@@ -415,8 +416,9 @@ export async function doctorCommand(
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
|
||||
if (options.workspaceSuggestions !== false) {
|
||||
const workspaceDir = resolveUserPath(
|
||||
cfg.agent?.workspace ?? DEFAULT_WORKSPACE,
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
cfg,
|
||||
resolveDefaultAgentId(cfg),
|
||||
);
|
||||
noteWorkspaceBackupTip(workspaceDir);
|
||||
if (await shouldSuggestMemorySystem(workspaceDir)) {
|
||||
|
||||
@@ -8,28 +8,28 @@ import {
|
||||
|
||||
describe("applyGoogleGeminiModelDefault", () => {
|
||||
it("sets gemini default when model is unset", () => {
|
||||
const cfg: ClawdbotConfig = { agent: {} };
|
||||
const cfg: ClawdbotConfig = { agents: { defaults: {} } };
|
||||
const applied = applyGoogleGeminiModelDefault(cfg);
|
||||
expect(applied.changed).toBe(true);
|
||||
expect(applied.next.agent?.model).toEqual({
|
||||
expect(applied.next.agents?.defaults?.model).toEqual({
|
||||
primary: GOOGLE_GEMINI_DEFAULT_MODEL,
|
||||
});
|
||||
});
|
||||
|
||||
it("overrides existing model", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: { model: "anthropic/claude-opus-4-5" },
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-5" } },
|
||||
};
|
||||
const applied = applyGoogleGeminiModelDefault(cfg);
|
||||
expect(applied.changed).toBe(true);
|
||||
expect(applied.next.agent?.model).toEqual({
|
||||
expect(applied.next.agents?.defaults?.model).toEqual({
|
||||
primary: GOOGLE_GEMINI_DEFAULT_MODEL,
|
||||
});
|
||||
});
|
||||
|
||||
it("no-ops when already gemini default", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: { model: GOOGLE_GEMINI_DEFAULT_MODEL },
|
||||
agents: { defaults: { model: GOOGLE_GEMINI_DEFAULT_MODEL } },
|
||||
};
|
||||
const applied = applyGoogleGeminiModelDefault(cfg);
|
||||
expect(applied.changed).toBe(false);
|
||||
|
||||
@@ -17,7 +17,7 @@ export function applyGoogleGeminiModelDefault(cfg: ClawdbotConfig): {
|
||||
next: ClawdbotConfig;
|
||||
changed: boolean;
|
||||
} {
|
||||
const current = resolvePrimaryModel(cfg.agent?.model)?.trim();
|
||||
const current = resolvePrimaryModel(cfg.agents?.defaults?.model)?.trim();
|
||||
if (current === GOOGLE_GEMINI_DEFAULT_MODEL) {
|
||||
return { next: cfg, changed: false };
|
||||
}
|
||||
@@ -25,12 +25,19 @@ export function applyGoogleGeminiModelDefault(cfg: ClawdbotConfig): {
|
||||
return {
|
||||
next: {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model:
|
||||
cfg.agent?.model && typeof cfg.agent.model === "object"
|
||||
? { ...cfg.agent.model, primary: GOOGLE_GEMINI_DEFAULT_MODEL }
|
||||
: { primary: GOOGLE_GEMINI_DEFAULT_MODEL },
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model:
|
||||
cfg.agents?.defaults?.model &&
|
||||
typeof cfg.agents.defaults.model === "object"
|
||||
? {
|
||||
...cfg.agents.defaults.model,
|
||||
primary: GOOGLE_GEMINI_DEFAULT_MODEL,
|
||||
}
|
||||
: { primary: GOOGLE_GEMINI_DEFAULT_MODEL },
|
||||
},
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
|
||||
@@ -57,7 +57,9 @@ function makeRuntime() {
|
||||
|
||||
describe("models list/status", () => {
|
||||
it("models status resolves z.ai alias to canonical zai", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const { modelsStatusCommand } = await import("./models/list.js");
|
||||
@@ -69,7 +71,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models status plain outputs canonical zai model", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const { modelsStatusCommand } = await import("./models/list.js");
|
||||
@@ -80,7 +84,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list outputs canonical zai key for configured z.ai model", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const model = {
|
||||
@@ -106,7 +112,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list plain outputs canonical zai key", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const model = {
|
||||
@@ -131,7 +139,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list provider filter normalizes z.ai alias", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const models = [
|
||||
@@ -171,7 +181,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list provider filter normalizes Z.AI alias casing", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const models = [
|
||||
@@ -211,7 +223,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list provider filter normalizes z-ai alias", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const models = [
|
||||
@@ -251,7 +265,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list marks auth as unavailable when ZAI key is missing", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const model = {
|
||||
|
||||
@@ -39,9 +39,11 @@ describe("models set + fallbacks", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(written.agent).toEqual({
|
||||
model: { primary: "zai/glm-4.7" },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
expect(written.agents).toEqual({
|
||||
defaults: {
|
||||
model: { primary: "zai/glm-4.7" },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,7 +54,7 @@ describe("models set + fallbacks", () => {
|
||||
raw: "{}",
|
||||
parsed: {},
|
||||
valid: true,
|
||||
config: { agent: { model: { fallbacks: [] } } },
|
||||
config: { agents: { defaults: { model: { fallbacks: [] } } } },
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
});
|
||||
@@ -67,9 +69,11 @@ describe("models set + fallbacks", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(written.agent).toEqual({
|
||||
model: { fallbacks: ["zai/glm-4.7"] },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
expect(written.agents).toEqual({
|
||||
defaults: {
|
||||
model: { fallbacks: ["zai/glm-4.7"] },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,9 +99,11 @@ describe("models set + fallbacks", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(written.agent).toEqual({
|
||||
model: { primary: "zai/glm-4.7" },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
expect(written.agents).toEqual({
|
||||
defaults: {
|
||||
model: { primary: "zai/glm-4.7" },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function modelsAliasesListCommand(
|
||||
) {
|
||||
ensureFlagCompatibility(opts);
|
||||
const cfg = loadConfig();
|
||||
const models = cfg.agent?.models ?? {};
|
||||
const models = cfg.agents?.defaults?.models ?? {};
|
||||
const aliases = Object.entries(models).reduce<Record<string, string>>(
|
||||
(acc, [modelKey, entry]) => {
|
||||
const alias = entry?.alias?.trim();
|
||||
@@ -53,7 +53,7 @@ export async function modelsAliasesAddCommand(
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg: loadConfig() });
|
||||
const _updated = await updateConfig((cfg) => {
|
||||
const modelKey = `${resolved.provider}/${resolved.model}`;
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
for (const [key, entry] of Object.entries(nextModels)) {
|
||||
const existing = entry?.alias?.trim();
|
||||
if (existing && existing === alias && key !== modelKey) {
|
||||
@@ -64,9 +64,12 @@ export async function modelsAliasesAddCommand(
|
||||
nextModels[modelKey] = { ...existing, alias };
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
models: nextModels,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models: nextModels,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -81,7 +84,7 @@ export async function modelsAliasesRemoveCommand(
|
||||
) {
|
||||
const alias = normalizeAlias(aliasRaw);
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
let found = false;
|
||||
for (const [key, entry] of Object.entries(nextModels)) {
|
||||
if (entry?.alias?.trim() === alias) {
|
||||
@@ -95,17 +98,22 @@ export async function modelsAliasesRemoveCommand(
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
models: nextModels,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models: nextModels,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
if (
|
||||
!updated.agent?.models ||
|
||||
Object.values(updated.agent.models).every((entry) => !entry?.alias?.trim())
|
||||
!updated.agents?.defaults?.models ||
|
||||
Object.values(updated.agents.defaults.models).every(
|
||||
(entry) => !entry?.alias?.trim(),
|
||||
)
|
||||
) {
|
||||
runtime.log("No aliases configured.");
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function modelsFallbacksListCommand(
|
||||
) {
|
||||
ensureFlagCompatibility(opts);
|
||||
const cfg = loadConfig();
|
||||
const fallbacks = cfg.agent?.model?.fallbacks ?? [];
|
||||
const fallbacks = cfg.agents?.defaults?.model?.fallbacks ?? [];
|
||||
|
||||
if (opts.json) {
|
||||
runtime.log(JSON.stringify({ fallbacks }, null, 2));
|
||||
@@ -44,13 +44,13 @@ export async function modelsFallbacksAddCommand(
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const targetKey = modelKey(resolved.provider, resolved.model);
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
if (!nextModels[targetKey]) nextModels[targetKey] = {};
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
const existing = cfg.agent?.model?.fallbacks ?? [];
|
||||
const existing = cfg.agents?.defaults?.model?.fallbacks ?? [];
|
||||
const existingKeys = existing
|
||||
.map((entry) =>
|
||||
resolveModelRefFromString({
|
||||
@@ -64,28 +64,31 @@ export async function modelsFallbacksAddCommand(
|
||||
|
||||
if (existingKeys.includes(targetKey)) return cfg;
|
||||
|
||||
const existingModel = cfg.agent?.model as
|
||||
const existingModel = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [...existing, targetKey],
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [...existing, targetKey],
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(
|
||||
`Fallbacks: ${(updated.agent?.model?.fallbacks ?? []).join(", ")}`,
|
||||
`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,7 +103,7 @@ export async function modelsFallbacksRemoveCommand(
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
const existing = cfg.agent?.model?.fallbacks ?? [];
|
||||
const existing = cfg.agents?.defaults?.model?.fallbacks ?? [];
|
||||
const filtered = existing.filter((entry) => {
|
||||
const resolvedEntry = resolveModelRefFromString({
|
||||
raw: String(entry ?? ""),
|
||||
@@ -118,19 +121,22 @@ export async function modelsFallbacksRemoveCommand(
|
||||
throw new Error(`Fallback not found: ${targetKey}`);
|
||||
}
|
||||
|
||||
const existingModel = cfg.agent?.model as
|
||||
const existingModel = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: filtered,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: filtered,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -138,24 +144,27 @@ export async function modelsFallbacksRemoveCommand(
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(
|
||||
`Fallbacks: ${(updated.agent?.model?.fallbacks ?? []).join(", ")}`,
|
||||
`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function modelsFallbacksClearCommand(runtime: RuntimeEnv) {
|
||||
await updateConfig((cfg) => {
|
||||
const existingModel = cfg.agent?.model as
|
||||
const existingModel = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [],
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function modelsImageFallbacksListCommand(
|
||||
) {
|
||||
ensureFlagCompatibility(opts);
|
||||
const cfg = loadConfig();
|
||||
const fallbacks = cfg.agent?.imageModel?.fallbacks ?? [];
|
||||
const fallbacks = cfg.agents?.defaults?.imageModel?.fallbacks ?? [];
|
||||
|
||||
if (opts.json) {
|
||||
runtime.log(JSON.stringify({ fallbacks }, null, 2));
|
||||
@@ -44,13 +44,13 @@ export async function modelsImageFallbacksAddCommand(
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const targetKey = modelKey(resolved.provider, resolved.model);
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
if (!nextModels[targetKey]) nextModels[targetKey] = {};
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
const existing = cfg.agent?.imageModel?.fallbacks ?? [];
|
||||
const existing = cfg.agents?.defaults?.imageModel?.fallbacks ?? [];
|
||||
const existingKeys = existing
|
||||
.map((entry) =>
|
||||
resolveModelRefFromString({
|
||||
@@ -64,28 +64,31 @@ export async function modelsImageFallbacksAddCommand(
|
||||
|
||||
if (existingKeys.includes(targetKey)) return cfg;
|
||||
|
||||
const existingModel = cfg.agent?.imageModel as
|
||||
const existingModel = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [...existing, targetKey],
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [...existing, targetKey],
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(
|
||||
`Image fallbacks: ${(updated.agent?.imageModel?.fallbacks ?? []).join(", ")}`,
|
||||
`Image fallbacks: ${(updated.agents?.defaults?.imageModel?.fallbacks ?? []).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,7 +103,7 @@ export async function modelsImageFallbacksRemoveCommand(
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
const existing = cfg.agent?.imageModel?.fallbacks ?? [];
|
||||
const existing = cfg.agents?.defaults?.imageModel?.fallbacks ?? [];
|
||||
const filtered = existing.filter((entry) => {
|
||||
const resolvedEntry = resolveModelRefFromString({
|
||||
raw: String(entry ?? ""),
|
||||
@@ -118,19 +121,22 @@ export async function modelsImageFallbacksRemoveCommand(
|
||||
throw new Error(`Image fallback not found: ${targetKey}`);
|
||||
}
|
||||
|
||||
const existingModel = cfg.agent?.imageModel as
|
||||
const existingModel = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: filtered,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: filtered,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -138,24 +144,27 @@ export async function modelsImageFallbacksRemoveCommand(
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(
|
||||
`Image fallbacks: ${(updated.agent?.imageModel?.fallbacks ?? []).join(", ")}`,
|
||||
`Image fallbacks: ${(updated.agents?.defaults?.imageModel?.fallbacks ?? []).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function modelsImageFallbacksClearCommand(runtime: RuntimeEnv) {
|
||||
await updateConfig((cfg) => {
|
||||
const existingModel = cfg.agent?.imageModel as
|
||||
const existingModel = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [],
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -63,9 +63,11 @@ const mocks = vi.hoisted(() => {
|
||||
.mockReturnValue(["OPENAI_API_KEY", "ANTHROPIC_OAUTH_TOKEN"]),
|
||||
shouldEnableShellEnvFallback: vi.fn().mockReturnValue(true),
|
||||
loadConfig: vi.fn().mockReturnValue({
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5", fallbacks: [] },
|
||||
models: { "anthropic/claude-opus-4-5": { alias: "Opus" } },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5", fallbacks: [] },
|
||||
models: { "anthropic/claude-opus-4-5": { alias: "Opus" } },
|
||||
},
|
||||
},
|
||||
models: { providers: {} },
|
||||
env: { shellEnv: { enabled: true } },
|
||||
|
||||
@@ -290,10 +290,10 @@ const resolveConfiguredEntries = (cfg: ClawdbotConfig) => {
|
||||
|
||||
addEntry(resolvedDefault, "default");
|
||||
|
||||
const modelConfig = cfg.agent?.model as
|
||||
const modelConfig = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
const imageModelConfig = cfg.agent?.imageModel as
|
||||
const imageModelConfig = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
const modelFallbacks =
|
||||
@@ -333,7 +333,7 @@ const resolveConfiguredEntries = (cfg: ClawdbotConfig) => {
|
||||
addEntry(resolved.ref, `img-fallback#${idx + 1}`);
|
||||
});
|
||||
|
||||
for (const key of Object.keys(cfg.agent?.models ?? {})) {
|
||||
for (const key of Object.keys(cfg.agents?.defaults?.models ?? {})) {
|
||||
const parsed = parseModelRef(String(key ?? ""), DEFAULT_PROVIDER);
|
||||
if (!parsed) continue;
|
||||
addEntry(parsed, "configured");
|
||||
@@ -623,11 +623,11 @@ export async function modelsStatusCommand(
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
|
||||
const modelConfig = cfg.agent?.model as
|
||||
const modelConfig = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| string
|
||||
| undefined;
|
||||
const imageConfig = cfg.agent?.imageModel as
|
||||
const imageConfig = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| string
|
||||
| undefined;
|
||||
@@ -645,14 +645,14 @@ export async function modelsStatusCommand(
|
||||
: (imageConfig?.primary?.trim() ?? "");
|
||||
const imageFallbacks =
|
||||
typeof imageConfig === "object" ? (imageConfig?.fallbacks ?? []) : [];
|
||||
const aliases = Object.entries(cfg.agent?.models ?? {}).reduce<
|
||||
const aliases = Object.entries(cfg.agents?.defaults?.models ?? {}).reduce<
|
||||
Record<string, string>
|
||||
>((acc, [key, entry]) => {
|
||||
const alias = entry?.alias?.trim();
|
||||
if (alias) acc[alias] = key;
|
||||
return acc;
|
||||
}, {});
|
||||
const allowed = Object.keys(cfg.agent?.models ?? {});
|
||||
const allowed = Object.keys(cfg.agents?.defaults?.models ?? {});
|
||||
|
||||
const agentDir = resolveClawdbotAgentDir();
|
||||
const store = ensureAuthProfileStore();
|
||||
|
||||
@@ -327,14 +327,14 @@ export async function modelsScanCommand(
|
||||
}
|
||||
|
||||
const _updated = await updateConfig((cfg) => {
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
for (const entry of selected) {
|
||||
if (!nextModels[entry]) nextModels[entry] = {};
|
||||
}
|
||||
for (const entry of selectedImages) {
|
||||
if (!nextModels[entry]) nextModels[entry] = {};
|
||||
}
|
||||
const existingImageModel = cfg.agent?.imageModel as
|
||||
const existingImageModel = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
const nextImageModel =
|
||||
@@ -346,12 +346,12 @@ export async function modelsScanCommand(
|
||||
fallbacks: selectedImages,
|
||||
...(opts.setImage ? { primary: selectedImages[0] } : {}),
|
||||
}
|
||||
: cfg.agent?.imageModel;
|
||||
const existingModel = cfg.agent?.model as
|
||||
: cfg.agents?.defaults?.imageModel;
|
||||
const existingModel = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
const agent = {
|
||||
...cfg.agent,
|
||||
const defaults = {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
@@ -361,10 +361,13 @@ export async function modelsScanCommand(
|
||||
},
|
||||
...(nextImageModel ? { imageModel: nextImageModel } : {}),
|
||||
models: nextModels,
|
||||
} satisfies NonNullable<typeof cfg.agent>;
|
||||
} satisfies NonNullable<NonNullable<typeof cfg.agents>["defaults"]>;
|
||||
return {
|
||||
...cfg,
|
||||
agent,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -9,26 +9,31 @@ export async function modelsSetImageCommand(
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const key = `${resolved.provider}/${resolved.model}`;
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
if (!nextModels[key]) nextModels[key] = {};
|
||||
const existingModel = cfg.agent?.imageModel as
|
||||
const existingModel = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
imageModel: {
|
||||
...(existingModel?.fallbacks
|
||||
? { fallbacks: existingModel.fallbacks }
|
||||
: undefined),
|
||||
primary: key,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.fallbacks
|
||||
? { fallbacks: existingModel.fallbacks }
|
||||
: undefined),
|
||||
primary: key,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(`Image model: ${updated.agent?.imageModel?.primary ?? modelRaw}`);
|
||||
runtime.log(
|
||||
`Image model: ${updated.agents?.defaults?.imageModel?.primary ?? modelRaw}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,26 +6,31 @@ export async function modelsSetCommand(modelRaw: string, runtime: RuntimeEnv) {
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const key = `${resolved.provider}/${resolved.model}`;
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
if (!nextModels[key]) nextModels[key] = {};
|
||||
const existingModel = cfg.agent?.model as
|
||||
const existingModel = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model: {
|
||||
...(existingModel?.fallbacks
|
||||
? { fallbacks: existingModel.fallbacks }
|
||||
: undefined),
|
||||
primary: key,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.fallbacks
|
||||
? { fallbacks: existingModel.fallbacks }
|
||||
: undefined),
|
||||
primary: key,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(`Default model: ${updated.agent?.model?.primary ?? modelRaw}`);
|
||||
runtime.log(
|
||||
`Default model: ${updated.agents?.defaults?.model?.primary ?? modelRaw}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export function resolveModelTarget(params: {
|
||||
|
||||
export function buildAllowlistSet(cfg: ClawdbotConfig): Set<string> {
|
||||
const allowed = new Set<string>();
|
||||
const models = cfg.agent?.models ?? {};
|
||||
const models = cfg.agents?.defaults?.models ?? {};
|
||||
for (const raw of Object.keys(models)) {
|
||||
const parsed = parseModelRef(String(raw ?? ""), DEFAULT_PROVIDER);
|
||||
if (!parsed) continue;
|
||||
|
||||
@@ -126,7 +126,7 @@ export function applyAuthProfileConfig(
|
||||
export function applyMinimaxProviderConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
): ClawdbotConfig {
|
||||
const models = { ...cfg.agent?.models };
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models["anthropic/claude-opus-4-5"] = {
|
||||
...models["anthropic/claude-opus-4-5"],
|
||||
alias: models["anthropic/claude-opus-4-5"]?.alias ?? "Opus",
|
||||
@@ -158,9 +158,12 @@ export function applyMinimaxProviderConfig(
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
models,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
@@ -224,17 +227,21 @@ export function applyMinimaxConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const next = applyMinimaxProviderConfig(cfg);
|
||||
return {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
model: {
|
||||
...(next.agent?.model &&
|
||||
"fallbacks" in (next.agent.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (next.agent.model as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: "lmstudio/minimax-m2.1-gs32",
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...next.agents?.defaults,
|
||||
model: {
|
||||
...(next.agents?.defaults?.model &&
|
||||
"fallbacks" in (next.agents.defaults.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (
|
||||
next.agents.defaults.model as { fallbacks?: string[] }
|
||||
).fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: "lmstudio/minimax-m2.1-gs32",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,13 +36,13 @@ export function guardCancel<T>(value: T, runtime: RuntimeEnv): T {
|
||||
|
||||
export function summarizeExistingConfig(config: ClawdbotConfig): string {
|
||||
const rows: string[] = [];
|
||||
if (config.agent?.workspace)
|
||||
rows.push(`workspace: ${config.agent.workspace}`);
|
||||
if (config.agent?.model) {
|
||||
const defaults = config.agents?.defaults;
|
||||
if (defaults?.workspace) rows.push(`workspace: ${defaults.workspace}`);
|
||||
if (defaults?.model) {
|
||||
const model =
|
||||
typeof config.agent.model === "string"
|
||||
? config.agent.model
|
||||
: config.agent.model.primary;
|
||||
typeof defaults.model === "string"
|
||||
? defaults.model
|
||||
: defaults.model.primary;
|
||||
if (model) rows.push(`model: ${model}`);
|
||||
}
|
||||
if (config.gateway?.mode) rows.push(`gateway.mode: ${config.gateway.mode}`);
|
||||
|
||||
@@ -96,14 +96,21 @@ export async function runNonInteractiveOnboarding(
|
||||
}
|
||||
|
||||
const workspaceDir = resolveUserPath(
|
||||
(opts.workspace ?? baseConfig.agent?.workspace ?? DEFAULT_WORKSPACE).trim(),
|
||||
(
|
||||
opts.workspace ??
|
||||
baseConfig.agents?.defaults?.workspace ??
|
||||
DEFAULT_WORKSPACE
|
||||
).trim(),
|
||||
);
|
||||
|
||||
let nextConfig: ClawdbotConfig = {
|
||||
...baseConfig,
|
||||
agent: {
|
||||
...baseConfig.agent,
|
||||
workspace: workspaceDir,
|
||||
agents: {
|
||||
...baseConfig.agents,
|
||||
defaults: {
|
||||
...baseConfig.agents?.defaults,
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
...baseConfig.gateway,
|
||||
@@ -311,7 +318,7 @@ export async function runNonInteractiveOnboarding(
|
||||
await writeConfigFile(nextConfig);
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap),
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
});
|
||||
|
||||
const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
|
||||
@@ -8,25 +8,29 @@ import {
|
||||
|
||||
describe("applyOpenAICodexModelDefault", () => {
|
||||
it("sets openai-codex default when model is unset", () => {
|
||||
const cfg: ClawdbotConfig = { agent: {} };
|
||||
const cfg: ClawdbotConfig = { agents: { defaults: {} } };
|
||||
const applied = applyOpenAICodexModelDefault(cfg);
|
||||
expect(applied.changed).toBe(true);
|
||||
expect(applied.next.agent?.model).toEqual({
|
||||
expect(applied.next.agents?.defaults?.model).toEqual({
|
||||
primary: OPENAI_CODEX_DEFAULT_MODEL,
|
||||
});
|
||||
});
|
||||
|
||||
it("sets openai-codex default when model is openai/*", () => {
|
||||
const cfg: ClawdbotConfig = { agent: { model: "openai/gpt-5.2" } };
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { defaults: { model: "openai/gpt-5.2" } },
|
||||
};
|
||||
const applied = applyOpenAICodexModelDefault(cfg);
|
||||
expect(applied.changed).toBe(true);
|
||||
expect(applied.next.agent?.model).toEqual({
|
||||
expect(applied.next.agents?.defaults?.model).toEqual({
|
||||
primary: OPENAI_CODEX_DEFAULT_MODEL,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not override openai-codex/*", () => {
|
||||
const cfg: ClawdbotConfig = { agent: { model: "openai-codex/gpt-5.2" } };
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { defaults: { model: "openai-codex/gpt-5.2" } },
|
||||
};
|
||||
const applied = applyOpenAICodexModelDefault(cfg);
|
||||
expect(applied.changed).toBe(false);
|
||||
expect(applied.next).toEqual(cfg);
|
||||
@@ -34,7 +38,7 @@ describe("applyOpenAICodexModelDefault", () => {
|
||||
|
||||
it("does not override non-openai models", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: { model: "anthropic/claude-opus-4-5" },
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-5" } },
|
||||
};
|
||||
const applied = applyOpenAICodexModelDefault(cfg);
|
||||
expect(applied.changed).toBe(false);
|
||||
|
||||
@@ -26,19 +26,26 @@ export function applyOpenAICodexModelDefault(cfg: ClawdbotConfig): {
|
||||
next: ClawdbotConfig;
|
||||
changed: boolean;
|
||||
} {
|
||||
const current = resolvePrimaryModel(cfg.agent?.model);
|
||||
const current = resolvePrimaryModel(cfg.agents?.defaults?.model);
|
||||
if (!shouldSetOpenAICodexModel(current)) {
|
||||
return { next: cfg, changed: false };
|
||||
}
|
||||
return {
|
||||
next: {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model:
|
||||
cfg.agent?.model && typeof cfg.agent.model === "object"
|
||||
? { ...cfg.agent.model, primary: OPENAI_CODEX_DEFAULT_MODEL }
|
||||
: { primary: OPENAI_CODEX_DEFAULT_MODEL },
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model:
|
||||
cfg.agents?.defaults?.model &&
|
||||
typeof cfg.agents.defaults.model === "object"
|
||||
? {
|
||||
...cfg.agents.defaults.model,
|
||||
primary: OPENAI_CODEX_DEFAULT_MODEL,
|
||||
}
|
||||
: { primary: OPENAI_CODEX_DEFAULT_MODEL },
|
||||
},
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
|
||||
@@ -12,10 +12,12 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () => ({
|
||||
agent: {
|
||||
model: { primary: "pi:opus" },
|
||||
models: { "pi:opus": {} },
|
||||
contextTokens: 32000,
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "pi:opus" },
|
||||
models: { "pi:opus": {} },
|
||||
contextTokens: 32000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -169,7 +169,7 @@ export async function sessionsCommand(
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
const configContextTokens =
|
||||
cfg.agent?.contextTokens ??
|
||||
cfg.agents?.defaults?.contextTokens ??
|
||||
lookupContextTokens(resolved.model) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
const configModel = resolved.model ?? DEFAULT_MODEL;
|
||||
|
||||
@@ -48,25 +48,28 @@ export async function setupCommand(
|
||||
|
||||
const existingRaw = await readConfigFileRaw();
|
||||
const cfg = existingRaw.parsed;
|
||||
const agent = cfg.agent ?? {};
|
||||
const defaults = cfg.agents?.defaults ?? {};
|
||||
|
||||
const workspace =
|
||||
desiredWorkspace ?? agent.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
desiredWorkspace ?? defaults.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
|
||||
const next: ClawdbotConfig = {
|
||||
...cfg,
|
||||
agent: {
|
||||
...agent,
|
||||
workspace,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...defaults,
|
||||
workspace,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!existingRaw.exists || agent.workspace !== workspace) {
|
||||
if (!existingRaw.exists || defaults.workspace !== workspace) {
|
||||
await writeConfigFile(next);
|
||||
runtime.log(
|
||||
!existingRaw.exists
|
||||
? `Wrote ${CONFIG_PATH_CLAWDBOT}`
|
||||
: `Updated ${CONFIG_PATH_CLAWDBOT} (set agent.workspace)`,
|
||||
: `Updated ${CONFIG_PATH_CLAWDBOT} (set agents.defaults.workspace)`,
|
||||
);
|
||||
} else {
|
||||
runtime.log(`Config OK: ${CONFIG_PATH_CLAWDBOT}`);
|
||||
@@ -74,7 +77,7 @@ export async function setupCommand(
|
||||
|
||||
const ws = await ensureAgentWorkspace({
|
||||
dir: workspace,
|
||||
ensureBootstrapFiles: !next.agent?.skipBootstrap,
|
||||
ensureBootstrapFiles: !next.agents?.defaults?.skipBootstrap,
|
||||
});
|
||||
runtime.log(`Workspace OK: ${ws.dir}`);
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function getStatusSummary(): Promise<StatusSummary> {
|
||||
});
|
||||
const configModel = resolved.model ?? DEFAULT_MODEL;
|
||||
const configContextTokens =
|
||||
cfg.agent?.contextTokens ??
|
||||
cfg.agents?.defaults?.contextTokens ??
|
||||
lookupContextTokens(configModel) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user