diff --git a/packages/cli/src/commands/agent.ts b/packages/cli/src/commands/agent.ts deleted file mode 100644 index b3ac00ed07f..00000000000 --- a/packages/cli/src/commands/agent.ts +++ /dev/null @@ -1,379 +0,0 @@ -// src/commands/agent.ts -import { MessageExampleSchema } from "@elizaos/core" -import prompts from "prompts" -import { z } from "zod" - -const agentSchema = z.object({ - id: z.string().uuid(), - name: z.string(), - username: z.string(), - description: z.string().optional(), - settings: z.record(z.string(), z.any()).optional(), - plugins: z.array(z.string()).optional(), - secrets: z.record(z.string(), z.string()).optional(), - bio: z.array(z.string()).optional(), - lore: z.array(z.string()).optional(), - adjectives: z.array(z.string()).optional(), - postExamples: z.array(z.string()).optional(), - messageExamples: z.array(z.array(MessageExampleSchema)).optional(), - topics: z.array(z.string()).optional(), - style: z.object({ - all: z.array(z.string()).optional(), - chat: z.array(z.string()).optional(), - post: z.array(z.string()).optional(), - }).optional(), -}) - -type AgentFormData = { - name: string; - bio: string[]; - lore: string[]; - adjectives: string[]; - postExamples: z.infer[]; - messageExamples: z.infer[][]; -} - -async function collectAgentData( - initialData?: Partial -): Promise { - const formData: Partial = { ...initialData }; - let currentStep = 0; - const steps = ['name', 'bio', 'lore', 'adjectives', 'postExamples', 'messageExamples']; - - while (currentStep < steps.length) { - const field = steps[currentStep]; - let response; - - switch (field) { - case 'name': - response = await prompts({ - type: 'text', - name: 'value', - message: 'Enter agent name:', - initial: formData.name, - }); - break; - - case 'bio': - case 'lore': - case 'postExamples': - case 'messageExamples': - response = await prompts({ - type: 'text', - name: 'value', - message: `Enter ${field} (use \\n for new lines):`, - initial: formData[field]?.join('\\n'), - }); - break; - - case 'adjectives': - response = await prompts({ - type: 'text', - name: 'value', - message: 'Enter adjectives (comma separated):', - initial: formData.adjectives?.join(', '), - }); - break; - } - - if (!response.value) { - return null; - } - - // Navigation commands - if (response.value === 'back') { - currentStep = Math.max(0, currentStep - 1); - continue; - } - if (response.value === 'forward') { - currentStep++; - continue; - } - - // Process and store the response - switch (field) { - case 'name': - formData.name = response.value; - break; - - case 'bio': - case 'lore': - case 'postExamples': - formData[field] = response.value - .split('\\n') - .map(line => line.trim()) - .filter(Boolean); - break; - - case 'messageExamples': - const examples = response.value - .split('\\n') - .map(line => line.trim()) - .filter(Boolean); - formData.messageExamples = examples.length > 0 ? examples : [`{{user1}}: hey how are you?\n${formData.name}`]; - break; - - case 'adjectives': - formData.adjectives = response.value - .split(',') - .map(adj => adj.trim()) - .filter(Boolean); - break; - } - - currentStep++; - } - - return formData as AgentFormData; -} - -// export const agent = new Command() -// .name("agent") -// .description("manage agents") - -// agent -// .command("list") -// .description("list all agents") -// .action(async () => { -// try { -// const cwd = process.cwd() -// const config = await getConfig(cwd) -// if (!config) { -// logger.error("No project.json found. Please run init first.") -// process.exit(1) -// } - -// const db = new Database((config.database.config as { path: string }).path) -// const adapter = new SqliteDatabaseAdapter(db) -// await adapter.init() - -// const agents = await adapter.listAgents() - -// if (agents.length === 0) { -// logger.info("No agents found") -// } else { -// logger.info("\nAgents:") -// for (const agent of agents) { -// logger.info(` ${agent.name} (${agent.id})`) -// } -// } - -// await adapter.close() -// } catch (error) { -// handleError(error) -// } -// }) - -// agent -// .command("create") -// .description("create a new agent") -// .action(async () => { -// try { -// const cwd = process.cwd() -// const config = await getConfig(cwd) -// if (!config) { -// logger.error("No project.json found. Please run init first.") -// process.exit(1) -// } - -// logger.info("\nCreating new agent (type 'back' or 'forward' to navigate)") - -// const formData = await collectAgentData() -// if (!formData) { -// logger.info("Agent creation cancelled") -// return -// } - -// const db = new Database((config.database.config as { path: string }).path) -// const adapter = new SqliteDatabaseAdapter(db) -// await adapter.init() - -// const agentData = { -// id: uuid() as UUID, -// name: formData.name, -// username: formData.name.toLowerCase().replace(/\s+/g, '_'), -// bio: formData.bio, -// lore: formData.lore, -// adjectives: formData.adjectives, -// postExamples: formData.postExamples, -// messageExamples: formData.messageExamples, -// topics: [], -// style: { // TODO: add style -// all: [], -// chat: [], -// post: [], -// }, -// plugins: [], -// settings: {}, -// } - -// await adapter.createAgent(agentData as any) - -// logger.success(`Created agent ${formData.name} (${agentData.id})`) -// await adapter.close() -// } catch (error) { -// handleError(error) -// } -// }) - -// agent -// .command("edit") -// .description("edit an agent") -// .argument("", "agent ID") -// .action(async (agentId) => { -// try { -// const cwd = process.cwd() -// const config = await getConfig(cwd) -// if (!config) { -// logger.error("No project.json found. Please run init first.") -// process.exit(1) -// } - -// const db = new Database((config.database.config as { path: string }).path) -// const adapter = new SqliteDatabaseAdapter(db) -// await adapter.init() - -// const existingAgent = await adapter.getAgent(agentId) -// if (!existingAgent) { -// logger.error(`Agent ${agentId} not found`) -// process.exit(1) -// } - -// logger.info(`\nEditing agent ${existingAgent.name} (type 'back' or 'forward' to navigate)`) - -// const formData = await collectAgentData({ -// name: existingAgent.name, -// bio: Array.isArray(existingAgent.bio) ? existingAgent.bio : [existingAgent.bio], -// lore: existingAgent.lore || [], -// adjectives: existingAgent.adjectives || [], -// postExamples: existingAgent.postExamples?.map(p => [{ user: "", content: { text: p } }]) || [], -// messageExamples: existingAgent.messageExamples || [], -// }) - -// if (!formData) { -// logger.info("Agent editing cancelled") -// return -// } - -// await adapter.updateAgent({ -// id: agentId, -// name: formData.name, -// bio: formData.bio, -// lore: formData.lore, -// adjectives: formData.adjectives, -// postExamples: formData.postExamples, -// messageExamples: formData.messageExamples, -// }) - -// logger.success(`Updated agent ${formData.name}`) -// await adapter.close() -// } catch (error) { -// handleError(error) -// } -// }) - -// agent -// .command("import") -// .description("import an agent from file") -// .argument("", "JSON file path") -// .action(async (file) => { -// try { -// const cwd = process.cwd() -// const config = await getConfig(cwd) -// if (!config) { -// logger.error("No project.json found. Please run init first.") -// process.exit(1) -// } - -// const agentData = JSON.parse(await fs.readFile(file, "utf8")) -// const agent = agentSchema.parse(agentData) - -// const db = new Database((config.database.config as { path: string }).path) -// const adapter = new SqliteDatabaseAdapter(db) -// await adapter.init() - -// await adapter.createAgent({ -// name: agent.name, -// bio: agent.bio || [], -// lore: agent.lore || [], -// messageExamples: agent.messageExamples || [], -// topics: agent.topics || [], -// style: { -// all: agent.style?.all || [], -// chat: agent.style?.chat || [], -// post: agent.style?.post || [] -// }, -// settings: agent.settings || {}, -// plugins: agent.plugins || [], -// adjectives: agent.adjectives || [], -// postExamples: agent.postExamples || [], -// id: stringToUuid(agent.id) -// }) - -// logger.success(`Imported agent ${agent.name}`) - -// await adapter.close() -// } catch (error) { -// handleError(error) -// } -// }) - -// agent -// .command("export") -// .description("export an agent to file") -// .argument("", "agent ID") -// .option("-o, --output ", "output file path") -// .action(async (agentId, opts) => { -// try { -// const cwd = process.cwd() -// const config = await getConfig(cwd) -// if (!config) { -// logger.error("No project.json found. Please run init first.") -// process.exit(1) -// } - -// const db = new Database((config.database.config as { path: string }).path) -// const adapter = new SqliteDatabaseAdapter(db) -// await adapter.init() - -// const agent = await adapter.getAgent(agentId) -// if (!agent) { -// logger.error(`Agent ${agentId} not found`) -// process.exit(1) -// } - -// const outputPath = opts.output || `${agent.name}.json` -// await fs.writeFile(outputPath, JSON.stringify(agent, null, 2)) -// logger.success(`Exported agent to ${outputPath}`) - -// await adapter.close() -// } catch (error) { -// handleError(error) -// } -// }) - -// agent -// .command("remove") -// .description("remove an agent") -// .argument("", "agent ID") -// .action(async (agentId) => { -// try { -// const cwd = process.cwd() -// const config = await getConfig(cwd) -// if (!config) { -// logger.error("No project.json found. Please run init first.") -// process.exit(1) -// } - -// const db = new Database((config.database.config as { path: string }).path) -// const adapter = new SqliteDatabaseAdapter(db) -// await adapter.init() - -// await adapter.removeAgent(agentId) -// logger.success(`Removed agent ${agentId}`) - -// await adapter.close() -// } catch (error) { -// handleError(error) -// } -// }) \ No newline at end of file diff --git a/packages/cli/src/commands/character.ts b/packages/cli/src/commands/character.ts new file mode 100644 index 00000000000..1e336e2c081 --- /dev/null +++ b/packages/cli/src/commands/character.ts @@ -0,0 +1,425 @@ +// src/commands/agent.ts +import { MessageExampleSchema } from "@elizaos/core" +import type { MessageExample } from "@elizaos/core"; +import { Command } from "commander" +import prompts from "prompts" +import { logger } from "../utils/logger" +import { z } from "zod" +import { getConfig } from "../utils/get-config" +import { handleError } from "../utils/handle-error" +import { Database, SqliteDatabaseAdapter } from "@elizaos-plugins/sqlite" +import { promises as fs } from "node:fs" +import { v4 as uuid } from "uuid"; +import type { UUID } from "@elizaos/core"; + +const characterSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + username: z.string(), + description: z.string().optional(), + settings: z.record(z.string(), z.any()).optional(), + plugins: z.array(z.string()).optional(), + secrets: z.record(z.string(), z.string()).optional(), + bio: z.array(z.string()).optional(), + lore: z.array(z.string()).optional(), + adjectives: z.array(z.string()).optional(), + postExamples: z.array(z.string()).optional(), + messageExamples: z.array(z.array(MessageExampleSchema)).optional(), + topics: z.array(z.string()).optional(), + style: z.object({ + all: z.array(z.string()).optional(), + chat: z.array(z.string()).optional(), + post: z.array(z.string()).optional(), + }).optional(), +}) + +type CharacterFormData = z.infer + +export const character = new Command() + .name("character") + .description("manage characters") + +async function collectCharacterData( + initialData?: Partial +): Promise { + const formData: Partial = { ...initialData }; + let currentStep = 0; + const steps = ['name', 'bio', 'lore', 'adjectives', 'postExamples', 'messageExamples']; + + while (currentStep < steps.length) { + const field = steps[currentStep]; + let response; + + switch (field) { + case 'name': + response = await prompts({ + type: 'text', + name: 'value', + message: 'Enter agent name:', + initial: formData.name, + }); + break; + + case 'bio': + case 'lore': + case 'postExamples': + case 'messageExamples': + response = await prompts({ + type: 'text', + name: 'value', + message: `Enter ${field} (use \\n for new lines):`, + initial: formData[field]?.join('\\n'), + }); + break; + + case 'adjectives': + response = await prompts({ + type: 'text', + name: 'value', + message: 'Enter adjectives (comma separated):', + initial: formData.adjectives?.join(', '), + }); + break; + } + + if (!response.value) { + return null; + } + + // Navigation commands + if (response.value === 'back') { + currentStep = Math.max(0, currentStep - 1); + continue; + } + if (response.value === 'forward') { + currentStep++; + continue; + } + + // Process and store the response + switch (field) { + case 'name': + formData.name = response.value; + break; + + case 'bio': + case 'lore': + case 'postExamples': + formData[field] = response.value + .split('\\n') + .map(line => line.trim()) + .filter(Boolean); + break; + + case 'messageExamples': { + const examples = response.value + .split('\\n') + .map(line => line.trim()) + .filter(Boolean); + formData.messageExamples = examples.length > 0 + ? examples + : [`{{user1}}: hey how are you?\n${formData.name}`]; + break; + } + + case 'adjectives': + formData.adjectives = response.value + .split(',') + .map(adj => adj.trim()) + .filter(Boolean); + break; + } + currentStep++; + } + + return formData as CharacterFormData; +} + +character + .command("list") + .description("list all characters") + .action(async () => { + try { + const cwd = process.cwd() + const config = await getConfig(cwd) + if (!config) { + logger.error("No project.json found. Please run init first.") + process.exit(1) + } + + const db = new Database((config.database.config as { path: string }).path) + const adapter = new SqliteDatabaseAdapter(db) + await adapter.init() + + const characters = await adapter.listCharacters() + + if (characters.length === 0) { + logger.info("No characters found") + } else { + logger.info("\nCharacters:") + for (const character of characters) { + logger.info(` ${character.name} (${character.id})`) + } + } + + await adapter.close() + } catch (error) { + handleError(error) + } + }) + +character + .command("create") + .description("create a new character") + .action(async () => { + try { + const cwd = process.cwd() + const config = await getConfig(cwd) + if (!config) { + logger.error("No project.json found. Please run init first.") + process.exit(1) + } + + logger.info("\nCreating new character (type 'back' or 'forward' to navigate)") + + const formData = await collectCharacterData() + if (!formData) { + logger.info("Character creation cancelled") + return + } + + const db = new Database((config.database.config as { path: string }).path) + const adapter = new SqliteDatabaseAdapter(db) + await adapter.init() + + const characterData = { + id: uuid() as UUID, + name: formData.name, + username: formData.name.toLowerCase().replace(/\s+/g, '_'), + bio: formData.bio, + lore: formData.lore, + adjectives: formData.adjectives, + postExamples: formData.postExamples, + messageExamples: formData.messageExamples, + topics: [], + style: { // TODO: add style + all: [], + chat: [], + post: [], + }, + plugins: [], + settings: {}, + } + + const characterToCreate = { + ...characterData, + messageExamples: characterData.messageExamples.map( + (msgArr: any) => msgArr.map((msg: any) => ({ + user: msg.user || "unknown", + content: msg.content + })) + ) + } + + await adapter.createCharacter({ + id: characterData.id, + name: characterData.name, + bio: characterData.bio || [], + lore: characterData.lore || [], + adjectives: characterData.adjectives || [], + postExamples: characterData.postExamples || [], + messageExamples: characterData.messageExamples as MessageExample[][], + topics: characterData.topics || [], + style: { + all: characterData.style?.all || [], + chat: characterData.style?.chat || [], + post: characterData.style?.post || [], + }, + plugins: characterData.plugins || [], + settings: characterData.settings || {}, + }) + + logger.success(`Created character ${formData.name} (${characterData.id})`) + await adapter.close() + } catch (error) { + handleError(error) + } + }) + +character + .command("edit") + .description("edit a character") + .argument("", "character ID") + .action(async (characterId) => { + try { + const cwd = process.cwd() + const config = await getConfig(cwd) + if (!config) { + logger.error("No project.json found. Please run init first.") + process.exit(1) + } + + const db = new Database((config.database.config as { path: string }).path) + const adapter = new SqliteDatabaseAdapter(db) + await adapter.init() + + const existingCharacter = await adapter.getCharacter(characterId) + if (!existingCharacter) { + logger.error(`Character ${characterId} not found`) + process.exit(1) + } + + logger.info(`\nEditing character ${existingCharacter.name} (type 'back' or 'forward' to navigate)`) + + const formData = await collectCharacterData({ + name: existingCharacter.name, + bio: Array.isArray(existingCharacter.bio) ? existingCharacter.bio : [existingCharacter.bio], + lore: existingCharacter.lore || [], + adjectives: existingCharacter.adjectives || [], + postExamples: existingCharacter.postExamples || [], + messageExamples: (existingCharacter.messageExamples || []).map( + (msgArr: any) => msgArr.map((msg: any) => ({ + user: msg.user ?? "unknown", + content: msg.content + })) + ), + }) + + if (!formData) { + logger.info("Character editing cancelled") + return + } + + const updatedCharacter = { + ...existingCharacter, + name: formData.name, + bio: formData.bio || [], + lore: formData.lore || [], + adjectives: formData.adjectives || [], + postExamples: formData.postExamples || [], + messageExamples: formData.messageExamples as MessageExample[][], + topics: existingCharacter.topics || [], + style: { + all: existingCharacter.style?.all || [], + chat: existingCharacter.style?.chat || [], + post: existingCharacter.style?.post || [], + }, + plugins: existingCharacter.plugins || [], + settings: existingCharacter.settings || {}, + } + await adapter.updateCharacter(updatedCharacter) + + logger.success(`Updated character ${formData.name}`) + await adapter.close() + } catch (error) { + handleError(error) + } + }) + +character + .command("import") + .description("import a character from file") + .argument("", "JSON file path") + .action(async (file) => { + try { + const cwd = process.cwd() + const config = await getConfig(cwd) + if (!config) { + logger.error("No project.json found. Please run init first.") + process.exit(1) + } + + const characterData = JSON.parse(await fs.readFile(file, "utf8")) + const character = characterSchema.parse(characterData) + + const db = new Database((config.database.config as { path: string }).path) + const adapter = new SqliteDatabaseAdapter(db) + await adapter.init() + + await adapter.createCharacter({ + name: character.name, + bio: character.bio || [], + lore: character.lore || [], + adjectives: character.adjectives || [], + postExamples: character.postExamples || [], + messageExamples: character.messageExamples as MessageExample[][], + topics: character.topics || [], + style: { + all: character.style?.all || [], + chat: character.style?.chat || [], + post: character.style?.post || [], + }, + plugins: character.plugins || [], + settings: character.settings || {}, + }) + + logger.success(`Imported character ${character.name}`) + + await adapter.close() + } catch (error) { + handleError(error) + } + }) + +character + .command("export") + .description("export a character to file") + .argument("", "character ID") + .option("-o, --output ", "output file path") + .action(async (characterId, opts) => { + try { + const cwd = process.cwd() + const config = await getConfig(cwd) + if (!config) { + logger.error("No project.json found. Please run init first.") + process.exit(1) + } + + const db = new Database((config.database.config as { path: string }).path) + const adapter = new SqliteDatabaseAdapter(db) + await adapter.init() + + const character = await adapter.getCharacter(characterId) + if (!character) { + logger.error(`Character ${characterId} not found`) + process.exit(1) + } + + const outputPath = opts.output || `${character.name}.json` + await fs.writeFile(outputPath, JSON.stringify(character, null, 2)) + logger.success(`Exported character to ${outputPath}`) + + await adapter.close() + } catch (error) { + handleError(error) + } + }) + +character + .command("remove") + .description("remove a character") + .argument("", "character ID") + .action(async (characterId) => { + try { + const cwd = process.cwd() + const config = await getConfig(cwd) + if (!config) { + logger.error("No project.json found. Please run init first.") + process.exit(1) + } + + const db = new Database((config.database.config as { path: string }).path) + const adapter = new SqliteDatabaseAdapter(db) + await adapter.init() + + await adapter.removeCharacter(characterId) + logger.success(`Removed character ${characterId}`) + + await adapter.close() + } catch (error) { + handleError(error) + } + }) + + + \ No newline at end of file diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 9b34b9e7c0d..672acec2b90 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { init } from "@/src/commands/init" import { plugins } from "@/src/commands/plugins" -// import { agent } from "@/src/commands/agent" +import { character } from "@/src/commands/character" import { Command } from "commander" import { logger } from "@/src/utils/logger" import { teeCommand as tee } from "@/src/commands/tee" @@ -18,7 +18,7 @@ async function main() { program .addCommand(init) .addCommand(plugins) - // .addCommand(agent) + .addCommand(character) .addCommand(tee) program.parse(process.argv) } diff --git a/packages/plugin-sqlite/src/index.ts b/packages/plugin-sqlite/src/index.ts index 046ee7e3844..679c95eb7d3 100644 --- a/packages/plugin-sqlite/src/index.ts +++ b/packages/plugin-sqlite/src/index.ts @@ -8,6 +8,7 @@ import type { Account, Actor, Adapter, + Character, Goal, GoalStatus, IAgentRuntime, @@ -700,6 +701,63 @@ export class SqliteDatabaseAdapter return false; } } + + + // NEW STUFF + // character methods + async listCharacters(): Promise { + const sql = "SELECT * FROM characters"; + return this.db.prepare(sql).all() as Character[]; + } + + async getCharacter(id: UUID): Promise { + const sql = "SELECT * FROM characters WHERE id = ?"; + return this.db.prepare(sql).get(id) as Character | undefined; + } + + async createCharacter(character: Character): Promise { + const sql = "INSERT INTO characters (id, name, bio, json, createdAt, updatedAt) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"; + this.db.prepare(sql).run( + character.id ?? v4(), + character.name, + character.bio, + JSON.stringify(character) + ); + } + + async updateCharacter(character: Character): Promise { + const sql = "UPDATE characters SET name = ?, bio = ?, json = ?, updatedAt = CURRENT_TIMESTAMP WHERE id = ?"; + await this.db + .prepare(sql) + .run( + character.name, + character.bio, + JSON.stringify(character), + character.id + ); + + } + + async removeCharacter(id: UUID): Promise { + const sql = "DELETE FROM characters WHERE id = ?"; + this.db.prepare(sql).run(id); + } + + async importCharacter(character: Character): Promise { + const sql = "INSERT INTO characters (id, name, bio, json, createdAt, updatedAt) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"; + this.db.prepare(sql).run( + character.id ?? v4(), + character.name, + character.bio, + JSON.stringify(character) + ); + } + + async exportCharacter(id: UUID): Promise { + const sql = "SELECT * FROM characters WHERE id = ?"; + return this.db.prepare(sql).get(id) as Character; + } + } const sqliteDatabaseAdapter: Adapter = { @@ -734,4 +792,7 @@ const sqlitePlugin: Plugin = { description: "SQLite database adapter plugin", adapters: [sqliteDatabaseAdapter], }; -export default sqlitePlugin; \ No newline at end of file +export default sqlitePlugin; + +// Export the Database constructor so CLI files may use it +export { Database }; \ No newline at end of file diff --git a/packages/plugin-sqlite/src/sqliteTables.ts b/packages/plugin-sqlite/src/sqliteTables.ts index c5e565dfc9a..7d773037778 100644 --- a/packages/plugin-sqlite/src/sqliteTables.ts +++ b/packages/plugin-sqlite/src/sqliteTables.ts @@ -2,6 +2,15 @@ export const sqliteTables = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; +-- Table: characters +CREATE TABLE IF NOT EXISTS "characters" ( + "id" TEXT PRIMARY KEY, + "name" TEXT, + "json" TEXT DEFAULT '{}' CHECK(json_valid("json")), + "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + -- Table: accounts CREATE TABLE IF NOT EXISTS "accounts" ( "id" TEXT PRIMARY KEY, @@ -126,4 +135,4 @@ CREATE INDEX IF NOT EXISTS "knowledge_content_key" ON "knowledge" CREATE INDEX IF NOT EXISTS "knowledge_created_key" ON "knowledge" ("agentId", "createdAt"); CREATE INDEX IF NOT EXISTS "knowledge_shared_key" ON "knowledge" ("isShared"); -COMMIT;`; \ No newline at end of file +COMMIT;`;