- Reset master to upstream/main (16,697 commits) - Overlay 2,271 local-only files (skills, tools, workspace, configs, apps) - Restore IDENTITY.md and USER.md templates - Build verified, gateway running, Discord working Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
171 lines
4.6 KiB
JavaScript
171 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
// outgoing_validator.js
|
|
// Usage:
|
|
// 1) echo '{"channel":"discord","message":"hello"}' | ./outgoing_validator.js
|
|
// 2) ./outgoing_validator.js --channel discord --message "hi"
|
|
// Options:
|
|
// --send : actually run `openclaw message send --channel ... --message ...` (needs openclaw CLI in PATH)
|
|
|
|
const { spawnSync } = require('child_process');
|
|
|
|
function usageAndExit() {
|
|
console.error('Usage: outgoing_validator.js [--channel <channel>] --message <message> [--send]');
|
|
process.exit(2);
|
|
}
|
|
|
|
function readStdinSync() {
|
|
try {
|
|
const fs = require('fs');
|
|
const stat = fs.fstatSync(0);
|
|
if (stat.size > 0) {
|
|
return fs.readFileSync(0, 'utf8');
|
|
}
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function cleanObject(o) {
|
|
if (Array.isArray(o)) return o.map(cleanObject);
|
|
if (o && typeof o === 'object') {
|
|
const rv = {};
|
|
for (const k of Object.keys(o)) {
|
|
const v = o[k];
|
|
if (v === null || v === undefined) continue;
|
|
const cv = cleanObject(v);
|
|
// skip empty strings? keep but trim
|
|
if (typeof cv === 'string' && cv.trim() === '') continue;
|
|
rv[k] = cv;
|
|
}
|
|
return rv;
|
|
}
|
|
if (typeof o === 'string') {
|
|
// Trim, collapse weird control chars, limit length
|
|
let s = o.trim();
|
|
// remove null bytes and vertical tabs
|
|
s = s.replace(/[\x00\x0B\x0C]/g, '');
|
|
// collapse repeated whitespace
|
|
s = s.replace(/\s{2,}/g, ' ');
|
|
return s;
|
|
}
|
|
return o;
|
|
}
|
|
|
|
function limitString(s, max) {
|
|
if (s.length <= max) return s;
|
|
return s.slice(0, max);
|
|
}
|
|
|
|
// Parse args
|
|
const argv = process.argv.slice(2);
|
|
let channel = null;
|
|
let message = null;
|
|
let doSend = false;
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === '--channel' && argv[i+1]) { channel = argv[++i]; continue; }
|
|
if (a === '--message' && argv[i+1]) { message = argv[++i]; continue; }
|
|
if (a === '--send') { doSend = true; continue; }
|
|
if (a === '--help' || a === '-h') usageAndExit();
|
|
}
|
|
|
|
// If no message provided via args, try stdin as JSON or plain text
|
|
if (!message) {
|
|
const stdin = readStdinSync();
|
|
if (stdin) {
|
|
// Try parse JSON
|
|
try {
|
|
const parsed = JSON.parse(stdin);
|
|
if (parsed) {
|
|
channel = channel || parsed.channel || parsed.to || parsed.channelName || 'discord';
|
|
message = parsed.message || parsed.text || parsed.body || JSON.stringify(parsed);
|
|
}
|
|
} catch (e) {
|
|
// treat stdin as plain text
|
|
message = stdin.trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!message) {
|
|
console.error('Aborted: empty message after reading args/stdin');
|
|
process.exit(2);
|
|
}
|
|
|
|
channel = channel || 'discord';
|
|
|
|
// Build payload
|
|
let payload = { channel, message };
|
|
|
|
// Clean
|
|
payload = cleanObject(payload);
|
|
if (!payload || !payload.message) {
|
|
console.error('Aborted: message cleaned to empty');
|
|
process.exit(2);
|
|
}
|
|
|
|
// Truncate long messages (OpenClaw/Discord limits)
|
|
const MAX_LEN = 1900;
|
|
if (typeof payload.message === 'string') {
|
|
if (payload.message.length > MAX_LEN) {
|
|
payload.message = limitString(payload.message, MAX_LEN);
|
|
payload.message += '\n\n[...trimmed]';
|
|
}
|
|
}
|
|
|
|
// Validate final JSON
|
|
let jsonOut;
|
|
try {
|
|
jsonOut = JSON.stringify(payload);
|
|
} catch (e) {
|
|
console.error('Failed to serialize payload JSON:', e.message);
|
|
process.exit(3);
|
|
}
|
|
|
|
// Log repaired payload locally
|
|
try {
|
|
const fs = require('fs');
|
|
const logDir = '/home/alex/clawd/logs';
|
|
try { fs.mkdirSync(logDir, { recursive: true }); } catch (e) {}
|
|
const ts = new Date().toISOString();
|
|
fs.appendFileSync(logDir + '/outgoing_validator.log', ts + ' ' + jsonOut + '\n');
|
|
} catch (e) {
|
|
// ignore logging errors
|
|
}
|
|
|
|
// Optional: increment Redis counter if redis-cli available and REDIS_KEY env set
|
|
if (process.env.REDIS_KEY) {
|
|
try {
|
|
const r = spawnSync('redis-cli', ['INCR', process.env.REDIS_KEY]);
|
|
if (r.error) {
|
|
// redis-cli not available or failed - ignore
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
// Output repaired JSON
|
|
console.log(jsonOut);
|
|
|
|
if (doSend) {
|
|
// Use openclaw CLI to send (if available). Don't fail loudly if not installed.
|
|
try {
|
|
const ocArgs = ['message', 'send', '--channel', payload.channel, '--message'];
|
|
// Use JSON string for message argument safely
|
|
const msgArg = payload.message;
|
|
const cmd = ['openclaw', ...ocArgs, msgArg];
|
|
const r = spawnSync('openclaw', ['message', 'send', '--channel', payload.channel, '--message', payload.message], { encoding: 'utf8' });
|
|
if (r.error) {
|
|
console.error('OpenClaw send failed to start:', r.error.message);
|
|
process.exit(4);
|
|
}
|
|
console.log('send result:', r.stdout || r.stderr);
|
|
} catch (e) {
|
|
console.error('Send attempt failed:', e.message);
|
|
process.exit(4);
|
|
}
|
|
}
|
|
|
|
process.exit(0);
|