@darksol/terminal 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.2**
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.2",
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
+ ];