@darksol/terminal 0.9.0 → 0.9.1

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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, c
15
15
  [![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-gold.svg)](https://www.gnu.org/licenses/gpl-3.0)
16
16
  [![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-green.svg)](https://nodejs.org/)
17
17
 
18
- - Current release: **0.9.0**
18
+ - Current release: **0.9.1**
19
19
  - Changelog: `CHANGELOG.md`
20
20
 
21
21
  ## Install
@@ -66,6 +66,12 @@ darksol ai chat
66
66
  darksol memory show
67
67
  darksol memory search "preferred chain"
68
68
 
69
+ # Autonomous agent task (ReAct loop)
70
+ darksol agent task "check AERO price and tell me if it's above $2"
71
+ darksol agent task "analyze my portfolio" --max-steps 5
72
+ darksol agent task "swap 0.1 ETH for USDC" --allow-actions
73
+ darksol agent plan "DCA strategy for AERO"
74
+
69
75
  # Agent email
70
76
  darksol mail setup
71
77
  darksol mail send --to user@example.com --subject "Hello"
@@ -123,6 +129,7 @@ ai <prompt> # chat with trading assistant
123
129
  | `dca` | Dollar-cost averaging engine | Gas only |
124
130
  | `soul` | Agent identity & personality configuration | Free |
125
131
  | `memory` | Persistent cross-session memory store | Free |
132
+ | `agent task` | Autonomous ReAct agent loop with tool use | Provider dependent |
126
133
  | `ai` | LLM-powered trading assistant & intent execution | Provider dependent |
127
134
  | `agent` | Secure agent signer (PK-isolated proxy) | Free |
128
135
  | `keys` | Encrypted API key vault (LLMs/data/RPCs) | Free |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,134 @@
1
+ import { getAllConfig, getConfig, setConfig } from '../config/store.js';
2
+ import { createLLM } from '../llm/engine.js';
3
+ import { getRecentMemories, saveMemory } from '../memory/index.js';
4
+ import { createToolExecutor, createToolRegistry, listTools } from './tools.js';
5
+ import { DEFAULT_MAX_STEPS, runAgentLoop } from './loop.js';
6
+
7
+ const AGENT_STATE_KEY = 'agentState';
8
+
9
+ function agentSystemPrompt({ goal, allowActions, maxSteps, recentMemories }) {
10
+ const cfg = getAllConfig();
11
+ return [
12
+ 'You are DARKSOL Terminal agent mode.',
13
+ 'Operate in a bounded ReAct loop and keep outputs terse and operational.',
14
+ `Goal: ${goal}`,
15
+ `Max steps: ${maxSteps}`,
16
+ `Actions enabled: ${allowActions ? 'yes' : 'no'}`,
17
+ `Chain: ${cfg.chain || 'base'}`,
18
+ `Active wallet: ${cfg.activeWallet || '(none)'}`,
19
+ `Slippage: ${cfg.slippage || 0.5}%`,
20
+ recentMemories.length
21
+ ? `Recent memories:\n${recentMemories.map((memory) => `- [${memory.category}] ${memory.content}`).join('\n')}`
22
+ : 'Recent memories: none',
23
+ 'Prefer read-only verification before any action.',
24
+ ].join('\n\n');
25
+ }
26
+
27
+ async function persistAgentMemories(result) {
28
+ const outcome = result.final || result.summary;
29
+ if (outcome) {
30
+ await saveMemory(`Agent outcome: ${outcome}`, 'lesson', 'agent');
31
+ }
32
+ if (result.goal) {
33
+ await saveMemory(`Agent worked on: ${result.goal}`, 'decision', 'agent');
34
+ }
35
+ }
36
+
37
+ function saveAgentStatus(state) {
38
+ const previous = getConfig(AGENT_STATE_KEY) || {};
39
+ setConfig(AGENT_STATE_KEY, {
40
+ ...previous,
41
+ ...state,
42
+ updatedAt: new Date().toISOString(),
43
+ });
44
+ }
45
+
46
+ export async function planAgentGoal(goal, opts = {}) {
47
+ const llm = opts.llm || await createLLM(opts);
48
+ const memories = await getRecentMemories(5);
49
+ llm.setSystemPrompt(agentSystemPrompt({
50
+ goal,
51
+ allowActions: false,
52
+ maxSteps: opts.maxSteps || DEFAULT_MAX_STEPS,
53
+ recentMemories: memories,
54
+ }));
55
+
56
+ const result = await llm.json([
57
+ 'Create a concise execution plan for this goal.',
58
+ `Goal: ${goal}`,
59
+ 'Return JSON only: {"summary":"string","steps":["step 1","step 2"]}',
60
+ ].join('\n\n'), {
61
+ ephemeral: true,
62
+ skipMemoryExtraction: true,
63
+ });
64
+
65
+ const parsed = result.parsed || {};
66
+ const plan = {
67
+ goal,
68
+ summary: String(parsed.summary || `Plan for: ${goal}`),
69
+ steps: Array.isArray(parsed.steps) ? parsed.steps.map((step) => String(step)) : [],
70
+ createdAt: new Date().toISOString(),
71
+ };
72
+
73
+ saveAgentStatus({
74
+ status: 'planned',
75
+ goal,
76
+ summary: plan.summary,
77
+ plan: plan.steps,
78
+ allowActions: false,
79
+ stepsTaken: 0,
80
+ startedAt: null,
81
+ completedAt: null,
82
+ });
83
+ await saveMemory(`Agent plan: ${plan.summary}`, 'decision', 'agent');
84
+ return plan;
85
+ }
86
+
87
+ export async function runAgentTask(goal, opts = {}) {
88
+ const maxSteps = Number(opts.maxSteps) > 0 ? Number(opts.maxSteps) : DEFAULT_MAX_STEPS;
89
+ const allowActions = Boolean(opts.allowActions);
90
+ const llm = opts.llm || await createLLM(opts);
91
+ const recentMemories = await getRecentMemories(5);
92
+ const registry = opts.registry || createToolRegistry(opts.toolDeps);
93
+ const tools = listTools(registry);
94
+ const executeTool = opts.executeTool || createToolExecutor({
95
+ registry,
96
+ allowActions,
97
+ onEvent: opts.onToolEvent,
98
+ });
99
+
100
+ llm.setSystemPrompt(agentSystemPrompt({ goal, allowActions, maxSteps, recentMemories }));
101
+
102
+ const result = await runAgentLoop({
103
+ goal,
104
+ llm,
105
+ tools,
106
+ executeTool,
107
+ maxSteps,
108
+ allowActions,
109
+ onProgress: opts.onProgress,
110
+ saveOutcome: async (state) => {
111
+ saveAgentStatus({
112
+ status: state.status,
113
+ goal: state.goal,
114
+ summary: state.final,
115
+ stepsTaken: state.stepsTaken,
116
+ maxSteps: state.maxSteps,
117
+ allowActions: state.allowActions,
118
+ startedAt: state.startedAt,
119
+ completedAt: state.completedAt,
120
+ stopReason: state.stopReason,
121
+ lastTool: state.steps[state.steps.length - 1]?.action || null,
122
+ plan: state.steps.map((step) => `${step.step}. ${step.action}`),
123
+ });
124
+ await persistAgentMemories(state);
125
+ },
126
+ persistStatus: async (state) => saveAgentStatus(state),
127
+ });
128
+
129
+ return result;
130
+ }
131
+
132
+ export function getAgentStatus() {
133
+ return getConfig(AGENT_STATE_KEY) || null;
134
+ }
@@ -0,0 +1,190 @@
1
+ export const DEFAULT_MAX_STEPS = 10;
2
+
3
+ function normalizeDecision(raw, fallbackStep) {
4
+ if (!raw || typeof raw !== 'object') {
5
+ return {
6
+ thought: 'No structured response returned.',
7
+ action: 'finish',
8
+ actionInput: {},
9
+ final: 'Agent stopped because the model did not return valid JSON.',
10
+ stop: true,
11
+ stopReason: 'invalid_response',
12
+ };
13
+ }
14
+
15
+ return {
16
+ thought: String(raw.thought || '').trim() || `Step ${fallbackStep}`,
17
+ action: String(raw.action || 'finish').trim(),
18
+ actionInput: raw.actionInput && typeof raw.actionInput === 'object' ? raw.actionInput : {},
19
+ final: raw.final ? String(raw.final).trim() : '',
20
+ stop: Boolean(raw.stop),
21
+ stopReason: raw.stopReason ? String(raw.stopReason).trim() : '',
22
+ };
23
+ }
24
+
25
+ function sameAction(a, b) {
26
+ return a && b && a.action === b.action && JSON.stringify(a.actionInput || {}) === JSON.stringify(b.actionInput || {});
27
+ }
28
+
29
+ function buildPrompt({ goal, allowActions, maxSteps, stepNumber, tools, steps }) {
30
+ const history = steps.length === 0
31
+ ? 'No prior steps yet.'
32
+ : steps.map((step) => {
33
+ const bits = [
34
+ `Step ${step.step}`,
35
+ `thought=${step.thought}`,
36
+ `action=${step.action}`,
37
+ `input=${JSON.stringify(step.actionInput || {})}`,
38
+ step.observation ? `observation=${step.observation}` : '',
39
+ ].filter(Boolean);
40
+ return bits.join('\n');
41
+ }).join('\n\n');
42
+
43
+ return [
44
+ 'You are the DARKSOL agent loop controller.',
45
+ `Goal: ${goal}`,
46
+ `Current step: ${stepNumber}/${maxSteps}`,
47
+ `Safe mode: ${allowActions ? 'off' : 'on'}${allowActions ? '' : ' (mutating tools are blocked)'}`,
48
+ 'Available tools:',
49
+ tools.map((tool) => `- ${tool.name}${tool.mutating ? ' [mutating]' : ''}: ${tool.description}`).join('\n'),
50
+ 'Prior step log:',
51
+ history,
52
+ 'Rules:',
53
+ '- Think briefly and act conservatively.',
54
+ '- Use read-only tools first unless the goal clearly requires an action and actions are allowed.',
55
+ '- If you have enough information, return action "finish" with a concise final answer.',
56
+ '- If the last tool was blocked or failed, adjust instead of repeating forever.',
57
+ '- Respond as JSON only.',
58
+ 'JSON schema:',
59
+ '{"thought":"string","action":"tool-name|finish","actionInput":{},"final":"string","stop":false,"stopReason":"string"}',
60
+ ].join('\n\n');
61
+ }
62
+
63
+ export async function runAgentLoop({
64
+ goal,
65
+ llm,
66
+ tools,
67
+ executeTool,
68
+ maxSteps = DEFAULT_MAX_STEPS,
69
+ allowActions = false,
70
+ onProgress = () => {},
71
+ saveOutcome = async () => {},
72
+ persistStatus = async () => {},
73
+ }) {
74
+ const startedAt = new Date().toISOString();
75
+ const steps = [];
76
+
77
+ await persistStatus({
78
+ status: 'running',
79
+ goal,
80
+ allowActions,
81
+ maxSteps,
82
+ startedAt,
83
+ completedAt: null,
84
+ stepsTaken: 0,
85
+ summary: '',
86
+ });
87
+
88
+ for (let stepNumber = 1; stepNumber <= maxSteps; stepNumber += 1) {
89
+ const prompt = buildPrompt({ goal, allowActions, maxSteps, stepNumber, tools, steps });
90
+ onProgress({ type: 'step-start', step: stepNumber, maxSteps });
91
+
92
+ const response = await llm.json(prompt, {
93
+ ephemeral: true,
94
+ skipMemoryExtraction: true,
95
+ });
96
+ const decision = normalizeDecision(response.parsed, stepNumber);
97
+ const stepLog = {
98
+ step: stepNumber,
99
+ thought: decision.thought,
100
+ action: decision.action,
101
+ actionInput: decision.actionInput,
102
+ observation: '',
103
+ };
104
+ steps.push(stepLog);
105
+ onProgress({ type: 'thought', step: stepNumber, thought: decision.thought, action: decision.action, actionInput: decision.actionInput });
106
+
107
+ const previous = steps.length > 1 ? steps[steps.length - 2] : null;
108
+ const repeatedLoop = sameAction(stepLog, previous) && sameAction(previous, steps.length > 2 ? steps[steps.length - 3] : null);
109
+ if (repeatedLoop) {
110
+ const completedAt = new Date().toISOString();
111
+ const result = {
112
+ status: 'stopped',
113
+ goal,
114
+ allowActions,
115
+ maxSteps,
116
+ startedAt,
117
+ completedAt,
118
+ stepsTaken: steps.length,
119
+ stopReason: 'repeat_guard',
120
+ final: 'Agent stopped after repeating the same step three times.',
121
+ steps,
122
+ };
123
+ await persistStatus({ ...result, summary: result.final });
124
+ await saveOutcome(result);
125
+ return result;
126
+ }
127
+
128
+ if (decision.stop || decision.action === 'finish' || decision.final) {
129
+ const completedAt = new Date().toISOString();
130
+ const final = decision.final || 'Agent stopped without a final answer.';
131
+ const result = {
132
+ status: 'completed',
133
+ goal,
134
+ allowActions,
135
+ maxSteps,
136
+ startedAt,
137
+ completedAt,
138
+ stepsTaken: steps.length,
139
+ stopReason: decision.stopReason || 'final',
140
+ final,
141
+ steps,
142
+ };
143
+ await persistStatus({ ...result, summary: final });
144
+ await saveOutcome(result);
145
+ onProgress({ type: 'final', final });
146
+ return result;
147
+ }
148
+
149
+ const observation = await executeTool(decision.action, decision.actionInput || {});
150
+ stepLog.observation = observation.summary || observation.error || JSON.stringify(observation);
151
+ stepLog.result = observation;
152
+ onProgress({ type: 'observation', step: stepNumber, tool: decision.action, observation });
153
+
154
+ if (observation.blocked) {
155
+ stepLog.observation = observation.error;
156
+ }
157
+
158
+ await persistStatus({
159
+ status: 'running',
160
+ goal,
161
+ allowActions,
162
+ maxSteps,
163
+ startedAt,
164
+ completedAt: null,
165
+ stepsTaken: steps.length,
166
+ lastTool: decision.action,
167
+ lastObservation: stepLog.observation,
168
+ summary: '',
169
+ });
170
+ }
171
+
172
+ const completedAt = new Date().toISOString();
173
+ const final = `Agent reached the step limit (${maxSteps}) before finishing.`;
174
+ const result = {
175
+ status: 'stopped',
176
+ goal,
177
+ allowActions,
178
+ maxSteps,
179
+ startedAt,
180
+ completedAt,
181
+ stepsTaken: steps.length,
182
+ stopReason: 'max_steps',
183
+ final,
184
+ steps,
185
+ };
186
+ await persistStatus({ ...result, summary: final });
187
+ await saveOutcome(result);
188
+ onProgress({ type: 'final', final });
189
+ return result;
190
+ }
@@ -0,0 +1,311 @@
1
+ import fetch from 'node-fetch';
2
+ import { ethers } from 'ethers';
3
+ import { getConfig, getRPC } from '../config/store.js';
4
+ import { loadWallet } from '../wallet/keystore.js';
5
+
6
+ const DEXSCREENER_API = 'https://api.dexscreener.com/latest/dex/search?q=';
7
+ const USDC_ADDRESSES = {
8
+ base: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
9
+ ethereum: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
10
+ arbitrum: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
11
+ optimism: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85',
12
+ polygon: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
13
+ };
14
+ const PORTFOLIO_CHAINS = {
15
+ base: { name: 'Base' },
16
+ ethereum: { name: 'Ethereum' },
17
+ arbitrum: { name: 'Arbitrum' },
18
+ optimism: { name: 'Optimism' },
19
+ polygon: { name: 'Polygon' },
20
+ };
21
+ const ERC20_ABI = [
22
+ 'function balanceOf(address) view returns (uint256)',
23
+ 'function decimals() view returns (uint8)',
24
+ ];
25
+
26
+ function compactNumber(value) {
27
+ const num = Number(value || 0);
28
+ if (!Number.isFinite(num)) return '0';
29
+ if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
30
+ if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
31
+ if (num >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
32
+ return num.toFixed(2);
33
+ }
34
+
35
+ function summarizeResult(result) {
36
+ if (!result) return 'No result';
37
+ if (typeof result === 'string') return result;
38
+ if (result.summary) return result.summary;
39
+ if (result.final) return result.final;
40
+ if (result.error) return result.error;
41
+ if (result.token && result.priceUsd) return `${result.token} at $${result.priceUsd}`;
42
+ return JSON.stringify(result).slice(0, 240);
43
+ }
44
+
45
+ async function fetchBestPair(query, fetchImpl = fetch) {
46
+ const response = await fetchImpl(`${DEXSCREENER_API}${encodeURIComponent(query)}`);
47
+ const data = await response.json();
48
+ const pairs = Array.isArray(data.pairs) ? data.pairs : [];
49
+ if (pairs.length === 0) return null;
50
+ return pairs.sort((a, b) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0))[0];
51
+ }
52
+
53
+ async function getEthPrice(fetchImpl = fetch) {
54
+ try {
55
+ const response = await fetchImpl('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
56
+ const data = await response.json();
57
+ return Number(data.ethereum?.usd) || 3000;
58
+ } catch {
59
+ return 3000;
60
+ }
61
+ }
62
+
63
+ async function readTokenBalance(provider, address, tokenAddress) {
64
+ if (!tokenAddress) return 0;
65
+ try {
66
+ const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
67
+ const raw = await contract.balanceOf(address);
68
+ const decimals = await contract.decimals();
69
+ return Number(ethers.formatUnits(raw, decimals));
70
+ } catch {
71
+ return 0;
72
+ }
73
+ }
74
+
75
+ function requireWallet(walletName) {
76
+ const resolved = walletName || getConfig('activeWallet');
77
+ if (!resolved) {
78
+ throw new Error('No active wallet configured');
79
+ }
80
+ return loadWallet(resolved);
81
+ }
82
+
83
+ function defaultRegistry(deps = {}) {
84
+ const fetchImpl = deps.fetch || fetch;
85
+ const providerFactory = deps.providerFactory || ((chain) => new ethers.JsonRpcProvider(getRPC(chain)));
86
+
87
+ return {
88
+ price: {
89
+ description: 'Fetch a live token price and liquidity snapshot',
90
+ mutating: false,
91
+ handler: async ({ token, query }) => {
92
+ const resolved = token || query;
93
+ if (!resolved) throw new Error('price requires token or query');
94
+ const pair = await fetchBestPair(resolved, fetchImpl);
95
+ if (!pair) return { ok: false, token: resolved, summary: `No market data for ${resolved}` };
96
+ return {
97
+ ok: true,
98
+ token: pair.baseToken?.symbol || resolved.toUpperCase(),
99
+ name: pair.baseToken?.name || resolved,
100
+ chain: pair.chainId,
101
+ dex: pair.dexId,
102
+ priceUsd: Number(pair.priceUsd || 0),
103
+ change24h: Number(pair.priceChange?.h24 || 0),
104
+ liquidityUsd: Number(pair.liquidity?.usd || 0),
105
+ volume24hUsd: Number(pair.volume?.h24 || 0),
106
+ pairAddress: pair.pairAddress,
107
+ summary: `${pair.baseToken?.symbol || resolved} ${Number(pair.priceUsd || 0).toFixed(6)} USD, 24h ${Number(pair.priceChange?.h24 || 0).toFixed(2)}%, liq $${compactNumber(pair.liquidity?.usd)}`,
108
+ };
109
+ },
110
+ },
111
+ gas: {
112
+ description: 'Fetch current gas data for a chain',
113
+ mutating: false,
114
+ handler: async ({ chain }) => {
115
+ const resolvedChain = chain || getConfig('chain') || 'base';
116
+ const provider = providerFactory(resolvedChain);
117
+ const feeData = await provider.getFeeData();
118
+ const gasPriceWei = feeData.gasPrice || 0n;
119
+ const gasPriceGwei = Number(ethers.formatUnits(gasPriceWei, 'gwei'));
120
+ const ethPrice = await getEthPrice(fetchImpl);
121
+ return {
122
+ ok: true,
123
+ chain: resolvedChain,
124
+ gasPriceGwei,
125
+ maxFeeGwei: Number(ethers.formatUnits(feeData.maxFeePerGas || 0n, 'gwei')),
126
+ priorityFeeGwei: Number(ethers.formatUnits(feeData.maxPriorityFeePerGas || 0n, 'gwei')),
127
+ ethPriceUsd: ethPrice,
128
+ summary: `${resolvedChain} gas ${gasPriceGwei.toFixed(2)} gwei`,
129
+ };
130
+ },
131
+ },
132
+ 'wallet-balance': {
133
+ description: 'Read the active or named wallet native and USDC balances',
134
+ mutating: false,
135
+ handler: async ({ wallet, chain }) => {
136
+ const walletData = requireWallet(wallet);
137
+ const resolvedChain = chain || getConfig('chain') || walletData.chain || 'base';
138
+ const provider = providerFactory(resolvedChain);
139
+ const native = Number(ethers.formatEther(await provider.getBalance(walletData.address)));
140
+ const usdc = await readTokenBalance(provider, walletData.address, USDC_ADDRESSES[resolvedChain]);
141
+ return {
142
+ ok: true,
143
+ wallet: walletData.name,
144
+ address: walletData.address,
145
+ chain: resolvedChain,
146
+ native,
147
+ nativeSymbol: 'ETH',
148
+ usdc,
149
+ summary: `${walletData.name} on ${resolvedChain}: ${native.toFixed(6)} ETH and ${usdc.toFixed(2)} USDC`,
150
+ };
151
+ },
152
+ },
153
+ portfolio: {
154
+ description: 'Read wallet balances across supported chains',
155
+ mutating: false,
156
+ handler: async ({ wallet }) => {
157
+ const walletData = requireWallet(wallet);
158
+ const ethPrice = await getEthPrice(fetchImpl);
159
+ const chains = await Promise.all(
160
+ Object.entries(PORTFOLIO_CHAINS).map(async ([chainId, meta]) => {
161
+ try {
162
+ const provider = providerFactory(chainId);
163
+ const native = Number(ethers.formatEther(await provider.getBalance(walletData.address)));
164
+ const usdc = await readTokenBalance(provider, walletData.address, USDC_ADDRESSES[chainId]);
165
+ const totalUsd = native * ethPrice + usdc;
166
+ return { chain: chainId, name: meta.name, native, usdc, totalUsd };
167
+ } catch (error) {
168
+ return { chain: chainId, name: meta.name, native: 0, usdc: 0, totalUsd: 0, error: error.message };
169
+ }
170
+ }),
171
+ );
172
+ const totalUsd = chains.reduce((sum, item) => sum + (item.totalUsd || 0), 0);
173
+ return {
174
+ ok: true,
175
+ wallet: walletData.name,
176
+ address: walletData.address,
177
+ totalUsd,
178
+ chains,
179
+ summary: `${walletData.name} portfolio totals $${totalUsd.toFixed(2)} across ${chains.length} chains`,
180
+ };
181
+ },
182
+ },
183
+ market: {
184
+ description: 'Get top market movers or token comparison context',
185
+ mutating: false,
186
+ handler: async ({ query, token, chain, limit }) => {
187
+ const resolved = query || token || chain || getConfig('chain') || 'base';
188
+ const pair = await fetchBestPair(resolved, fetchImpl);
189
+ if (!pair) return { ok: false, summary: `No market snapshot for ${resolved}` };
190
+ return {
191
+ ok: true,
192
+ query: resolved,
193
+ token: pair.baseToken?.symbol || resolved,
194
+ chain: pair.chainId,
195
+ priceUsd: Number(pair.priceUsd || 0),
196
+ change24h: Number(pair.priceChange?.h24 || 0),
197
+ liquidityUsd: Number(pair.liquidity?.usd || 0),
198
+ volume24hUsd: Number(pair.volume?.h24 || 0),
199
+ limit: limit || 1,
200
+ summary: `Market snapshot ${pair.baseToken?.symbol || resolved}: $${Number(pair.priceUsd || 0).toFixed(6)}, vol $${compactNumber(pair.volume?.h24)}`,
201
+ };
202
+ },
203
+ },
204
+ watch: {
205
+ description: 'Take a quick watch snapshot for a token',
206
+ mutating: false,
207
+ handler: async ({ token, query }) => {
208
+ const resolved = token || query;
209
+ if (!resolved) throw new Error('watch requires token or query');
210
+ const pair = await fetchBestPair(resolved, fetchImpl);
211
+ if (!pair) return { ok: false, summary: `No watch data for ${resolved}` };
212
+ return {
213
+ ok: true,
214
+ token: pair.baseToken?.symbol || resolved,
215
+ priceUsd: Number(pair.priceUsd || 0),
216
+ change24h: Number(pair.priceChange?.h24 || 0),
217
+ liquidityUsd: Number(pair.liquidity?.usd || 0),
218
+ note: 'Watch tool returns a single snapshot inside the agent loop',
219
+ summary: `Watch snapshot ${pair.baseToken?.symbol || resolved}: $${Number(pair.priceUsd || 0).toFixed(6)}`,
220
+ };
221
+ },
222
+ },
223
+ swap: {
224
+ description: 'Execute a token swap',
225
+ mutating: true,
226
+ handler: deps.swapHandler || (async (args) => {
227
+ const { executeSwap } = await import('../trading/swap.js');
228
+ return executeSwap(args);
229
+ }),
230
+ },
231
+ send: {
232
+ description: 'Send ETH or ERC-20 tokens',
233
+ mutating: true,
234
+ handler: deps.sendHandler || (async (args) => {
235
+ const { sendFunds } = await import('../wallet/manager.js');
236
+ return sendFunds(args);
237
+ }),
238
+ },
239
+ 'script-run': {
240
+ description: 'Execute a saved automation script',
241
+ mutating: true,
242
+ handler: deps.scriptRunHandler || (async (args) => {
243
+ const { runScript } = await import('../scripts/engine.js');
244
+ return runScript(args.name, args);
245
+ }),
246
+ },
247
+ };
248
+ }
249
+
250
+ export function createToolRegistry(deps = {}) {
251
+ const base = defaultRegistry(deps);
252
+ return {
253
+ ...base,
254
+ ...(deps.overrides || {}),
255
+ };
256
+ }
257
+
258
+ export function listTools(registry) {
259
+ return Object.entries(registry).map(([name, tool]) => ({
260
+ name,
261
+ description: tool.description,
262
+ mutating: Boolean(tool.mutating),
263
+ }));
264
+ }
265
+
266
+ export function createToolExecutor({ registry = createToolRegistry(), allowActions = false, onEvent = () => {} } = {}) {
267
+ return async function executeTool(name, input = {}) {
268
+ const tool = registry[name];
269
+ if (!tool) {
270
+ return { ok: false, blocked: true, error: `Unknown tool: ${name}`, summary: `Unknown tool ${name}` };
271
+ }
272
+
273
+ if (tool.mutating && !allowActions) {
274
+ return {
275
+ ok: false,
276
+ blocked: true,
277
+ error: `Tool "${name}" is blocked in safe mode. Re-run with --allow-actions to enable mutating tools.`,
278
+ summary: `Blocked ${name} in safe mode`,
279
+ };
280
+ }
281
+
282
+ onEvent({ type: 'tool-start', tool: name, input, mutating: Boolean(tool.mutating) });
283
+ try {
284
+ const result = await tool.handler(input);
285
+ const normalized = typeof result === 'object' && result !== null ? result : { value: result };
286
+ const finalResult = {
287
+ ok: normalized.ok !== false,
288
+ ...normalized,
289
+ summary: summarizeResult(normalized),
290
+ };
291
+ onEvent({ type: 'tool-result', tool: name, result: finalResult });
292
+ return finalResult;
293
+ } catch (error) {
294
+ const failure = { ok: false, error: error.message, summary: error.message };
295
+ onEvent({ type: 'tool-error', tool: name, error: error.message });
296
+ return failure;
297
+ }
298
+ };
299
+ }
300
+
301
+ export const AGENT_TOOL_NAMES = [
302
+ 'price',
303
+ 'gas',
304
+ 'wallet-balance',
305
+ 'portfolio',
306
+ 'market',
307
+ 'watch',
308
+ 'swap',
309
+ 'send',
310
+ 'script-run',
311
+ ];
package/src/cli.js CHANGED
@@ -29,6 +29,7 @@ import { listSkills, installSkill, skillInfo, uninstallSkill } from './services/
29
29
  import { runSetupWizard } from './setup/wizard.js';
30
30
  import { displaySoul, hasSoul, resetSoul, runSoulSetup } from './soul/index.js';
31
31
  import { clearMemories, exportMemories, getRecentMemories, searchMemories } from './memory/index.js';
32
+ import { getAgentStatus, planAgentGoal, runAgentTask } from './agent/index.js';
32
33
  import { createRequire } from 'module';
33
34
  import { resolve } from 'path';
34
35
  const require = createRequire(import.meta.url);
@@ -764,7 +765,7 @@ export function cli(argv) {
764
765
  ai
765
766
  .command('chat')
766
767
  .description('Start interactive AI trading chat')
767
- .option('-p, --provider <name>', 'LLM provider (openai, anthropic, openrouter, ollama)')
768
+ .option('-p, --provider <name>', 'LLM provider (openai, anthropic, openrouter, minimax, ollama)')
768
769
  .option('-m, --model <model>', 'Model name')
769
770
  .action((opts) => startChat(opts));
770
771
 
@@ -883,6 +884,93 @@ export function cli(argv) {
883
884
  .command('agent')
884
885
  .description('Secure agent signer — PK-isolated wallet for AI agents');
885
886
 
887
+ agent
888
+ .command('task <goal...>')
889
+ .description('Run the agent loop against a goal')
890
+ .option('--max-steps <n>', 'Maximum loop steps', '10')
891
+ .option('--allow-actions', 'Allow mutating tools such as swap/send/script-run')
892
+ .action(async (goalParts, opts) => {
893
+ showMiniBanner();
894
+ showSection('AGENT TASK');
895
+ const goal = goalParts.join(' ').trim();
896
+ info(`Goal: ${goal}`);
897
+ info(`Mode: ${opts.allowActions ? 'actions enabled' : 'safe mode'}`);
898
+ console.log('');
899
+
900
+ const result = await runAgentTask(goal, {
901
+ maxSteps: parseInt(opts.maxSteps, 10),
902
+ allowActions: opts.allowActions,
903
+ onProgress: (event) => {
904
+ if (event.type === 'thought') {
905
+ info(`Step ${event.step}: ${event.action}`);
906
+ if (event.thought) console.log(` ${theme.dim(event.thought)}`);
907
+ }
908
+ if (event.type === 'observation') {
909
+ const summary = event.observation?.summary || event.observation?.error || '';
910
+ if (summary) console.log(` ${theme.dim(summary)}`);
911
+ console.log('');
912
+ }
913
+ },
914
+ });
915
+
916
+ showSection('AGENT RESULT');
917
+ kvDisplay([
918
+ ['Status', result.status],
919
+ ['Steps', `${result.stepsTaken}/${result.maxSteps}`],
920
+ ['Stop Reason', result.stopReason],
921
+ ]);
922
+ console.log('');
923
+ if (result.final) {
924
+ success(result.final);
925
+ console.log('');
926
+ }
927
+ });
928
+
929
+ agent
930
+ .command('plan <goal...>')
931
+ .description('Generate a concise agent plan for a goal')
932
+ .action(async (goalParts) => {
933
+ showMiniBanner();
934
+ showSection('AGENT PLAN');
935
+ const goal = goalParts.join(' ').trim();
936
+ const plan = await planAgentGoal(goal);
937
+ info(plan.summary);
938
+ console.log('');
939
+ plan.steps.forEach((step, index) => console.log(` ${theme.gold(String(index + 1).padStart(2, ' '))}. ${step}`));
940
+ console.log('');
941
+ });
942
+
943
+ agent
944
+ .command('status')
945
+ .description('Show the latest agent task or plan status')
946
+ .action(() => {
947
+ showMiniBanner();
948
+ showSection('AGENT STATUS');
949
+ const status = getAgentStatus();
950
+ if (!status || !status.status) {
951
+ warn('No agent runs recorded yet.');
952
+ console.log('');
953
+ return;
954
+ }
955
+
956
+ kvDisplay([
957
+ ['Status', status.status || '-'],
958
+ ['Goal', status.goal || '-'],
959
+ ['Summary', status.summary || '-'],
960
+ ['Steps', status.maxSteps ? `${status.stepsTaken || 0}/${status.maxSteps}` : String(status.stepsTaken || 0)],
961
+ ['Actions', status.allowActions ? 'enabled' : 'safe mode'],
962
+ ['Started', status.startedAt || '-'],
963
+ ['Completed', status.completedAt || '-'],
964
+ ['Updated', status.updatedAt || '-'],
965
+ ]);
966
+ if (Array.isArray(status.plan) && status.plan.length > 0) {
967
+ console.log('');
968
+ showSection('LAST PLAN');
969
+ status.plan.forEach((step) => console.log(` ${theme.dim(step)}`));
970
+ }
971
+ console.log('');
972
+ });
973
+
886
974
  agent
887
975
  .command('start [wallet]')
888
976
  .description('Start the agent signing proxy')
@@ -1304,7 +1392,7 @@ function showCommandList() {
1304
1392
  ['dca', 'Dollar-cost averaging orders'],
1305
1393
  ['ai chat', 'Standalone AI chat session'],
1306
1394
  ['ai execute', 'Parse + execute a trade via AI'],
1307
- ['agent start', 'Start secure agent signer'],
1395
+ ['agent task', 'Run bounded agent loop for a goal'],
1308
1396
  ['keys', 'API key vault'],
1309
1397
  ['soul', 'Identity and agent personality'],
1310
1398
  ['memory', 'Persistent cross-session memory'],
@@ -84,6 +84,14 @@ export const SERVICES = {
84
84
  docsUrl: 'https://openrouter.ai/keys',
85
85
  validate: (key) => key.startsWith('sk-or-'),
86
86
  },
87
+ minimax: {
88
+ name: 'MiniMax',
89
+ category: 'llm',
90
+ description: 'MiniMax-M2.5 via OpenAI-compatible chat completions',
91
+ envVar: 'MINIMAX_API_KEY',
92
+ docsUrl: 'https://platform.minimax.io/docs/guides/models-intro',
93
+ validate: (key) => key.length > 10,
94
+ },
87
95
  ollama: {
88
96
  name: 'Ollama (Local)',
89
97
  category: 'llm',
@@ -420,7 +428,7 @@ export function hasKey(service) {
420
428
  */
421
429
  export function hasAnyLLM() {
422
430
  // Cloud providers — need real validated API keys
423
- if (['openai', 'anthropic', 'openrouter', 'bankr'].some(s => hasKey(s))) return true;
431
+ if (['openai', 'anthropic', 'openrouter', 'minimax', 'bankr'].some(s => hasKey(s))) return true;
424
432
  // Ollama — check if explicitly configured via hasKey (validates URL format)
425
433
  if (hasKey('ollama')) return true;
426
434
  return false;
@@ -28,6 +28,22 @@ const config = new Conf({
28
28
  createdAt: '',
29
29
  },
30
30
  },
31
+ agentState: {
32
+ type: 'object',
33
+ default: {
34
+ status: '',
35
+ goal: '',
36
+ summary: '',
37
+ plan: [],
38
+ stepsTaken: 0,
39
+ maxSteps: 0,
40
+ allowActions: false,
41
+ startedAt: null,
42
+ completedAt: null,
43
+ stopReason: '',
44
+ updatedAt: null,
45
+ },
46
+ },
31
47
  dca: {
32
48
  type: 'object',
33
49
  default: {
package/src/llm/engine.js CHANGED
@@ -39,6 +39,13 @@ const PROVIDERS = {
39
39
  parseResponse: (data) => data.choices?.[0]?.message?.content,
40
40
  parseUsage: (data) => data.usage,
41
41
  },
42
+ minimax: {
43
+ url: 'https://api.minimax.io/v1/chat/completions',
44
+ defaultModel: 'MiniMax-M2.5',
45
+ authHeader: (key) => ({ Authorization: `Bearer ${key}` }),
46
+ parseResponse: (data) => data.choices?.[0]?.message?.content,
47
+ parseUsage: (data) => data.usage,
48
+ },
42
49
  ollama: {
43
50
  url: null,
44
51
  defaultModel: 'llama3.1',
@@ -52,11 +52,12 @@ export async function runSetupWizard(opts = {}) {
52
52
  name: 'provider',
53
53
  message: theme.gold('Choose your AI provider:'),
54
54
  choices: [
55
- { name: '🤖 OpenAI (GPT-4o, GPT-5) API key or OAuth', value: 'openai' },
56
- { name: '🧠 Anthropic (Claude Opus, Sonnet) API key or OAuth', value: 'anthropic' },
57
- { name: '🔀 OpenRouter (any model, one key) API key', value: 'openrouter' },
58
- { name: '🏠 Ollama (local models, free, private) no key needed', value: 'ollama' },
59
- { name: '⏭️ Skip for now', value: 'skip' },
55
+ { name: 'OpenAI (GPT-4o, GPT-5) - API key or OAuth', value: 'openai' },
56
+ { name: 'Anthropic (Claude Opus, Sonnet) - API key or OAuth', value: 'anthropic' },
57
+ { name: 'OpenRouter (any model, one key) - API key', value: 'openrouter' },
58
+ { name: 'MiniMax (MiniMax-M2.5) - API key', value: 'minimax' },
59
+ { name: 'Ollama (local models, free, private) - no key needed', value: 'ollama' },
60
+ { name: 'Skip for now', value: 'skip' },
60
61
  ],
61
62
  }]);
62
63
 
@@ -112,7 +113,7 @@ export async function runSetupWizard(opts = {}) {
112
113
  }
113
114
 
114
115
  /**
115
- * Setup a cloud provider (OpenAI, Anthropic, OpenRouter)
116
+ * Setup a cloud provider (OpenAI, Anthropic, OpenRouter, MiniMax)
116
117
  */
117
118
  async function setupCloudProvider(provider) {
118
119
  const supportsOAuth = ['openai', 'anthropic'].includes(provider);
@@ -120,6 +121,7 @@ async function setupCloudProvider(provider) {
120
121
  openai: 'OpenAI',
121
122
  anthropic: 'Anthropic',
122
123
  openrouter: 'OpenRouter',
124
+ minimax: 'MiniMax',
123
125
  }[provider];
124
126
 
125
127
  if (supportsOAuth) {
@@ -156,6 +158,7 @@ async function setupAPIKey(provider) {
156
158
  openai: 'OpenAI',
157
159
  anthropic: 'Anthropic',
158
160
  openrouter: 'OpenRouter',
161
+ minimax: 'MiniMax',
159
162
  }[provider];
160
163
 
161
164
  const { key } = await inquirer.prompt([{
@@ -173,6 +176,7 @@ async function setupAPIKey(provider) {
173
176
  success(`${providerName} key saved (encrypted)`);
174
177
 
175
178
  // Set as default provider
179
+ setConfig('llm.provider', provider);
176
180
  setConfig('llmProvider', provider);
177
181
  info(`Default AI provider set to ${provider}`);
178
182
  }
@@ -193,6 +197,7 @@ async function setupOllama() {
193
197
  default: 'http://localhost:11434',
194
198
  }]);
195
199
 
200
+ setConfig('llm.ollamaHost', host);
196
201
  setConfig('ollamaHost', host);
197
202
 
198
203
  const { model } = await inquirer.prompt([{
@@ -202,7 +207,9 @@ async function setupOllama() {
202
207
  default: 'llama3',
203
208
  }]);
204
209
 
210
+ setConfig('llm.model', model);
205
211
  setConfig('ollamaModel', model);
212
+ setConfig('llm.provider', 'ollama');
206
213
  setConfig('llmProvider', 'ollama');
207
214
 
208
215
  success(`Ollama configured: ${host} / ${model}`);
@@ -231,8 +238,14 @@ function showKeyInstructions(provider) {
231
238
  console.log(theme.dim(' 3. Copy the key (starts with sk-ant-)'));
232
239
  console.log(theme.dim(' 4. Paste it below'));
233
240
  console.log('');
234
- console.log(theme.dim(' 💡 If you have a Claude Pro/Team subscription,'));
241
+ console.log(theme.dim(' If you have a Claude Pro/Team subscription,'));
235
242
  console.log(theme.dim(' you can use OAuth instead.'));
243
+ } else if (provider === 'minimax') {
244
+ showSection('GET A MINIMAX API KEY');
245
+ console.log(theme.dim(' 1. Go to https://platform.minimax.io/docs/guides/models-intro'));
246
+ console.log(theme.dim(' 2. Open your MiniMax developer console / API key page'));
247
+ console.log(theme.dim(' 3. Create or copy an API key'));
248
+ console.log(theme.dim(' 4. Paste it below'));
236
249
  }
237
250
 
238
251
  console.log('');
@@ -432,6 +445,7 @@ async function executeOAuthFlow(provider, clientId, clientSecret) {
432
445
  if (tokenData.refresh_token) {
433
446
  addKeyDirect(`${provider}_refresh`, tokenData.refresh_token);
434
447
  }
448
+ setConfig('llm.provider', provider);
435
449
  setConfig('llmProvider', provider);
436
450
 
437
451
  res.writeHead(200, { 'Content-Type': 'text/html' });
@@ -452,6 +452,7 @@ export async function handlePromptResponse(id, value, meta, ws) {
452
452
  if (service === 'openai') ws.sendLine(` ${ANSI.dim}Key should start with sk-${ANSI.reset}`);
453
453
  if (service === 'anthropic') ws.sendLine(` ${ANSI.dim}Key should start with sk-ant-${ANSI.reset}`);
454
454
  if (service === 'openrouter') ws.sendLine(` ${ANSI.dim}Key should start with sk-or-${ANSI.reset}`);
455
+ if (service === 'minimax') ws.sendLine(` ${ANSI.dim}Get a key: ${svc.docsUrl}${ANSI.reset}`);
455
456
  if (service === 'ollama') ws.sendLine(` ${ANSI.dim}Should be a URL like http://localhost:11434${ANSI.reset}`);
456
457
  ws.sendLine('');
457
458
  return {};
@@ -782,7 +783,7 @@ export function getAIStatus() {
782
783
  const dim = '\x1b[38;2;102;102;102m';
783
784
  const reset = '\x1b[0m';
784
785
 
785
- const providers = ['openai', 'anthropic', 'openrouter', 'ollama', 'bankr'];
786
+ const providers = ['openai', 'anthropic', 'openrouter', 'minimax', 'ollama', 'bankr'];
786
787
  const connected = providers.filter(p => hasKey(p));
787
788
  const soul = hasSoul() ? getSoul() : null;
788
789
 
@@ -798,6 +799,7 @@ export function getAIStatus() {
798
799
  ` ${green}keys add openai sk-...${reset} ${dim}OpenAI (GPT-4o)${reset}`,
799
800
  ` ${green}keys add anthropic sk-ant-...${reset} ${dim}Anthropic (Claude)${reset}`,
800
801
  ` ${green}keys add openrouter sk-or-...${reset} ${dim}OpenRouter (any model)${reset}`,
802
+ ` ${green}keys add minimax <key>${reset} ${dim}MiniMax (MiniMax-M2.5)${reset}`,
801
803
  ` ${green}keys add bankr bk_...${reset} ${dim}Bankr LLM Gateway (crypto credits)${reset}`,
802
804
  ` ${green}keys add ollama http://...${reset} ${dim}Ollama (free, local)${reset}`,
803
805
  '',
@@ -850,6 +852,8 @@ export async function handleCommand(cmd, ws) {
850
852
  case 'agent':
851
853
  case 'signer':
852
854
  return await cmdAgent(args, ws);
855
+ case 'task':
856
+ return await cmdAgent(['task', ...args], ws);
853
857
  case 'ai':
854
858
  case 'ask':
855
859
  case 'chat':
@@ -1339,7 +1343,84 @@ async function showWalletDetail(name, ws) {
1339
1343
  async function cmdAgent(args, ws) {
1340
1344
  const sub = (args[0] || 'menu').toLowerCase();
1341
1345
 
1346
+ if (sub === 'task') {
1347
+ const goal = args.slice(1).join(' ').trim();
1348
+ if (!goal) {
1349
+ return {
1350
+ output: `\r\n ${ANSI.dim}Usage: agent task <goal> [--max-steps N] [--allow-actions]${ANSI.reset}\r\n ${ANSI.dim}Shortcut: task <goal>${ANSI.reset}\r\n\r\n`,
1351
+ };
1352
+ }
1353
+
1354
+ const allowActions = args.includes('--allow-actions');
1355
+ const maxIndex = args.findIndex((arg) => arg === '--max-steps');
1356
+ const maxSteps = maxIndex >= 0 ? parseInt(args[maxIndex + 1], 10) || 10 : 10;
1357
+ const filteredGoal = args
1358
+ .slice(1)
1359
+ .filter((arg, index, arr) => arg !== '--allow-actions' && !(arg === '--max-steps' || arr[index - 1] === '--max-steps'))
1360
+ .join(' ')
1361
+ .trim();
1362
+
1363
+ const { runAgentTask } = await import('../agent/index.js');
1364
+ ws.sendLine(`${ANSI.gold} ◆ AGENT TASK${ANSI.reset}`);
1365
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1366
+ ws.sendLine(` ${ANSI.white}Goal:${ANSI.reset} ${filteredGoal}`);
1367
+ ws.sendLine(` ${ANSI.darkGold}Mode:${ANSI.reset} ${allowActions ? 'actions enabled' : 'safe mode'}`);
1368
+ ws.sendLine('');
1369
+
1370
+ const result = await runAgentTask(filteredGoal, {
1371
+ maxSteps,
1372
+ allowActions,
1373
+ onProgress: (event) => {
1374
+ if (event.type === 'thought') {
1375
+ ws.sendLine(` ${ANSI.darkGold}[step ${event.step}]${ANSI.reset} ${ANSI.white}${event.action}${ANSI.reset}`);
1376
+ if (event.thought) {
1377
+ ws.sendLine(` ${ANSI.dim}${event.thought}${ANSI.reset}`);
1378
+ }
1379
+ }
1380
+ if (event.type === 'observation') {
1381
+ const summary = event.observation?.summary || event.observation?.error;
1382
+ if (summary) ws.sendLine(` ${ANSI.dim}${summary}${ANSI.reset}`);
1383
+ ws.sendLine('');
1384
+ }
1385
+ },
1386
+ });
1387
+
1388
+ ws.sendLine(` ${ANSI.green}Final:${ANSI.reset} ${result.final}`);
1389
+ ws.sendLine(` ${ANSI.dim}Status ${result.status} • ${result.stepsTaken}/${result.maxSteps} steps • ${result.stopReason}${ANSI.reset}`);
1390
+ ws.sendLine('');
1391
+ return {};
1392
+ }
1393
+
1394
+ if (sub === 'plan') {
1395
+ const goal = args.slice(1).join(' ').trim();
1396
+ if (!goal) {
1397
+ return { output: ` ${ANSI.dim}Usage: agent plan <goal>${ANSI.reset}\r\n` };
1398
+ }
1399
+ const { planAgentGoal } = await import('../agent/index.js');
1400
+ const plan = await planAgentGoal(goal);
1401
+ ws.sendLine(`${ANSI.gold} ◆ AGENT PLAN${ANSI.reset}`);
1402
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1403
+ ws.sendLine(` ${ANSI.white}${plan.summary}${ANSI.reset}`);
1404
+ ws.sendLine('');
1405
+ plan.steps.forEach((step, index) => ws.sendLine(` ${ANSI.darkGold}${index + 1}.${ANSI.reset} ${step}`));
1406
+ ws.sendLine('');
1407
+ return {};
1408
+ }
1409
+
1342
1410
  if (sub === 'status') {
1411
+ const { getAgentStatus } = await import('../agent/index.js');
1412
+ const status = getAgentStatus();
1413
+ if (status?.goal || status?.summary) {
1414
+ ws.sendLine(`${ANSI.gold} ◆ AGENT STATUS${ANSI.reset}`);
1415
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1416
+ ws.sendLine(` ${ANSI.darkGold}Status${ANSI.reset} ${ANSI.white}${status.status || '-'}${ANSI.reset}`);
1417
+ ws.sendLine(` ${ANSI.darkGold}Goal${ANSI.reset} ${ANSI.white}${status.goal || '-'}${ANSI.reset}`);
1418
+ ws.sendLine(` ${ANSI.darkGold}Summary${ANSI.reset} ${ANSI.white}${status.summary || '-'}${ANSI.reset}`);
1419
+ ws.sendLine(` ${ANSI.darkGold}Steps${ANSI.reset} ${ANSI.white}${status.stepsTaken || 0}${status.maxSteps ? `/${status.maxSteps}` : ''}${ANSI.reset}`);
1420
+ ws.sendLine(` ${ANSI.darkGold}Actions${ANSI.reset} ${ANSI.white}${status.allowActions ? 'enabled' : 'safe mode'}${ANSI.reset}`);
1421
+ ws.sendLine('');
1422
+ return {};
1423
+ }
1343
1424
  return await showSignerStatus(ws);
1344
1425
  }
1345
1426
 
@@ -1938,7 +2019,7 @@ async function cmdKeys(args, ws) {
1938
2019
 
1939
2020
  if (!svc) {
1940
2021
  ws.sendLine(` ${ANSI.red}✗ Unknown service: ${service}${ANSI.reset}`);
1941
- ws.sendLine(` ${ANSI.dim}Available: openai, anthropic, openrouter, ollama${ANSI.reset}`);
2022
+ ws.sendLine(` ${ANSI.dim}Available: openai, anthropic, openrouter, minimax, ollama, bankr${ANSI.reset}`);
1942
2023
  ws.sendLine('');
1943
2024
  return {};
1944
2025
  }
@@ -1986,7 +2067,7 @@ async function cmdKeys(args, ws) {
1986
2067
  ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1987
2068
  ws.sendLine('');
1988
2069
 
1989
- const llmProviders = ['openai', 'anthropic', 'openrouter', 'ollama', 'bankr'];
2070
+ const llmProviders = ['openai', 'anthropic', 'openrouter', 'minimax', 'ollama', 'bankr'];
1990
2071
  ws.sendLine(` ${ANSI.gold}LLM Providers:${ANSI.reset}`);
1991
2072
  for (const p of llmProviders) {
1992
2073
  const svc = SERVICES[p];
@@ -2000,6 +2081,7 @@ async function cmdKeys(args, ws) {
2000
2081
  ws.sendLine(` ${ANSI.green}keys add openai sk-...${ANSI.reset} ${ANSI.dim}Add OpenAI key${ANSI.reset}`);
2001
2082
  ws.sendLine(` ${ANSI.green}keys add anthropic sk-ant-...${ANSI.reset} ${ANSI.dim}Add Anthropic key${ANSI.reset}`);
2002
2083
  ws.sendLine(` ${ANSI.green}keys add openrouter sk-or-...${ANSI.reset} ${ANSI.dim}Add OpenRouter key${ANSI.reset}`);
2084
+ ws.sendLine(` ${ANSI.green}keys add minimax <key>${ANSI.reset} ${ANSI.dim}Add MiniMax key${ANSI.reset}`);
2003
2085
  ws.sendLine(` ${ANSI.green}keys add ollama http://...${ANSI.reset} ${ANSI.dim}Add Ollama host${ANSI.reset}`);
2004
2086
  ws.sendLine('');
2005
2087