swarmclaw.js
1 #!/usr/bin/env node 2 'use strict' 3 /* eslint-disable @typescript-eslint/no-require-imports */ 4 5 const path = require('node:path') 6 const { spawnSync } = require('node:child_process') 7 8 // Legacy TS CLI groups/actions that provide richer, command-specific options. 9 const TS_CLI_ACTIONS = Object.freeze({ 10 agents: new Set(['list', 'get']), 11 tasks: new Set(['list', 'get', 'create', 'update', 'delete', 'archive']), 12 schedules: new Set(['list', 'get', 'create']), 13 runs: new Set(['list', 'get']), 14 sessions: new Set(['list', 'get', 'create', 'update', 'delete', 'history', 'mailbox', 'stop']), 15 memory: new Set(['get', 'search', 'store', 'maintenance']), 16 'memory-images': new Set(['get']), 17 setup: new Set(['init', 'check-provider', 'doctor', 'openclaw-device']), 18 connectors: new Set(['list', 'get', 'create', 'update', 'delete', 'start', 'stop', 'repair']), 19 webhooks: new Set(['list', 'get', 'create', 'update', 'delete', 'trigger']), 20 }) 21 22 const LEGACY_TS_CLI_ALIAS_MAP = Object.freeze({ 23 '--base-url': '--url', 24 '--access-key': '--key', 25 }) 26 27 function shouldUseLegacyTsCli(argv) { 28 const group = argv[0] 29 const action = argv[1] 30 31 // Default to mapped CLI for top-level help/version and unknown groups. 32 if (!group || group.startsWith('-')) return false 33 34 const actions = TS_CLI_ACTIONS[group] 35 if (!actions) return false 36 37 // Prefer mapped CLI for group help so all API-backed actions are discoverable. 38 if (!action || action === 'help' || action.startsWith('-')) return false 39 40 return actions.has(action) 41 } 42 43 function supportsStripTypes() { 44 return process.allowedNodeEnvironmentFlags.has('--experimental-strip-types') 45 } 46 47 function hasTsxRuntime() { 48 try { 49 require.resolve('tsx/package.json') 50 return true 51 } catch { 52 return false 53 } 54 } 55 56 function buildLegacyTsCliArgs(cliPath, argv, options = {}) { 57 const stripTypesSupported = options.supportsStripTypes ?? supportsStripTypes() 58 if (stripTypesSupported) { 59 return ['--no-warnings', '--experimental-strip-types', cliPath, ...argv] 60 } 61 62 const tsxAvailable = options.hasTsxRuntime ?? hasTsxRuntime() 63 if (tsxAvailable) { 64 return ['--no-warnings', '--import', 'tsx', cliPath, ...argv] 65 } 66 67 return null 68 } 69 70 function normalizeLegacyTsCliArgv(argv) { 71 const normalized = [] 72 73 for (const token of argv) { 74 if (!token.startsWith('--')) { 75 normalized.push(token) 76 continue 77 } 78 79 const eqIndex = token.indexOf('=') 80 const flag = eqIndex > -1 ? token.slice(0, eqIndex) : token 81 const mappedFlag = LEGACY_TS_CLI_ALIAS_MAP[flag] 82 83 if (!mappedFlag) { 84 normalized.push(token) 85 continue 86 } 87 88 if (eqIndex > -1) { 89 normalized.push(`${mappedFlag}=${token.slice(eqIndex + 1)}`) 90 } else { 91 normalized.push(mappedFlag) 92 } 93 } 94 95 return normalized 96 } 97 98 function runLegacyTsCli(argv) { 99 const cliPath = path.join(__dirname, '..', 'src', 'cli', 'index.ts') 100 const args = buildLegacyTsCliArgs(cliPath, normalizeLegacyTsCliArgv(argv)) 101 const env = normalizeLegacyCliEnv(process.env) 102 if (!args) { 103 process.stderr.write('Legacy CLI commands require Node 22.6+ or an available local tsx runtime.\n') 104 return 1 105 } 106 const child = spawnSync( 107 process.execPath, 108 args, 109 { stdio: 'inherit', env }, 110 ) 111 112 if (child.error) { 113 process.stderr.write(`${child.error.message}\n`) 114 return 1 115 } 116 if (typeof child.status === 'number') return child.status 117 return 1 118 } 119 120 function normalizeLegacyCliEnv(env) { 121 const nextEnv = { ...env } 122 if (!nextEnv.SWARMCLAW_URL && nextEnv.SWARMCLAW_BASE_URL) { 123 nextEnv.SWARMCLAW_URL = nextEnv.SWARMCLAW_BASE_URL 124 } 125 if (!nextEnv.SWARMCLAW_ACCESS_KEY) { 126 const key = nextEnv.SWARMCLAW_API_KEY || nextEnv.SC_ACCESS_KEY || '' 127 if (key) nextEnv.SWARMCLAW_ACCESS_KEY = key 128 } 129 return nextEnv 130 } 131 132 function printPackageVersion() { 133 const pkg = require('../package.json') 134 process.stdout.write(`${pkg.name || 'swarmclaw'} ${pkg.version || '0.0.0'}\n`) 135 } 136 137 function printVersionHelp() { 138 process.stdout.write(` 139 Usage: swarmclaw version 140 141 Show the installed SwarmClaw package version. 142 `.trim() + '\n') 143 } 144 145 async function runMappedCli(argv) { 146 const cliPath = path.join(__dirname, '..', 'src', 'cli', 'index.js') 147 const cliModule = await import(cliPath) 148 const runCli = cliModule.runCli || (cliModule.default && cliModule.default.runCli) 149 if (typeof runCli !== 'function') { 150 throw new Error('Unable to load API-mapped CLI runtime') 151 } 152 return runCli(argv) 153 } 154 155 async function runHelp(argv) { 156 const [target, ...rest] = argv 157 if (!target) { 158 const code = await runMappedCli(['--help']) 159 process.exitCode = typeof code === 'number' ? code : 1 160 return 161 } 162 163 if (target === 'run' || target === 'start' || target === 'stop' || target === 'status' || target === 'server') { 164 await require('./server-cmd.js').main(['--help']) 165 return 166 } 167 if (target === 'daemon') { 168 await require('./daemon-cmd.js').main(['--help']) 169 return 170 } 171 if (target === 'worker') { 172 require('./worker-cmd.js').main(['--help']) 173 return 174 } 175 if (target === 'doctor') { 176 require('./doctor-cmd.js').main(['--help']) 177 return 178 } 179 if (target === 'update') { 180 require('./update-cmd.js').main(['--help']) 181 return 182 } 183 if (target === 'version') { 184 printVersionHelp() 185 return 186 } 187 188 const forwarded = rest.includes('--help') || rest.includes('-h') 189 ? [target, ...rest] 190 : [target, ...rest, '--help'] 191 const code = shouldUseLegacyTsCli(forwarded) 192 ? runLegacyTsCli(forwarded) 193 : await runMappedCli(forwarded) 194 195 process.exitCode = typeof code === 'number' ? code : 1 196 } 197 198 async function main() { 199 const argv = process.argv.slice(2) 200 const top = argv[0] 201 202 // Default to 'server' when invoked with no arguments. 203 if (!top) { 204 await require('./server-cmd.js').main([]) 205 return 206 } 207 208 if (top === '-v') { 209 printPackageVersion() 210 return 211 } 212 213 if (top === 'version' && argv.length === 1) { 214 printPackageVersion() 215 return 216 } 217 218 if (top === 'help') { 219 await runHelp(argv.slice(1)) 220 return 221 } 222 223 // Route local lifecycle/maintenance commands to CJS scripts (no TS dependency). 224 if (top === 'server') { 225 await require('./server-cmd.js').main(argv.slice(1)) 226 return 227 } 228 if (top === 'daemon') { 229 const subcommand = argv[1] 230 if (!subcommand || subcommand === 'run' || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') { 231 await require('./daemon-cmd.js').main(argv.slice(1)) 232 return 233 } 234 } 235 if (top === 'run' || top === 'start') { 236 await require('./server-cmd.js').main(argv.slice(1)) 237 return 238 } 239 if (top === 'status' || top === 'stop') { 240 await require('./server-cmd.js').main([top, ...argv.slice(1)]) 241 return 242 } 243 if (top === 'worker') { 244 require('./worker-cmd.js').main() 245 return 246 } 247 if (top === 'doctor') { 248 require('./doctor-cmd.js').main(argv.slice(1)) 249 return 250 } 251 if (top === 'update') { 252 require('./update-cmd.js').main() 253 return 254 } 255 256 const code = shouldUseLegacyTsCli(argv) 257 ? runLegacyTsCli(argv) 258 : await runMappedCli(argv) 259 260 process.exitCode = typeof code === 'number' ? code : 1 261 } 262 263 if (require.main === module) { 264 void main().catch((err) => { 265 process.stderr.write(`${err?.message || String(err)}\n`) 266 process.exit(1) 267 }) 268 } 269 270 module.exports = { 271 buildLegacyTsCliArgs, 272 hasTsxRuntime, 273 normalizeLegacyTsCliArgv, 274 TS_CLI_ACTIONS, 275 normalizeLegacyCliEnv, 276 printPackageVersion, 277 supportsStripTypes, 278 shouldUseLegacyTsCli, 279 }