@clawnch/clawtomaton 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,456 +1,1078 @@
1
1
  /**
2
- * Skill: hummingbot — Market making via Hummingbot integration.
2
+ * Skill: hummingbot — Professional market making via Hummingbot integration.
3
3
  *
4
- * Hummingbot is an open-source HFT framework (17k+ stars, 40+ exchange connectors).
5
- * This skill lets the agent manage Hummingbot instances for professional
6
- * market-making strategies on Clawnch tokens.
4
+ * Full-featured integration with the Hummingbot API server covering all 14
5
+ * upstream MCP tool capabilities: connector setup, multi-server config,
6
+ * portfolio, order placement, leverage/position mode, executors (with
7
+ * preferences), market data (all order book query types), controllers,
8
+ * bots, gateway (container/config/swaps/CLMM), and history search.
7
9
  *
8
- * Integration approach: External engine via Docker/CLI + Hummingbot's API.
9
- * The agent controls strategy parameters; Hummingbot executes the trades.
10
- *
11
- * Supported strategies:
12
- * - pure_market_making: Provide liquidity with configurable spread/order size
13
- * - avellaneda_market_making: Optimal market making (Avellaneda-Stoikov model)
14
- * - grid: Grid trading with defined price levels
15
- * - twap: Time-weighted average price execution
16
- *
17
- * Prerequisites:
18
- * - Docker installed and running
19
- * - Hummingbot image: docker pull hummingbot/hummingbot:latest
20
- * - Or Hummingbot MCP server running locally
10
+ * Also includes Clawnch-specific strategy templates.
21
11
  *
22
12
  * @see https://github.com/hummingbot/hummingbot
23
13
  * @see https://github.com/hummingbot/mcp
24
14
  */
15
+ import { HummingbotClient } from '@clawnch/clawncher-sdk';
25
16
  // ============================================================================
26
- // Hummingbot Docker management
17
+ // Lazy client singleton
27
18
  // ============================================================================
28
- /** Container name prefix for Hummingbot instances */
29
- const HB_CONTAINER_PREFIX = 'clawnch-hb';
30
- /** Regex for valid Docker container names: alphanumeric, dash, underscore */
31
- const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
32
- /** Regex for valid Ethereum addresses */
33
- const ETH_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
34
- /**
35
- * Sanitize a string for use in shell commands.
36
- * Only allows Docker-safe characters: alphanumeric, dash, underscore, dot.
37
- * Throws if the name contains invalid characters.
38
- */
39
- function sanitizeName(name) {
40
- if (!SAFE_NAME_RE.test(name) || name.length > 128) {
41
- throw new Error(`Invalid name "${name}": must be 1-128 chars, alphanumeric/dash/underscore/dot, start with alphanumeric`);
19
+ let _client = null;
20
+ function getClient(ctx) {
21
+ if (!_client) {
22
+ const config = {
23
+ apiUrl: process.env.HUMMINGBOT_API_URL ?? 'http://localhost:8000',
24
+ username: process.env.HUMMINGBOT_USERNAME ?? 'admin',
25
+ password: process.env.HUMMINGBOT_PASSWORD ?? 'admin',
26
+ timeout: parseInt(process.env.HUMMINGBOT_TIMEOUT ?? '30000', 10),
27
+ maxRetries: parseInt(process.env.HUMMINGBOT_MAX_RETRIES ?? '3', 10),
28
+ retryDelay: parseInt(process.env.HUMMINGBOT_RETRY_DELAY ?? '2000', 10),
29
+ };
30
+ _client = new HummingbotClient(config);
42
31
  }
43
- return name;
32
+ return _client;
44
33
  }
45
- /**
46
- * Execute a shell command and return stdout.
47
- * All arguments must be pre-sanitized before being interpolated into the command.
48
- */
49
- async function exec(command) {
50
- const { execSync } = await import('child_process');
51
- try {
52
- return execSync(command, { encoding: 'utf8', timeout: 30000 }).trim();
53
- }
54
- catch (err) {
55
- throw new Error(`Command failed: ${command}\n${err.stderr ?? err.message}`);
56
- }
57
- }
58
- /**
59
- * Check if Docker is available and Hummingbot image exists.
60
- */
61
- async function checkPrerequisites() {
62
- let docker = false;
63
- let image = false;
64
- let mcp = false;
65
- try {
66
- await exec('docker info --format "{{.ID}}"');
67
- docker = true;
68
- }
69
- catch { /* Docker not available */ }
70
- if (docker) {
71
- try {
72
- const images = await exec('docker images hummingbot/hummingbot --format "{{.Tag}}"');
73
- image = images.length > 0;
74
- }
75
- catch { /* Image not found */ }
76
- }
77
- // Check for MCP server
78
- try {
79
- const response = await fetch('http://localhost:8000/health', { signal: AbortSignal.timeout(2000) });
80
- mcp = response.ok;
81
- }
82
- catch { /* MCP not running */ }
83
- return { docker, image, mcp };
84
- }
85
- /**
86
- * Generate a Hummingbot strategy config file content.
87
- */
88
- function generateStrategyConfig(strategy, params, tokenAddress, walletAddress) {
89
- const base = {
90
- strategy,
91
- exchange: 'uniswap_base_base',
92
- market: `${tokenAddress}-WETH`,
93
- };
94
- switch (strategy) {
95
- case 'pure_market_making':
96
- return yamlify({
97
- ...base,
98
- bid_spread: params.bid_spread ?? 0.01,
99
- ask_spread: params.ask_spread ?? 0.01,
100
- order_amount: params.order_amount ?? 0.001,
101
- order_refresh_time: params.order_refresh_time ?? 30,
102
- inventory_skew_enabled: true,
103
- inventory_target_base_pct: params.inventory_target ?? 0.5,
104
- filled_order_delay: 10,
105
- });
106
- case 'avellaneda_market_making':
107
- return yamlify({
108
- ...base,
109
- risk_factor: params.risk_factor ?? 0.5,
110
- order_amount: params.order_amount ?? 0.001,
111
- order_refresh_time: params.order_refresh_time ?? 10,
112
- inventory_target_base_pct: params.inventory_target ?? 0.5,
113
- min_spread: params.min_spread ?? 0.002,
114
- max_spread: params.max_spread ?? 0.05,
115
- vol_to_spread_multiplier: params.vol_multiplier ?? 1.0,
116
- execution_timeframe: 'infinite',
117
- });
118
- case 'grid':
119
- return yamlify({
120
- ...base,
121
- n_levels: params.levels ?? 5,
122
- grid_price_ceiling: params.price_ceiling ?? 0,
123
- grid_price_floor: params.price_floor ?? 0,
124
- order_amount: params.order_amount ?? 0.001,
125
- order_refresh_time: params.order_refresh_time ?? 300,
126
- start_order_spread: params.start_spread ?? 0.01,
127
- });
128
- case 'twap':
129
- return yamlify({
130
- ...base,
131
- trade_side: params.side ?? 'buy',
132
- target_asset_amount: params.total_amount ?? 0.01,
133
- order_step_size: params.step_size ?? 0.001,
134
- order_delay_time: params.delay_seconds ?? 60,
135
- is_time_span_execution: true,
136
- cancel_order_wait_time: params.cancel_wait ?? 30,
137
- });
138
- default:
139
- return yamlify(base);
140
- }
141
- }
142
- /** Simple YAML serializer for config objects */
143
- function yamlify(obj) {
144
- const lines = [];
145
- for (const [key, value] of Object.entries(obj)) {
146
- if (value === undefined || value === null)
147
- continue;
148
- if (typeof value === 'boolean') {
149
- lines.push(`${key}: ${value ? 'true' : 'false'}`);
150
- }
151
- else if (typeof value === 'number') {
152
- lines.push(`${key}: ${value}`);
153
- }
154
- else {
155
- lines.push(`${key}: "${value}"`);
156
- }
157
- }
158
- return lines.join('\n');
159
- }
160
- /**
161
- * List running Hummingbot instances managed by Clawtomaton.
162
- */
163
- async function listInstances() {
164
- try {
165
- const output = await exec(`docker ps -a --filter "name=${HB_CONTAINER_PREFIX}" --format "{{.Names}}|{{.Status}}|{{.CreatedAt}}"`);
166
- if (!output)
167
- return [];
168
- return output.split('\n').filter(Boolean).map(line => {
169
- const [name, status, created] = line.split('|');
170
- return { name, status, created };
171
- });
172
- }
173
- catch {
174
- return [];
175
- }
34
+ /** Helper to split comma-separated strings into arrays */
35
+ function splitCsv(val) {
36
+ if (!val)
37
+ return undefined;
38
+ return String(val).split(',').map(s => s.trim()).filter(Boolean);
176
39
  }
177
40
  // ============================================================================
178
41
  // Skill definition
179
42
  // ============================================================================
180
43
  export const hummingbotSkill = {
181
44
  name: 'hummingbot',
182
- description: 'Market making via Hummingbot. Start/stop/configure professional trading bots for Clawnch tokens. ' +
183
- 'Strategies: pure_market_making, avellaneda_market_making (optimal), grid, twap. ' +
184
- 'Requires Docker with hummingbot/hummingbot image, or Hummingbot MCP server.',
45
+ description: 'Professional market making and trading via Hummingbot. ' +
46
+ 'Actions: "status", "portfolio", "order", "leverage", "executor", "market_data", ' +
47
+ '"controller", "bot", "gateway", "history", "templates", "connector", "servers", ' +
48
+ '"backtest", "discovery", "archived", "scripts", "analytics", "accounts". ' +
49
+ 'Requires Hummingbot API server (default: localhost:8000).',
185
50
  parameters: [
186
51
  {
187
52
  name: 'action',
188
53
  type: 'string',
189
- description: 'Action: "status" (check prerequisites + running instances), "start" (launch a strategy), ' +
190
- '"stop" (stop an instance), "list" (list running instances), "logs" (get instance logs), "configure" (generate strategy config).',
54
+ description: 'Action: "status", "portfolio", "order", "leverage", "executor", "market_data", ' +
55
+ '"controller", "bot", "gateway", "history", "templates", "connector", "servers", ' +
56
+ '"backtest", "discovery", "archived", "scripts", "analytics", "accounts".',
191
57
  required: true,
192
58
  },
193
59
  {
194
- name: 'strategy',
195
- type: 'string',
196
- description: 'Strategy name for start/configure: "pure_market_making" (simple spread), ' +
197
- '"avellaneda_market_making" (optimal, Avellaneda-Stoikov model), "grid" (grid trading), "twap" (time-weighted execution).',
198
- required: false,
199
- },
200
- {
201
- name: 'token_address',
202
- type: 'string',
203
- description: 'Token address to market-make. Uses agent\'s deployed token by default.',
204
- required: false,
205
- },
206
- {
207
- name: 'instance_name',
208
- type: 'string',
209
- description: 'Instance name for stop/logs. Auto-generated if not provided for start.',
210
- required: false,
211
- },
212
- {
213
- name: 'bid_spread',
214
- type: 'number',
215
- description: 'Bid spread as decimal (0.01 = 1%). Default: 0.01.',
216
- required: false,
217
- },
218
- {
219
- name: 'ask_spread',
220
- type: 'number',
221
- description: 'Ask spread as decimal (0.01 = 1%). Default: 0.01.',
222
- required: false,
223
- },
224
- {
225
- name: 'order_amount',
60
+ name: 'sub_action',
226
61
  type: 'string',
227
- description: 'Order size in base token (e.g. "0.001" ETH). Default: 0.001.',
228
- required: false,
229
- },
230
- {
231
- name: 'risk_factor',
232
- type: 'number',
233
- description: 'Risk factor for Avellaneda strategy (0-1). Lower = wider spreads, lower risk. Default: 0.5.',
234
- required: false,
235
- },
236
- {
237
- name: 'levels',
238
- type: 'number',
239
- description: 'Number of grid levels for grid strategy. Default: 5.',
62
+ description: 'Sub-action for compound actions. ' +
63
+ 'executor: create|search|stop|logs|positions|get_preferences|save_preferences|reset_preferences|clear_position|types|type_config|summary|get_by_id. ' +
64
+ 'bot: deploy|status|logs|stop|stop_controllers|start_controllers|history|runs|archive. ' +
65
+ 'gateway: container_status|container_start|container_stop|container_restart|container_logs|' +
66
+ 'config_list|config_get|config_update|config_add|config_delete|' +
67
+ 'swap_quote|swap_execute|swap_search|swap_status|' +
68
+ 'clmm_list|clmm_info|clmm_open|clmm_close|clmm_fees|clmm_positions. ' +
69
+ 'market_data: prices|candles|historical_candles|funding|orderbook|vwap|add_pair|remove_pair. ' +
70
+ 'history: orders|perp_positions|clmm_positions. ' +
71
+ 'templates: list|describe|build. ' +
72
+ 'connector: list|setup|delete. ' +
73
+ 'controller: list|describe|upsert|delete. ' +
74
+ 'servers: list|add|modify|set_default|remove. ' +
75
+ 'discovery: connectors|config_map|trading_rules|order_types. ' +
76
+ 'archived: list|summary|performance|trades|orders. ' +
77
+ 'scripts: list|get|upsert|delete|configs|get_config|upsert_config|delete_config|template. ' +
78
+ 'analytics: history|distribution|accounts_distribution|funding_payments|rates|rate. ' +
79
+ 'accounts: list|credentials|create|delete.',
240
80
  required: false,
241
81
  },
82
+ // --- Shared ---
83
+ { name: 'connector_name', type: 'string', description: 'Exchange connector name.', required: false },
84
+ { name: 'trading_pair', type: 'string', description: 'Trading pair (e.g. "ETH-USDT").', required: false },
85
+ { name: 'trading_pairs', type: 'string', description: 'Comma-separated trading pairs.', required: false },
86
+ { name: 'account_name', type: 'string', description: 'Account name.', required: false },
87
+ { name: 'account_names', type: 'string', description: 'Comma-separated account names.', required: false },
88
+ { name: 'connector_names', type: 'string', description: 'Comma-separated connector names for filtering.', required: false },
89
+ // --- Order ---
90
+ { name: 'side', type: 'string', description: 'Trade side: "BUY" or "SELL".', required: false },
91
+ { name: 'amount', type: 'string', description: 'Order amount (base currency, or "$100" for USD value).', required: false },
92
+ { name: 'order_type', type: 'string', description: 'Order type: "MARKET", "LIMIT", "LIMIT_MAKER".', required: false },
93
+ { name: 'price', type: 'string', description: 'Price for limit orders.', required: false },
94
+ { name: 'position_action', type: 'string', description: 'Position action for perps: "OPEN" or "CLOSE".', required: false },
95
+ // --- Leverage ---
96
+ { name: 'leverage', type: 'number', description: 'Leverage multiplier for perpetuals.', required: false },
97
+ { name: 'position_mode', type: 'string', description: 'Position mode: "HEDGE" or "ONE-WAY".', required: false },
98
+ // --- Executor ---
99
+ { name: 'executor_type', type: 'string', description: 'Executor type: grid_executor, dca_executor, position_executor, order_executor.', required: false },
100
+ { name: 'executor_id', type: 'string', description: 'Executor ID for stop/logs/search.', required: false },
101
+ { name: 'executor_config', type: 'string', description: 'JSON string of executor/controller configuration.', required: false },
102
+ { name: 'keep_position', type: 'boolean', description: 'Keep position when stopping executor.', required: false },
103
+ { name: 'preferences_content', type: 'string', description: 'Markdown content for save_preferences.', required: false },
104
+ // --- Bot ---
105
+ { name: 'bot_name', type: 'string', description: 'Bot name.', required: false },
106
+ { name: 'controllers_config', type: 'string', description: 'Comma-separated controller config/names.', required: false },
107
+ // --- Market data ---
108
+ { name: 'interval', type: 'string', description: 'Candle interval: "1m", "5m", "15m", "30m", "1h", "4h", "1d".', required: false },
109
+ { name: 'days', type: 'number', description: 'Days of historical data for candles.', required: false },
110
+ { name: 'query_type', type: 'string', description: 'Order book query type: snapshot, volume_for_price, price_for_volume, quote_volume_for_price, price_for_quote_volume.', required: false },
111
+ { name: 'query_value', type: 'number', description: 'Query value for order book queries (price or volume).', required: false },
112
+ { name: 'is_buy', type: 'boolean', description: 'Side for order book queries (default: true).', required: false },
113
+ // --- Controller ---
114
+ { name: 'target', type: 'string', description: 'Controller target: "controller" or "config".', required: false },
115
+ { name: 'controller_type', type: 'string', description: 'Controller type: directional_trading, market_making, generic.', required: false },
116
+ { name: 'controller_name', type: 'string', description: 'Controller name.', required: false },
117
+ { name: 'controller_code', type: 'string', description: 'Python code for controller upsert.', required: false },
118
+ { name: 'config_name', type: 'string', description: 'Config name for controller operations.', required: false },
119
+ { name: 'config_data', type: 'string', description: 'JSON config data for controller upsert.', required: false },
120
+ { name: 'confirm_override', type: 'boolean', description: 'Confirm overwrite for controller/connector.', required: false },
121
+ // --- Templates ---
122
+ { name: 'template', type: 'string', description: 'Strategy template name.', required: false },
123
+ // --- History ---
124
+ { name: 'limit', type: 'number', description: 'Max results.', required: false },
125
+ { name: 'offset', type: 'number', description: 'Pagination offset.', required: false },
126
+ { name: 'status', type: 'string', description: 'Status filter (OPEN, CLOSED, FILLED, CANCELED, RUNNING, TERMINATED, etc).', required: false },
127
+ { name: 'start_time', type: 'number', description: 'Start time (unix seconds) for history.', required: false },
128
+ { name: 'end_time', type: 'number', description: 'End time (unix seconds) for history.', required: false },
129
+ // --- Gateway ---
130
+ { name: 'network', type: 'string', description: 'Network for gateway (e.g. "solana-mainnet-beta").', required: false },
131
+ { name: 'pool_address', type: 'string', description: 'Pool address for CLMM.', required: false },
132
+ { name: 'position_address', type: 'string', description: 'Position NFT address for CLMM.', required: false },
133
+ { name: 'wallet_address', type: 'string', description: 'Wallet address for gateway operations.', required: false },
134
+ { name: 'slippage_pct', type: 'string', description: 'Slippage tolerance percentage (e.g. "1.0").', required: false },
135
+ { name: 'lower_price', type: 'string', description: 'Lower price bound for CLMM open_position.', required: false },
136
+ { name: 'upper_price', type: 'string', description: 'Upper price bound for CLMM open_position.', required: false },
137
+ { name: 'base_token_amount', type: 'string', description: 'Base token amount for CLMM.', required: false },
138
+ { name: 'quote_token_amount', type: 'string', description: 'Quote token amount for CLMM.', required: false },
139
+ { name: 'transaction_hash', type: 'string', description: 'Transaction hash for swap status lookup.', required: false },
140
+ // --- Gateway config ---
141
+ { name: 'resource_type', type: 'string', description: 'Gateway config resource: chains, networks, tokens, connectors, pools, wallets.', required: false },
142
+ { name: 'chain', type: 'string', description: 'Blockchain chain name.', required: false },
143
+ { name: 'private_key', type: 'string', description: 'Private key for wallet operations.', required: false },
144
+ { name: 'token_address', type: 'string', description: 'Token contract address.', required: false },
145
+ { name: 'token_symbol', type: 'string', description: 'Token symbol.', required: false },
146
+ { name: 'token_decimals', type: 'number', description: 'Token decimals.', required: false },
147
+ { name: 'pool_type', type: 'string', description: 'Pool type: CLMM or AMM.', required: false },
148
+ { name: 'search', type: 'string', description: 'Search/filter term.', required: false },
149
+ // --- Connector setup ---
150
+ { name: 'credentials', type: 'string', description: 'JSON credentials for connector setup.', required: false },
151
+ // --- Server config ---
152
+ { name: 'host', type: 'string', description: 'API server host for servers action.', required: false },
153
+ { name: 'port', type: 'number', description: 'API server port.', required: false },
154
+ { name: 'server_name', type: 'string', description: 'Server name for servers action.', required: false },
155
+ // --- Backtesting ---
156
+ { name: 'config', type: 'string', description: 'Config name or JSON for backtesting.', required: false },
157
+ { name: 'resolution', type: 'string', description: 'Backtesting resolution (1m, 5m, 1h).', required: false },
158
+ { name: 'trade_cost', type: 'number', description: 'Trade cost as decimal (0.0006 = 0.06%).', required: false },
159
+ // --- Scripts ---
160
+ { name: 'script_name', type: 'string', description: 'Script name.', required: false },
161
+ { name: 'script_content', type: 'string', description: 'Python source code for script upsert.', required: false },
162
+ // --- Rate Oracle ---
163
+ { name: 'db_path', type: 'string', description: 'Archived bot database path.', required: false },
164
+ // --- Active Orders / Trades ---
165
+ { name: 'client_order_id', type: 'string', description: 'Client order ID for cancellation.', required: false },
166
+ { name: 'trade_types', type: 'string', description: 'Comma-separated trade types filter.', required: false },
167
+ { name: 'verbose', type: 'boolean', description: 'Verbose output for bot history.', required: false },
168
+ { name: 'run_status', type: 'string', description: 'Bot run status filter.', required: false },
169
+ { name: 'volume', type: 'number', description: 'Volume for VWAP calculation.', required: false },
170
+ // --- Portfolio ---
171
+ { name: 'refresh', type: 'boolean', description: 'Force refresh portfolio from exchanges.', required: false },
172
+ { name: 'as_distribution', type: 'boolean', description: 'Show portfolio as distribution percentages.', required: false },
173
+ { name: 'include_balances', type: 'boolean', description: 'Include token balances in portfolio.', required: false },
174
+ { name: 'include_perp_positions', type: 'boolean', description: 'Include perp positions in portfolio.', required: false },
175
+ { name: 'include_lp_positions', type: 'boolean', description: 'Include LP positions in portfolio.', required: false },
176
+ { name: 'include_active_orders', type: 'boolean', description: 'Include active orders in portfolio.', required: false },
177
+ // --- Gateway container ---
178
+ { name: 'passphrase', type: 'string', description: 'Gateway container passphrase.', required: false },
179
+ { name: 'image', type: 'string', description: 'Docker image for gateway/bot.', required: false },
180
+ { name: 'tail', type: 'number', description: 'Number of log lines.', required: false },
242
181
  ],
243
182
  execute: async (params, ctx) => {
244
183
  try {
184
+ const client = getClient(ctx);
245
185
  const action = params.action;
246
186
  switch (action) {
247
187
  // ================================================================
248
- // Status: check prerequisites and running instances
188
+ // Status
249
189
  // ================================================================
250
190
  case 'status': {
251
- const prereqs = await checkPrerequisites();
252
- const instances = prereqs.docker ? await listInstances() : [];
191
+ const prereqs = await client.checkPrerequisites();
192
+ const dockerInstances = prereqs.docker ? await client.listDockerInstances() : [];
253
193
  const lines = [
254
194
  'Hummingbot Status:',
255
- ` Docker: ${prereqs.docker ? 'available' : 'NOT FOUND — install Docker to use Hummingbot'}`,
256
- ` Image: ${prereqs.image ? 'hummingbot/hummingbot installed' : 'NOT FOUND — run: docker pull hummingbot/hummingbot:latest'}`,
257
- ` MCP Server: ${prereqs.mcp ? 'running at localhost:8000' : 'not running'}`,
258
- ` Active instances: ${instances.length}`,
195
+ ` API Server: ${prereqs.api ? `connected at ${process.env.HUMMINGBOT_API_URL ?? 'localhost:8000'}` : 'NOT CONNECTED'}`,
196
+ ` Docker: ${prereqs.docker ? 'available' : 'NOT FOUND'}`,
197
+ ` HB Image: ${prereqs.image ? 'installed' : 'not found'}`,
198
+ ` MCP Server: ${prereqs.mcp ? 'running' : 'not running'}`,
259
199
  ];
260
- if (instances.length > 0) {
261
- lines.push('');
262
- lines.push('Running instances:');
263
- for (const inst of instances) {
200
+ if (prereqs.api) {
201
+ lines.push('', 'Full capabilities available: portfolio, order, leverage, executor, market_data, controller, bot, gateway, history, templates, connector, servers');
202
+ }
203
+ else {
204
+ lines.push('', 'Set HUMMINGBOT_API_URL, HUMMINGBOT_USERNAME, HUMMINGBOT_PASSWORD to connect.');
205
+ }
206
+ if (dockerInstances.length > 0) {
207
+ lines.push('', `Docker instances (${dockerInstances.length}):`);
208
+ for (const inst of dockerInstances) {
264
209
  lines.push(` ${inst.name}: ${inst.status} (created ${inst.created})`);
265
210
  }
266
211
  }
267
- if (!prereqs.docker && !prereqs.mcp) {
268
- lines.push('');
269
- lines.push('To get started:');
270
- lines.push(' 1. Install Docker: https://docs.docker.com/get-docker/');
271
- lines.push(' 2. Pull image: docker pull hummingbot/hummingbot:latest');
272
- lines.push(' 3. Or install Hummingbot MCP: https://github.com/hummingbot/mcp');
273
- }
274
212
  return { callId: '', success: true, result: lines.join('\n') };
275
213
  }
276
214
  // ================================================================
277
- // Start: launch a market-making instance
215
+ // Portfolio
216
+ // ================================================================
217
+ case 'portfolio': {
218
+ const result = await client.getPortfolioOverview({
219
+ accountNames: splitCsv(params.account_names),
220
+ connectorNames: splitCsv(params.connector_names) ?? (params.connector_name ? [params.connector_name] : undefined),
221
+ includeBalances: params.include_balances ?? true,
222
+ includePerpPositions: params.include_perp_positions ?? true,
223
+ includeLPPositions: params.include_lp_positions ?? true,
224
+ includeActiveOrders: params.include_active_orders ?? true,
225
+ asDistribution: params.as_distribution ?? false,
226
+ refresh: params.refresh ?? true,
227
+ });
228
+ return { callId: '', success: true, result };
229
+ }
230
+ // ================================================================
231
+ // Order
232
+ // ================================================================
233
+ case 'order': {
234
+ const connectorName = params.connector_name;
235
+ const tradingPair = params.trading_pair;
236
+ const side = params.side;
237
+ const amount = params.amount;
238
+ if (!connectorName || !tradingPair || !side || !amount) {
239
+ return {
240
+ callId: '', success: false, result: null,
241
+ error: 'Required: connector_name, trading_pair, side (BUY/SELL), amount.',
242
+ };
243
+ }
244
+ const result = await client.placeOrder({
245
+ connectorName,
246
+ tradingPair,
247
+ tradeType: side.toUpperCase(),
248
+ amount,
249
+ orderType: params.order_type?.toUpperCase() ?? 'MARKET',
250
+ price: params.price,
251
+ positionAction: params.position_action?.toUpperCase() ?? 'OPEN',
252
+ accountName: params.account_name,
253
+ });
254
+ return {
255
+ callId: '', success: result.success, result: result.result,
256
+ error: result.success ? undefined : result.result,
257
+ };
258
+ }
259
+ // ================================================================
260
+ // Leverage / Position Mode
261
+ // ================================================================
262
+ case 'leverage': {
263
+ const accountName = params.account_name ?? 'master_account';
264
+ const connectorName = params.connector_name;
265
+ if (!connectorName) {
266
+ return { callId: '', success: false, result: null, error: 'connector_name required for leverage.' };
267
+ }
268
+ const result = await client.setPositionModeAndLeverage({
269
+ accountName,
270
+ connectorName,
271
+ tradingPair: params.trading_pair,
272
+ positionMode: params.position_mode?.toUpperCase(),
273
+ leverage: params.leverage,
274
+ });
275
+ return { callId: '', success: true, result };
276
+ }
277
+ // ================================================================
278
+ // Executor
279
+ // ================================================================
280
+ case 'executor': {
281
+ const subAction = params.sub_action ?? 'search';
282
+ switch (subAction) {
283
+ case 'create': {
284
+ const configStr = params.executor_config;
285
+ if (!configStr) {
286
+ const executorType = params.executor_type;
287
+ if (executorType) {
288
+ const result = await client.getExecutorTypeConfig(executorType);
289
+ return { callId: '', success: true, result };
290
+ }
291
+ const result = await client.getExecutorTypes();
292
+ return { callId: '', success: true, result };
293
+ }
294
+ let config;
295
+ try {
296
+ config = JSON.parse(configStr);
297
+ }
298
+ catch {
299
+ return { callId: '', success: false, result: null, error: 'executor_config must be valid JSON.' };
300
+ }
301
+ const result = await client.createExecutor(config);
302
+ return { callId: '', success: true, result };
303
+ }
304
+ case 'search': {
305
+ const result = await client.searchExecutors({
306
+ executorId: params.executor_id,
307
+ executorTypes: params.executor_type ? [params.executor_type] : undefined,
308
+ connectorNames: splitCsv(params.connector_names) ?? (params.connector_name ? [params.connector_name] : undefined),
309
+ tradingPairs: splitCsv(params.trading_pairs) ?? (params.trading_pair ? [params.trading_pair] : undefined),
310
+ status: params.status,
311
+ limit: params.limit ?? 50,
312
+ });
313
+ return { callId: '', success: true, result };
314
+ }
315
+ case 'stop': {
316
+ if (!params.executor_id) {
317
+ return { callId: '', success: false, result: null, error: 'executor_id required to stop.' };
318
+ }
319
+ const result = await client.stopExecutor(params.executor_id, params.keep_position ?? false);
320
+ return { callId: '', success: true, result };
321
+ }
322
+ case 'logs': {
323
+ if (!params.executor_id) {
324
+ return { callId: '', success: false, result: null, error: 'executor_id required for logs.' };
325
+ }
326
+ const result = await client.getExecutorLogs(params.executor_id);
327
+ return { callId: '', success: true, result };
328
+ }
329
+ case 'positions': {
330
+ const result = await client.getPositionsSummary(params.connector_name, params.trading_pair);
331
+ return { callId: '', success: true, result };
332
+ }
333
+ case 'clear_position': {
334
+ if (!params.connector_name || !params.trading_pair) {
335
+ return { callId: '', success: false, result: null, error: 'connector_name and trading_pair required.' };
336
+ }
337
+ const result = await client.clearPosition(params.connector_name, params.trading_pair);
338
+ return { callId: '', success: true, result };
339
+ }
340
+ case 'types': {
341
+ const result = await client.getExecutorTypes();
342
+ return { callId: '', success: true, result };
343
+ }
344
+ case 'type_config': {
345
+ if (!params.executor_type)
346
+ return { callId: '', success: false, result: null, error: 'executor_type required.' };
347
+ const result = await client.getExecutorTypeConfig(params.executor_type);
348
+ return { callId: '', success: true, result };
349
+ }
350
+ case 'summary': {
351
+ const result = await client.getExecutorsSummary();
352
+ return { callId: '', success: true, result };
353
+ }
354
+ case 'get_by_id': {
355
+ if (!params.executor_id)
356
+ return { callId: '', success: false, result: null, error: 'executor_id required.' };
357
+ const result = await client.getExecutorById(params.executor_id);
358
+ return { callId: '', success: true, result };
359
+ }
360
+ default:
361
+ return {
362
+ callId: '', success: false, result: null,
363
+ error: `Unknown executor sub_action: "${subAction}". Use: create, search, stop, logs, positions, clear_position, types, type_config, summary, get_by_id.`,
364
+ };
365
+ }
366
+ }
367
+ // ================================================================
368
+ // Market Data
369
+ // ================================================================
370
+ case 'market_data': {
371
+ const subAction = params.sub_action ?? 'prices';
372
+ const connectorName = params.connector_name;
373
+ if (!connectorName) {
374
+ return { callId: '', success: false, result: null, error: 'connector_name required for market_data.' };
375
+ }
376
+ switch (subAction) {
377
+ case 'prices': {
378
+ const pairs = splitCsv(params.trading_pairs) ?? splitCsv(params.trading_pair) ?? [];
379
+ if (pairs.length === 0) {
380
+ return { callId: '', success: false, result: null, error: 'trading_pairs or trading_pair required.' };
381
+ }
382
+ const result = await client.getPrices(connectorName, pairs);
383
+ return { callId: '', success: true, result };
384
+ }
385
+ case 'candles': {
386
+ if (!params.trading_pair) {
387
+ return { callId: '', success: false, result: null, error: 'trading_pair required for candles.' };
388
+ }
389
+ const result = await client.getCandles(connectorName, params.trading_pair, params.interval ?? '1h', params.days ?? 30);
390
+ return { callId: '', success: true, result };
391
+ }
392
+ case 'funding': {
393
+ if (!params.trading_pair) {
394
+ return { callId: '', success: false, result: null, error: 'trading_pair required for funding.' };
395
+ }
396
+ const result = await client.getFundingRate(connectorName, params.trading_pair);
397
+ return { callId: '', success: true, result };
398
+ }
399
+ case 'orderbook': {
400
+ if (!params.trading_pair) {
401
+ return { callId: '', success: false, result: null, error: 'trading_pair required for orderbook.' };
402
+ }
403
+ const result = await client.getOrderBook(connectorName, params.trading_pair, params.query_type ?? 'snapshot', params.query_value, params.is_buy ?? true);
404
+ return { callId: '', success: true, result };
405
+ }
406
+ case 'historical_candles': {
407
+ if (!params.trading_pair)
408
+ return { callId: '', success: false, result: null, error: 'trading_pair required.' };
409
+ if (!params.start_time || !params.end_time)
410
+ return { callId: '', success: false, result: null, error: 'start_time and end_time required.' };
411
+ const result = await client.getHistoricalCandles({
412
+ connectorName,
413
+ tradingPair: params.trading_pair,
414
+ interval: params.interval ?? '1h',
415
+ startTime: params.start_time,
416
+ endTime: params.end_time,
417
+ });
418
+ return { callId: '', success: true, result };
419
+ }
420
+ case 'vwap': {
421
+ if (!params.trading_pair)
422
+ return { callId: '', success: false, result: null, error: 'trading_pair required.' };
423
+ if (!params.volume)
424
+ return { callId: '', success: false, result: null, error: 'volume required for VWAP.' };
425
+ const result = await client.getVWAP(connectorName, params.trading_pair, params.is_buy ?? true, params.volume);
426
+ return { callId: '', success: true, result };
427
+ }
428
+ case 'add_pair': {
429
+ if (!params.trading_pair)
430
+ return { callId: '', success: false, result: null, error: 'trading_pair required.' };
431
+ const result = await client.addTradingPair(connectorName, params.trading_pair, params.account_name);
432
+ return { callId: '', success: true, result };
433
+ }
434
+ case 'remove_pair': {
435
+ if (!params.trading_pair)
436
+ return { callId: '', success: false, result: null, error: 'trading_pair required.' };
437
+ const result = await client.removeTradingPair(connectorName, params.trading_pair, params.account_name);
438
+ return { callId: '', success: true, result };
439
+ }
440
+ default:
441
+ return {
442
+ callId: '', success: false, result: null,
443
+ error: `Unknown market_data sub_action: "${subAction}". Use: prices, candles, historical_candles, funding, orderbook, vwap, add_pair, remove_pair.`,
444
+ };
445
+ }
446
+ }
447
+ // ================================================================
448
+ // Controller management
278
449
  // ================================================================
279
- case 'start': {
280
- const strategy = params.strategy;
281
- if (!strategy) {
282
- return { callId: '', success: false, result: null, error: 'strategy is required for start. Options: pure_market_making, avellaneda_market_making, grid, twap.' };
450
+ case 'controller': {
451
+ const subAction = params.sub_action ?? 'list';
452
+ const target = params.target ?? 'controller';
453
+ switch (subAction) {
454
+ case 'list': {
455
+ if (target === 'config') {
456
+ const result = await client.listControllerConfigs();
457
+ return { callId: '', success: true, result };
458
+ }
459
+ const result = await client.listControllers(params.controller_type);
460
+ return { callId: '', success: true, result };
461
+ }
462
+ case 'describe': {
463
+ if (target === 'config' && params.config_name) {
464
+ const result = await client.getControllerConfig(params.config_name);
465
+ return { callId: '', success: true, result };
466
+ }
467
+ if (!params.controller_name) {
468
+ return { callId: '', success: false, result: null, error: 'controller_name required.' };
469
+ }
470
+ const result = await client.getController(params.controller_name);
471
+ return { callId: '', success: true, result };
472
+ }
473
+ case 'upsert': {
474
+ if (target === 'config') {
475
+ if (!params.config_name || !params.config_data) {
476
+ return { callId: '', success: false, result: null, error: 'config_name and config_data required.' };
477
+ }
478
+ let configData;
479
+ try {
480
+ configData = JSON.parse(params.config_data);
481
+ }
482
+ catch {
483
+ return { callId: '', success: false, result: null, error: 'config_data must be valid JSON.' };
484
+ }
485
+ const result = await client.upsertControllerConfig(params.config_name, configData);
486
+ return { callId: '', success: true, result };
487
+ }
488
+ if (!params.controller_name) {
489
+ return { callId: '', success: false, result: null, error: 'controller_name required.' };
490
+ }
491
+ const result = await client.upsertController(params.controller_name, {
492
+ controllerType: params.controller_type,
493
+ controllerCode: params.controller_code,
494
+ });
495
+ return { callId: '', success: true, result };
496
+ }
497
+ case 'delete': {
498
+ if (target === 'config' && params.config_name) {
499
+ const result = await client.deleteControllerConfig(params.config_name);
500
+ return { callId: '', success: true, result };
501
+ }
502
+ if (!params.controller_name) {
503
+ return { callId: '', success: false, result: null, error: 'controller_name required.' };
504
+ }
505
+ const result = await client.deleteController(params.controller_name);
506
+ return { callId: '', success: true, result };
507
+ }
508
+ default:
509
+ return { callId: '', success: false, result: null, error: `Unknown controller sub_action: "${subAction}". Use: list, describe, upsert, delete.` };
283
510
  }
284
- const validStrategies = ['pure_market_making', 'avellaneda_market_making', 'grid', 'twap'];
285
- if (!validStrategies.includes(strategy)) {
286
- return { callId: '', success: false, result: null, error: `Invalid strategy "${strategy}". Valid: ${validStrategies.join(', ')}` };
511
+ }
512
+ // ================================================================
513
+ // Bot management
514
+ // ================================================================
515
+ case 'bot': {
516
+ const subAction = params.sub_action ?? 'status';
517
+ switch (subAction) {
518
+ case 'deploy': {
519
+ const botName = params.bot_name;
520
+ const configsStr = params.controllers_config;
521
+ if (!botName || !configsStr) {
522
+ return { callId: '', success: false, result: null, error: 'bot_name and controllers_config required.' };
523
+ }
524
+ const result = await client.deployBot({
525
+ botName,
526
+ controllersConfig: configsStr.split(',').map(s => s.trim()),
527
+ accountName: params.account_name,
528
+ image: params.image,
529
+ });
530
+ return { callId: '', success: true, result };
531
+ }
532
+ case 'status': {
533
+ const result = await client.getBotsStatus();
534
+ return { callId: '', success: true, result };
535
+ }
536
+ case 'logs': {
537
+ if (!params.bot_name) {
538
+ return { callId: '', success: false, result: null, error: 'bot_name required.' };
539
+ }
540
+ const result = await client.getBotLogs({
541
+ botName: params.bot_name,
542
+ limit: params.limit ?? 50,
543
+ searchTerm: params.search,
544
+ });
545
+ return { callId: '', success: true, result };
546
+ }
547
+ case 'stop': {
548
+ if (!params.bot_name) {
549
+ return { callId: '', success: false, result: null, error: 'bot_name required.' };
550
+ }
551
+ const result = await client.stopBot(params.bot_name);
552
+ return { callId: '', success: true, result };
553
+ }
554
+ case 'stop_controllers':
555
+ case 'start_controllers': {
556
+ if (!params.bot_name || !params.controllers_config) {
557
+ return { callId: '', success: false, result: null, error: 'bot_name and controllers_config required.' };
558
+ }
559
+ const names = params.controllers_config.split(',').map(s => s.trim());
560
+ const result = subAction === 'stop_controllers'
561
+ ? await client.stopControllers(params.bot_name, names)
562
+ : await client.startControllers(params.bot_name, names);
563
+ return { callId: '', success: true, result };
564
+ }
565
+ case 'history': {
566
+ const name = params.bot_name;
567
+ if (!name)
568
+ return { callId: '', success: false, result: null, error: 'bot_name required.' };
569
+ const result = await client.getBotHistory(name, params.days ?? 0, params.verbose ?? false);
570
+ return { callId: '', success: true, result };
571
+ }
572
+ case 'runs': {
573
+ const result = await client.getBotRuns({
574
+ botName: params.bot_name,
575
+ accountName: params.account_name,
576
+ runStatus: params.run_status,
577
+ limit: params.limit,
578
+ offset: params.offset,
579
+ });
580
+ return { callId: '', success: true, result };
581
+ }
582
+ case 'archive': {
583
+ const name = params.bot_name;
584
+ if (!name)
585
+ return { callId: '', success: false, result: null, error: 'bot_name required.' };
586
+ const result = await client.archiveBot(name);
587
+ return { callId: '', success: true, result };
588
+ }
589
+ default:
590
+ return {
591
+ callId: '', success: false, result: null,
592
+ error: `Unknown bot sub_action: "${subAction}". Use: deploy, status, logs, stop, stop_controllers, start_controllers, history, runs, archive.`,
593
+ };
594
+ }
595
+ }
596
+ // ================================================================
597
+ // Gateway: container, config, swaps, CLMM
598
+ // ================================================================
599
+ case 'gateway': {
600
+ const subAction = params.sub_action ?? 'container_status';
601
+ // --- Container lifecycle ---
602
+ if (subAction === 'container_status') {
603
+ const result = await client.getGatewayStatus();
604
+ return { callId: '', success: true, result };
287
605
  }
288
- const prereqs = await checkPrerequisites();
289
- if (!prereqs.docker) {
290
- return { callId: '', success: false, result: null, error: 'Docker not available. Install Docker first or start the Hummingbot MCP server.' };
606
+ if (subAction === 'container_start') {
607
+ const result = await client.startGateway({
608
+ passphrase: params.passphrase ?? 'clawnch',
609
+ image: params.image ?? 'hummingbot/gateway:latest',
610
+ port: params.port,
611
+ environment: undefined,
612
+ });
613
+ return { callId: '', success: true, result };
291
614
  }
292
- if (!prereqs.image) {
293
- return { callId: '', success: false, result: null, error: 'Hummingbot image not found. Run: docker pull hummingbot/hummingbot:latest' };
615
+ if (subAction === 'container_stop') {
616
+ const result = await client.stopGateway();
617
+ return { callId: '', success: true, result };
294
618
  }
295
- const tokenAddr = params.token_address ?? ctx.identity.tokenAddress;
296
- if (!tokenAddr) {
297
- return { callId: '', success: false, result: null, error: 'No token address. Deploy a token first or specify token_address.' };
619
+ if (subAction === 'container_logs') {
620
+ const result = await client.getGatewayLogs(params.tail);
621
+ return { callId: '', success: true, result };
298
622
  }
299
- if (!ETH_ADDRESS_RE.test(tokenAddr)) {
300
- return { callId: '', success: false, result: null, error: `Invalid token address: "${tokenAddr}". Must be a 0x-prefixed 40-hex-char address.` };
623
+ // --- Gateway config ---
624
+ if (subAction === 'config_chains') {
625
+ const result = await client.listGatewayChains();
626
+ return { callId: '', success: true, result };
301
627
  }
302
- if (!ctx.identity.address || !ETH_ADDRESS_RE.test(ctx.identity.address)) {
303
- return { callId: '', success: false, result: null, error: 'Agent identity has no valid wallet address.' };
628
+ if (subAction === 'config_networks') {
629
+ const result = await client.listGatewayNetworks(params.chain);
630
+ return { callId: '', success: true, result };
304
631
  }
305
- const rawInstanceName = params.instance_name ??
306
- `${HB_CONTAINER_PREFIX}-${strategy.replace(/_/g, '-')}-${Date.now().toString(36)}`;
307
- let instanceName;
308
- try {
309
- instanceName = sanitizeName(rawInstanceName);
310
- }
311
- catch (err) {
312
- return { callId: '', success: false, result: null, error: err.message };
313
- }
314
- // Generate strategy config
315
- const config = generateStrategyConfig(strategy, params, tokenAddr, ctx.identity.address);
316
- // Write config to temp file and mount into container
317
- const { writeFileSync, mkdirSync } = await import('fs');
318
- const { join } = await import('path');
319
- const configDir = join(process.env.HOME ?? '/tmp', '.clawncher', 'hummingbot', instanceName);
320
- mkdirSync(configDir, { recursive: true });
321
- writeFileSync(join(configDir, `${strategy}.yml`), config);
322
- // Start Hummingbot container
323
- // Uses bridge networking (not --network host) to limit container's access
324
- const dockerCmd = [
325
- 'docker run -d',
326
- `--name "${instanceName}"`,
327
- `-v "${configDir}":/home/hummingbot/conf`,
328
- `hummingbot/hummingbot:latest`,
329
- ].join(' ');
330
- try {
331
- const containerId = await exec(dockerCmd);
632
+ if (subAction === 'config_connectors') {
633
+ const result = await client.listGatewayConnectors();
634
+ return { callId: '', success: true, result };
635
+ }
636
+ if (subAction === 'config_tokens') {
637
+ const result = await client.listGatewayTokens(params.chain, params.network, params.search);
638
+ return { callId: '', success: true, result };
639
+ }
640
+ if (subAction === 'config_wallets') {
641
+ const result = await client.listGatewayWallets(params.chain);
642
+ return { callId: '', success: true, result };
643
+ }
644
+ if (subAction === 'config_add_token') {
645
+ const result = await client.addGatewayToken({
646
+ chain: params.chain,
647
+ network: params.network,
648
+ tokenAddress: params.token_address,
649
+ tokenSymbol: params.token_symbol,
650
+ tokenDecimals: params.token_decimals,
651
+ tokenName: params.token_name,
652
+ });
653
+ return { callId: '', success: true, result };
654
+ }
655
+ if (subAction === 'config_add_wallet') {
656
+ const result = await client.addGatewayWallet(params.chain, params.private_key);
657
+ return { callId: '', success: true, result };
658
+ }
659
+ // --- Swaps ---
660
+ if (subAction === 'swap_quote') {
661
+ const result = await client.getGatewayAmmPrice({
662
+ connector: params.connector_name,
663
+ chain: params.chain,
664
+ network: params.network,
665
+ tradingPair: params.trading_pair,
666
+ side: params.side?.toUpperCase(),
667
+ amount: params.amount,
668
+ });
669
+ return { callId: '', success: true, result };
670
+ }
671
+ if (subAction === 'swap_execute') {
672
+ const result = await client.executeGatewayAmmTrade({
673
+ connector: params.connector_name,
674
+ chain: params.chain,
675
+ network: params.network,
676
+ tradingPair: params.trading_pair,
677
+ side: params.side?.toUpperCase(),
678
+ amount: params.amount,
679
+ slippagePct: params.slippage_pct,
680
+ walletAddress: params.wallet_address ?? ctx.identity.address,
681
+ });
682
+ return { callId: '', success: true, result };
683
+ }
684
+ // --- CLMM ---
685
+ if (subAction === 'clmm_list') {
686
+ const result = await client.listCLMMPools({
687
+ connector: params.connector_name,
688
+ chain: params.chain,
689
+ network: params.network,
690
+ limit: params.limit ?? 50,
691
+ searchTerm: params.search,
692
+ });
693
+ return { callId: '', success: true, result };
694
+ }
695
+ if (subAction === 'clmm_info') {
696
+ const result = await client.getCLMMPoolInfo({
697
+ connector: params.connector_name,
698
+ chain: params.chain,
699
+ network: params.network,
700
+ poolAddress: params.pool_address,
701
+ });
702
+ return { callId: '', success: true, result };
703
+ }
704
+ if (subAction === 'clmm_positions') {
705
+ const result = await client.getCLMMPositions({
706
+ connector: params.connector_name,
707
+ chain: params.chain,
708
+ network: params.network,
709
+ walletAddress: params.wallet_address ?? ctx.identity.address,
710
+ poolAddress: params.pool_address,
711
+ });
712
+ return { callId: '', success: true, result };
713
+ }
714
+ if (subAction === 'clmm_open') {
715
+ const result = await client.addCLMMLiquidity({
716
+ connector: params.connector_name,
717
+ chain: params.chain,
718
+ network: params.network,
719
+ poolAddress: params.pool_address,
720
+ walletAddress: params.wallet_address ?? ctx.identity.address,
721
+ lowerPrice: params.lower_price,
722
+ upperPrice: params.upper_price,
723
+ baseTokenAmount: params.base_token_amount,
724
+ quoteTokenAmount: params.quote_token_amount,
725
+ slippagePct: params.slippage_pct,
726
+ });
727
+ return { callId: '', success: true, result };
728
+ }
729
+ if (subAction === 'clmm_close') {
730
+ const result = await client.removeCLMMLiquidity({
731
+ connector: params.connector_name,
732
+ chain: params.chain,
733
+ network: params.network,
734
+ positionAddress: params.position_address,
735
+ walletAddress: params.wallet_address ?? ctx.identity.address,
736
+ slippagePct: params.slippage_pct,
737
+ });
738
+ return { callId: '', success: true, result };
739
+ }
740
+ return {
741
+ callId: '', success: false, result: null,
742
+ error: `Unknown gateway sub_action: "${subAction}". Prefixes: container_, config_, swap_, clmm_.`,
743
+ };
744
+ }
745
+ // ================================================================
746
+ // History
747
+ // ================================================================
748
+ case 'history': {
749
+ const dataType = params.sub_action ?? 'orders';
750
+ const validTypes = ['orders', 'perp_positions', 'clmm_positions'];
751
+ if (!validTypes.includes(dataType)) {
332
752
  return {
333
- callId: '',
334
- success: true,
335
- result: [
336
- `Hummingbot instance started.`,
337
- ` Instance: ${instanceName}`,
338
- ` Container: ${containerId.substring(0, 12)}`,
339
- ` Strategy: ${strategy}`,
340
- ` Token: ${tokenAddr}`,
341
- ` Config: ${configDir}/${strategy}.yml`,
342
- '',
343
- `Monitor: use action "logs" with instance_name "${instanceName}"`,
344
- `Stop: use action "stop" with instance_name "${instanceName}"`,
345
- ].join('\n'),
346
- metadata: { instance_name: instanceName, container_id: containerId.substring(0, 12) },
753
+ callId: '', success: false, result: null,
754
+ error: `Invalid history sub_action: "${dataType}". Use: orders, perp_positions, clmm_positions.`,
347
755
  };
348
756
  }
349
- catch (err) {
350
- return { callId: '', success: false, result: null, error: `Failed to start container: ${err.message}` };
351
- }
757
+ const result = await client.searchHistory({
758
+ dataType: dataType,
759
+ accountNames: splitCsv(params.account_names),
760
+ connectorNames: splitCsv(params.connector_names) ?? (params.connector_name ? [params.connector_name] : undefined),
761
+ tradingPairs: splitCsv(params.trading_pairs) ?? (params.trading_pair ? [params.trading_pair] : undefined),
762
+ status: params.status,
763
+ startTime: params.start_time,
764
+ endTime: params.end_time,
765
+ limit: params.limit ?? 50,
766
+ offset: params.offset ?? 0,
767
+ network: params.network,
768
+ walletAddress: params.wallet_address,
769
+ });
770
+ return { callId: '', success: true, result };
352
771
  }
353
772
  // ================================================================
354
- // Stop: terminate a market-making instance
773
+ // Templates
355
774
  // ================================================================
356
- case 'stop': {
357
- const rawStopName = params.instance_name;
358
- if (!rawStopName) {
359
- const instances = await listInstances();
360
- if (instances.length === 0) {
361
- return { callId: '', success: true, result: 'No running Hummingbot instances.' };
775
+ case 'templates': {
776
+ const subAction = params.sub_action ?? 'list';
777
+ if (subAction === 'list') {
778
+ const templates = client.getStrategyTemplates();
779
+ const clawnchTemplates = templates.filter((t) => t.template.startsWith('clawnch_'));
780
+ const genericTemplates = templates.filter((t) => !t.template.startsWith('clawnch_'));
781
+ const lines = [
782
+ 'Clawnch Token Strategy Templates:',
783
+ ...clawnchTemplates.map((t) => ` ${t.template}: ${t.description} (executor: ${t.executorType})`),
784
+ '',
785
+ 'Generic Strategy Templates:',
786
+ ...genericTemplates.map((t) => ` ${t.template}: ${t.description} (executor: ${t.executorType})`),
787
+ '', 'Use sub_action="describe" with template to see details, or sub_action="build" to create.',
788
+ ];
789
+ return { callId: '', success: true, result: lines.join('\n') };
790
+ }
791
+ if (subAction === 'describe') {
792
+ if (!params.template) {
793
+ return { callId: '', success: false, result: null, error: 'template parameter required.' };
794
+ }
795
+ const tmpl = client.getStrategyTemplate(params.template);
796
+ if (!tmpl) {
797
+ return { callId: '', success: false, result: null, error: `Unknown template: ${params.template}` };
362
798
  }
363
799
  return {
364
- callId: '',
365
- success: false,
366
- result: null,
367
- error: `instance_name is required. Running instances:\n${instances.map(i => ` ${i.name}: ${i.status}`).join('\n')}`,
800
+ callId: '', success: true,
801
+ result: `Template: ${tmpl.template}\nDescription: ${tmpl.description}\nExecutor Type: ${tmpl.executorType}\nDefaults: ${JSON.stringify(tmpl.defaultConfig, null, 2)}\nRequired: ${tmpl.requiredOverrides.join(', ')}`,
368
802
  };
369
803
  }
370
- let instanceName;
371
- try {
372
- instanceName = sanitizeName(rawStopName);
804
+ if (subAction === 'build') {
805
+ if (!params.template)
806
+ return { callId: '', success: false, result: null, error: 'template required.' };
807
+ if (!params.executor_config)
808
+ return { callId: '', success: false, result: null, error: 'executor_config (JSON) required.' };
809
+ let overrides;
810
+ try {
811
+ overrides = JSON.parse(params.executor_config);
812
+ }
813
+ catch {
814
+ return { callId: '', success: false, result: null, error: 'executor_config must be valid JSON.' };
815
+ }
816
+ const config = client.buildFromTemplate(params.template, overrides);
817
+ const result = await client.createExecutor(config);
818
+ return { callId: '', success: true, result };
373
819
  }
374
- catch (err) {
375
- return { callId: '', success: false, result: null, error: err.message };
820
+ return { callId: '', success: false, result: null, error: `Unknown templates sub_action: "${subAction}". Use: list, describe, build.` };
821
+ }
822
+ // ================================================================
823
+ // Connector setup
824
+ // ================================================================
825
+ case 'connector': {
826
+ const subAction = params.sub_action ?? 'list';
827
+ if (subAction === 'list') {
828
+ const result = await client.listConnectors();
829
+ return { callId: '', success: true, result };
376
830
  }
831
+ if (subAction === 'config_map') {
832
+ if (!params.connector_name) {
833
+ return { callId: '', success: false, result: null, error: 'connector_name required.' };
834
+ }
835
+ const result = await client.getConnectorConfigMap(params.connector_name);
836
+ return { callId: '', success: true, result };
837
+ }
838
+ if (subAction === 'setup') {
839
+ if (!params.connector_name || !params.credentials) {
840
+ return { callId: '', success: false, result: null, error: 'connector_name and credentials (JSON) required.' };
841
+ }
842
+ let creds;
843
+ try {
844
+ creds = JSON.parse(params.credentials);
845
+ }
846
+ catch {
847
+ return { callId: '', success: false, result: null, error: 'credentials must be valid JSON.' };
848
+ }
849
+ const accountName = params.account_name ?? 'master_account';
850
+ const result = await client.addConnector(accountName, params.connector_name, creds);
851
+ return { callId: '', success: true, result };
852
+ }
853
+ if (subAction === 'trading_rules') {
854
+ if (!params.connector_name) {
855
+ return { callId: '', success: false, result: null, error: 'connector_name required.' };
856
+ }
857
+ const tradingPairs = params.trading_pair ? [params.trading_pair] : undefined;
858
+ const result = await client.getTradingRules(params.connector_name, tradingPairs);
859
+ return { callId: '', success: true, result };
860
+ }
861
+ if (subAction === 'order_types') {
862
+ if (!params.connector_name) {
863
+ return { callId: '', success: false, result: null, error: 'connector_name required.' };
864
+ }
865
+ const result = await client.getOrderTypes(params.connector_name);
866
+ return { callId: '', success: true, result };
867
+ }
868
+ return { callId: '', success: false, result: null, error: `Unknown connector sub_action: "${subAction}". Use: list, config_map, setup, trading_rules, order_types.` };
869
+ }
870
+ // ================================================================
871
+ // Backtesting
872
+ // ================================================================
873
+ case 'backtest': {
874
+ let config = params.config;
377
875
  try {
378
- await exec(`docker stop "${instanceName}"`);
379
- await exec(`docker rm "${instanceName}"`);
380
- return { callId: '', success: true, result: `Stopped and removed instance: ${instanceName}` };
876
+ config = JSON.parse(config);
877
+ }
878
+ catch { /* keep as string (config name) */ }
879
+ const result = await client.runBacktest({
880
+ config,
881
+ startTime: params.start_time,
882
+ endTime: params.end_time,
883
+ resolution: params.resolution,
884
+ tradeCost: params.trade_cost,
885
+ });
886
+ return { callId: '', success: true, result };
887
+ }
888
+ // ================================================================
889
+ // Connector / Market Discovery
890
+ // ================================================================
891
+ case 'discovery': {
892
+ const subAction = params.sub_action;
893
+ if (subAction === 'connectors' || !subAction) {
894
+ const result = await client.listConnectors();
895
+ return { callId: '', success: true, result };
896
+ }
897
+ if (subAction === 'config_map') {
898
+ const result = await client.getConnectorConfigMap(params.connector_name);
899
+ return { callId: '', success: true, result };
381
900
  }
382
- catch (err) {
383
- return { callId: '', success: false, result: null, error: `Failed to stop ${instanceName}: ${err.message}` };
901
+ if (subAction === 'trading_rules') {
902
+ const result = await client.getTradingRules(params.connector_name, splitCsv(params.trading_pairs));
903
+ return { callId: '', success: true, result };
384
904
  }
905
+ if (subAction === 'order_types') {
906
+ const result = await client.getOrderTypes(params.connector_name);
907
+ return { callId: '', success: true, result };
908
+ }
909
+ return { callId: '', success: false, result: null, error: `Unknown discovery sub_action: "${subAction}". Use: connectors, config_map, trading_rules, order_types.` };
385
910
  }
386
911
  // ================================================================
387
- // List: show running instances
912
+ // Archived Bot Analytics
388
913
  // ================================================================
389
- case 'list': {
390
- const instances = await listInstances();
391
- if (instances.length === 0) {
392
- return { callId: '', success: true, result: 'No running Hummingbot instances.' };
914
+ case 'archived': {
915
+ const subAction = params.sub_action;
916
+ if (subAction === 'list' || !subAction) {
917
+ const result = await client.listArchivedBots();
918
+ return { callId: '', success: true, result };
393
919
  }
394
- const lines = instances.map(i => ` ${i.name}: ${i.status} (created ${i.created})`);
395
- return {
396
- callId: '',
397
- success: true,
398
- result: `${instances.length} Hummingbot instance(s):\n${lines.join('\n')}`,
399
- };
920
+ const dbPath = params.db_path;
921
+ if (!dbPath)
922
+ return { callId: '', success: false, result: null, error: 'db_path required for archived bot queries.' };
923
+ if (subAction === 'summary') {
924
+ const result = await client.getArchivedBotSummary(dbPath);
925
+ return { callId: '', success: true, result };
926
+ }
927
+ if (subAction === 'performance') {
928
+ const result = await client.getArchivedBotPerformance(dbPath);
929
+ return { callId: '', success: true, result };
930
+ }
931
+ if (subAction === 'trades') {
932
+ const result = await client.getArchivedBotTrades(dbPath, params.limit ?? 100, params.offset ?? 0);
933
+ return { callId: '', success: true, result };
934
+ }
935
+ if (subAction === 'orders') {
936
+ const result = await client.getArchivedBotOrders(dbPath, params.limit ?? 100, params.offset ?? 0, params.status);
937
+ return { callId: '', success: true, result };
938
+ }
939
+ return { callId: '', success: false, result: null, error: `Unknown archived sub_action: "${subAction}". Use: list, summary, performance, trades, orders.` };
400
940
  }
401
941
  // ================================================================
402
- // Logs: get recent logs from an instance
942
+ // Script Management
403
943
  // ================================================================
404
- case 'logs': {
405
- const rawLogsName = params.instance_name;
406
- if (!rawLogsName) {
407
- return { callId: '', success: false, result: null, error: 'instance_name is required for logs.' };
944
+ case 'scripts': {
945
+ const subAction = params.sub_action;
946
+ if (subAction === 'list' || !subAction) {
947
+ const result = await client.listScripts();
948
+ return { callId: '', success: true, result };
408
949
  }
409
- let instanceName;
410
- try {
411
- instanceName = sanitizeName(rawLogsName);
950
+ if (subAction === 'get') {
951
+ const result = await client.getScript(params.script_name);
952
+ return { callId: '', success: true, result };
412
953
  }
413
- catch (err) {
414
- return { callId: '', success: false, result: null, error: err.message };
954
+ if (subAction === 'upsert') {
955
+ const result = await client.upsertScript(params.script_name, params.script_content);
956
+ return { callId: '', success: true, result };
415
957
  }
416
- try {
417
- const logs = await exec(`docker logs --tail 50 "${instanceName}"`);
418
- return { callId: '', success: true, result: `Last 50 lines from ${instanceName}:\n${logs}` };
958
+ if (subAction === 'delete') {
959
+ const result = await client.deleteScript(params.script_name);
960
+ return { callId: '', success: true, result };
961
+ }
962
+ if (subAction === 'configs') {
963
+ const result = await client.listScriptConfigs();
964
+ return { callId: '', success: true, result };
965
+ }
966
+ if (subAction === 'get_config') {
967
+ const result = await client.getScriptConfig(params.config_name);
968
+ return { callId: '', success: true, result };
969
+ }
970
+ if (subAction === 'upsert_config') {
971
+ let data;
972
+ try {
973
+ data = JSON.parse(params.config_data);
974
+ }
975
+ catch {
976
+ return { callId: '', success: false, result: null, error: 'config_data must be valid JSON.' };
977
+ }
978
+ const result = await client.upsertScriptConfig(params.config_name, data);
979
+ return { callId: '', success: true, result };
980
+ }
981
+ if (subAction === 'delete_config') {
982
+ const result = await client.deleteScriptConfig(params.config_name);
983
+ return { callId: '', success: true, result };
419
984
  }
420
- catch (err) {
421
- return { callId: '', success: false, result: null, error: `Failed to get logs for ${instanceName}: ${err.message}` };
985
+ if (subAction === 'template') {
986
+ const result = await client.getScriptConfigTemplate(params.script_name);
987
+ return { callId: '', success: true, result };
422
988
  }
989
+ return { callId: '', success: false, result: null, error: `Unknown scripts sub_action: "${subAction}". Use: list, get, upsert, delete, configs, get_config, upsert_config, delete_config, template.` };
423
990
  }
424
991
  // ================================================================
425
- // Configure: preview strategy config without starting
992
+ // Portfolio Analytics & Rate Oracle
426
993
  // ================================================================
427
- case 'configure': {
428
- const strategy = params.strategy;
429
- if (!strategy) {
430
- return { callId: '', success: false, result: null, error: 'strategy is required for configure.' };
994
+ case 'analytics': {
995
+ const subAction = params.sub_action;
996
+ if (subAction === 'history') {
997
+ const result = await client.getPortfolioHistory({
998
+ accountNames: splitCsv(params.account_names),
999
+ connectorNames: splitCsv(params.connector_names),
1000
+ startTime: params.start_time,
1001
+ endTime: params.end_time,
1002
+ interval: params.interval,
1003
+ limit: params.limit,
1004
+ });
1005
+ return { callId: '', success: true, result };
431
1006
  }
432
- const tokenAddr = params.token_address ?? ctx.identity.tokenAddress ?? '0x0000000000000000000000000000000000000000';
433
- if (tokenAddr !== '0x0000000000000000000000000000000000000000' && !ETH_ADDRESS_RE.test(tokenAddr)) {
434
- return { callId: '', success: false, result: null, error: `Invalid token address: "${tokenAddr}"` };
1007
+ if (subAction === 'distribution') {
1008
+ const result = await client.getPortfolioDistribution({
1009
+ accountNames: splitCsv(params.account_names),
1010
+ connectorNames: splitCsv(params.connector_names),
1011
+ });
1012
+ return { callId: '', success: true, result };
435
1013
  }
436
- const config = generateStrategyConfig(strategy, params, tokenAddr, ctx.identity.address);
437
- return {
438
- callId: '',
439
- success: true,
440
- result: `Strategy config preview (${strategy}):\n\n${config}\n\nUse action "start" with the same parameters to launch.`,
441
- };
1014
+ if (subAction === 'accounts_distribution') {
1015
+ const result = await client.getAccountsDistribution();
1016
+ return { callId: '', success: true, result };
1017
+ }
1018
+ if (subAction === 'funding_payments') {
1019
+ const result = await client.getFundingPayments({
1020
+ accountNames: splitCsv(params.account_names),
1021
+ connectorNames: splitCsv(params.connector_names),
1022
+ tradingPair: params.trading_pair,
1023
+ limit: params.limit,
1024
+ });
1025
+ return { callId: '', success: true, result };
1026
+ }
1027
+ if (subAction === 'rates') {
1028
+ const pairs = splitCsv(params.trading_pairs);
1029
+ if (!pairs?.length)
1030
+ return { callId: '', success: false, result: null, error: 'trading_pairs required.' };
1031
+ const result = await client.getRates(pairs);
1032
+ return { callId: '', success: true, result };
1033
+ }
1034
+ if (subAction === 'rate') {
1035
+ const result = await client.getRate(params.trading_pair);
1036
+ return { callId: '', success: true, result };
1037
+ }
1038
+ return { callId: '', success: false, result: null, error: `Unknown analytics sub_action: "${subAction}". Use: history, distribution, accounts_distribution, funding_payments, rates, rate.` };
1039
+ }
1040
+ // ================================================================
1041
+ // Account Management
1042
+ // ================================================================
1043
+ case 'accounts': {
1044
+ const subAction = params.sub_action;
1045
+ if (subAction === 'list' || !subAction) {
1046
+ const result = await client.listAccounts();
1047
+ return { callId: '', success: true, result };
1048
+ }
1049
+ if (subAction === 'credentials') {
1050
+ const result = await client.getAccountCredentials(params.account_name);
1051
+ return { callId: '', success: true, result };
1052
+ }
1053
+ if (subAction === 'create') {
1054
+ const result = await client.createAccount(params.account_name);
1055
+ return { callId: '', success: true, result };
1056
+ }
1057
+ if (subAction === 'delete') {
1058
+ const result = await client.deleteAccount(params.account_name);
1059
+ return { callId: '', success: true, result };
1060
+ }
1061
+ return { callId: '', success: false, result: null, error: `Unknown accounts sub_action: "${subAction}". Use: list, credentials, create, delete.` };
442
1062
  }
1063
+ // ================================================================
1064
+ // Unknown
1065
+ // ================================================================
443
1066
  default:
444
1067
  return {
445
- callId: '',
446
- success: false,
447
- result: null,
448
- error: `Unknown hummingbot action: "${action}". Valid: status, start, stop, list, logs, configure.`,
1068
+ callId: '', success: false, result: null,
1069
+ error: `Unknown hummingbot action: "${action}". Valid: status, portfolio, order, leverage, executor, market_data, controller, bot, gateway, history, templates, connector, servers, backtest, discovery, archived, scripts, analytics, accounts.`,
449
1070
  };
450
1071
  }
451
1072
  }
452
1073
  catch (err) {
453
- return { callId: '', success: false, result: null, error: err instanceof Error ? err.message : String(err) };
1074
+ const message = err?.code ? `[${err.code}] ${err.message}` : err?.message ?? String(err);
1075
+ return { callId: '', success: false, result: null, error: message };
454
1076
  }
455
1077
  },
456
1078
  };