#!/usr/bin/env node /* eslint-disable no-console */ const https = require('https'); const http = require('http'); const fs = require('fs'); const path = require('path'); // --- DEPRECATION WARNING --- // In v4, live-status CLI is deprecated. The status-watcher daemon handles // all updates automatically by tailing JSONL transcripts. You do not need // to call this tool manually. It remains available for backward compatibility. if (process.stderr.isTTY) { console.error('NOTE: live-status CLI is deprecated as of v4. Status updates are now automatic.'); } // --- PARSE ARGS --- const args = process.argv.slice(2); let command = null; const options = {}; const otherArgs = []; for (let i = 0; i < args.length; i++) { const arg = args[i]; // eslint-disable-line security/detect-object-injection const next = args[i + 1]; // eslint-disable-line security/detect-object-injection if (arg === '--channel' && next) { options.channel = next; i++; } else if (arg === '--reply-to' && next) { options.replyTo = next; i++; } else if (arg === '--agent' && next) { options.agent = next; i++; } else if (arg === '--token' && next) { options.token = next; i++; } else if (arg === '--host' && next) { options.host = next; i++; } else if (arg === '--rich') { options.rich = true; } else if ( !command && ['create', 'update', 'complete', 'error', 'delete', 'start-watcher', 'stop-watcher'].includes( arg, ) ) { command = arg; } else { otherArgs.push(arg); } } // --- LOAD CONFIG --- function loadConfig() { const searchPaths = [ process.env.OPENCLAW_CONFIG_DIR && path.join(process.env.OPENCLAW_CONFIG_DIR, 'openclaw.json'), process.env.XDG_CONFIG_HOME && path.join(process.env.XDG_CONFIG_HOME, 'openclaw.json'), path.join(process.env.HOME || '/root', '.openclaw', 'openclaw.json'), '/home/node/.openclaw/openclaw.json', ].filter(Boolean); for (const p of searchPaths) { try { // eslint-disable-next-line security/detect-non-literal-fs-filename return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_e) { /* file not found or invalid JSON — try next path */ } } return null; } function resolveToken(config) { if (options.token) return options.token; if (process.env.MM_BOT_TOKEN) return process.env.MM_BOT_TOKEN; if (!config) return null; const mm = config.mattermost || (config.channels && config.channels.mattermost) || {}; const accounts = mm.accounts || {}; if (options.agent) { try { const mapPath = path.join(__dirname, 'agent-accounts.json'); const agentMap = JSON.parse(fs.readFileSync(mapPath, 'utf8')); // eslint-disable-next-line security/detect-object-injection const accName = agentMap[options.agent]; // eslint-disable-next-line security/detect-object-injection if (accName && accounts[accName] && accounts[accName].botToken) { // eslint-disable-next-line security/detect-object-injection return accounts[accName].botToken; } } catch (_e) { /* agent-accounts.json not found or agent not mapped */ } } if (accounts.default && accounts.default.botToken) return accounts.default.botToken; for (const acc of Object.values(accounts)) { if (acc.botToken) return acc.botToken; } return null; } function resolveHost(config) { if (options.host) return options.host; if (process.env.MM_HOST) return process.env.MM_HOST; if (config) { const mm = config.mattermost || (config.channels && config.channels.mattermost) || {}; const baseUrl = mm.baseUrl || ''; if (baseUrl) { try { return new URL(baseUrl).hostname; } catch (_e) { /* invalid URL */ } } } return 'localhost'; } function resolvePort(config) { if (process.env.MM_PORT) return parseInt(process.env.MM_PORT, 10); if (config) { const mm = config.mattermost || (config.channels && config.channels.mattermost) || {}; const baseUrl = mm.baseUrl || ''; if (baseUrl) { try { const url = new URL(baseUrl); return url.port ? parseInt(url.port, 10) : url.protocol === 'https:' ? 443 : 80; } catch (_e) { /* invalid URL — use default port */ } } } return 443; } function resolveProtocol(config) { if (config) { const mm = config.mattermost || (config.channels && config.channels.mattermost) || {}; if ((mm.baseUrl || '').startsWith('http://')) return 'http'; } return 'https'; } // --- BUILD CONFIG --- const ocConfig = loadConfig(); const CONFIG = { host: resolveHost(ocConfig), port: resolvePort(ocConfig), protocol: resolveProtocol(ocConfig), token: resolveToken(ocConfig), channel_id: options.channel || process.env.MM_CHANNEL_ID || process.env.CHANNEL_ID, }; if (!CONFIG.token) { console.error('Error: No bot token found.'); console.error(' Set MM_BOT_TOKEN, use --token, or configure openclaw.json'); process.exit(1); } // --- HTTP REQUEST --- function request(method, apiPath, data) { const transport = CONFIG.protocol === 'https' ? https : http; return new Promise((resolve, reject) => { const req = transport.request( { hostname: CONFIG.host, port: CONFIG.port, path: '/api/v4' + apiPath, method, headers: { Authorization: `Bearer ${CONFIG.token}`, 'Content-Type': 'application/json' }, }, (res) => { let body = ''; res.on('data', (chunk) => (body += chunk)); res.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) { try { resolve(JSON.parse(body)); } catch (_e) { resolve(body); } } else { let msg = `HTTP ${res.statusCode}`; try { msg = JSON.parse(body).message || msg; } catch (_e) { /* use default msg */ } reject(new Error(msg)); } }); }, ); req.on('error', (e) => reject(e)); if (data) req.write(JSON.stringify(data)); req.end(); }); } // --- RICH ATTACHMENT HELPERS --- const RICH_STYLES = { create: { color: '#FFA500', prefix: '⏳', status: '🔄 Running' }, update: { color: '#FFA500', prefix: '🔄', status: '🔄 Running' }, complete: { color: '#36A64F', prefix: '✅', status: '✅ Complete' }, error: { color: '#DC3545', prefix: '❌', status: '❌ Error' }, }; function buildAttachment(cmd, text) { const style = RICH_STYLES[cmd] || RICH_STYLES.update; // eslint-disable-line security/detect-object-injection const agentName = options.agent || 'unknown'; // Split text: first line = title, rest = log body const lines = text.split('\n'); const title = `${style.prefix} ${lines[0]}`; const body = lines.length > 1 ? lines.slice(1).join('\n') : ''; return { color: style.color, title: title, text: body || undefined, fields: [ { short: true, title: 'Agent', value: agentName }, { short: true, title: 'Status', value: style.status }, ], }; } // --- COMMANDS --- async function createPost(text, cmd) { if (!CONFIG.channel_id) { console.error('Error: Channel ID required. Use --channel or set MM_CHANNEL_ID.'); process.exit(1); } if (!text || !text.trim()) { console.error('Error: Message text is required for create.'); process.exit(1); } try { let payload; if (options.rich) { payload = { channel_id: CONFIG.channel_id, message: '', props: { attachments: [buildAttachment(cmd || 'create', text)] }, }; } else { payload = { channel_id: CONFIG.channel_id, message: text }; } if (options.replyTo) payload.root_id = options.replyTo; const result = await request('POST', '/posts', payload); console.log(result.id); } catch (e) { console.error('Error (create):', e.message); process.exit(1); } } async function updatePost(postId, text, cmd) { if (!postId) { console.error('Error: Post ID is required for update.'); process.exit(1); } if (!text || !text.trim()) { console.error('Error: Message text is required for update.'); process.exit(1); } try { if (options.rich) { await request('PUT', `/posts/${postId}`, { id: postId, message: '', props: { attachments: [buildAttachment(cmd || 'update', text)] }, }); } else { const current = await request('GET', `/posts/${postId}`); await request('PUT', `/posts/${postId}`, { id: postId, message: text, props: current.props, }); } console.log('updated'); } catch (e) { console.error('Error (update):', e.message); process.exit(1); } } async function deletePost(postId) { if (!postId) { console.error('Error: Post ID is required for delete.'); process.exit(1); } try { await request('DELETE', `/posts/${postId}`); console.log('deleted'); } catch (e) { console.error('Error (delete):', e.message); process.exit(1); } } // --- CLI ROUTER --- if (command === 'start-watcher' || command === 'stop-watcher') { // Pass-through to watcher-manager.js const { spawnSync } = require('child_process'); const watcherPath = path.join(__dirname, 'watcher-manager.js'); const subCmd = command === 'start-watcher' ? 'start' : 'stop'; const result = spawnSync(process.execPath, [watcherPath, subCmd], { stdio: 'inherit', env: process.env, }); process.exit(result.status || 0); } else if (command === 'create') { createPost(otherArgs.join(' '), 'create'); } else if (command === 'update') { updatePost(otherArgs[0], otherArgs.slice(1).join(' '), 'update'); } else if (command === 'complete') { updatePost(otherArgs[0], otherArgs.slice(1).join(' '), 'complete'); } else if (command === 'error') { updatePost(otherArgs[0], otherArgs.slice(1).join(' '), 'error'); } else if (command === 'delete') { deletePost(otherArgs[0]); } else { console.log('Usage:'); console.log(' live-status [options] create '); console.log(' live-status [options] update '); console.log(' live-status [options] complete '); console.log(' live-status [options] error '); console.log(' live-status [options] delete '); console.log(' live-status start-watcher (pass-through to watcher-manager start)'); console.log(' live-status stop-watcher (pass-through to watcher-manager stop)'); console.log(''); console.log('Options:'); console.log(' --rich Use rich message attachments (colored cards)'); console.log(' --channel ID Target channel'); console.log(' --reply-to ID Post as thread reply'); console.log(' --agent NAME Use bot token mapped to this agent'); console.log(' --token TOKEN Explicit bot token (overrides all)'); console.log(' --host HOST Mattermost hostname'); console.log(''); console.log('Rich mode colors:'); console.log(' create/update → Orange (running)'); console.log(' complete → Green (done)'); console.log(' error → Red (failed)'); process.exit(1); }