import fs from "node:fs/promises"; import path from "node:path"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { DEFAULT_IDENTITY_FILENAME } from "../agents/workspace.js"; import { CONFIG_PATH_CLAWDBOT, writeConfigFile } from "../config/config.js"; import type { IdentityConfig } from "../config/types.js"; import { normalizeAgentId } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { resolveUserPath } from "../utils.js"; import { requireValidConfig } from "./agents.command-shared.js"; import { type AgentIdentity, findAgentEntryIndex, listAgentEntries, loadAgentIdentity, parseIdentityMarkdown, } from "./agents.config.js"; type AgentsSetIdentityOptions = { agent?: string; workspace?: string; identityFile?: string; name?: string; emoji?: string; theme?: string; fromIdentity?: boolean; json?: boolean; }; const normalizeWorkspacePath = (input: string) => path.resolve(resolveUserPath(input)); const coerceTrimmed = (value?: string) => { const trimmed = value?.trim(); return trimmed ? trimmed : undefined; }; async function loadIdentityFromFile(filePath: string): Promise { try { const content = await fs.readFile(filePath, "utf-8"); const parsed = parseIdentityMarkdown(content); if (!parsed.name && !parsed.emoji && !parsed.theme && !parsed.creature && !parsed.vibe) { return null; } return parsed; } catch { return null; } } function resolveAgentIdByWorkspace( cfg: Parameters[0], workspaceDir: string, ): string[] { const list = listAgentEntries(cfg); const ids = list.length > 0 ? list.map((entry) => normalizeAgentId(entry.id)) : [resolveDefaultAgentId(cfg)]; const normalizedTarget = normalizeWorkspacePath(workspaceDir); return ids.filter( (id) => normalizeWorkspacePath(resolveAgentWorkspaceDir(cfg, id)) === normalizedTarget, ); } export async function agentsSetIdentityCommand( opts: AgentsSetIdentityOptions, runtime: RuntimeEnv = defaultRuntime, ) { const cfg = await requireValidConfig(runtime); if (!cfg) return; const agentRaw = coerceTrimmed(opts.agent); const nameRaw = coerceTrimmed(opts.name); const emojiRaw = coerceTrimmed(opts.emoji); const themeRaw = coerceTrimmed(opts.theme); const hasExplicitIdentity = Boolean(nameRaw || emojiRaw || themeRaw); const identityFileRaw = coerceTrimmed(opts.identityFile); const workspaceRaw = coerceTrimmed(opts.workspace); const wantsIdentityFile = Boolean(opts.fromIdentity || identityFileRaw || !hasExplicitIdentity); let identityFilePath: string | undefined; let workspaceDir: string | undefined; if (identityFileRaw) { identityFilePath = normalizeWorkspacePath(identityFileRaw); workspaceDir = path.dirname(identityFilePath); } else if (workspaceRaw) { workspaceDir = normalizeWorkspacePath(workspaceRaw); } else if (wantsIdentityFile || !agentRaw) { workspaceDir = path.resolve(process.cwd()); } let agentId = agentRaw ? normalizeAgentId(agentRaw) : undefined; if (!agentId) { if (!workspaceDir) { runtime.error("Select an agent with --agent or provide a workspace via --workspace."); runtime.exit(1); return; } const matches = resolveAgentIdByWorkspace(cfg, workspaceDir); if (matches.length === 0) { runtime.error( `No agent workspace matches ${workspaceDir}. Pass --agent to target a specific agent.`, ); runtime.exit(1); return; } if (matches.length > 1) { runtime.error( `Multiple agents match ${workspaceDir}: ${matches.join(", ")}. Pass --agent to choose one.`, ); runtime.exit(1); return; } agentId = matches[0]; } let identityFromFile: AgentIdentity | null = null; if (wantsIdentityFile) { if (identityFilePath) { identityFromFile = await loadIdentityFromFile(identityFilePath); } else if (workspaceDir) { identityFromFile = loadAgentIdentity(workspaceDir); } if (!identityFromFile) { const targetPath = identityFilePath ?? (workspaceDir ? path.join(workspaceDir, DEFAULT_IDENTITY_FILENAME) : "IDENTITY.md"); runtime.error(`No identity data found in ${targetPath}.`); runtime.exit(1); return; } } const fileTheme = identityFromFile?.theme ?? identityFromFile?.creature ?? identityFromFile?.vibe ?? undefined; const incomingIdentity: IdentityConfig = { ...(nameRaw || identityFromFile?.name ? { name: nameRaw ?? identityFromFile?.name } : {}), ...(emojiRaw || identityFromFile?.emoji ? { emoji: emojiRaw ?? identityFromFile?.emoji } : {}), ...(themeRaw || fileTheme ? { theme: themeRaw ?? fileTheme } : {}), }; if (!incomingIdentity.name && !incomingIdentity.emoji && !incomingIdentity.theme) { runtime.error("No identity fields provided. Use --name/--emoji/--theme or --from-identity."); runtime.exit(1); return; } const list = listAgentEntries(cfg); const index = findAgentEntryIndex(list, agentId); const base = index >= 0 ? list[index] : { id: agentId }; const nextIdentity: IdentityConfig = { ...base.identity, ...incomingIdentity, }; const nextEntry = { ...base, identity: nextIdentity, }; const nextList = [...list]; if (index >= 0) { nextList[index] = nextEntry; } else { const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg)); if (nextList.length === 0 && agentId !== defaultId) { nextList.push({ id: defaultId }); } nextList.push(nextEntry); } const nextConfig = { ...cfg, agents: { ...cfg.agents, list: nextList, }, }; await writeConfigFile(nextConfig); if (opts.json) { runtime.log( JSON.stringify( { agentId, identity: nextIdentity, workspace: workspaceDir ?? null, identityFile: identityFilePath ?? null, }, null, 2, ), ); return; } runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); runtime.log(`Agent: ${agentId}`); if (nextIdentity.name) runtime.log(`Name: ${nextIdentity.name}`); if (nextIdentity.theme) runtime.log(`Theme: ${nextIdentity.theme}`); if (nextIdentity.emoji) runtime.log(`Emoji: ${nextIdentity.emoji}`); if (workspaceDir) runtime.log(`Workspace: ${workspaceDir}`); }