fix: add agents identity helper
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.clawd.bot
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- UI: enable shell mode for sync Windows spawns to avoid `pnpm ui:build` EINVAL. (#1212) — thanks @longmaba.
|
- UI: enable shell mode for sync Windows spawns to avoid `pnpm ui:build` EINVAL. (#1212) — thanks @longmaba.
|
||||||
|
- Agents: add `clawdbot agents set-identity` helper and update bootstrap guidance for multi-agent setups. (#1222) — thanks @ThePickle31.
|
||||||
- Plugins: surface plugin load/register/config errors in gateway logs with plugin/source context.
|
- Plugins: surface plugin load/register/config errors in gateway logs with plugin/source context.
|
||||||
- Agents: propagate accountId into embedded runs so sub-agent announce routing honors the originating account. (#1058)
|
- Agents: propagate accountId into embedded runs so sub-agent announce routing honors the originating account. (#1058)
|
||||||
- Compaction: include tool failure summaries in safeguard compaction to prevent retry loops. (#1084)
|
- Compaction: include tool failure summaries in safeguard compaction to prevent retry loops. (#1084)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
summary: "CLI reference for `clawdbot agents` (list/add/delete isolated agents)"
|
summary: "CLI reference for `clawdbot agents` (list/add/delete/set identity)"
|
||||||
read_when:
|
read_when:
|
||||||
- You want multiple isolated agents (workspaces + routing + auth)
|
- You want multiple isolated agents (workspaces + routing + auth)
|
||||||
---
|
---
|
||||||
@@ -17,6 +17,6 @@ Related:
|
|||||||
```bash
|
```bash
|
||||||
clawdbot agents list
|
clawdbot agents list
|
||||||
clawdbot agents add work --workspace ~/clawd-work
|
clawdbot agents add work --workspace ~/clawd-work
|
||||||
|
clawdbot agents set-identity --workspace ~/clawd --from-identity
|
||||||
clawdbot agents delete work
|
clawdbot agents delete work
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ After the user chooses, update:
|
|||||||
- Notes
|
- Notes
|
||||||
|
|
||||||
3) ~/.clawdbot/clawdbot.json
|
3) ~/.clawdbot/clawdbot.json
|
||||||
Set identity.name, identity.theme, identity.emoji to match IDENTITY.md.
|
Run: clawdbot agents set-identity --workspace "<this workspace>" --from-identity
|
||||||
|
If multiple agents share a host, add --agent <id>.
|
||||||
|
|
||||||
## Cleanup
|
## Cleanup
|
||||||
Delete BOOTSTRAP.md once this is complete.
|
Delete BOOTSTRAP.md once this is complete.
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
|
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
|
||||||
import { agentCliCommand } from "../../commands/agent-via-gateway.js";
|
import { agentCliCommand } from "../../commands/agent-via-gateway.js";
|
||||||
import { agentsAddCommand, agentsDeleteCommand, agentsListCommand } from "../../commands/agents.js";
|
import {
|
||||||
|
agentsAddCommand,
|
||||||
|
agentsDeleteCommand,
|
||||||
|
agentsListCommand,
|
||||||
|
agentsSetIdentityCommand,
|
||||||
|
} from "../../commands/agents.js";
|
||||||
import { setVerbose } from "../../globals.js";
|
import { setVerbose } from "../../globals.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
import { formatDocsLink } from "../../terminal/links.js";
|
import { formatDocsLink } from "../../terminal/links.js";
|
||||||
@@ -120,6 +125,45 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
agents
|
||||||
|
.command("set-identity")
|
||||||
|
.description("Update an agent identity (name/theme/emoji)")
|
||||||
|
.option("--agent <id>", "Agent id to update")
|
||||||
|
.option("--workspace <dir>", "Workspace directory used to locate the agent + IDENTITY.md")
|
||||||
|
.option("--identity-file <path>", "Explicit IDENTITY.md path to read")
|
||||||
|
.option("--from-identity", "Read values from IDENTITY.md", false)
|
||||||
|
.option("--name <name>", "Identity name")
|
||||||
|
.option("--theme <theme>", "Identity theme")
|
||||||
|
.option("--emoji <emoji>", "Identity emoji")
|
||||||
|
.option("--json", "Output JSON summary", false)
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
() =>
|
||||||
|
`
|
||||||
|
Examples:
|
||||||
|
clawdbot agents set-identity --agent main --name "Clawd" --emoji "🦞"
|
||||||
|
clawdbot agents set-identity --workspace ~/clawd --from-identity
|
||||||
|
clawdbot agents set-identity --identity-file ~/clawd/IDENTITY.md --agent main
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||||
|
await agentsSetIdentityCommand(
|
||||||
|
{
|
||||||
|
agent: opts.agent as string | undefined,
|
||||||
|
workspace: opts.workspace as string | undefined,
|
||||||
|
identityFile: opts.identityFile as string | undefined,
|
||||||
|
fromIdentity: Boolean(opts.fromIdentity),
|
||||||
|
name: opts.name as string | undefined,
|
||||||
|
theme: opts.theme as string | undefined,
|
||||||
|
emoji: opts.emoji as string | undefined,
|
||||||
|
json: Boolean(opts.json),
|
||||||
|
},
|
||||||
|
defaultRuntime,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
agents
|
agents
|
||||||
.command("delete <id>")
|
.command("delete <id>")
|
||||||
.description("Delete an agent and prune workspace/state")
|
.description("Delete an agent and prune workspace/state")
|
||||||
|
|||||||
209
src/commands/agents.commands.identity.ts
Normal file
209
src/commands/agents.commands.identity.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
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<AgentIdentity | null> {
|
||||||
|
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<typeof resolveAgentWorkspaceDir>[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}`);
|
||||||
|
}
|
||||||
@@ -28,11 +28,12 @@ export type AgentSummary = {
|
|||||||
|
|
||||||
type AgentEntry = NonNullable<NonNullable<ClawdbotConfig["agents"]>["list"]>[number];
|
type AgentEntry = NonNullable<NonNullable<ClawdbotConfig["agents"]>["list"]>[number];
|
||||||
|
|
||||||
type AgentIdentity = {
|
export type AgentIdentity = {
|
||||||
name?: string;
|
name?: string;
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
creature?: string;
|
creature?: string;
|
||||||
vibe?: string;
|
vibe?: string;
|
||||||
|
theme?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function listAgentEntries(cfg: ClawdbotConfig): AgentEntry[] {
|
export function listAgentEntries(cfg: ClawdbotConfig): AgentEntry[] {
|
||||||
@@ -71,29 +72,40 @@ function resolveAgentModel(cfg: ClawdbotConfig, agentId: string) {
|
|||||||
return raw?.primary?.trim() || undefined;
|
return raw?.primary?.trim() || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseIdentityMarkdown(content: string): AgentIdentity {
|
export function parseIdentityMarkdown(content: string): AgentIdentity {
|
||||||
const identity: AgentIdentity = {};
|
const identity: AgentIdentity = {};
|
||||||
const lines = content.split(/\r?\n/);
|
const lines = content.split(/\r?\n/);
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const match = line.match(/^\s*(?:-\s*)?([A-Za-z ]+):\s*(.+?)\s*$/);
|
const cleaned = line.trim().replace(/^\s*-\s*/, "");
|
||||||
if (!match) continue;
|
const colonIndex = cleaned.indexOf(":");
|
||||||
const label = match[1]?.trim().toLowerCase();
|
if (colonIndex === -1) continue;
|
||||||
const value = match[2]?.trim();
|
const label = cleaned
|
||||||
|
.slice(0, colonIndex)
|
||||||
|
.replace(/[*_]/g, "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
const value = cleaned
|
||||||
|
.slice(colonIndex + 1)
|
||||||
|
.replace(/^[*_]+|[*_]+$/g, "")
|
||||||
|
.trim();
|
||||||
if (!value) continue;
|
if (!value) continue;
|
||||||
if (label === "name") identity.name = value;
|
if (label === "name") identity.name = value;
|
||||||
if (label === "emoji") identity.emoji = value;
|
if (label === "emoji") identity.emoji = value;
|
||||||
if (label === "creature") identity.creature = value;
|
if (label === "creature") identity.creature = value;
|
||||||
if (label === "vibe") identity.vibe = value;
|
if (label === "vibe") identity.vibe = value;
|
||||||
|
if (label === "theme") identity.theme = value;
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadAgentIdentity(workspace: string): AgentIdentity | null {
|
export function loadAgentIdentity(workspace: string): AgentIdentity | null {
|
||||||
const identityPath = path.join(workspace, DEFAULT_IDENTITY_FILENAME);
|
const identityPath = path.join(workspace, DEFAULT_IDENTITY_FILENAME);
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(identityPath, "utf-8");
|
const content = fs.readFileSync(identityPath, "utf-8");
|
||||||
const parsed = parseIdentityMarkdown(content);
|
const parsed = parseIdentityMarkdown(content);
|
||||||
if (!parsed.name && !parsed.emoji) return null;
|
if (!parsed.name && !parsed.emoji && !parsed.theme && !parsed.creature && !parsed.vibe) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
185
src/commands/agents.identity.test.ts
Normal file
185
src/commands/agents.identity.test.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
|
const configMocks = vi.hoisted(() => ({
|
||||||
|
readConfigFileSnapshot: vi.fn(),
|
||||||
|
writeConfigFile: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../config/config.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
readConfigFileSnapshot: configMocks.readConfigFileSnapshot,
|
||||||
|
writeConfigFile: configMocks.writeConfigFile,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { agentsSetIdentityCommand } from "./agents.js";
|
||||||
|
|
||||||
|
const runtime: RuntimeEnv = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseSnapshot = {
|
||||||
|
path: "/tmp/clawdbot.json",
|
||||||
|
exists: true,
|
||||||
|
raw: "{}",
|
||||||
|
parsed: {},
|
||||||
|
valid: true,
|
||||||
|
config: {},
|
||||||
|
issues: [],
|
||||||
|
legacyIssues: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("agents set-identity command", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
configMocks.readConfigFileSnapshot.mockReset();
|
||||||
|
configMocks.writeConfigFile.mockClear();
|
||||||
|
runtime.log.mockClear();
|
||||||
|
runtime.error.mockClear();
|
||||||
|
runtime.exit.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets identity from workspace IDENTITY.md", async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-"));
|
||||||
|
const workspace = path.join(root, "work");
|
||||||
|
await fs.mkdir(workspace, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(workspace, "IDENTITY.md"),
|
||||||
|
["- Name: Clawd", "- Creature: helpful sloth", "- Emoji: :)", ""].join("\n"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||||
|
...baseSnapshot,
|
||||||
|
config: {
|
||||||
|
agents: {
|
||||||
|
list: [
|
||||||
|
{ id: "main", workspace },
|
||||||
|
{ id: "ops", workspace: path.join(root, "ops") },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentsSetIdentityCommand({ workspace }, runtime);
|
||||||
|
|
||||||
|
expect(configMocks.writeConfigFile).toHaveBeenCalledTimes(1);
|
||||||
|
const written = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||||
|
agents?: { list?: Array<{ id: string; identity?: Record<string, string> }> };
|
||||||
|
};
|
||||||
|
const main = written.agents?.list?.find((entry) => entry.id === "main");
|
||||||
|
expect(main?.identity).toEqual({
|
||||||
|
name: "Clawd",
|
||||||
|
theme: "helpful sloth",
|
||||||
|
emoji: ":)",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when multiple agents match the same workspace", async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-"));
|
||||||
|
const workspace = path.join(root, "shared");
|
||||||
|
await fs.mkdir(workspace, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(workspace, "IDENTITY.md"), "- Name: Echo\n", "utf-8");
|
||||||
|
|
||||||
|
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||||
|
...baseSnapshot,
|
||||||
|
config: {
|
||||||
|
agents: { list: [{ id: "main", workspace }, { id: "ops", workspace }] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentsSetIdentityCommand({ workspace }, runtime);
|
||||||
|
|
||||||
|
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("Multiple agents match"));
|
||||||
|
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||||
|
expect(configMocks.writeConfigFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overrides identity file values with explicit flags", async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-"));
|
||||||
|
const workspace = path.join(root, "work");
|
||||||
|
await fs.mkdir(workspace, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(workspace, "IDENTITY.md"),
|
||||||
|
["- Name: Clawd", "- Theme: space lobster", "- Emoji: :)", ""].join("\n"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||||
|
...baseSnapshot,
|
||||||
|
config: { agents: { list: [{ id: "main", workspace }] } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentsSetIdentityCommand(
|
||||||
|
{ workspace, fromIdentity: true, name: "Nova", emoji: "🦞" },
|
||||||
|
runtime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const written = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||||
|
agents?: { list?: Array<{ id: string; identity?: Record<string, string> }> };
|
||||||
|
};
|
||||||
|
const main = written.agents?.list?.find((entry) => entry.id === "main");
|
||||||
|
expect(main?.identity).toEqual({
|
||||||
|
name: "Nova",
|
||||||
|
theme: "space lobster",
|
||||||
|
emoji: "🦞",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads identity from an explicit IDENTITY.md path", async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-"));
|
||||||
|
const workspace = path.join(root, "work");
|
||||||
|
const identityPath = path.join(workspace, "IDENTITY.md");
|
||||||
|
await fs.mkdir(workspace, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
identityPath,
|
||||||
|
["- **Name:** C-3PO", "- **Creature:** Flustered Protocol Droid", "- **Emoji:** 🤖", ""].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||||
|
...baseSnapshot,
|
||||||
|
config: { agents: { list: [{ id: "main" }] } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentsSetIdentityCommand({ agent: "main", identityFile: identityPath }, runtime);
|
||||||
|
|
||||||
|
const written = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
|
||||||
|
agents?: { list?: Array<{ id: string; identity?: Record<string, string> }> };
|
||||||
|
};
|
||||||
|
const main = written.agents?.list?.find((entry) => entry.id === "main");
|
||||||
|
expect(main?.identity).toEqual({
|
||||||
|
name: "C-3PO",
|
||||||
|
theme: "Flustered Protocol Droid",
|
||||||
|
emoji: "🤖",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when identity data is missing", async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-"));
|
||||||
|
const workspace = path.join(root, "work");
|
||||||
|
await fs.mkdir(workspace, { recursive: true });
|
||||||
|
|
||||||
|
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||||
|
...baseSnapshot,
|
||||||
|
config: { agents: { list: [{ id: "main", workspace }] } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentsSetIdentityCommand({ workspace, fromIdentity: true }, runtime);
|
||||||
|
|
||||||
|
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("No identity data found"));
|
||||||
|
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||||
|
expect(configMocks.writeConfigFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./agents.bindings.js";
|
export * from "./agents.bindings.js";
|
||||||
export * from "./agents.commands.add.js";
|
export * from "./agents.commands.add.js";
|
||||||
export * from "./agents.commands.delete.js";
|
export * from "./agents.commands.delete.js";
|
||||||
|
export * from "./agents.commands.identity.js";
|
||||||
export * from "./agents.commands.list.js";
|
export * from "./agents.commands.list.js";
|
||||||
export * from "./agents.config.js";
|
export * from "./agents.config.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user