Files
Dirigent/plugin/core/sidecar-process.ts
hzhang 32dc9a4233 refactor: new design — sidecar services, moderator Gateway client, tool execute API
- Replace standalone no-reply-api Docker service with unified sidecar (services/main.mjs)
  that routes /no-reply/* and /moderator/* and starts/stops with openclaw-gateway
- Add moderator Discord Gateway client (services/moderator/index.mjs) for real-time
  MESSAGE_CREATE push instead of polling; notifies plugin via HTTP callback
- Add plugin HTTP routes (plugin/web/dirigent-api.ts) for moderator → plugin callbacks
  (wake-from-dormant, interrupt tail-match)
- Fix tool registration format: AgentTool requires execute: not handler:; factory form
  for tools needing ctx
- Rename no-reply-process.ts → sidecar-process.ts, startNoReplyApi → startSideCar
- Remove dead config fields from openclaw.plugin.json (humanList, agentList, listMode,
  channelPoliciesFile, endSymbols, waitIdentifier, multiMessage*, bypassUserIds, etc.)
- Rename noReplyPort → sideCarPort
- Remove docker-compose.yml, dev-up/down scripts, package-plugin.mjs, test-no-reply-api.mjs
- Update install.mjs: clean dist before build, copy services/, drop dead config writes
- Update README, Makefile, smoke script for new architecture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 08:07:59 +01:00

120 lines
3.2 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawn, type ChildProcess } from "node:child_process";
let noReplyProcess: ChildProcess | null = null;
const LOCK_FILE = path.join(os.tmpdir(), "dirigent-sidecar.lock");
function readLock(): { pid: number } | null {
try {
const raw = fs.readFileSync(LOCK_FILE, "utf8").trim();
return { pid: Number(raw) };
} catch {
return null;
}
}
function writeLock(pid: number): void {
try { fs.writeFileSync(LOCK_FILE, String(pid)); } catch { /* ignore */ }
}
function clearLock(): void {
try { fs.unlinkSync(LOCK_FILE); } catch { /* ignore */ }
}
function isLockHeld(): boolean {
const lock = readLock();
if (!lock) return false;
try {
process.kill(lock.pid, 0);
return true;
} catch {
return false;
}
}
export function startSideCar(
logger: { info: (m: string) => void; warn: (m: string) => void },
pluginDir: string,
port = 8787,
moderatorToken?: string,
pluginApiToken?: string,
gatewayPort?: number,
debugMode?: boolean,
): void {
logger.info(`dirigent: startSideCar called, pluginDir=${pluginDir}`);
if (noReplyProcess) {
logger.info("dirigent: no-reply API already running (local ref), skipping");
return;
}
if (isLockHeld()) {
logger.info("dirigent: no-reply API already running (lock file), skipping");
return;
}
// services/main.mjs lives alongside the plugin directory in the distribution
const serverPath = path.resolve(pluginDir, "services", "main.mjs");
logger.info(`dirigent: resolved serverPath=${serverPath}`);
if (!fs.existsSync(serverPath)) {
logger.warn(`dirigent: services/main.mjs not found at ${serverPath}, skipping`);
return;
}
logger.info("dirigent: services/main.mjs found, spawning process...");
// Build plugin API URL from gateway port, or use a default
const pluginApiUrl = gatewayPort
? `http://127.0.0.1:${gatewayPort}`
: "http://127.0.0.1:18789";
const env: NodeJS.ProcessEnv = {
...process.env,
SERVICES_PORT: String(port),
PLUGIN_API_URL: pluginApiUrl,
};
if (moderatorToken) {
env.MODERATOR_TOKEN = moderatorToken;
}
if (pluginApiToken) {
env.PLUGIN_API_TOKEN = pluginApiToken;
}
if (debugMode !== undefined) {
env.DEBUG_MODE = debugMode ? "true" : "false";
}
noReplyProcess = spawn(process.execPath, [serverPath], {
env,
stdio: ["ignore", "pipe", "pipe"],
detached: false,
});
if (noReplyProcess.pid) {
writeLock(noReplyProcess.pid);
}
noReplyProcess.stdout?.on("data", (d: Buffer) => logger.info(`dirigent: services: ${d.toString().trim()}`));
noReplyProcess.stderr?.on("data", (d: Buffer) => logger.warn(`dirigent: services: ${d.toString().trim()}`));
noReplyProcess.on("exit", (code, signal) => {
logger.info(`dirigent: services exited (code=${code}, signal=${signal})`);
clearLock();
noReplyProcess = null;
});
logger.info(`dirigent: services started (pid=${noReplyProcess.pid}, port=${port})`);
}
export function stopSideCar(logger: { info: (m: string) => void }): void {
if (!noReplyProcess) return;
logger.info("dirigent: stopping sidecar");
noReplyProcess.kill("SIGTERM");
noReplyProcess = null;
clearLock();
}