Files
2026-02-04 00:27:04 +02:00

160 lines
4.4 KiB
TypeScript

/**
* X Integration IPC Handler
*
* Handles all x_* IPC messages from container agents.
* This is the entry point for X integration in the host process.
*/
import { spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: { target: 'pino-pretty', options: { colorize: true } }
});
interface SkillResult {
success: boolean;
message: string;
data?: unknown;
}
// Run a skill script as subprocess
async function runScript(script: string, args: object): Promise<SkillResult> {
const scriptPath = path.join(process.cwd(), '.claude', 'skills', 'x-integration', 'scripts', `${script}.ts`);
return new Promise((resolve) => {
const proc = spawn('npx', ['tsx', scriptPath], {
cwd: process.cwd(),
env: { ...process.env, NANOCLAW_ROOT: process.cwd() },
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
proc.stdout.on('data', (data) => { stdout += data.toString(); });
proc.stdin.write(JSON.stringify(args));
proc.stdin.end();
const timer = setTimeout(() => {
proc.kill('SIGTERM');
resolve({ success: false, message: 'Script timed out (120s)' });
}, 120000);
proc.on('close', (code) => {
clearTimeout(timer);
if (code !== 0) {
resolve({ success: false, message: `Script exited with code: ${code}` });
return;
}
try {
const lines = stdout.trim().split('\n');
resolve(JSON.parse(lines[lines.length - 1]));
} catch {
resolve({ success: false, message: `Failed to parse output: ${stdout.slice(0, 200)}` });
}
});
proc.on('error', (err) => {
clearTimeout(timer);
resolve({ success: false, message: `Failed to spawn: ${err.message}` });
});
});
}
// Write result to IPC results directory
function writeResult(dataDir: string, sourceGroup: string, requestId: string, result: SkillResult): void {
const resultsDir = path.join(dataDir, 'ipc', sourceGroup, 'x_results');
fs.mkdirSync(resultsDir, { recursive: true });
fs.writeFileSync(path.join(resultsDir, `${requestId}.json`), JSON.stringify(result));
}
/**
* Handle X integration IPC messages
*
* @returns true if message was handled, false if not an X message
*/
export async function handleXIpc(
data: Record<string, unknown>,
sourceGroup: string,
isMain: boolean,
dataDir: string
): Promise<boolean> {
const type = data.type as string;
// Only handle x_* types
if (!type?.startsWith('x_')) {
return false;
}
// Only main group can use X integration
if (!isMain) {
logger.warn({ sourceGroup, type }, 'X integration blocked: not main group');
return true;
}
const requestId = data.requestId as string;
if (!requestId) {
logger.warn({ type }, 'X integration blocked: missing requestId');
return true;
}
logger.info({ type, requestId }, 'Processing X request');
let result: SkillResult;
switch (type) {
case 'x_post':
if (!data.content) {
result = { success: false, message: 'Missing content' };
break;
}
result = await runScript('post', { content: data.content });
break;
case 'x_like':
if (!data.tweetUrl) {
result = { success: false, message: 'Missing tweetUrl' };
break;
}
result = await runScript('like', { tweetUrl: data.tweetUrl });
break;
case 'x_reply':
if (!data.tweetUrl || !data.content) {
result = { success: false, message: 'Missing tweetUrl or content' };
break;
}
result = await runScript('reply', { tweetUrl: data.tweetUrl, content: data.content });
break;
case 'x_retweet':
if (!data.tweetUrl) {
result = { success: false, message: 'Missing tweetUrl' };
break;
}
result = await runScript('retweet', { tweetUrl: data.tweetUrl });
break;
case 'x_quote':
if (!data.tweetUrl || !data.comment) {
result = { success: false, message: 'Missing tweetUrl or comment' };
break;
}
result = await runScript('quote', { tweetUrl: data.tweetUrl, comment: data.comment });
break;
default:
return false;
}
writeResult(dataDir, sourceGroup, requestId, result);
if (result.success) {
logger.info({ type, requestId }, 'X request completed');
} else {
logger.error({ type, requestId, message: result.message }, 'X request failed');
}
return true;
}