@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 +8 -1
- package/package.json +1 -1
- package/src/agent/index.js +134 -0
- package/src/agent/loop.js +190 -0
- package/src/agent/tools.js +311 -0
- package/src/cli.js +90 -2
- package/src/config/keys.js +9 -1
- package/src/config/store.js +16 -0
- package/src/llm/engine.js +7 -0
- package/src/setup/wizard.js +21 -7
- package/src/web/commands.js +85 -3
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, c
|
|
|
15
15
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
16
16
|
[](https://nodejs.org/)
|
|
17
17
|
|
|
18
|
-
- Current release: **0.9.
|
|
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
|
@@ -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
|
|
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'],
|
package/src/config/keys.js
CHANGED
|
@@ -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;
|
package/src/config/store.js
CHANGED
|
@@ -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',
|
package/src/setup/wizard.js
CHANGED
|
@@ -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: '
|
|
56
|
-
{ name: '
|
|
57
|
-
{ name: '
|
|
58
|
-
{ name: '
|
|
59
|
-
{ name: '
|
|
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('
|
|
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' });
|
package/src/web/commands.js
CHANGED
|
@@ -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
|
|