Files
PaddedCell/plugin/commands/slash-commands.ts
zhi 787d88cd33 chore: convert plugin to ESM and migrate to current openclaw plugin SDK
ESM conversion:
- package.json: add "type": "module"; drop stale "main": "index.ts"
- tsconfig.json: switch module/moduleResolution to "nodenext"
- plugin/index.ts: replace `module.exports = { register }` and
  `module.exports.X = X` with `export default definePluginEntry({ ... })`
  plus named ESM re-exports; replace `require('os')`/`require('path')`
  with proper imports.
- plugin/tools/pcexec.ts: replace `require('child_process')` with import
  from "node:child_process".
- plugin/commands/ego-mgr-slash.ts: replace `require('path')` with
  proper path import.
- All relative imports/exports across plugin/ now carry .js extensions
  as required by Node ESM (nodenext module resolution).

Plugin SDK convention update:
- Wrap default export with definePluginEntry({ id, name, description,
  register }) per the current openclaw authoring contract.
- Type api parameter as OpenClawPluginApi (was `any`); the non-standard
  api.registerSlashCommand call is preserved behind a guarded any-cast,
  so the plugin remains a no-op for slash commands when the host doesn't
  expose that hook (matches the previous defensive guard).
- Add openclaw as a devDependency (file:/usr/lib/node_modules/openclaw)
  so tsc can resolve openclaw/plugin-sdk/* subpath types at build time.
- Modernize openclaw.plugin.json: drop entry/version, add
  activation.onStartup so gateway_start fires for this plugin at boot,
  declare contracts.tools listing pcexec/proxy-pcexec/safe_restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:02:30 +00:00

183 lines
5.0 KiB
TypeScript

import { StatusManager } from '../core/status-manager.js';
export interface SlashCommandOptions {
statusManager: StatusManager;
/** List of authorized user IDs */
authorizedUsers: string[];
/** Cooldown duration in seconds */
cooldownSeconds?: number;
/** Callback for replies */
onReply: (message: string) => Promise<void>;
}
interface CommandState {
passMgrEnabled: boolean;
safeRestartEnabled: boolean;
lastToggle: {
'pass-mgr': number;
'safe-restart': number;
};
}
export class SlashCommandHandler {
private statusManager: StatusManager;
private authorizedUsers: string[];
private cooldownMs: number;
private onReply: (message: string) => Promise<void>;
private state: CommandState;
constructor(options: SlashCommandOptions) {
this.statusManager = options.statusManager;
this.authorizedUsers = options.authorizedUsers;
this.cooldownMs = (options.cooldownSeconds || 10) * 1000;
this.onReply = options.onReply;
this.state = {
passMgrEnabled: true,
safeRestartEnabled: true,
lastToggle: {
'pass-mgr': 0,
'safe-restart': 0,
},
};
}
/**
* Handle a slash command
*/
async handle(command: string, userId: string): Promise<void> {
// Check authorization
if (!this.authorizedUsers.includes(userId)) {
await this.onReply('Unauthorized');
return;
}
const parts = command.trim().split(/\s+/);
const subcommand = parts[1];
const feature = parts[2] as 'pass-mgr' | 'safe-restart';
switch (subcommand) {
case 'status':
await this.handleStatus();
break;
case 'enable':
await this.handleEnable(feature);
break;
case 'disable':
await this.handleDisable(feature);
break;
default:
await this.onReply(
'Usage:\n' +
'`/padded-cell-ctrl status` - Show status\n' +
'`/padded-cell-ctrl enable pass-mgr|safe-restart` - Enable feature\n' +
'`/padded-cell-ctrl disable pass-mgr|safe-restart` - Disable feature'
);
}
}
private async handleStatus(): Promise<void> {
const global = this.statusManager.getGlobalStatus();
const agents = this.statusManager.getAllAgents();
const lines = [
'**PaddedCell Status**',
'',
`Secret Manager: ${this.state.passMgrEnabled ? 'Enabled' : 'Disabled'}`,
`Safe Restart: ${this.state.safeRestartEnabled ? 'Enabled' : 'Disabled'}`,
'',
'**Agent Status:**',
];
for (const agent of agents) {
const emoji = this.getStateEmoji(agent.state);
lines.push(`${emoji} ${agent.agentId}: ${agent.state}`);
}
if (agents.length === 0) {
lines.push('(No agents registered)');
}
if (global.restartStatus !== 'idle') {
lines.push('');
lines.push(`Restart Status: ${global.restartStatus}`);
if (global.restartScheduledBy) {
lines.push(` Initiated by ${global.restartScheduledBy}`);
}
}
await this.onReply(lines.join('\n'));
}
private async handleEnable(feature: 'pass-mgr' | 'safe-restart'): Promise<void> {
if (!this.isValidFeature(feature)) {
await this.onReply('Unknown feature. Available: pass-mgr, safe-restart');
return;
}
if (this.isOnCooldown(feature)) {
await this.onReply('This feature was recently modified. Please try again later.');
return;
}
if (feature === 'pass-mgr') {
this.state.passMgrEnabled = true;
} else {
this.state.safeRestartEnabled = true;
}
this.state.lastToggle[feature] = Date.now();
await this.onReply(`Enabled ${feature}`);
}
private async handleDisable(feature: 'pass-mgr' | 'safe-restart'): Promise<void> {
if (!this.isValidFeature(feature)) {
await this.onReply('Unknown feature. Available: pass-mgr, safe-restart');
return;
}
if (this.isOnCooldown(feature)) {
await this.onReply('This feature was recently modified. Please try again later.');
return;
}
if (feature === 'pass-mgr') {
this.state.passMgrEnabled = false;
} else {
this.state.safeRestartEnabled = false;
}
this.state.lastToggle[feature] = Date.now();
await this.onReply(`Disabled ${feature}`);
}
private isValidFeature(feature: string): feature is 'pass-mgr' | 'safe-restart' {
return feature === 'pass-mgr' || feature === 'safe-restart';
}
private isOnCooldown(feature: 'pass-mgr' | 'safe-restart'): boolean {
const lastToggle = this.state.lastToggle[feature];
return Date.now() - lastToggle < this.cooldownMs;
}
private getStateEmoji(state: string): string {
switch (state) {
case 'idle': return '💤';
case 'busy': return '⚡';
case 'focus': return '🎯';
case 'freeze': return '🧊';
case 'pre-freeze': return '⏳';
case 'pre-freeze-focus': return '📝';
default: return '❓';
}
}
isPassMgrEnabled(): boolean {
return this.state.passMgrEnabled;
}
isSafeRestartEnabled(): boolean {
return this.state.safeRestartEnabled;
}
}