160 lines
4.4 KiB
TypeScript
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;
|
|
}
|