feat: add agent avatar support (#1329) (thanks @dlauer)

This commit is contained in:
Peter Steinberger
2026-01-22 03:54:31 +00:00
parent 7edc464b82
commit a2bea8e366
25 changed files with 547 additions and 84 deletions

View File

@@ -0,0 +1,110 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import type { ClawdbotConfig } from "../config/config.js";
import { resolveAgentAvatar } from "./identity-avatar.js";
async function writeFile(filePath: string, contents = "avatar") {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, contents, "utf-8");
}
describe("resolveAgentAvatar", () => {
it("resolves local avatar from config when inside workspace", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-avatar-"));
const workspace = path.join(root, "work");
const avatarPath = path.join(workspace, "avatars", "main.png");
await writeFile(avatarPath);
const cfg: ClawdbotConfig = {
agents: {
list: [
{
id: "main",
workspace,
identity: { avatar: "avatars/main.png" },
},
],
},
};
const expectedPath = await fs.realpath(avatarPath);
const resolved = resolveAgentAvatar(cfg, "main");
expect(resolved.kind).toBe("local");
if (resolved.kind === "local") {
expect(resolved.filePath).toBe(expectedPath);
}
});
it("rejects avatars outside the workspace", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-avatar-"));
const workspace = path.join(root, "work");
await fs.mkdir(workspace, { recursive: true });
const outsidePath = path.join(root, "outside.png");
await writeFile(outsidePath);
const cfg: ClawdbotConfig = {
agents: {
list: [
{
id: "main",
workspace,
identity: { avatar: outsidePath },
},
],
},
};
const resolved = resolveAgentAvatar(cfg, "main");
expect(resolved.kind).toBe("none");
if (resolved.kind === "none") {
expect(resolved.reason).toBe("outside_workspace");
}
});
it("falls back to IDENTITY.md when config has no avatar", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-avatar-"));
const workspace = path.join(root, "work");
const avatarPath = path.join(workspace, "avatars", "fallback.png");
await writeFile(avatarPath);
await fs.mkdir(workspace, { recursive: true });
await fs.writeFile(
path.join(workspace, "IDENTITY.md"),
"- Avatar: avatars/fallback.png\n",
"utf-8",
);
const cfg: ClawdbotConfig = {
agents: {
list: [{ id: "main", workspace }],
},
};
const expectedPath = await fs.realpath(avatarPath);
const resolved = resolveAgentAvatar(cfg, "main");
expect(resolved.kind).toBe("local");
if (resolved.kind === "local") {
expect(resolved.filePath).toBe(expectedPath);
}
});
it("accepts remote and data avatars", () => {
const cfg: ClawdbotConfig = {
agents: {
list: [
{ id: "main", identity: { avatar: "https://example.com/avatar.png" } },
{ id: "data", identity: { avatar: "data:image/png;base64,aaaa" } },
],
},
};
const remote = resolveAgentAvatar(cfg, "main");
expect(remote.kind).toBe("remote");
const data = resolveAgentAvatar(cfg, "data");
expect(data.kind).toBe("data");
});
});