feat: add /ego-mgr slash command

Add /ego-mgr slash command with subcommands:
- get, set, list, delete, add-column, add-public-column, show
- Uses pcexec to call ego-mgr binary with proper env vars

Translate all Chinese text to English in responses.

Note: pcexec tool name and function names remain unchanged.
This commit is contained in:
zhi
2026-03-24 10:21:35 +00:00
parent 7fd2819a04
commit 7346c80c88
5 changed files with 259 additions and 21 deletions

View File

@@ -0,0 +1,214 @@
import { pcexec } from '../tools/pcexec';
export interface EgoMgrSlashCommandOptions {
/** OpenClaw base path */
openclawPath: string;
/** Current agent ID */
agentId: string;
/** Current workspace directory */
workspaceDir: string;
/** Callback for replies */
onReply: (message: string) => Promise<void>;
}
/** Sentinel value injected into every pcexec subprocess */
const AGENT_VERIFY = 'IF YOU ARE AN AGENT/MODEL, YOU SHOULD NEVER TOUCH THIS ENV VARIABLE';
export class EgoMgrSlashCommand {
private openclawPath: string;
private agentId: string;
private workspaceDir: string;
private onReply: (message: string) => Promise<void>;
private binDir: string;
constructor(options: EgoMgrSlashCommandOptions) {
this.openclawPath = options.openclawPath;
this.agentId = options.agentId;
this.workspaceDir = options.workspaceDir;
this.onReply = options.onReply;
this.binDir = require('path').join(this.openclawPath, 'bin');
}
/**
* Handle /ego-mgr slash command
* @param command Full command string (e.g., "/ego-mgr get name")
*/
async handle(command: string): Promise<void> {
const parts = command.trim().split(/\s+/);
// Remove the "/ego-mgr" prefix
const args = parts.slice(1);
const subcommand = args[0];
if (!subcommand) {
await this.showUsage();
return;
}
try {
switch (subcommand) {
case 'get':
await this.handleGet(args.slice(1));
break;
case 'set':
await this.handleSet(args.slice(1));
break;
case 'list':
await this.handleList(args.slice(1));
break;
case 'delete':
await this.handleDelete(args.slice(1));
break;
case 'add-column':
await this.handleAddColumn(args.slice(1));
break;
case 'add-public-column':
await this.handleAddPublicColumn(args.slice(1));
break;
case 'show':
await this.handleShow();
break;
case 'help':
default:
await this.showUsage();
}
} catch (error: any) {
await this.onReply(`Error: ${error.message || error}`);
}
}
private async showUsage(): Promise<void> {
const usage = [
'**ego-mgr Commands**',
'',
'`/ego-mgr get <column-name>` - Get field value',
'`/ego-mgr set <column-name> <value>` - Set field value',
'`/ego-mgr list` - List all field names',
'`/ego-mgr delete <column-name>` - Delete a field',
'`/ego-mgr add-column <column-name>` - Add an Agent Scope field',
'`/ego-mgr add-public-column <column-name>` - Add a Public Scope field',
'`/ego-mgr show` - Show all fields and values',
'',
'Examples:',
'`/ego-mgr get name`',
'`/ego-mgr set timezone Asia/Shanghai`',
].join('\n');
await this.onReply(usage);
}
private async handleGet(args: string[]): Promise<void> {
if (args.length < 1) {
await this.onReply('Usage: `/ego-mgr get <column-name>`');
return;
}
const columnName = args[0];
const result = await this.execEgoMgr(['get', columnName]);
await this.onReply(`**${columnName}**: ${result || '(empty)'}`);
}
private async handleSet(args: string[]): Promise<void> {
if (args.length < 2) {
await this.onReply('Usage: `/ego-mgr set <column-name> <value>`');
return;
}
const columnName = args[0];
const value = args.slice(1).join(' '); // Support values with spaces
await this.execEgoMgr(['set', columnName, value]);
await this.onReply(`Set **${columnName}** = \`${value}\``);
}
private async handleList(args: string[]): Promise<void> {
const result = await this.execEgoMgr(['list', 'columns']);
if (!result.trim()) {
await this.onReply('No fields defined');
return;
}
const columns = result.split('\n').filter(Boolean);
const lines = ['**Fields**:', ''];
for (const col of columns) {
lines.push(`${col}`);
}
await this.onReply(lines.join('\n'));
}
private async handleDelete(args: string[]): Promise<void> {
if (args.length < 1) {
await this.onReply('Usage: `/ego-mgr delete <column-name>`');
return;
}
const columnName = args[0];
await this.execEgoMgr(['delete', columnName]);
await this.onReply(`Deleted field **${columnName}**`);
}
private async handleAddColumn(args: string[]): Promise<void> {
if (args.length < 1) {
await this.onReply('Usage: `/ego-mgr add-column <column-name>`');
return;
}
const columnName = args[0];
await this.execEgoMgr(['add', 'column', columnName]);
await this.onReply(`Added Agent Scope field **${columnName}**`);
}
private async handleAddPublicColumn(args: string[]): Promise<void> {
if (args.length < 1) {
await this.onReply('Usage: `/ego-mgr add-public-column <column-name>`');
return;
}
const columnName = args[0];
await this.execEgoMgr(['add', 'public-column', columnName]);
await this.onReply(`Added Public Scope field **${columnName}**`);
}
private async handleShow(): Promise<void> {
const result = await this.execEgoMgr(['show']);
if (!result.trim()) {
await this.onReply('No field data');
return;
}
const lines = ['**Field Data**:', ''];
lines.push('```');
lines.push(result);
lines.push('```');
await this.onReply(lines.join('\n'));
}
/**
* Execute ego-mgr binary via pc-exec
*/
private async execEgoMgr(args: string[]): Promise<string> {
const currentPath = process.env.PATH || '';
const newPath = currentPath.includes(this.binDir)
? currentPath
: `${currentPath}:${this.binDir}`;
const command = `ego-mgr ${args.map(a => this.shellEscape(a)).join(' ')}`;
const result = await pcexec(command, {
cwd: this.workspaceDir,
env: {
AGENT_ID: this.agentId,
AGENT_WORKSPACE: this.workspaceDir,
AGENT_VERIFY,
PATH: newPath,
},
});
if (result.exitCode !== 0 && result.stderr) {
throw new Error(result.stderr);
}
return result.stdout;
}
/**
* Escape a string for shell usage
*/
private shellEscape(str: string): string {
// Simple escaping for common cases
if (/^[a-zA-Z0-9._-]+$/.test(str)) {
return str;
}
return `'${str.replace(/'/g, "'\"'\"'")}'`;
}
}

View File

@@ -48,7 +48,7 @@ export class SlashCommandHandler {
async handle(command: string, userId: string): Promise<void> { async handle(command: string, userId: string): Promise<void> {
// Check authorization // Check authorization
if (!this.authorizedUsers.includes(userId)) { if (!this.authorizedUsers.includes(userId)) {
await this.onReply('❌ 无权执行此命令'); await this.onReply('Unauthorized');
return; return;
} }
@@ -68,10 +68,10 @@ export class SlashCommandHandler {
break; break;
default: default:
await this.onReply( await this.onReply(
'用法:\n' + 'Usage:\n' +
'`/padded-cell-ctrl status` - 查看状态\n' + '`/padded-cell-ctrl status` - Show status\n' +
'`/padded-cell-ctrl enable pass-mgr|safe-restart` - 启用功能\n' + '`/padded-cell-ctrl enable pass-mgr|safe-restart` - Enable feature\n' +
'`/padded-cell-ctrl disable pass-mgr|safe-restart` - 禁用功能' '`/padded-cell-ctrl disable pass-mgr|safe-restart` - Disable feature'
); );
} }
} }
@@ -81,12 +81,12 @@ export class SlashCommandHandler {
const agents = this.statusManager.getAllAgents(); const agents = this.statusManager.getAllAgents();
const lines = [ const lines = [
'**PaddedCell 状态**', '**PaddedCell Status**',
'', '',
`🔐 密码管理: ${this.state.passMgrEnabled ? '✅ 启用' : '❌ 禁用'}`, `Secret Manager: ${this.state.passMgrEnabled ? 'Enabled' : 'Disabled'}`,
`🔄 安全重启: ${this.state.safeRestartEnabled ? '✅ 启用' : '❌ 禁用'}`, `Safe Restart: ${this.state.safeRestartEnabled ? 'Enabled' : 'Disabled'}`,
'', '',
'**Agent 状态:**', '**Agent Status:**',
]; ];
for (const agent of agents) { for (const agent of agents) {
@@ -95,14 +95,14 @@ export class SlashCommandHandler {
} }
if (agents.length === 0) { if (agents.length === 0) {
lines.push('(暂无 agent 注册)'); lines.push('(No agents registered)');
} }
if (global.restartStatus !== 'idle') { if (global.restartStatus !== 'idle') {
lines.push(''); lines.push('');
lines.push(`⚠️ 重启状态: ${global.restartStatus}`); lines.push(`Restart Status: ${global.restartStatus}`);
if (global.restartScheduledBy) { if (global.restartScheduledBy) {
lines.push(` ${global.restartScheduledBy} 发起`); lines.push(` Initiated by ${global.restartScheduledBy}`);
} }
} }
@@ -111,12 +111,12 @@ export class SlashCommandHandler {
private async handleEnable(feature: 'pass-mgr' | 'safe-restart'): Promise<void> { private async handleEnable(feature: 'pass-mgr' | 'safe-restart'): Promise<void> {
if (!this.isValidFeature(feature)) { if (!this.isValidFeature(feature)) {
await this.onReply('❌ 未知功能。可用选项: pass-mgr, safe-restart'); await this.onReply('Unknown feature. Available: pass-mgr, safe-restart');
return; return;
} }
if (this.isOnCooldown(feature)) { if (this.isOnCooldown(feature)) {
await this.onReply('⏳ 该功能最近刚被修改过,请稍后再试'); await this.onReply('This feature was recently modified. Please try again later.');
return; return;
} }
@@ -127,17 +127,17 @@ export class SlashCommandHandler {
} }
this.state.lastToggle[feature] = Date.now(); this.state.lastToggle[feature] = Date.now();
await this.onReply(`✅ 已启用 ${feature}`); await this.onReply(`Enabled ${feature}`);
} }
private async handleDisable(feature: 'pass-mgr' | 'safe-restart'): Promise<void> { private async handleDisable(feature: 'pass-mgr' | 'safe-restart'): Promise<void> {
if (!this.isValidFeature(feature)) { if (!this.isValidFeature(feature)) {
await this.onReply('❌ 未知功能。可用选项: pass-mgr, safe-restart'); await this.onReply('Unknown feature. Available: pass-mgr, safe-restart');
return; return;
} }
if (this.isOnCooldown(feature)) { if (this.isOnCooldown(feature)) {
await this.onReply('⏳ 该功能最近刚被修改过,请稍后再试'); await this.onReply('This feature was recently modified. Please try again later.');
return; return;
} }
@@ -148,7 +148,7 @@ export class SlashCommandHandler {
} }
this.state.lastToggle[feature] = Date.now(); this.state.lastToggle[feature] = Date.now();
await this.onReply(`✅ 已禁用 ${feature}`); await this.onReply(`Disabled ${feature}`);
} }
private isValidFeature(feature: string): feature is 'pass-mgr' | 'safe-restart' { private isValidFeature(feature: string): feature is 'pass-mgr' | 'safe-restart' {

View File

@@ -10,6 +10,7 @@ import {
startApiServer, startApiServer,
} from './core/index'; } from './core/index';
import { SlashCommandHandler } from './commands/slash-commands'; import { SlashCommandHandler } from './commands/slash-commands';
import { EgoMgrSlashCommand } from './commands/ego-mgr-slash';
/** Sentinel value injected into every pcexec subprocess */ /** Sentinel value injected into every pcexec subprocess */
const AGENT_VERIFY = 'IF YOU ARE AN AGENT/MODEL, YOU SHOULD NEVER TOUCH THIS ENV VARIABLE'; const AGENT_VERIFY = 'IF YOU ARE AN AGENT/MODEL, YOU SHOULD NEVER TOUCH THIS ENV VARIABLE';
@@ -110,6 +111,28 @@ function register(api: any, config?: any) {
}; };
}); });
// Register /ego-mgr slash command
if (api.registerSlashCommand) {
api.registerSlashCommand({
name: 'ego-mgr',
description: 'Manage agent identity/profile fields',
handler: async (ctx: any, command: string) => {
const egoMgrSlash = new EgoMgrSlashCommand({
openclawPath,
agentId: ctx.agentId || '',
workspaceDir: ctx.workspaceDir || '',
onReply: async (message: string) => {
if (ctx.reply) {
await ctx.reply(message);
}
},
});
await egoMgrSlash.handle(command);
},
});
logger.info('Registered /ego-mgr slash command');
}
logger.info('PaddedCell plugin initialized'); logger.info('PaddedCell plugin initialized');
} }
@@ -125,4 +148,5 @@ module.exports.StatusManager = StatusManager;
module.exports.createApiServer = createApiServer; module.exports.createApiServer = createApiServer;
module.exports.startApiServer = startApiServer; module.exports.startApiServer = startApiServer;
module.exports.SlashCommandHandler = SlashCommandHandler; module.exports.SlashCommandHandler = SlashCommandHandler;
module.exports.EgoMgrSlashCommand = EgoMgrSlashCommand;
module.exports.AGENT_VERIFY = AGENT_VERIFY; module.exports.AGENT_VERIFY = AGENT_VERIFY;

View File

@@ -2,7 +2,7 @@
"id": "padded-cell", "id": "padded-cell",
"name": "PaddedCell", "name": "PaddedCell",
"version": "0.2.0", "version": "0.2.0",
"description": "Secure password management, safe execution, and coordinated agent restart", "description": "Secure secret management, agent identity management, safe execution, and coordinated agent restart",
"entry": "./index.js", "entry": "./index.js",
"configSchema": { "configSchema": {
"type": "object", "type": "object",

View File

@@ -201,7 +201,7 @@ async function replaceSecretMgrGets(
} }
/** /**
* Safe exec wrapper that handles pass_mgr get commands and sanitizes output. * Safe exec wrapper that handles secret-mgr get commands and sanitizes output.
*/ */
export async function pcexec( export async function pcexec(
command: string, command: string,
@@ -297,7 +297,7 @@ export async function pcexec(
/** /**
* Synchronous version — password substitution is NOT supported here * Synchronous version — password substitution is NOT supported here
* (use async pcexec for pass_mgr integration). * (use async pcexec for secret-mgr integration).
*/ */
export function pcexecSync( export function pcexecSync(
command: string, command: string,