@quantish/agent 0.1.0
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/LICENSE +22 -0
- package/README.md +262 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4042 -0
- package/package.json +68 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4042 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import React2 from "react";
|
|
5
|
+
import { render } from "ink";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import chalk3 from "chalk";
|
|
8
|
+
|
|
9
|
+
// src/config/manager.ts
|
|
10
|
+
import Conf from "conf";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var DEFAULT_TRADING_MCP_URL = "https://quantish-sdk-production.up.railway.app/mcp";
|
|
14
|
+
var DISCOVERY_MCP_URL = "https://quantish.live/mcp";
|
|
15
|
+
var DISCOVERY_MCP_PUBLIC_KEY = "qm_ueQeqrmvZyHtR1zuVbLYkhx0fKyVAuV8";
|
|
16
|
+
var DEFAULT_MCP_URL = DEFAULT_TRADING_MCP_URL;
|
|
17
|
+
var schema = {
|
|
18
|
+
anthropicApiKey: {
|
|
19
|
+
type: "string"
|
|
20
|
+
},
|
|
21
|
+
quantishApiKey: {
|
|
22
|
+
type: "string"
|
|
23
|
+
},
|
|
24
|
+
mcpServerUrl: {
|
|
25
|
+
type: "string",
|
|
26
|
+
default: DEFAULT_MCP_URL
|
|
27
|
+
},
|
|
28
|
+
model: {
|
|
29
|
+
type: "string",
|
|
30
|
+
default: "claude-sonnet-4-5-20250929"
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var ConfigManager = class {
|
|
34
|
+
conf;
|
|
35
|
+
constructor() {
|
|
36
|
+
this.conf = new Conf({
|
|
37
|
+
projectName: "quantish",
|
|
38
|
+
schema,
|
|
39
|
+
cwd: join(homedir(), ".quantish"),
|
|
40
|
+
configName: "config"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the Anthropic API key
|
|
45
|
+
*/
|
|
46
|
+
getAnthropicApiKey() {
|
|
47
|
+
const envKey = process.env.ANTHROPIC_API_KEY;
|
|
48
|
+
if (envKey) return envKey;
|
|
49
|
+
return this.conf.get("anthropicApiKey");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Set the Anthropic API key
|
|
53
|
+
*/
|
|
54
|
+
setAnthropicApiKey(key) {
|
|
55
|
+
this.conf.set("anthropicApiKey", key);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the Quantish API key
|
|
59
|
+
*/
|
|
60
|
+
getQuantishApiKey() {
|
|
61
|
+
const envKey = process.env.QUANTISH_API_KEY;
|
|
62
|
+
if (envKey) return envKey;
|
|
63
|
+
return this.conf.get("quantishApiKey");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Set the Quantish API key
|
|
67
|
+
*/
|
|
68
|
+
setQuantishApiKey(key) {
|
|
69
|
+
this.conf.set("quantishApiKey", key);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get the Trading MCP server URL (user's wallet/orders)
|
|
73
|
+
*/
|
|
74
|
+
getMcpServerUrl() {
|
|
75
|
+
return this.conf.get("mcpServerUrl") ?? DEFAULT_MCP_URL;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Alias for getMcpServerUrl for clarity
|
|
79
|
+
*/
|
|
80
|
+
getTradingMcpUrl() {
|
|
81
|
+
return this.getMcpServerUrl();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get the Discovery MCP server URL (public market data)
|
|
85
|
+
*/
|
|
86
|
+
getDiscoveryMcpUrl() {
|
|
87
|
+
return DISCOVERY_MCP_URL;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get the Discovery MCP public API key
|
|
91
|
+
*/
|
|
92
|
+
getDiscoveryApiKey() {
|
|
93
|
+
return DISCOVERY_MCP_PUBLIC_KEY;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Set the MCP server URL
|
|
97
|
+
*/
|
|
98
|
+
setMcpServerUrl(url) {
|
|
99
|
+
this.conf.set("mcpServerUrl", url);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get the model to use
|
|
103
|
+
*/
|
|
104
|
+
getModel() {
|
|
105
|
+
return this.conf.get("model") ?? "claude-sonnet-4-5-20250929";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Set the model to use
|
|
109
|
+
*/
|
|
110
|
+
setModel(model) {
|
|
111
|
+
this.conf.set("model", model);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if the CLI is configured (has at least Anthropic key)
|
|
115
|
+
* Discovery MCP works without any user key (embedded public key)
|
|
116
|
+
* Trading MCP requires a user key
|
|
117
|
+
*/
|
|
118
|
+
isConfigured() {
|
|
119
|
+
return !!this.getAnthropicApiKey();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Check if trading is enabled (has Quantish API key)
|
|
123
|
+
*/
|
|
124
|
+
isTradingEnabled() {
|
|
125
|
+
return !!this.getQuantishApiKey();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get all configuration values
|
|
129
|
+
*/
|
|
130
|
+
getAll() {
|
|
131
|
+
return {
|
|
132
|
+
anthropicApiKey: this.getAnthropicApiKey(),
|
|
133
|
+
quantishApiKey: this.getQuantishApiKey(),
|
|
134
|
+
mcpServerUrl: this.getMcpServerUrl(),
|
|
135
|
+
model: this.getModel()
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Clear all configuration
|
|
140
|
+
*/
|
|
141
|
+
clear() {
|
|
142
|
+
this.conf.clear();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get the path to the config file
|
|
146
|
+
*/
|
|
147
|
+
getConfigPath() {
|
|
148
|
+
return this.conf.path;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var configManager = null;
|
|
152
|
+
function getConfigManager() {
|
|
153
|
+
if (!configManager) {
|
|
154
|
+
configManager = new ConfigManager();
|
|
155
|
+
}
|
|
156
|
+
return configManager;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/config/setup.ts
|
|
160
|
+
import * as readline from "readline";
|
|
161
|
+
import chalk from "chalk";
|
|
162
|
+
|
|
163
|
+
// src/mcp/client.ts
|
|
164
|
+
var MCPClient = class {
|
|
165
|
+
baseUrl;
|
|
166
|
+
apiKey;
|
|
167
|
+
toolsCache = null;
|
|
168
|
+
source;
|
|
169
|
+
constructor(baseUrl, apiKey, source = "trading") {
|
|
170
|
+
this.baseUrl = baseUrl;
|
|
171
|
+
this.apiKey = apiKey;
|
|
172
|
+
this.source = source;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* List available tools from the MCP server
|
|
176
|
+
* Discovery MCP uses REST endpoints, Trading MCP uses JSON-RPC
|
|
177
|
+
*/
|
|
178
|
+
async listTools() {
|
|
179
|
+
if (this.toolsCache) {
|
|
180
|
+
return this.toolsCache;
|
|
181
|
+
}
|
|
182
|
+
if (this.source === "discovery") {
|
|
183
|
+
const response2 = await fetch(`${this.baseUrl}/tools`, {
|
|
184
|
+
method: "GET",
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": "application/json",
|
|
187
|
+
"X-API-Key": this.apiKey
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
if (!response2.ok) {
|
|
191
|
+
throw new Error(`MCP server error: ${response2.status} ${response2.statusText}`);
|
|
192
|
+
}
|
|
193
|
+
const data2 = await response2.json();
|
|
194
|
+
this.toolsCache = data2.tools || [];
|
|
195
|
+
return this.toolsCache;
|
|
196
|
+
}
|
|
197
|
+
const response = await fetch(this.baseUrl, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
"x-api-key": this.apiKey
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
jsonrpc: "2.0",
|
|
205
|
+
method: "tools/list",
|
|
206
|
+
params: {},
|
|
207
|
+
id: Date.now()
|
|
208
|
+
})
|
|
209
|
+
});
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
throw new Error(`MCP server error: ${response.status} ${response.statusText}`);
|
|
212
|
+
}
|
|
213
|
+
const data = await response.json();
|
|
214
|
+
if (data.error) {
|
|
215
|
+
throw new Error(`MCP error: ${data.error.message}`);
|
|
216
|
+
}
|
|
217
|
+
const tools = data.result?.tools || [];
|
|
218
|
+
this.toolsCache = tools;
|
|
219
|
+
return tools;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Call a tool on the MCP server
|
|
223
|
+
* Discovery MCP uses REST endpoints, Trading MCP uses JSON-RPC
|
|
224
|
+
*/
|
|
225
|
+
async callTool(name, args) {
|
|
226
|
+
if (this.source === "discovery") {
|
|
227
|
+
const response2 = await fetch(`${this.baseUrl}/execute`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: {
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
"X-API-Key": this.apiKey
|
|
232
|
+
},
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
name,
|
|
235
|
+
arguments: args
|
|
236
|
+
})
|
|
237
|
+
});
|
|
238
|
+
if (!response2.ok) {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
error: `MCP server error: ${response2.status} ${response2.statusText}`
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const data2 = await response2.json();
|
|
245
|
+
if (data2.error) {
|
|
246
|
+
return {
|
|
247
|
+
success: false,
|
|
248
|
+
error: typeof data2.error === "string" ? data2.error : JSON.stringify(data2.error)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
success: true,
|
|
253
|
+
data: data2.data ?? data2.result ?? data2
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const response = await fetch(this.baseUrl, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: {
|
|
259
|
+
"Content-Type": "application/json",
|
|
260
|
+
"x-api-key": this.apiKey
|
|
261
|
+
},
|
|
262
|
+
body: JSON.stringify({
|
|
263
|
+
jsonrpc: "2.0",
|
|
264
|
+
method: "tools/call",
|
|
265
|
+
params: {
|
|
266
|
+
name,
|
|
267
|
+
arguments: args
|
|
268
|
+
},
|
|
269
|
+
id: Date.now()
|
|
270
|
+
})
|
|
271
|
+
});
|
|
272
|
+
if (!response.ok) {
|
|
273
|
+
return {
|
|
274
|
+
success: false,
|
|
275
|
+
error: `MCP server error: ${response.status} ${response.statusText}`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const data = await response.json();
|
|
279
|
+
if (data.error) {
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
error: data.error.message
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const content = data.result?.content;
|
|
286
|
+
if (content && content.length > 0) {
|
|
287
|
+
const textContent = content.find((c) => c.type === "text");
|
|
288
|
+
if (textContent?.text) {
|
|
289
|
+
try {
|
|
290
|
+
return {
|
|
291
|
+
success: true,
|
|
292
|
+
data: JSON.parse(textContent.text)
|
|
293
|
+
};
|
|
294
|
+
} catch {
|
|
295
|
+
return {
|
|
296
|
+
success: true,
|
|
297
|
+
data: textContent.text
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
data: data.result
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Clear the tools cache (useful if server tools are updated)
|
|
309
|
+
*/
|
|
310
|
+
clearCache() {
|
|
311
|
+
this.toolsCache = null;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Check if the MCP server is reachable
|
|
315
|
+
*/
|
|
316
|
+
async healthCheck() {
|
|
317
|
+
try {
|
|
318
|
+
await this.listTools();
|
|
319
|
+
return true;
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
function createMCPClient(baseUrl, apiKey, source = "trading") {
|
|
326
|
+
return new MCPClient(baseUrl, apiKey, source);
|
|
327
|
+
}
|
|
328
|
+
var MCPClientManager = class {
|
|
329
|
+
discoveryClient;
|
|
330
|
+
tradingClient;
|
|
331
|
+
toolSourceMap = /* @__PURE__ */ new Map();
|
|
332
|
+
allToolsCache = null;
|
|
333
|
+
constructor(discoveryUrl, discoveryApiKey, tradingUrl, tradingApiKey) {
|
|
334
|
+
this.discoveryClient = new MCPClient(discoveryUrl, discoveryApiKey, "discovery");
|
|
335
|
+
this.tradingClient = tradingUrl && tradingApiKey ? new MCPClient(tradingUrl, tradingApiKey, "trading") : null;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Check if trading is enabled
|
|
339
|
+
*/
|
|
340
|
+
isTradingEnabled() {
|
|
341
|
+
return this.tradingClient !== null;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Get the discovery client
|
|
345
|
+
*/
|
|
346
|
+
getDiscoveryClient() {
|
|
347
|
+
return this.discoveryClient;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get the trading client (may be null)
|
|
351
|
+
*/
|
|
352
|
+
getTradingClient() {
|
|
353
|
+
return this.tradingClient;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* List all tools from both servers
|
|
357
|
+
*/
|
|
358
|
+
async listAllTools() {
|
|
359
|
+
if (this.allToolsCache) {
|
|
360
|
+
return this.allToolsCache;
|
|
361
|
+
}
|
|
362
|
+
const allTools = [];
|
|
363
|
+
this.toolSourceMap.clear();
|
|
364
|
+
try {
|
|
365
|
+
const discoveryTools = await this.discoveryClient.listTools();
|
|
366
|
+
for (const tool of discoveryTools) {
|
|
367
|
+
allTools.push({ ...tool, source: "discovery" });
|
|
368
|
+
this.toolSourceMap.set(tool.name, "discovery");
|
|
369
|
+
}
|
|
370
|
+
} catch (error2) {
|
|
371
|
+
console.warn("Failed to fetch Discovery MCP tools:", error2);
|
|
372
|
+
}
|
|
373
|
+
if (this.tradingClient) {
|
|
374
|
+
try {
|
|
375
|
+
const tradingTools = await this.tradingClient.listTools();
|
|
376
|
+
for (const tool of tradingTools) {
|
|
377
|
+
allTools.push({ ...tool, source: "trading" });
|
|
378
|
+
this.toolSourceMap.set(tool.name, "trading");
|
|
379
|
+
}
|
|
380
|
+
} catch (error2) {
|
|
381
|
+
console.warn("Failed to fetch Trading MCP tools:", error2);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
this.allToolsCache = allTools;
|
|
385
|
+
return allTools;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get which server a tool belongs to
|
|
389
|
+
*/
|
|
390
|
+
getToolSource(toolName) {
|
|
391
|
+
return this.toolSourceMap.get(toolName);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Call a tool on the appropriate server
|
|
395
|
+
*/
|
|
396
|
+
async callTool(name, args) {
|
|
397
|
+
if (this.toolSourceMap.size === 0) {
|
|
398
|
+
await this.listAllTools();
|
|
399
|
+
}
|
|
400
|
+
const source = this.toolSourceMap.get(name);
|
|
401
|
+
if (!source) {
|
|
402
|
+
return {
|
|
403
|
+
success: false,
|
|
404
|
+
error: `Unknown MCP tool: ${name}`
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (source === "discovery") {
|
|
408
|
+
const result = await this.discoveryClient.callTool(name, args);
|
|
409
|
+
return { ...result, source: "discovery" };
|
|
410
|
+
}
|
|
411
|
+
if (source === "trading") {
|
|
412
|
+
if (!this.tradingClient) {
|
|
413
|
+
return {
|
|
414
|
+
success: false,
|
|
415
|
+
error: `Trading not enabled. Run 'quantish init' to set up trading.`
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const result = await this.tradingClient.callTool(name, args);
|
|
419
|
+
return { ...result, source: "trading" };
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
success: false,
|
|
423
|
+
error: `Unknown tool source: ${source}`
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Clear all caches
|
|
428
|
+
*/
|
|
429
|
+
clearCache() {
|
|
430
|
+
this.discoveryClient.clearCache();
|
|
431
|
+
this.tradingClient?.clearCache();
|
|
432
|
+
this.allToolsCache = null;
|
|
433
|
+
this.toolSourceMap.clear();
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Health check both servers
|
|
437
|
+
*/
|
|
438
|
+
async healthCheck() {
|
|
439
|
+
const discovery = await this.discoveryClient.healthCheck();
|
|
440
|
+
const trading = this.tradingClient ? await this.tradingClient.healthCheck() : null;
|
|
441
|
+
return { discovery, trading };
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
function createMCPClientManager(discoveryUrl, discoveryApiKey, tradingUrl, tradingApiKey) {
|
|
445
|
+
return new MCPClientManager(discoveryUrl, discoveryApiKey, tradingUrl, tradingApiKey);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/mcp/tools.ts
|
|
449
|
+
function convertToClaudeTools(mcpTools) {
|
|
450
|
+
return mcpTools.map((tool) => ({
|
|
451
|
+
name: tool.name,
|
|
452
|
+
description: tool.description,
|
|
453
|
+
input_schema: tool.inputSchema
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/config/setup.ts
|
|
458
|
+
async function prompt(question, isSecret = false) {
|
|
459
|
+
const rl = readline.createInterface({
|
|
460
|
+
input: process.stdin,
|
|
461
|
+
output: process.stdout
|
|
462
|
+
});
|
|
463
|
+
return new Promise((resolve2) => {
|
|
464
|
+
if (isSecret) {
|
|
465
|
+
console.log(chalk.dim("(Input will be visible)"));
|
|
466
|
+
}
|
|
467
|
+
rl.question(question, (answer) => {
|
|
468
|
+
rl.close();
|
|
469
|
+
resolve2(answer.trim());
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function printArchitectureInfo() {
|
|
474
|
+
console.log(chalk.bold.yellow("\n\u{1F4CB} How Quantish Works\n"));
|
|
475
|
+
console.log(chalk.dim("\u2500".repeat(60)));
|
|
476
|
+
console.log();
|
|
477
|
+
console.log(chalk.bold("Two Capabilities:"));
|
|
478
|
+
console.log();
|
|
479
|
+
console.log(chalk.cyan("\u{1F50D} Market Discovery") + chalk.dim(" (Free, always available)"));
|
|
480
|
+
console.log(chalk.dim(" Search markets across Polymarket, Kalshi, and more"));
|
|
481
|
+
console.log(chalk.dim(" Uses our Discovery MCP with embedded public key"));
|
|
482
|
+
console.log();
|
|
483
|
+
console.log(chalk.magenta("\u{1F4B0} Polymarket Trading") + chalk.dim(" (Optional, your own wallet)"));
|
|
484
|
+
console.log(chalk.dim(" Trade on Polymarket with a managed wallet"));
|
|
485
|
+
console.log(chalk.dim(" Uses the Quantish Signing Server"));
|
|
486
|
+
console.log();
|
|
487
|
+
console.log(chalk.bold("How Trading Works:"));
|
|
488
|
+
console.log(chalk.dim(" 1. We create a wallet for you on the Quantish Signing Server"));
|
|
489
|
+
console.log(chalk.dim(" 2. Orders are signed and relayed through Polymarket's system"));
|
|
490
|
+
console.log(chalk.dim(" 3. Gas fees are covered by Polymarket's relayer - FREE!"));
|
|
491
|
+
console.log(chalk.dim(" 4. You control your wallet via your personal API key\n"));
|
|
492
|
+
console.log(chalk.bold("Security:"));
|
|
493
|
+
console.log(chalk.dim(" \u2022 Your wallet is non-custodial - only you can authorize trades"));
|
|
494
|
+
console.log(chalk.dim(" \u2022 Export your private key anytime with: ") + chalk.cyan("export_private_key"));
|
|
495
|
+
console.log(chalk.dim(" \u2022 Discovery is read-only - it can't access your wallet"));
|
|
496
|
+
console.log();
|
|
497
|
+
console.log(chalk.dim("\u2500".repeat(60)));
|
|
498
|
+
console.log();
|
|
499
|
+
}
|
|
500
|
+
async function runSetup() {
|
|
501
|
+
const config = getConfigManager();
|
|
502
|
+
console.log();
|
|
503
|
+
console.log(chalk.bold.yellow("\u{1F680} Welcome to Quantish CLI"));
|
|
504
|
+
console.log(chalk.dim("AI-powered trading agent for Polymarket\n"));
|
|
505
|
+
if (config.isConfigured()) {
|
|
506
|
+
console.log(chalk.yellow("You already have a configuration."));
|
|
507
|
+
const overwrite = await prompt("Do you want to reconfigure? (y/N): ");
|
|
508
|
+
if (overwrite.toLowerCase() !== "y") {
|
|
509
|
+
console.log(chalk.dim("Setup cancelled."));
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
console.log();
|
|
513
|
+
}
|
|
514
|
+
printArchitectureInfo();
|
|
515
|
+
const proceed = await prompt('Press Enter to continue with setup (or "q" to quit): ');
|
|
516
|
+
if (proceed.toLowerCase() === "q") {
|
|
517
|
+
console.log(chalk.dim("Setup cancelled."));
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
console.log();
|
|
521
|
+
console.log(chalk.bold("Step 1: Anthropic API Key"));
|
|
522
|
+
console.log(chalk.dim("Powers the AI agent. Get yours at https://console.anthropic.com/"));
|
|
523
|
+
let anthropicKey = config.getAnthropicApiKey();
|
|
524
|
+
if (anthropicKey) {
|
|
525
|
+
console.log(chalk.dim(`Current: ${anthropicKey.slice(0, 10)}...`));
|
|
526
|
+
const newKey = await prompt("Enter new key (or press Enter to keep current): ", true);
|
|
527
|
+
if (newKey) {
|
|
528
|
+
anthropicKey = newKey;
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
anthropicKey = await prompt("Enter your Anthropic API key: ", true);
|
|
532
|
+
}
|
|
533
|
+
if (!anthropicKey) {
|
|
534
|
+
console.log(chalk.red("Anthropic API key is required."));
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
if (!anthropicKey.startsWith("sk-ant-")) {
|
|
538
|
+
console.log(chalk.yellow("Warning: Key doesn't look like an Anthropic key (should start with sk-ant-)"));
|
|
539
|
+
}
|
|
540
|
+
config.setAnthropicApiKey(anthropicKey);
|
|
541
|
+
console.log(chalk.green("\u2713 Anthropic API key saved\n"));
|
|
542
|
+
console.log(chalk.bold("Step 2: Polymarket Trading (Optional)"));
|
|
543
|
+
console.log(chalk.dim("Enable trading on Polymarket with your own managed wallet."));
|
|
544
|
+
console.log(chalk.dim("Skip this if you only want to search/discover markets.\n"));
|
|
545
|
+
let quantishKey = config.getQuantishApiKey();
|
|
546
|
+
let skipTrading = false;
|
|
547
|
+
if (quantishKey) {
|
|
548
|
+
console.log(chalk.dim(`Current trading key: ${quantishKey.slice(0, 12)}...`));
|
|
549
|
+
const action = await prompt("Keep current key (Enter), enter new key (n), or disable trading (d): ");
|
|
550
|
+
if (action.toLowerCase() === "n") {
|
|
551
|
+
quantishKey = await prompt("Enter your Quantish Trading API key: ", true);
|
|
552
|
+
} else if (action.toLowerCase() === "d") {
|
|
553
|
+
quantishKey = void 0;
|
|
554
|
+
skipTrading = true;
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
console.log("Options:");
|
|
558
|
+
console.log(chalk.dim(" 1. Enter an existing API key"));
|
|
559
|
+
console.log(chalk.dim(" 2. Create a new wallet (recommended for new users)"));
|
|
560
|
+
console.log(chalk.dim(" 3. Skip trading for now\n"));
|
|
561
|
+
const choice = await prompt("Choose (1/2/3): ");
|
|
562
|
+
if (choice === "1") {
|
|
563
|
+
quantishKey = await prompt("Enter your Quantish Trading API key: ", true);
|
|
564
|
+
} else if (choice === "2") {
|
|
565
|
+
console.log(chalk.dim("\nCreating a new wallet on Quantish Signing Server..."));
|
|
566
|
+
const externalId = await prompt("Enter a unique identifier (e.g., email or username): ");
|
|
567
|
+
if (!externalId) {
|
|
568
|
+
console.log(chalk.red("Identifier is required to create an account."));
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const mcpClient = createMCPClient(config.getTradingMcpUrl(), "");
|
|
573
|
+
const result = await mcpClient.callTool("request_api_key", { externalId });
|
|
574
|
+
if (result.success && typeof result.data === "object" && result.data !== null) {
|
|
575
|
+
const data = result.data;
|
|
576
|
+
quantishKey = data.apiKey;
|
|
577
|
+
console.log(chalk.green("\n\u2713 Wallet created on Quantish Signing Server!"));
|
|
578
|
+
console.log(chalk.dim(` EOA Address: ${data.eoaAddress}`));
|
|
579
|
+
console.log(chalk.dim(" (Your Safe wallet will be deployed on first trade)\n"));
|
|
580
|
+
if (data.apiSecret) {
|
|
581
|
+
console.log(chalk.yellow("\u26A0\uFE0F Save your API secret (shown only once):"));
|
|
582
|
+
console.log(chalk.bold.yellow(` ${String(data.apiSecret)}`));
|
|
583
|
+
console.log();
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
console.log(chalk.red("Failed to create account: " + (result.error || "Unknown error")));
|
|
587
|
+
console.log(chalk.dim('You can continue without trading - run "quantish init" later to set up.'));
|
|
588
|
+
skipTrading = true;
|
|
589
|
+
}
|
|
590
|
+
} catch (error2) {
|
|
591
|
+
console.log(chalk.red("Failed to connect to Quantish Trading Server."));
|
|
592
|
+
console.log(chalk.dim(String(error2)));
|
|
593
|
+
console.log(chalk.dim('You can continue without trading - run "quantish init" later to set up.'));
|
|
594
|
+
skipTrading = true;
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
skipTrading = true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (quantishKey) {
|
|
601
|
+
config.setQuantishApiKey(quantishKey);
|
|
602
|
+
console.log(chalk.green("\u2713 Trading API key saved\n"));
|
|
603
|
+
} else if (skipTrading) {
|
|
604
|
+
console.log(chalk.dim("\u2713 Trading disabled - you can still search markets via Discovery\n"));
|
|
605
|
+
} else {
|
|
606
|
+
console.log(chalk.dim("\u2713 No trading key - you can still search markets via Discovery\n"));
|
|
607
|
+
}
|
|
608
|
+
console.log(chalk.bold("Step 3: Exa API Key (Optional)"));
|
|
609
|
+
console.log(chalk.dim("Powers web search. Get one free at https://dashboard.exa.ai"));
|
|
610
|
+
console.log(chalk.dim("Without this, web search will use DuckDuckGo as fallback.\n"));
|
|
611
|
+
const exaKey = await prompt("Enter your Exa API key (or press Enter to skip): ", true);
|
|
612
|
+
if (exaKey) {
|
|
613
|
+
console.log(chalk.green("\u2713 Great! Add this to your shell profile:"));
|
|
614
|
+
console.log(chalk.cyan(` export EXA_API_KEY="${exaKey}"`));
|
|
615
|
+
console.log();
|
|
616
|
+
} else {
|
|
617
|
+
console.log(chalk.dim("Skipped. Web search will use DuckDuckGo.\n"));
|
|
618
|
+
}
|
|
619
|
+
console.log(chalk.bold("Step 4: Verifying connections..."));
|
|
620
|
+
try {
|
|
621
|
+
const discoveryClient = createMCPClient(DISCOVERY_MCP_URL, DISCOVERY_MCP_PUBLIC_KEY, "discovery");
|
|
622
|
+
const discoveryResult = await discoveryClient.callTool("get_market_stats", {});
|
|
623
|
+
if (discoveryResult.success) {
|
|
624
|
+
console.log(chalk.green("\u2713 Discovery MCP connected"));
|
|
625
|
+
} else {
|
|
626
|
+
console.log(chalk.yellow("\u26A0 Discovery MCP: " + (discoveryResult.error || "Unknown error")));
|
|
627
|
+
}
|
|
628
|
+
} catch (error2) {
|
|
629
|
+
console.log(chalk.yellow("\u26A0 Could not verify Discovery MCP"));
|
|
630
|
+
console.log(chalk.dim(String(error2)));
|
|
631
|
+
}
|
|
632
|
+
if (quantishKey) {
|
|
633
|
+
try {
|
|
634
|
+
const tradingClient = createMCPClient(config.getTradingMcpUrl(), quantishKey, "trading");
|
|
635
|
+
const result = await tradingClient.callTool("get_wallet_status", {});
|
|
636
|
+
if (result.success && typeof result.data === "object" && result.data !== null) {
|
|
637
|
+
const data = result.data;
|
|
638
|
+
console.log(chalk.green("\u2713 Trading MCP connected"));
|
|
639
|
+
console.log(chalk.dim(` Safe Address: ${data.safeAddress || "Not yet deployed"}`));
|
|
640
|
+
console.log(chalk.dim(` Status: ${data.status}`));
|
|
641
|
+
console.log(chalk.dim(` Ready to trade: ${data.isReady ? "Yes" : "Run setup_wallet first"}`));
|
|
642
|
+
} else {
|
|
643
|
+
console.log(chalk.yellow("\u26A0 Trading MCP: " + (result.error || "Unknown error")));
|
|
644
|
+
}
|
|
645
|
+
} catch (error2) {
|
|
646
|
+
console.log(chalk.yellow("\u26A0 Could not verify Trading MCP connection."));
|
|
647
|
+
console.log(chalk.dim(String(error2)));
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
console.log(chalk.dim("\u23ED Trading MCP skipped (no API key)"));
|
|
651
|
+
}
|
|
652
|
+
console.log();
|
|
653
|
+
console.log(chalk.bold.green("\u{1F389} Setup complete!"));
|
|
654
|
+
console.log();
|
|
655
|
+
console.log(chalk.bold("\u{1F4C1} Your credentials are saved:"));
|
|
656
|
+
console.log(chalk.dim(` Local config: ${config.getConfigPath()}`));
|
|
657
|
+
console.log(chalk.dim(" Wallet keys: Encrypted on Quantish server (accessible via your API key)"));
|
|
658
|
+
console.log();
|
|
659
|
+
console.log("You can now use Quantish CLI:");
|
|
660
|
+
console.log(chalk.yellow(" quantish") + " - Start interactive chat");
|
|
661
|
+
console.log(chalk.yellow(' quantish -p "check my balance"') + " - One-shot command");
|
|
662
|
+
console.log(chalk.yellow(" quantish tools") + " - List available tools");
|
|
663
|
+
console.log(chalk.yellow(" quantish config") + " - View configuration");
|
|
664
|
+
console.log(chalk.yellow(" quantish config --export") + " - Export keys for your own agents");
|
|
665
|
+
console.log();
|
|
666
|
+
console.log(chalk.dim("Your wallet is managed by the Quantish Signing Server."));
|
|
667
|
+
console.log(chalk.dim("The CLOB signing credentials are stored encrypted on the server."));
|
|
668
|
+
console.log(chalk.dim('To export your private key: quantish -p "export my private key"'));
|
|
669
|
+
console.log();
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
async function ensureConfigured() {
|
|
673
|
+
const config = getConfigManager();
|
|
674
|
+
if (!config.isConfigured()) {
|
|
675
|
+
console.log(chalk.yellow("Quantish CLI is not configured yet."));
|
|
676
|
+
console.log("Run " + chalk.yellow("quantish init") + " to set up.\n");
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/agent/loop.ts
|
|
683
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
684
|
+
|
|
685
|
+
// src/tools/filesystem.ts
|
|
686
|
+
import * as fs from "fs/promises";
|
|
687
|
+
import * as path from "path";
|
|
688
|
+
import { existsSync } from "fs";
|
|
689
|
+
async function readFile2(filePath, options) {
|
|
690
|
+
try {
|
|
691
|
+
const resolvedPath = path.resolve(filePath);
|
|
692
|
+
if (!existsSync(resolvedPath)) {
|
|
693
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
694
|
+
}
|
|
695
|
+
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
696
|
+
if (options?.offset !== void 0 || options?.limit !== void 0) {
|
|
697
|
+
const lines = content.split("\n");
|
|
698
|
+
const start = options.offset ?? 0;
|
|
699
|
+
const end = options.limit ? start + options.limit : lines.length;
|
|
700
|
+
const selectedLines = lines.slice(start, end);
|
|
701
|
+
const numbered = selectedLines.map((line, i) => `${(start + i + 1).toString().padStart(6)}|${line}`).join("\n");
|
|
702
|
+
return { success: true, data: numbered };
|
|
703
|
+
}
|
|
704
|
+
return { success: true, data: content };
|
|
705
|
+
} catch (error2) {
|
|
706
|
+
return { success: false, error: `Failed to read file: ${error2 instanceof Error ? error2.message : String(error2)}` };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async function writeFile2(filePath, content) {
|
|
710
|
+
try {
|
|
711
|
+
const resolvedPath = path.resolve(filePath);
|
|
712
|
+
const dir = path.dirname(resolvedPath);
|
|
713
|
+
await fs.mkdir(dir, { recursive: true });
|
|
714
|
+
await fs.writeFile(resolvedPath, content, "utf-8");
|
|
715
|
+
return { success: true, data: { path: resolvedPath, bytesWritten: Buffer.byteLength(content) } };
|
|
716
|
+
} catch (error2) {
|
|
717
|
+
return { success: false, error: `Failed to write file: ${error2 instanceof Error ? error2.message : String(error2)}` };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function listDir(dirPath, options) {
|
|
721
|
+
try {
|
|
722
|
+
const resolvedPath = path.resolve(dirPath);
|
|
723
|
+
if (!existsSync(resolvedPath)) {
|
|
724
|
+
return { success: false, error: `Directory not found: ${dirPath}` };
|
|
725
|
+
}
|
|
726
|
+
const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
|
727
|
+
const items = entries.map((entry) => ({
|
|
728
|
+
name: entry.name,
|
|
729
|
+
type: entry.isDirectory() ? "directory" : "file",
|
|
730
|
+
path: path.join(resolvedPath, entry.name)
|
|
731
|
+
}));
|
|
732
|
+
items.sort((a, b) => {
|
|
733
|
+
if (a.type === b.type) return a.name.localeCompare(b.name);
|
|
734
|
+
return a.type === "directory" ? -1 : 1;
|
|
735
|
+
});
|
|
736
|
+
return { success: true, data: items };
|
|
737
|
+
} catch (error2) {
|
|
738
|
+
return { success: false, error: `Failed to list directory: ${error2 instanceof Error ? error2.message : String(error2)}` };
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function deleteFile(filePath) {
|
|
742
|
+
try {
|
|
743
|
+
const resolvedPath = path.resolve(filePath);
|
|
744
|
+
if (!existsSync(resolvedPath)) {
|
|
745
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
746
|
+
}
|
|
747
|
+
await fs.unlink(resolvedPath);
|
|
748
|
+
return { success: true, data: { deleted: resolvedPath } };
|
|
749
|
+
} catch (error2) {
|
|
750
|
+
return { success: false, error: `Failed to delete file: ${error2 instanceof Error ? error2.message : String(error2)}` };
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async function fileExists(filePath) {
|
|
754
|
+
try {
|
|
755
|
+
const resolvedPath = path.resolve(filePath);
|
|
756
|
+
const exists = existsSync(resolvedPath);
|
|
757
|
+
if (exists) {
|
|
758
|
+
const stats = await fs.stat(resolvedPath);
|
|
759
|
+
return {
|
|
760
|
+
success: true,
|
|
761
|
+
data: {
|
|
762
|
+
exists: true,
|
|
763
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
764
|
+
size: stats.size,
|
|
765
|
+
modified: stats.mtime.toISOString()
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
return { success: true, data: { exists: false } };
|
|
770
|
+
} catch (error2) {
|
|
771
|
+
return { success: false, error: `Failed to check file: ${error2 instanceof Error ? error2.message : String(error2)}` };
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function editFile(filePath, oldString, newString, options) {
|
|
775
|
+
try {
|
|
776
|
+
const resolvedPath = path.resolve(filePath);
|
|
777
|
+
if (!existsSync(resolvedPath)) {
|
|
778
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
779
|
+
}
|
|
780
|
+
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
781
|
+
if (!content.includes(oldString)) {
|
|
782
|
+
return {
|
|
783
|
+
success: false,
|
|
784
|
+
error: `The string to replace was not found in the file. Make sure to include exact whitespace and formatting.`
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const occurrences = content.split(oldString).length - 1;
|
|
788
|
+
if (!options?.replaceAll && occurrences > 1) {
|
|
789
|
+
return {
|
|
790
|
+
success: false,
|
|
791
|
+
error: `Found ${occurrences} occurrences of the string. Use replaceAll: true to replace all, or provide a more unique string.`
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
const newContent = options?.replaceAll ? content.replaceAll(oldString, newString) : content.replace(oldString, newString);
|
|
795
|
+
await fs.writeFile(resolvedPath, newContent, "utf-8");
|
|
796
|
+
return {
|
|
797
|
+
success: true,
|
|
798
|
+
data: {
|
|
799
|
+
path: resolvedPath,
|
|
800
|
+
replacements: options?.replaceAll ? occurrences : 1,
|
|
801
|
+
bytesWritten: Buffer.byteLength(newContent)
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
} catch (error2) {
|
|
805
|
+
return { success: false, error: `Failed to edit file: ${error2 instanceof Error ? error2.message : String(error2)}` };
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
var filesystemTools = [
|
|
809
|
+
{
|
|
810
|
+
name: "read_file",
|
|
811
|
+
description: "Read the contents of a file from the local filesystem. Returns the file content as text. Supports optional line offset and limit for large files.",
|
|
812
|
+
input_schema: {
|
|
813
|
+
type: "object",
|
|
814
|
+
properties: {
|
|
815
|
+
path: {
|
|
816
|
+
type: "string",
|
|
817
|
+
description: "The path to the file to read (absolute or relative to current directory)"
|
|
818
|
+
},
|
|
819
|
+
offset: {
|
|
820
|
+
type: "number",
|
|
821
|
+
description: "Optional: Start reading from this line number (0-indexed)"
|
|
822
|
+
},
|
|
823
|
+
limit: {
|
|
824
|
+
type: "number",
|
|
825
|
+
description: "Optional: Maximum number of lines to read"
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
required: ["path"]
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
name: "write_file",
|
|
833
|
+
description: "Write content to a file on the local filesystem. Creates the file if it doesn't exist, or overwrites if it does. Creates parent directories as needed.",
|
|
834
|
+
input_schema: {
|
|
835
|
+
type: "object",
|
|
836
|
+
properties: {
|
|
837
|
+
path: {
|
|
838
|
+
type: "string",
|
|
839
|
+
description: "The path to the file to write (absolute or relative)"
|
|
840
|
+
},
|
|
841
|
+
content: {
|
|
842
|
+
type: "string",
|
|
843
|
+
description: "The content to write to the file"
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
required: ["path", "content"]
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
name: "list_dir",
|
|
851
|
+
description: "List files and directories in a given path. Returns entries with name, type (file/directory), and full path.",
|
|
852
|
+
input_schema: {
|
|
853
|
+
type: "object",
|
|
854
|
+
properties: {
|
|
855
|
+
path: {
|
|
856
|
+
type: "string",
|
|
857
|
+
description: "The directory path to list"
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
required: ["path"]
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
name: "delete_file",
|
|
865
|
+
description: "Delete a file from the local filesystem.",
|
|
866
|
+
input_schema: {
|
|
867
|
+
type: "object",
|
|
868
|
+
properties: {
|
|
869
|
+
path: {
|
|
870
|
+
type: "string",
|
|
871
|
+
description: "The path to the file to delete"
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
required: ["path"]
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
name: "file_exists",
|
|
879
|
+
description: "Check if a file or directory exists, and get basic info (type, size, modified date).",
|
|
880
|
+
input_schema: {
|
|
881
|
+
type: "object",
|
|
882
|
+
properties: {
|
|
883
|
+
path: {
|
|
884
|
+
type: "string",
|
|
885
|
+
description: "The path to check"
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
required: ["path"]
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
name: "edit_file",
|
|
893
|
+
description: "Edit a file by replacing a specific string with new content. Safer than write_file as it only modifies the targeted section. The old_string must match exactly (including whitespace).",
|
|
894
|
+
input_schema: {
|
|
895
|
+
type: "object",
|
|
896
|
+
properties: {
|
|
897
|
+
path: {
|
|
898
|
+
type: "string",
|
|
899
|
+
description: "The path to the file to edit"
|
|
900
|
+
},
|
|
901
|
+
old_string: {
|
|
902
|
+
type: "string",
|
|
903
|
+
description: "The exact string to find and replace. Must be unique in the file unless using replaceAll."
|
|
904
|
+
},
|
|
905
|
+
new_string: {
|
|
906
|
+
type: "string",
|
|
907
|
+
description: "The new string to replace the old one with"
|
|
908
|
+
},
|
|
909
|
+
replace_all: {
|
|
910
|
+
type: "boolean",
|
|
911
|
+
description: "If true, replace all occurrences. Default false (only replace first, and fail if multiple found)."
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
required: ["path", "old_string", "new_string"]
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
];
|
|
918
|
+
async function executeFilesystemTool(name, args) {
|
|
919
|
+
switch (name) {
|
|
920
|
+
case "read_file":
|
|
921
|
+
return readFile2(args.path, {
|
|
922
|
+
offset: args.offset,
|
|
923
|
+
limit: args.limit
|
|
924
|
+
});
|
|
925
|
+
case "write_file":
|
|
926
|
+
return writeFile2(args.path, args.content);
|
|
927
|
+
case "list_dir":
|
|
928
|
+
return listDir(args.path);
|
|
929
|
+
case "delete_file":
|
|
930
|
+
return deleteFile(args.path);
|
|
931
|
+
case "file_exists":
|
|
932
|
+
return fileExists(args.path);
|
|
933
|
+
case "edit_file":
|
|
934
|
+
return editFile(
|
|
935
|
+
args.path,
|
|
936
|
+
args.old_string,
|
|
937
|
+
args.new_string,
|
|
938
|
+
{ replaceAll: args.replace_all }
|
|
939
|
+
);
|
|
940
|
+
default:
|
|
941
|
+
return { success: false, error: `Unknown filesystem tool: ${name}` };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/tools/shell.ts
|
|
946
|
+
import { exec } from "child_process";
|
|
947
|
+
import { promisify } from "util";
|
|
948
|
+
|
|
949
|
+
// src/tools/process-manager.ts
|
|
950
|
+
import { spawn } from "child_process";
|
|
951
|
+
import { EventEmitter } from "events";
|
|
952
|
+
var ProcessManager = class extends EventEmitter {
|
|
953
|
+
processes = /* @__PURE__ */ new Map();
|
|
954
|
+
nextId = 1;
|
|
955
|
+
maxOutputLines = 100;
|
|
956
|
+
constructor() {
|
|
957
|
+
super();
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Spawn a new background process
|
|
961
|
+
*/
|
|
962
|
+
spawn(command, options = {}) {
|
|
963
|
+
const id = this.nextId++;
|
|
964
|
+
const cwd = options.cwd || process.cwd();
|
|
965
|
+
const name = options.name || command.split(" ")[0];
|
|
966
|
+
const child = spawn("bash", ["-c", command], {
|
|
967
|
+
cwd,
|
|
968
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
969
|
+
detached: false,
|
|
970
|
+
// Keep attached so we can track it
|
|
971
|
+
env: { ...process.env, FORCE_COLOR: "1" }
|
|
972
|
+
// Enable colors
|
|
973
|
+
});
|
|
974
|
+
const spawnedProcess = {
|
|
975
|
+
id,
|
|
976
|
+
pid: child.pid,
|
|
977
|
+
command,
|
|
978
|
+
name,
|
|
979
|
+
cwd,
|
|
980
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
981
|
+
status: "running",
|
|
982
|
+
child,
|
|
983
|
+
outputBuffer: [],
|
|
984
|
+
lastOutput: [],
|
|
985
|
+
onOutput: options.onOutput
|
|
986
|
+
};
|
|
987
|
+
child.stdout?.on("data", (data) => {
|
|
988
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
989
|
+
for (const line of lines) {
|
|
990
|
+
this.addOutput(spawnedProcess, line);
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
child.stderr?.on("data", (data) => {
|
|
994
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
995
|
+
for (const line of lines) {
|
|
996
|
+
this.addOutput(spawnedProcess, `[stderr] ${line}`);
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
child.on("exit", (code, signal) => {
|
|
1000
|
+
spawnedProcess.status = code === 0 ? "stopped" : "error";
|
|
1001
|
+
this.addOutput(spawnedProcess, `[Process exited with code ${code}${signal ? `, signal ${signal}` : ""}]`);
|
|
1002
|
+
this.emit("exit", id, code, signal);
|
|
1003
|
+
});
|
|
1004
|
+
child.on("error", (err) => {
|
|
1005
|
+
spawnedProcess.status = "error";
|
|
1006
|
+
this.addOutput(spawnedProcess, `[Error: ${err.message}]`);
|
|
1007
|
+
this.emit("error", id, err);
|
|
1008
|
+
});
|
|
1009
|
+
this.processes.set(id, spawnedProcess);
|
|
1010
|
+
this.emit("spawn", id, spawnedProcess);
|
|
1011
|
+
return this.getProcessInfo(spawnedProcess);
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Add output to process buffer
|
|
1015
|
+
*/
|
|
1016
|
+
addOutput(process2, line) {
|
|
1017
|
+
process2.outputBuffer.push(line);
|
|
1018
|
+
process2.lastOutput.push(line);
|
|
1019
|
+
if (process2.outputBuffer.length > this.maxOutputLines) {
|
|
1020
|
+
process2.outputBuffer.shift();
|
|
1021
|
+
}
|
|
1022
|
+
if (process2.lastOutput.length > 20) {
|
|
1023
|
+
process2.lastOutput.shift();
|
|
1024
|
+
}
|
|
1025
|
+
process2.onOutput?.(line);
|
|
1026
|
+
this.emit("output", process2.id, line);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Get process info without the child process object
|
|
1030
|
+
*/
|
|
1031
|
+
getProcessInfo(process2) {
|
|
1032
|
+
return {
|
|
1033
|
+
id: process2.id,
|
|
1034
|
+
pid: process2.pid,
|
|
1035
|
+
command: process2.command,
|
|
1036
|
+
name: process2.name,
|
|
1037
|
+
cwd: process2.cwd,
|
|
1038
|
+
startedAt: process2.startedAt,
|
|
1039
|
+
status: process2.status,
|
|
1040
|
+
lastOutput: [...process2.lastOutput]
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Kill a process by ID
|
|
1045
|
+
*/
|
|
1046
|
+
kill(id) {
|
|
1047
|
+
const process2 = this.processes.get(id);
|
|
1048
|
+
if (!process2) {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
if (process2.status !== "running") {
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
process2.child.kill("SIGTERM");
|
|
1056
|
+
setTimeout(() => {
|
|
1057
|
+
if (process2.status === "running") {
|
|
1058
|
+
process2.child.kill("SIGKILL");
|
|
1059
|
+
}
|
|
1060
|
+
}, 3e3);
|
|
1061
|
+
process2.status = "stopped";
|
|
1062
|
+
this.emit("kill", id);
|
|
1063
|
+
return true;
|
|
1064
|
+
} catch (error2) {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Kill all running processes
|
|
1070
|
+
*/
|
|
1071
|
+
killAll() {
|
|
1072
|
+
for (const [id, process2] of this.processes) {
|
|
1073
|
+
if (process2.status === "running") {
|
|
1074
|
+
this.kill(id);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* List all processes
|
|
1080
|
+
*/
|
|
1081
|
+
list() {
|
|
1082
|
+
return Array.from(this.processes.values()).map((p) => this.getProcessInfo(p));
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* List running processes only
|
|
1086
|
+
*/
|
|
1087
|
+
listRunning() {
|
|
1088
|
+
return this.list().filter((p) => p.status === "running");
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Get a specific process
|
|
1092
|
+
*/
|
|
1093
|
+
get(id) {
|
|
1094
|
+
const process2 = this.processes.get(id);
|
|
1095
|
+
return process2 ? this.getProcessInfo(process2) : void 0;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Get recent output from a process
|
|
1099
|
+
*/
|
|
1100
|
+
getOutput(id, lines = 20) {
|
|
1101
|
+
const process2 = this.processes.get(id);
|
|
1102
|
+
if (!process2) {
|
|
1103
|
+
return [];
|
|
1104
|
+
}
|
|
1105
|
+
return process2.outputBuffer.slice(-lines);
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Check if any processes are running
|
|
1109
|
+
*/
|
|
1110
|
+
hasRunning() {
|
|
1111
|
+
return this.listRunning().length > 0;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Get count of running processes
|
|
1115
|
+
*/
|
|
1116
|
+
runningCount() {
|
|
1117
|
+
return this.listRunning().length;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Set output callback for a process
|
|
1121
|
+
*/
|
|
1122
|
+
setOutputCallback(id, callback) {
|
|
1123
|
+
const process2 = this.processes.get(id);
|
|
1124
|
+
if (process2) {
|
|
1125
|
+
process2.onOutput = callback;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
var processManager = new ProcessManager();
|
|
1130
|
+
|
|
1131
|
+
// src/tools/shell.ts
|
|
1132
|
+
var execPromise = promisify(exec);
|
|
1133
|
+
var BLOCKED_COMMANDS = [
|
|
1134
|
+
"rm -rf /",
|
|
1135
|
+
"rm -rf ~",
|
|
1136
|
+
"rm -rf /*",
|
|
1137
|
+
"mkfs",
|
|
1138
|
+
"dd if=/dev/zero",
|
|
1139
|
+
":(){:|:&};:",
|
|
1140
|
+
// Fork bomb
|
|
1141
|
+
"chmod -R 777 /",
|
|
1142
|
+
"chown -R"
|
|
1143
|
+
];
|
|
1144
|
+
var DANGEROUS_PATTERNS = [
|
|
1145
|
+
/rm\s+-rf?\s+/,
|
|
1146
|
+
/sudo\s+/,
|
|
1147
|
+
/>\s*\/dev\//,
|
|
1148
|
+
/chmod\s+.*\s+\//
|
|
1149
|
+
];
|
|
1150
|
+
var PACKAGE_MANAGER_PATTERNS = [
|
|
1151
|
+
/^(npm|yarn|pnpm|bun)\s+(install|i|add|ci|update|upgrade)/,
|
|
1152
|
+
/^(pip|pip3)\s+install/,
|
|
1153
|
+
/^cargo\s+(build|install)/,
|
|
1154
|
+
/^go\s+(build|get|mod)/
|
|
1155
|
+
];
|
|
1156
|
+
var LONG_RUNNING_PATTERNS = [
|
|
1157
|
+
/^(npm|yarn|pnpm|bun)\s+(build|test|run)/,
|
|
1158
|
+
/webpack|vite|esbuild|rollup/,
|
|
1159
|
+
/docker\s+(build|pull|push)/
|
|
1160
|
+
];
|
|
1161
|
+
function getSmartTimeout(command, explicitTimeout) {
|
|
1162
|
+
if (explicitTimeout !== void 0) {
|
|
1163
|
+
return explicitTimeout;
|
|
1164
|
+
}
|
|
1165
|
+
for (const pattern of PACKAGE_MANAGER_PATTERNS) {
|
|
1166
|
+
if (pattern.test(command)) {
|
|
1167
|
+
return 3e5;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
for (const pattern of LONG_RUNNING_PATTERNS) {
|
|
1171
|
+
if (pattern.test(command)) {
|
|
1172
|
+
return 18e4;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return 3e4;
|
|
1176
|
+
}
|
|
1177
|
+
function checkCommand(command) {
|
|
1178
|
+
for (const blocked of BLOCKED_COMMANDS) {
|
|
1179
|
+
if (command.includes(blocked)) {
|
|
1180
|
+
return { allowed: false, reason: `Blocked command pattern: ${blocked}` };
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
1184
|
+
if (pattern.test(command)) {
|
|
1185
|
+
return { allowed: false, reason: `Dangerous command pattern detected. Use allowDangerous option to override.` };
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return { allowed: true };
|
|
1189
|
+
}
|
|
1190
|
+
async function runCommand(command, options = {}) {
|
|
1191
|
+
const {
|
|
1192
|
+
cwd = process.cwd(),
|
|
1193
|
+
timeout: explicitTimeout,
|
|
1194
|
+
maxBuffer = 10 * 1024 * 1024,
|
|
1195
|
+
// 10MB
|
|
1196
|
+
allowDangerous = false
|
|
1197
|
+
} = options;
|
|
1198
|
+
const timeout = getSmartTimeout(command, explicitTimeout);
|
|
1199
|
+
if (!allowDangerous) {
|
|
1200
|
+
const check = checkCommand(command);
|
|
1201
|
+
if (!check.allowed) {
|
|
1202
|
+
return { success: false, error: check.reason };
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
const { stdout, stderr } = await execPromise(command, {
|
|
1207
|
+
cwd,
|
|
1208
|
+
timeout,
|
|
1209
|
+
maxBuffer,
|
|
1210
|
+
shell: "/bin/bash",
|
|
1211
|
+
// Explicit bash for compound command support
|
|
1212
|
+
env: { ...process.env }
|
|
1213
|
+
});
|
|
1214
|
+
return {
|
|
1215
|
+
success: true,
|
|
1216
|
+
data: {
|
|
1217
|
+
stdout: stdout.trim(),
|
|
1218
|
+
stderr: stderr.trim(),
|
|
1219
|
+
command,
|
|
1220
|
+
cwd,
|
|
1221
|
+
timeoutUsed: timeout
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
} catch (error2) {
|
|
1225
|
+
const execError = error2;
|
|
1226
|
+
if (execError.killed) {
|
|
1227
|
+
return {
|
|
1228
|
+
success: false,
|
|
1229
|
+
error: `Command timed out after ${timeout / 1e3}s. For long-running commands, use start_background_process or increase timeout.`,
|
|
1230
|
+
data: {
|
|
1231
|
+
stdout: execError.stdout || "",
|
|
1232
|
+
stderr: execError.stderr || "",
|
|
1233
|
+
timedOut: true
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
success: false,
|
|
1239
|
+
error: execError.message || "Command failed",
|
|
1240
|
+
data: {
|
|
1241
|
+
stdout: execError.stdout || "",
|
|
1242
|
+
stderr: execError.stderr || "",
|
|
1243
|
+
exitCode: execError.code
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async function grep(pattern, path2, options = {}) {
|
|
1249
|
+
const { ignoreCase = false, contextLines = 0 } = options;
|
|
1250
|
+
const rgArgs = [
|
|
1251
|
+
pattern,
|
|
1252
|
+
path2,
|
|
1253
|
+
"--no-heading",
|
|
1254
|
+
"--line-number",
|
|
1255
|
+
"--color=never"
|
|
1256
|
+
];
|
|
1257
|
+
if (ignoreCase) rgArgs.push("-i");
|
|
1258
|
+
if (contextLines > 0) rgArgs.push(`-C${contextLines}`);
|
|
1259
|
+
try {
|
|
1260
|
+
const { stdout } = await execPromise(`rg ${rgArgs.join(" ")}`, {
|
|
1261
|
+
timeout: 3e4,
|
|
1262
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1263
|
+
});
|
|
1264
|
+
return {
|
|
1265
|
+
success: true,
|
|
1266
|
+
data: {
|
|
1267
|
+
matches: stdout.trim().split("\n").filter(Boolean),
|
|
1268
|
+
pattern,
|
|
1269
|
+
path: path2
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
} catch {
|
|
1273
|
+
try {
|
|
1274
|
+
const grepArgs = [
|
|
1275
|
+
ignoreCase ? "-i" : "",
|
|
1276
|
+
contextLines > 0 ? `-C${contextLines}` : "",
|
|
1277
|
+
"-rn",
|
|
1278
|
+
pattern,
|
|
1279
|
+
path2
|
|
1280
|
+
].filter(Boolean);
|
|
1281
|
+
const { stdout } = await execPromise(`grep ${grepArgs.join(" ")}`, {
|
|
1282
|
+
timeout: 3e4,
|
|
1283
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1284
|
+
});
|
|
1285
|
+
return {
|
|
1286
|
+
success: true,
|
|
1287
|
+
data: {
|
|
1288
|
+
matches: stdout.trim().split("\n").filter(Boolean),
|
|
1289
|
+
pattern,
|
|
1290
|
+
path: path2
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
} catch (error2) {
|
|
1294
|
+
const execError = error2;
|
|
1295
|
+
if (execError.code === 1) {
|
|
1296
|
+
return { success: true, data: { matches: [], pattern, path: path2 } };
|
|
1297
|
+
}
|
|
1298
|
+
return { success: false, error: `Search failed: ${execError.message}` };
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
async function findFiles(pattern, directory = ".") {
|
|
1303
|
+
try {
|
|
1304
|
+
const { stdout } = await execPromise(
|
|
1305
|
+
`find "${directory}" -name "${pattern}" -type f 2>/dev/null | head -100`,
|
|
1306
|
+
{ timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }
|
|
1307
|
+
);
|
|
1308
|
+
return {
|
|
1309
|
+
success: true,
|
|
1310
|
+
data: {
|
|
1311
|
+
files: stdout.trim().split("\n").filter(Boolean),
|
|
1312
|
+
pattern,
|
|
1313
|
+
directory
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
} catch (error2) {
|
|
1317
|
+
const execError = error2;
|
|
1318
|
+
return { success: false, error: `Find failed: ${execError.message}` };
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
var shellTools = [
|
|
1322
|
+
{
|
|
1323
|
+
name: "run_command",
|
|
1324
|
+
description: "Execute a shell command on the local machine. Returns stdout, stderr, and exit code. Some dangerous commands are blocked for safety. Supports compound commands (&&, ||). Smart timeout: 5 min for npm/yarn install, 3 min for builds, 30s default. For dev servers or long-running processes, use start_background_process instead.",
|
|
1325
|
+
input_schema: {
|
|
1326
|
+
type: "object",
|
|
1327
|
+
properties: {
|
|
1328
|
+
command: {
|
|
1329
|
+
type: "string",
|
|
1330
|
+
description: "The shell command to execute. Compound commands with && and || are supported."
|
|
1331
|
+
},
|
|
1332
|
+
cwd: {
|
|
1333
|
+
type: "string",
|
|
1334
|
+
description: "Optional: Working directory for the command (defaults to current directory)"
|
|
1335
|
+
},
|
|
1336
|
+
timeout: {
|
|
1337
|
+
type: "number",
|
|
1338
|
+
description: "Optional: Timeout in milliseconds. Smart defaults: 300000 for npm install, 180000 for builds, 30000 for quick commands."
|
|
1339
|
+
}
|
|
1340
|
+
},
|
|
1341
|
+
required: ["command"]
|
|
1342
|
+
}
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
name: "grep",
|
|
1346
|
+
description: "Search for a text pattern in files. Uses ripgrep if available, falls back to grep. Returns matching lines with file paths and line numbers.",
|
|
1347
|
+
input_schema: {
|
|
1348
|
+
type: "object",
|
|
1349
|
+
properties: {
|
|
1350
|
+
pattern: {
|
|
1351
|
+
type: "string",
|
|
1352
|
+
description: "The regex pattern to search for"
|
|
1353
|
+
},
|
|
1354
|
+
path: {
|
|
1355
|
+
type: "string",
|
|
1356
|
+
description: "The file or directory to search in"
|
|
1357
|
+
},
|
|
1358
|
+
ignore_case: {
|
|
1359
|
+
type: "boolean",
|
|
1360
|
+
description: "Optional: Case-insensitive search (default: false)"
|
|
1361
|
+
},
|
|
1362
|
+
context_lines: {
|
|
1363
|
+
type: "number",
|
|
1364
|
+
description: "Optional: Number of context lines before and after matches"
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
required: ["pattern", "path"]
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
name: "find_files",
|
|
1372
|
+
description: "Find files by name pattern (glob). Returns up to 100 matching file paths.",
|
|
1373
|
+
input_schema: {
|
|
1374
|
+
type: "object",
|
|
1375
|
+
properties: {
|
|
1376
|
+
pattern: {
|
|
1377
|
+
type: "string",
|
|
1378
|
+
description: 'The glob pattern to match (e.g., "*.ts", "package.json")'
|
|
1379
|
+
},
|
|
1380
|
+
directory: {
|
|
1381
|
+
type: "string",
|
|
1382
|
+
description: "Optional: Directory to search in (default: current directory)"
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
required: ["pattern"]
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
name: "start_background_process",
|
|
1390
|
+
description: "Start a long-running process (like a dev server) in the background. The process runs independently and its output is captured. Returns a process ID that can be used to stop it later. Use this for: npm start, npm run dev, python servers, watch mode commands, etc.",
|
|
1391
|
+
input_schema: {
|
|
1392
|
+
type: "object",
|
|
1393
|
+
properties: {
|
|
1394
|
+
command: {
|
|
1395
|
+
type: "string",
|
|
1396
|
+
description: 'The command to run (e.g., "npm start", "python -m http.server 8000")'
|
|
1397
|
+
},
|
|
1398
|
+
cwd: {
|
|
1399
|
+
type: "string",
|
|
1400
|
+
description: "Optional: Working directory for the process"
|
|
1401
|
+
},
|
|
1402
|
+
name: {
|
|
1403
|
+
type: "string",
|
|
1404
|
+
description: 'Optional: Friendly name for the process (e.g., "React Dev Server")'
|
|
1405
|
+
}
|
|
1406
|
+
},
|
|
1407
|
+
required: ["command"]
|
|
1408
|
+
}
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
name: "stop_process",
|
|
1412
|
+
description: "Stop a background process by its process ID. Use list_processes to see running processes.",
|
|
1413
|
+
input_schema: {
|
|
1414
|
+
type: "object",
|
|
1415
|
+
properties: {
|
|
1416
|
+
process_id: {
|
|
1417
|
+
type: "number",
|
|
1418
|
+
description: "The process ID returned by start_background_process"
|
|
1419
|
+
}
|
|
1420
|
+
},
|
|
1421
|
+
required: ["process_id"]
|
|
1422
|
+
}
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
name: "list_processes",
|
|
1426
|
+
description: "List all background processes started by this session, including their status and recent output.",
|
|
1427
|
+
input_schema: {
|
|
1428
|
+
type: "object",
|
|
1429
|
+
properties: {},
|
|
1430
|
+
required: []
|
|
1431
|
+
}
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
name: "get_process_output",
|
|
1435
|
+
description: "Get recent output from a background process.",
|
|
1436
|
+
input_schema: {
|
|
1437
|
+
type: "object",
|
|
1438
|
+
properties: {
|
|
1439
|
+
process_id: {
|
|
1440
|
+
type: "number",
|
|
1441
|
+
description: "The process ID"
|
|
1442
|
+
},
|
|
1443
|
+
lines: {
|
|
1444
|
+
type: "number",
|
|
1445
|
+
description: "Number of output lines to retrieve (default: 20)"
|
|
1446
|
+
}
|
|
1447
|
+
},
|
|
1448
|
+
required: ["process_id"]
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
];
|
|
1452
|
+
function startBackgroundProcess(command, options = {}) {
|
|
1453
|
+
try {
|
|
1454
|
+
const processInfo = processManager.spawn(command, {
|
|
1455
|
+
cwd: options.cwd,
|
|
1456
|
+
name: options.name
|
|
1457
|
+
});
|
|
1458
|
+
return {
|
|
1459
|
+
success: true,
|
|
1460
|
+
data: {
|
|
1461
|
+
processId: processInfo.id,
|
|
1462
|
+
pid: processInfo.pid,
|
|
1463
|
+
name: processInfo.name,
|
|
1464
|
+
command: processInfo.command,
|
|
1465
|
+
message: `Started background process "${processInfo.name}" (ID: ${processInfo.id}, PID: ${processInfo.pid}). Use stop_process with ID ${processInfo.id} to stop it.`
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
} catch (error2) {
|
|
1469
|
+
const err = error2;
|
|
1470
|
+
return { success: false, error: `Failed to start background process: ${err.message}` };
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function stopProcess(processId) {
|
|
1474
|
+
const process2 = processManager.get(processId);
|
|
1475
|
+
if (!process2) {
|
|
1476
|
+
return { success: false, error: `Process with ID ${processId} not found` };
|
|
1477
|
+
}
|
|
1478
|
+
const killed = processManager.kill(processId);
|
|
1479
|
+
if (killed) {
|
|
1480
|
+
return {
|
|
1481
|
+
success: true,
|
|
1482
|
+
data: {
|
|
1483
|
+
processId,
|
|
1484
|
+
name: process2.name,
|
|
1485
|
+
message: `Stopped process "${process2.name}" (ID: ${processId})`
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
} else {
|
|
1489
|
+
return { success: false, error: `Failed to stop process ${processId}` };
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
function listProcesses() {
|
|
1493
|
+
const processes = processManager.list();
|
|
1494
|
+
const running = processes.filter((p) => p.status === "running");
|
|
1495
|
+
const stopped = processes.filter((p) => p.status !== "running");
|
|
1496
|
+
return {
|
|
1497
|
+
success: true,
|
|
1498
|
+
data: {
|
|
1499
|
+
running: running.map((p) => ({
|
|
1500
|
+
id: p.id,
|
|
1501
|
+
pid: p.pid,
|
|
1502
|
+
name: p.name,
|
|
1503
|
+
command: p.command,
|
|
1504
|
+
startedAt: p.startedAt.toISOString(),
|
|
1505
|
+
uptime: Math.round((Date.now() - p.startedAt.getTime()) / 1e3) + "s"
|
|
1506
|
+
})),
|
|
1507
|
+
stopped: stopped.map((p) => ({
|
|
1508
|
+
id: p.id,
|
|
1509
|
+
name: p.name,
|
|
1510
|
+
status: p.status
|
|
1511
|
+
})),
|
|
1512
|
+
summary: `${running.length} running, ${stopped.length} stopped`
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function getProcessOutput(processId, lines = 20) {
|
|
1517
|
+
const process2 = processManager.get(processId);
|
|
1518
|
+
if (!process2) {
|
|
1519
|
+
return { success: false, error: `Process with ID ${processId} not found` };
|
|
1520
|
+
}
|
|
1521
|
+
const output = processManager.getOutput(processId, lines);
|
|
1522
|
+
return {
|
|
1523
|
+
success: true,
|
|
1524
|
+
data: {
|
|
1525
|
+
processId,
|
|
1526
|
+
name: process2.name,
|
|
1527
|
+
status: process2.status,
|
|
1528
|
+
output,
|
|
1529
|
+
lineCount: output.length
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
async function executeShellTool(name, args) {
|
|
1534
|
+
switch (name) {
|
|
1535
|
+
case "run_command":
|
|
1536
|
+
return runCommand(args.command, {
|
|
1537
|
+
cwd: args.cwd,
|
|
1538
|
+
timeout: args.timeout
|
|
1539
|
+
});
|
|
1540
|
+
case "grep":
|
|
1541
|
+
return grep(args.pattern, args.path, {
|
|
1542
|
+
ignoreCase: args.ignore_case,
|
|
1543
|
+
contextLines: args.context_lines
|
|
1544
|
+
});
|
|
1545
|
+
case "find_files":
|
|
1546
|
+
return findFiles(args.pattern, args.directory);
|
|
1547
|
+
case "start_background_process":
|
|
1548
|
+
return startBackgroundProcess(args.command, {
|
|
1549
|
+
cwd: args.cwd,
|
|
1550
|
+
name: args.name
|
|
1551
|
+
});
|
|
1552
|
+
case "stop_process":
|
|
1553
|
+
return stopProcess(args.process_id);
|
|
1554
|
+
case "list_processes":
|
|
1555
|
+
return listProcesses();
|
|
1556
|
+
case "get_process_output":
|
|
1557
|
+
return getProcessOutput(args.process_id, args.lines);
|
|
1558
|
+
default:
|
|
1559
|
+
return { success: false, error: `Unknown shell tool: ${name}` };
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/tools/git.ts
|
|
1564
|
+
import { exec as exec2 } from "child_process";
|
|
1565
|
+
import { promisify as promisify2 } from "util";
|
|
1566
|
+
var execPromise2 = promisify2(exec2);
|
|
1567
|
+
async function gitExec(command, cwd) {
|
|
1568
|
+
return execPromise2(`git ${command}`, {
|
|
1569
|
+
cwd: cwd || process.cwd(),
|
|
1570
|
+
timeout: 3e4,
|
|
1571
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
async function gitStatus(cwd) {
|
|
1575
|
+
try {
|
|
1576
|
+
const { stdout } = await gitExec("status --porcelain", cwd);
|
|
1577
|
+
const { stdout: branch } = await gitExec("branch --show-current", cwd);
|
|
1578
|
+
const files = stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
1579
|
+
const status = line.slice(0, 2);
|
|
1580
|
+
const file = line.slice(3);
|
|
1581
|
+
return { status: status.trim(), file };
|
|
1582
|
+
});
|
|
1583
|
+
return {
|
|
1584
|
+
success: true,
|
|
1585
|
+
data: {
|
|
1586
|
+
branch: branch.trim(),
|
|
1587
|
+
files,
|
|
1588
|
+
clean: files.length === 0
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
} catch (error2) {
|
|
1592
|
+
const execError = error2;
|
|
1593
|
+
return { success: false, error: `Git status failed: ${execError.message}` };
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async function gitDiff(options, cwd) {
|
|
1597
|
+
try {
|
|
1598
|
+
const args = ["diff"];
|
|
1599
|
+
if (options?.staged) args.push("--staged");
|
|
1600
|
+
if (options?.file) args.push(options.file);
|
|
1601
|
+
const { stdout } = await gitExec(args.join(" "), cwd);
|
|
1602
|
+
return {
|
|
1603
|
+
success: true,
|
|
1604
|
+
data: {
|
|
1605
|
+
diff: stdout,
|
|
1606
|
+
hasChanges: stdout.trim().length > 0
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
} catch (error2) {
|
|
1610
|
+
const execError = error2;
|
|
1611
|
+
return { success: false, error: `Git diff failed: ${execError.message}` };
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
async function gitAdd(files, cwd) {
|
|
1615
|
+
try {
|
|
1616
|
+
const fileList = Array.isArray(files) ? files.join(" ") : files;
|
|
1617
|
+
await gitExec(`add ${fileList}`, cwd);
|
|
1618
|
+
return {
|
|
1619
|
+
success: true,
|
|
1620
|
+
data: { added: Array.isArray(files) ? files : [files] }
|
|
1621
|
+
};
|
|
1622
|
+
} catch (error2) {
|
|
1623
|
+
const execError = error2;
|
|
1624
|
+
return { success: false, error: `Git add failed: ${execError.message}` };
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
async function gitCommit(message, cwd) {
|
|
1628
|
+
try {
|
|
1629
|
+
const { stdout } = await gitExec(`commit -m "${message.replace(/"/g, '\\"')}"`, cwd);
|
|
1630
|
+
const match = stdout.match(/\[[\w-]+\s+([a-f0-9]+)\]/);
|
|
1631
|
+
const hash = match ? match[1] : void 0;
|
|
1632
|
+
return {
|
|
1633
|
+
success: true,
|
|
1634
|
+
data: {
|
|
1635
|
+
message,
|
|
1636
|
+
hash,
|
|
1637
|
+
output: stdout.trim()
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
} catch (error2) {
|
|
1641
|
+
const execError = error2;
|
|
1642
|
+
return { success: false, error: `Git commit failed: ${execError.message}` };
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
async function gitLog(options, cwd) {
|
|
1646
|
+
try {
|
|
1647
|
+
const args = ["log"];
|
|
1648
|
+
if (options?.count) args.push(`-${options.count}`);
|
|
1649
|
+
if (options?.oneline) args.push("--oneline");
|
|
1650
|
+
const { stdout } = await gitExec(args.join(" "), cwd);
|
|
1651
|
+
const commits = stdout.trim().split("\n").filter(Boolean);
|
|
1652
|
+
return {
|
|
1653
|
+
success: true,
|
|
1654
|
+
data: { commits }
|
|
1655
|
+
};
|
|
1656
|
+
} catch (error2) {
|
|
1657
|
+
const execError = error2;
|
|
1658
|
+
return { success: false, error: `Git log failed: ${execError.message}` };
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
async function gitCheckout(target, options, cwd) {
|
|
1662
|
+
try {
|
|
1663
|
+
const args = ["checkout"];
|
|
1664
|
+
if (options?.create) args.push("-b");
|
|
1665
|
+
args.push(target);
|
|
1666
|
+
const { stdout, stderr } = await gitExec(args.join(" "), cwd);
|
|
1667
|
+
return {
|
|
1668
|
+
success: true,
|
|
1669
|
+
data: {
|
|
1670
|
+
target,
|
|
1671
|
+
created: options?.create || false,
|
|
1672
|
+
output: (stdout || stderr).trim()
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
} catch (error2) {
|
|
1676
|
+
const execError = error2;
|
|
1677
|
+
return { success: false, error: `Git checkout failed: ${execError.message}` };
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
var gitTools = [
|
|
1681
|
+
{
|
|
1682
|
+
name: "git_status",
|
|
1683
|
+
description: "Get the current git status including branch name, modified files, and staged changes.",
|
|
1684
|
+
input_schema: {
|
|
1685
|
+
type: "object",
|
|
1686
|
+
properties: {
|
|
1687
|
+
cwd: {
|
|
1688
|
+
type: "string",
|
|
1689
|
+
description: "Optional: Working directory (defaults to current)"
|
|
1690
|
+
}
|
|
1691
|
+
},
|
|
1692
|
+
required: []
|
|
1693
|
+
}
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
name: "git_diff",
|
|
1697
|
+
description: "Show git diff of changes. Can show staged or unstaged changes, and optionally for a specific file.",
|
|
1698
|
+
input_schema: {
|
|
1699
|
+
type: "object",
|
|
1700
|
+
properties: {
|
|
1701
|
+
staged: {
|
|
1702
|
+
type: "boolean",
|
|
1703
|
+
description: "Show staged changes only (default: false, shows unstaged)"
|
|
1704
|
+
},
|
|
1705
|
+
file: {
|
|
1706
|
+
type: "string",
|
|
1707
|
+
description: "Optional: Show diff for a specific file only"
|
|
1708
|
+
},
|
|
1709
|
+
cwd: {
|
|
1710
|
+
type: "string",
|
|
1711
|
+
description: "Optional: Working directory"
|
|
1712
|
+
}
|
|
1713
|
+
},
|
|
1714
|
+
required: []
|
|
1715
|
+
}
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
name: "git_add",
|
|
1719
|
+
description: 'Stage files for commit. Can stage specific files or use "." to stage all.',
|
|
1720
|
+
input_schema: {
|
|
1721
|
+
type: "object",
|
|
1722
|
+
properties: {
|
|
1723
|
+
files: {
|
|
1724
|
+
oneOf: [
|
|
1725
|
+
{ type: "string" },
|
|
1726
|
+
{ type: "array", items: { type: "string" } }
|
|
1727
|
+
],
|
|
1728
|
+
description: 'File(s) to stage. Use "." for all files.'
|
|
1729
|
+
},
|
|
1730
|
+
cwd: {
|
|
1731
|
+
type: "string",
|
|
1732
|
+
description: "Optional: Working directory"
|
|
1733
|
+
}
|
|
1734
|
+
},
|
|
1735
|
+
required: ["files"]
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
name: "git_commit",
|
|
1740
|
+
description: "Create a git commit with the staged changes.",
|
|
1741
|
+
input_schema: {
|
|
1742
|
+
type: "object",
|
|
1743
|
+
properties: {
|
|
1744
|
+
message: {
|
|
1745
|
+
type: "string",
|
|
1746
|
+
description: "The commit message"
|
|
1747
|
+
},
|
|
1748
|
+
cwd: {
|
|
1749
|
+
type: "string",
|
|
1750
|
+
description: "Optional: Working directory"
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
required: ["message"]
|
|
1754
|
+
}
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
name: "git_log",
|
|
1758
|
+
description: "Show recent git commits.",
|
|
1759
|
+
input_schema: {
|
|
1760
|
+
type: "object",
|
|
1761
|
+
properties: {
|
|
1762
|
+
count: {
|
|
1763
|
+
type: "number",
|
|
1764
|
+
description: "Number of commits to show (default: 10)"
|
|
1765
|
+
},
|
|
1766
|
+
oneline: {
|
|
1767
|
+
type: "boolean",
|
|
1768
|
+
description: "Show compact one-line format (default: false)"
|
|
1769
|
+
},
|
|
1770
|
+
cwd: {
|
|
1771
|
+
type: "string",
|
|
1772
|
+
description: "Optional: Working directory"
|
|
1773
|
+
}
|
|
1774
|
+
},
|
|
1775
|
+
required: []
|
|
1776
|
+
}
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
name: "git_checkout",
|
|
1780
|
+
description: "Switch branches or restore files. Can create a new branch with the create option.",
|
|
1781
|
+
input_schema: {
|
|
1782
|
+
type: "object",
|
|
1783
|
+
properties: {
|
|
1784
|
+
target: {
|
|
1785
|
+
type: "string",
|
|
1786
|
+
description: "Branch name or commit to checkout"
|
|
1787
|
+
},
|
|
1788
|
+
create: {
|
|
1789
|
+
type: "boolean",
|
|
1790
|
+
description: "Create a new branch (default: false)"
|
|
1791
|
+
},
|
|
1792
|
+
cwd: {
|
|
1793
|
+
type: "string",
|
|
1794
|
+
description: "Optional: Working directory"
|
|
1795
|
+
}
|
|
1796
|
+
},
|
|
1797
|
+
required: ["target"]
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
];
|
|
1801
|
+
async function executeGitTool(name, args) {
|
|
1802
|
+
const cwd = args.cwd;
|
|
1803
|
+
switch (name) {
|
|
1804
|
+
case "git_status":
|
|
1805
|
+
return gitStatus(cwd);
|
|
1806
|
+
case "git_diff":
|
|
1807
|
+
return gitDiff({
|
|
1808
|
+
staged: args.staged,
|
|
1809
|
+
file: args.file
|
|
1810
|
+
}, cwd);
|
|
1811
|
+
case "git_add":
|
|
1812
|
+
return gitAdd(args.files, cwd);
|
|
1813
|
+
case "git_commit":
|
|
1814
|
+
return gitCommit(args.message, cwd);
|
|
1815
|
+
case "git_log":
|
|
1816
|
+
return gitLog({
|
|
1817
|
+
count: args.count,
|
|
1818
|
+
oneline: args.oneline
|
|
1819
|
+
}, cwd);
|
|
1820
|
+
case "git_checkout":
|
|
1821
|
+
return gitCheckout(args.target, {
|
|
1822
|
+
create: args.create
|
|
1823
|
+
}, cwd);
|
|
1824
|
+
default:
|
|
1825
|
+
return { success: false, error: `Unknown git tool: ${name}` };
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// src/tools/web.ts
|
|
1830
|
+
async function searchWithExa(query, apiKey, options = {}) {
|
|
1831
|
+
const { maxResults = 10, includeText = true } = options;
|
|
1832
|
+
try {
|
|
1833
|
+
const response = await fetch("https://api.exa.ai/search", {
|
|
1834
|
+
method: "POST",
|
|
1835
|
+
headers: {
|
|
1836
|
+
"Content-Type": "application/json",
|
|
1837
|
+
"x-api-key": apiKey
|
|
1838
|
+
},
|
|
1839
|
+
body: JSON.stringify({
|
|
1840
|
+
query,
|
|
1841
|
+
numResults: maxResults,
|
|
1842
|
+
type: "auto",
|
|
1843
|
+
// Let Exa decide between neural and keyword search
|
|
1844
|
+
contents: includeText ? {
|
|
1845
|
+
text: {
|
|
1846
|
+
maxCharacters: 1e3
|
|
1847
|
+
// Limit text length per result
|
|
1848
|
+
}
|
|
1849
|
+
} : void 0
|
|
1850
|
+
})
|
|
1851
|
+
});
|
|
1852
|
+
if (!response.ok) {
|
|
1853
|
+
const errorText = await response.text();
|
|
1854
|
+
throw new Error(`Exa API error: ${response.status} - ${errorText}`);
|
|
1855
|
+
}
|
|
1856
|
+
const data = await response.json();
|
|
1857
|
+
return {
|
|
1858
|
+
success: true,
|
|
1859
|
+
data: {
|
|
1860
|
+
query,
|
|
1861
|
+
source: "exa",
|
|
1862
|
+
results: data.results.map((r) => ({
|
|
1863
|
+
title: r.title,
|
|
1864
|
+
url: r.url,
|
|
1865
|
+
snippet: r.text?.slice(0, 500) || "",
|
|
1866
|
+
publishedDate: r.publishedDate,
|
|
1867
|
+
author: r.author
|
|
1868
|
+
}))
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
} catch (error2) {
|
|
1872
|
+
return {
|
|
1873
|
+
success: false,
|
|
1874
|
+
error: `Exa search failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
async function answerWithExa(query, apiKey) {
|
|
1879
|
+
try {
|
|
1880
|
+
const response = await fetch("https://api.exa.ai/answer", {
|
|
1881
|
+
method: "POST",
|
|
1882
|
+
headers: {
|
|
1883
|
+
"Content-Type": "application/json",
|
|
1884
|
+
"x-api-key": apiKey
|
|
1885
|
+
},
|
|
1886
|
+
body: JSON.stringify({
|
|
1887
|
+
query,
|
|
1888
|
+
text: true
|
|
1889
|
+
})
|
|
1890
|
+
});
|
|
1891
|
+
if (!response.ok) {
|
|
1892
|
+
const errorText = await response.text();
|
|
1893
|
+
throw new Error(`Exa Answer API error: ${response.status} - ${errorText}`);
|
|
1894
|
+
}
|
|
1895
|
+
const data = await response.json();
|
|
1896
|
+
return {
|
|
1897
|
+
success: true,
|
|
1898
|
+
data: {
|
|
1899
|
+
query,
|
|
1900
|
+
source: "exa",
|
|
1901
|
+
answer: data.answer,
|
|
1902
|
+
citations: data.citations?.map((c) => ({
|
|
1903
|
+
title: c.title,
|
|
1904
|
+
url: c.url
|
|
1905
|
+
})) || []
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
} catch (error2) {
|
|
1909
|
+
return {
|
|
1910
|
+
success: false,
|
|
1911
|
+
error: `Exa answer failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
async function searchWithDuckDuckGo(query, options = {}) {
|
|
1916
|
+
const { maxResults = 10 } = options;
|
|
1917
|
+
try {
|
|
1918
|
+
const response = await fetch(
|
|
1919
|
+
`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
|
|
1920
|
+
{
|
|
1921
|
+
headers: {
|
|
1922
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
);
|
|
1926
|
+
if (!response.ok) {
|
|
1927
|
+
throw new Error(`DuckDuckGo error: ${response.status}`);
|
|
1928
|
+
}
|
|
1929
|
+
const html = await response.text();
|
|
1930
|
+
const results = [];
|
|
1931
|
+
const resultPattern = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/g;
|
|
1932
|
+
const snippetPattern = /<a[^>]*class="result__snippet"[^>]*>([^<]*)/g;
|
|
1933
|
+
let linkMatch;
|
|
1934
|
+
const snippets = [];
|
|
1935
|
+
let snippetMatch;
|
|
1936
|
+
while ((snippetMatch = snippetPattern.exec(html)) !== null) {
|
|
1937
|
+
snippets.push(snippetMatch[1].trim());
|
|
1938
|
+
}
|
|
1939
|
+
let i = 0;
|
|
1940
|
+
while ((linkMatch = resultPattern.exec(html)) !== null && results.length < maxResults) {
|
|
1941
|
+
let url = linkMatch[1];
|
|
1942
|
+
const uddgMatch = url.match(/uddg=([^&]+)/);
|
|
1943
|
+
if (uddgMatch) {
|
|
1944
|
+
url = decodeURIComponent(uddgMatch[1]);
|
|
1945
|
+
}
|
|
1946
|
+
results.push({
|
|
1947
|
+
title: linkMatch[2].trim(),
|
|
1948
|
+
url,
|
|
1949
|
+
snippet: snippets[i] || ""
|
|
1950
|
+
});
|
|
1951
|
+
i++;
|
|
1952
|
+
}
|
|
1953
|
+
if (results.length === 0) {
|
|
1954
|
+
return {
|
|
1955
|
+
success: true,
|
|
1956
|
+
data: {
|
|
1957
|
+
query,
|
|
1958
|
+
source: "duckduckgo",
|
|
1959
|
+
results: [],
|
|
1960
|
+
note: "No results found. DuckDuckGo may be rate-limiting. Consider setting EXA_API_KEY for better results."
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
return {
|
|
1965
|
+
success: true,
|
|
1966
|
+
data: {
|
|
1967
|
+
query,
|
|
1968
|
+
source: "duckduckgo",
|
|
1969
|
+
results,
|
|
1970
|
+
note: "Using DuckDuckGo (free). Set EXA_API_KEY for better AI-powered search results."
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
} catch (error2) {
|
|
1974
|
+
return {
|
|
1975
|
+
success: false,
|
|
1976
|
+
error: `DuckDuckGo search failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
async function webSearch(query, options = {}) {
|
|
1981
|
+
const exaKey = process.env.EXA_API_KEY;
|
|
1982
|
+
if (exaKey) {
|
|
1983
|
+
return searchWithExa(query, exaKey, options);
|
|
1984
|
+
}
|
|
1985
|
+
return searchWithDuckDuckGo(query, options);
|
|
1986
|
+
}
|
|
1987
|
+
async function webAnswer(query) {
|
|
1988
|
+
const exaKey = process.env.EXA_API_KEY;
|
|
1989
|
+
if (!exaKey) {
|
|
1990
|
+
return {
|
|
1991
|
+
success: false,
|
|
1992
|
+
error: "EXA_API_KEY is required for web_answer. Get one at https://dashboard.exa.ai"
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
return answerWithExa(query, exaKey);
|
|
1996
|
+
}
|
|
1997
|
+
async function fetchUrl(url, options = {}) {
|
|
1998
|
+
const { maxLength = 5e4 } = options;
|
|
1999
|
+
try {
|
|
2000
|
+
const response = await fetch(url, {
|
|
2001
|
+
headers: {
|
|
2002
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
2003
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
if (!response.ok) {
|
|
2007
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
2008
|
+
}
|
|
2009
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2010
|
+
let content = await response.text();
|
|
2011
|
+
if (content.length > maxLength) {
|
|
2012
|
+
content = content.slice(0, maxLength) + "\n\n[Content truncated...]";
|
|
2013
|
+
}
|
|
2014
|
+
if (contentType.includes("text/html")) {
|
|
2015
|
+
content = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
2016
|
+
}
|
|
2017
|
+
return {
|
|
2018
|
+
success: true,
|
|
2019
|
+
data: {
|
|
2020
|
+
url,
|
|
2021
|
+
contentType,
|
|
2022
|
+
length: content.length,
|
|
2023
|
+
content
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
} catch (error2) {
|
|
2027
|
+
return {
|
|
2028
|
+
success: false,
|
|
2029
|
+
error: `Failed to fetch URL: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
var webTools = [
|
|
2034
|
+
{
|
|
2035
|
+
name: "web_search",
|
|
2036
|
+
description: "Search the web for information. Returns titles, URLs, and snippets from search results. Uses Exa AI search if EXA_API_KEY is set (recommended), otherwise falls back to DuckDuckGo.",
|
|
2037
|
+
input_schema: {
|
|
2038
|
+
type: "object",
|
|
2039
|
+
properties: {
|
|
2040
|
+
query: {
|
|
2041
|
+
type: "string",
|
|
2042
|
+
description: "The search query"
|
|
2043
|
+
},
|
|
2044
|
+
max_results: {
|
|
2045
|
+
type: "number",
|
|
2046
|
+
description: "Maximum number of results to return (default: 10)"
|
|
2047
|
+
}
|
|
2048
|
+
},
|
|
2049
|
+
required: ["query"]
|
|
2050
|
+
}
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
name: "web_answer",
|
|
2054
|
+
description: "Get an AI-generated answer to a question with citations, powered by Exa. Requires EXA_API_KEY. Best for factual questions that need grounded answers.",
|
|
2055
|
+
input_schema: {
|
|
2056
|
+
type: "object",
|
|
2057
|
+
properties: {
|
|
2058
|
+
query: {
|
|
2059
|
+
type: "string",
|
|
2060
|
+
description: "The question to answer"
|
|
2061
|
+
}
|
|
2062
|
+
},
|
|
2063
|
+
required: ["query"]
|
|
2064
|
+
}
|
|
2065
|
+
},
|
|
2066
|
+
{
|
|
2067
|
+
name: "fetch_url",
|
|
2068
|
+
description: "Fetch the content of a URL. Returns the text content of the page. Useful for reading articles, documentation, or any web page.",
|
|
2069
|
+
input_schema: {
|
|
2070
|
+
type: "object",
|
|
2071
|
+
properties: {
|
|
2072
|
+
url: {
|
|
2073
|
+
type: "string",
|
|
2074
|
+
description: "The URL to fetch"
|
|
2075
|
+
},
|
|
2076
|
+
max_length: {
|
|
2077
|
+
type: "number",
|
|
2078
|
+
description: "Maximum content length to return (default: 50000 characters)"
|
|
2079
|
+
}
|
|
2080
|
+
},
|
|
2081
|
+
required: ["url"]
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
];
|
|
2085
|
+
async function executeWebTool(name, args) {
|
|
2086
|
+
switch (name) {
|
|
2087
|
+
case "web_search":
|
|
2088
|
+
return webSearch(args.query, {
|
|
2089
|
+
maxResults: args.max_results
|
|
2090
|
+
});
|
|
2091
|
+
case "web_answer":
|
|
2092
|
+
return webAnswer(args.query);
|
|
2093
|
+
case "fetch_url":
|
|
2094
|
+
return fetchUrl(args.url, {
|
|
2095
|
+
maxLength: args.max_length
|
|
2096
|
+
});
|
|
2097
|
+
default:
|
|
2098
|
+
return { success: false, error: `Unknown web tool: ${name}` };
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// src/tools/index.ts
|
|
2103
|
+
var localTools = [
|
|
2104
|
+
...filesystemTools,
|
|
2105
|
+
...shellTools,
|
|
2106
|
+
...gitTools,
|
|
2107
|
+
...webTools
|
|
2108
|
+
];
|
|
2109
|
+
var localToolNames = new Set(localTools.map((t) => t.name));
|
|
2110
|
+
function isLocalTool(name) {
|
|
2111
|
+
return localToolNames.has(name);
|
|
2112
|
+
}
|
|
2113
|
+
async function executeLocalTool(name, args) {
|
|
2114
|
+
if (filesystemTools.some((t) => t.name === name)) {
|
|
2115
|
+
return executeFilesystemTool(name, args);
|
|
2116
|
+
}
|
|
2117
|
+
if (shellTools.some((t) => t.name === name)) {
|
|
2118
|
+
return executeShellTool(name, args);
|
|
2119
|
+
}
|
|
2120
|
+
if (gitTools.some((t) => t.name === name)) {
|
|
2121
|
+
return executeGitTool(name, args);
|
|
2122
|
+
}
|
|
2123
|
+
if (webTools.some((t) => t.name === name)) {
|
|
2124
|
+
return executeWebTool(name, args);
|
|
2125
|
+
}
|
|
2126
|
+
return { success: false, error: `Unknown local tool: ${name}` };
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// src/agent/compaction.ts
|
|
2130
|
+
var COMPACTION_PROMPT = `Your context window is filling up. Please create a concise summary of our conversation so far that will allow you to continue working effectively.
|
|
2131
|
+
|
|
2132
|
+
The summary should be wrapped in <summary></summary> tags and include:
|
|
2133
|
+
|
|
2134
|
+
# Task Overview
|
|
2135
|
+
- The user's core request and goals
|
|
2136
|
+
- Success criteria and constraints
|
|
2137
|
+
- Any specific preferences mentioned
|
|
2138
|
+
|
|
2139
|
+
# Current State
|
|
2140
|
+
- What has been completed so far
|
|
2141
|
+
- Files created or modified (with paths)
|
|
2142
|
+
- Artifacts or outputs produced
|
|
2143
|
+
- Current working directory if relevant
|
|
2144
|
+
|
|
2145
|
+
# Important Discoveries
|
|
2146
|
+
- Technical constraints or requirements found
|
|
2147
|
+
- Key decisions made and why
|
|
2148
|
+
- Errors encountered and how they were resolved
|
|
2149
|
+
- Approaches that didn't work (to avoid repeating)
|
|
2150
|
+
|
|
2151
|
+
# Next Steps
|
|
2152
|
+
- Specific actions still needed
|
|
2153
|
+
- Priority order if multiple steps remain
|
|
2154
|
+
- Any blockers or dependencies
|
|
2155
|
+
|
|
2156
|
+
# Context to Preserve
|
|
2157
|
+
- User preferences or style requirements
|
|
2158
|
+
- Domain-specific details that matter
|
|
2159
|
+
- Any commitments or promises made
|
|
2160
|
+
|
|
2161
|
+
Be thorough but concise. The goal is to capture everything needed to continue seamlessly, while reducing token usage significantly.`;
|
|
2162
|
+
function parseCompactedSummary(response) {
|
|
2163
|
+
const match = response.match(/<summary>([\s\S]*?)<\/summary>/);
|
|
2164
|
+
if (match && match[1]) {
|
|
2165
|
+
return match[1].trim();
|
|
2166
|
+
}
|
|
2167
|
+
return response.trim() || null;
|
|
2168
|
+
}
|
|
2169
|
+
async function createCompactedSummary(anthropic, history, model = "claude-sonnet-4-5-20250929", customPrompt) {
|
|
2170
|
+
const prompt2 = customPrompt || COMPACTION_PROMPT;
|
|
2171
|
+
const compactionMessages = [
|
|
2172
|
+
...history,
|
|
2173
|
+
{
|
|
2174
|
+
role: "user",
|
|
2175
|
+
content: prompt2
|
|
2176
|
+
}
|
|
2177
|
+
];
|
|
2178
|
+
const response = await anthropic.messages.create({
|
|
2179
|
+
model,
|
|
2180
|
+
max_tokens: 4096,
|
|
2181
|
+
messages: compactionMessages
|
|
2182
|
+
});
|
|
2183
|
+
const textBlocks = response.content.filter((block) => block.type === "text");
|
|
2184
|
+
const fullText = textBlocks.map((block) => block.text).join("\n");
|
|
2185
|
+
const summary = parseCompactedSummary(fullText);
|
|
2186
|
+
if (!summary) {
|
|
2187
|
+
throw new Error("Failed to parse compacted summary from response");
|
|
2188
|
+
}
|
|
2189
|
+
return summary;
|
|
2190
|
+
}
|
|
2191
|
+
function historyFromSummary(summary) {
|
|
2192
|
+
return [
|
|
2193
|
+
{
|
|
2194
|
+
role: "assistant",
|
|
2195
|
+
content: summary
|
|
2196
|
+
}
|
|
2197
|
+
];
|
|
2198
|
+
}
|
|
2199
|
+
async function compactConversation(anthropic, history, model, systemPrompt, tools) {
|
|
2200
|
+
let originalTokens = 0;
|
|
2201
|
+
try {
|
|
2202
|
+
const countResult = await anthropic.messages.countTokens({
|
|
2203
|
+
model,
|
|
2204
|
+
system: systemPrompt,
|
|
2205
|
+
tools,
|
|
2206
|
+
messages: history
|
|
2207
|
+
});
|
|
2208
|
+
originalTokens = countResult.input_tokens;
|
|
2209
|
+
} catch (e) {
|
|
2210
|
+
const contentLength = JSON.stringify(history).length;
|
|
2211
|
+
originalTokens = Math.ceil(contentLength / 4);
|
|
2212
|
+
}
|
|
2213
|
+
const summaryModel = "claude-sonnet-4-5-20250929";
|
|
2214
|
+
const summary = await createCompactedSummary(anthropic, history, summaryModel);
|
|
2215
|
+
const newHistory = historyFromSummary(summary);
|
|
2216
|
+
let newTokens = 0;
|
|
2217
|
+
try {
|
|
2218
|
+
const countResult = await anthropic.messages.countTokens({
|
|
2219
|
+
model,
|
|
2220
|
+
system: systemPrompt,
|
|
2221
|
+
tools,
|
|
2222
|
+
messages: newHistory
|
|
2223
|
+
});
|
|
2224
|
+
newTokens = countResult.input_tokens;
|
|
2225
|
+
} catch (e) {
|
|
2226
|
+
const contentLength = JSON.stringify(newHistory).length;
|
|
2227
|
+
newTokens = Math.ceil(contentLength / 4);
|
|
2228
|
+
}
|
|
2229
|
+
return {
|
|
2230
|
+
newHistory,
|
|
2231
|
+
summary,
|
|
2232
|
+
originalTokens,
|
|
2233
|
+
newTokens
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// src/agent/pricing.ts
|
|
2238
|
+
var MODELS = {
|
|
2239
|
+
"claude-opus-4-5-20250929": {
|
|
2240
|
+
id: "claude-opus-4-5-20250929",
|
|
2241
|
+
name: "opus-4.5",
|
|
2242
|
+
displayName: "Claude Opus 4.5",
|
|
2243
|
+
pricing: {
|
|
2244
|
+
inputPerMTok: 5,
|
|
2245
|
+
outputPerMTok: 25,
|
|
2246
|
+
cacheWritePerMTok: 6.25,
|
|
2247
|
+
// 1.25x input
|
|
2248
|
+
cacheReadPerMTok: 0.5
|
|
2249
|
+
// 0.1x input
|
|
2250
|
+
},
|
|
2251
|
+
contextWindow: 2e5,
|
|
2252
|
+
description: "Most capable model. Best for complex reasoning and creative tasks."
|
|
2253
|
+
},
|
|
2254
|
+
"claude-sonnet-4-5-20250929": {
|
|
2255
|
+
id: "claude-sonnet-4-5-20250929",
|
|
2256
|
+
name: "sonnet-4.5",
|
|
2257
|
+
displayName: "Claude Sonnet 4.5",
|
|
2258
|
+
pricing: {
|
|
2259
|
+
inputPerMTok: 3,
|
|
2260
|
+
outputPerMTok: 15,
|
|
2261
|
+
cacheWritePerMTok: 3.75,
|
|
2262
|
+
// 1.25x input
|
|
2263
|
+
cacheReadPerMTok: 0.3
|
|
2264
|
+
// 0.1x input
|
|
2265
|
+
},
|
|
2266
|
+
contextWindow: 2e5,
|
|
2267
|
+
description: "Balanced performance and cost. Great for most coding and trading tasks."
|
|
2268
|
+
},
|
|
2269
|
+
"claude-haiku-4-5-20250929": {
|
|
2270
|
+
id: "claude-haiku-4-5-20250929",
|
|
2271
|
+
name: "haiku-4.5",
|
|
2272
|
+
displayName: "Claude Haiku 4.5",
|
|
2273
|
+
pricing: {
|
|
2274
|
+
inputPerMTok: 1,
|
|
2275
|
+
outputPerMTok: 5,
|
|
2276
|
+
cacheWritePerMTok: 1.25,
|
|
2277
|
+
// 1.25x input
|
|
2278
|
+
cacheReadPerMTok: 0.1
|
|
2279
|
+
// 0.1x input
|
|
2280
|
+
},
|
|
2281
|
+
contextWindow: 2e5,
|
|
2282
|
+
description: "Fastest and most economical. Good for simple tasks and high volume."
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
|
|
2286
|
+
var MODEL_ALIASES = {
|
|
2287
|
+
"opus": "claude-opus-4-5-20250929",
|
|
2288
|
+
"opus-4.5": "claude-opus-4-5-20250929",
|
|
2289
|
+
"sonnet": "claude-sonnet-4-5-20250929",
|
|
2290
|
+
"sonnet-4.5": "claude-sonnet-4-5-20250929",
|
|
2291
|
+
"haiku": "claude-haiku-4-5-20250929",
|
|
2292
|
+
"haiku-4.5": "claude-haiku-4-5-20250929"
|
|
2293
|
+
};
|
|
2294
|
+
function resolveModelId(nameOrAlias) {
|
|
2295
|
+
const lower = nameOrAlias.toLowerCase();
|
|
2296
|
+
if (MODELS[lower]) {
|
|
2297
|
+
return lower;
|
|
2298
|
+
}
|
|
2299
|
+
if (MODEL_ALIASES[lower]) {
|
|
2300
|
+
return MODEL_ALIASES[lower];
|
|
2301
|
+
}
|
|
2302
|
+
for (const [id, config] of Object.entries(MODELS)) {
|
|
2303
|
+
if (config.name.toLowerCase() === lower) {
|
|
2304
|
+
return id;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
return null;
|
|
2308
|
+
}
|
|
2309
|
+
function getModelPricing(modelId) {
|
|
2310
|
+
const model = MODELS[modelId];
|
|
2311
|
+
return model?.pricing ?? null;
|
|
2312
|
+
}
|
|
2313
|
+
function getModelConfig(modelId) {
|
|
2314
|
+
return MODELS[modelId] ?? null;
|
|
2315
|
+
}
|
|
2316
|
+
function calculateCost(modelId, inputTokens, outputTokens, cacheCreationTokens = 0, cacheReadTokens = 0) {
|
|
2317
|
+
const pricing = getModelPricing(modelId);
|
|
2318
|
+
if (!pricing) {
|
|
2319
|
+
const defaultPricing = MODELS[DEFAULT_MODEL].pricing;
|
|
2320
|
+
return calculateCostWithPricing(
|
|
2321
|
+
defaultPricing,
|
|
2322
|
+
inputTokens,
|
|
2323
|
+
outputTokens,
|
|
2324
|
+
cacheCreationTokens,
|
|
2325
|
+
cacheReadTokens
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
return calculateCostWithPricing(
|
|
2329
|
+
pricing,
|
|
2330
|
+
inputTokens,
|
|
2331
|
+
outputTokens,
|
|
2332
|
+
cacheCreationTokens,
|
|
2333
|
+
cacheReadTokens
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
function calculateCostWithPricing(pricing, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
|
|
2337
|
+
const inputCost = inputTokens / 1e6 * pricing.inputPerMTok;
|
|
2338
|
+
const outputCost = outputTokens / 1e6 * pricing.outputPerMTok;
|
|
2339
|
+
const cacheWriteCost = cacheCreationTokens / 1e6 * pricing.cacheWritePerMTok;
|
|
2340
|
+
const cacheReadCost = cacheReadTokens / 1e6 * pricing.cacheReadPerMTok;
|
|
2341
|
+
return {
|
|
2342
|
+
inputCost,
|
|
2343
|
+
outputCost,
|
|
2344
|
+
cacheWriteCost,
|
|
2345
|
+
cacheReadCost,
|
|
2346
|
+
totalCost: inputCost + outputCost + cacheWriteCost + cacheReadCost
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
function formatCost(cost) {
|
|
2350
|
+
if (cost < 0.01) {
|
|
2351
|
+
return `$${(cost * 100).toFixed(3)}\xA2`;
|
|
2352
|
+
}
|
|
2353
|
+
return `$${cost.toFixed(4)}`;
|
|
2354
|
+
}
|
|
2355
|
+
function listModels() {
|
|
2356
|
+
return Object.values(MODELS);
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// src/agent/loop.ts
|
|
2360
|
+
var MAX_TOOL_RESULT_CHARS = 8e3;
|
|
2361
|
+
function truncateToolResult(result, toolName) {
|
|
2362
|
+
const resultStr = JSON.stringify(result);
|
|
2363
|
+
if (resultStr.length <= MAX_TOOL_RESULT_CHARS) {
|
|
2364
|
+
return result;
|
|
2365
|
+
}
|
|
2366
|
+
if (typeof result === "object" && result !== null) {
|
|
2367
|
+
const obj = result;
|
|
2368
|
+
if (Array.isArray(obj.content) && obj.content.length > 0) {
|
|
2369
|
+
const firstContent = obj.content[0];
|
|
2370
|
+
if (firstContent?.type === "text" && typeof firstContent.text === "string") {
|
|
2371
|
+
try {
|
|
2372
|
+
const innerData = JSON.parse(firstContent.text);
|
|
2373
|
+
const truncatedInner = truncateDataObject(innerData);
|
|
2374
|
+
return {
|
|
2375
|
+
content: [{
|
|
2376
|
+
type: "text",
|
|
2377
|
+
text: JSON.stringify(truncatedInner)
|
|
2378
|
+
}]
|
|
2379
|
+
};
|
|
2380
|
+
} catch {
|
|
2381
|
+
const truncatedText = firstContent.text.length > MAX_TOOL_RESULT_CHARS ? firstContent.text.substring(0, MAX_TOOL_RESULT_CHARS) + "... [truncated]" : firstContent.text;
|
|
2382
|
+
return {
|
|
2383
|
+
content: [{
|
|
2384
|
+
type: "text",
|
|
2385
|
+
text: truncatedText
|
|
2386
|
+
}]
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
if (Array.isArray(result)) {
|
|
2393
|
+
return truncateArray(result);
|
|
2394
|
+
}
|
|
2395
|
+
if (typeof result === "object" && result !== null) {
|
|
2396
|
+
return truncateDataObject(result);
|
|
2397
|
+
}
|
|
2398
|
+
if (typeof result === "string" && result.length > MAX_TOOL_RESULT_CHARS) {
|
|
2399
|
+
return result.substring(0, MAX_TOOL_RESULT_CHARS) + "... [truncated]";
|
|
2400
|
+
}
|
|
2401
|
+
return result;
|
|
2402
|
+
}
|
|
2403
|
+
function truncateArray(arr) {
|
|
2404
|
+
const MAX_ITEMS = 5;
|
|
2405
|
+
const truncated = arr.slice(0, MAX_ITEMS).map(
|
|
2406
|
+
(item) => typeof item === "object" && item !== null ? truncateObject(item) : item
|
|
2407
|
+
);
|
|
2408
|
+
return {
|
|
2409
|
+
_truncated: arr.length > MAX_ITEMS,
|
|
2410
|
+
_originalCount: arr.length,
|
|
2411
|
+
_note: arr.length > MAX_ITEMS ? `Showing ${MAX_ITEMS} of ${arr.length} items.` : void 0,
|
|
2412
|
+
items: truncated
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
function truncateDataObject(obj) {
|
|
2416
|
+
const truncated = {};
|
|
2417
|
+
const MAX_ARRAY_ITEMS = 5;
|
|
2418
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2419
|
+
if (Array.isArray(value)) {
|
|
2420
|
+
if (value.length > MAX_ARRAY_ITEMS) {
|
|
2421
|
+
truncated[key] = value.slice(0, MAX_ARRAY_ITEMS).map(
|
|
2422
|
+
(item) => typeof item === "object" && item !== null ? truncateObject(item) : item
|
|
2423
|
+
);
|
|
2424
|
+
truncated[`_${key}Count`] = value.length;
|
|
2425
|
+
truncated["_truncated"] = true;
|
|
2426
|
+
} else {
|
|
2427
|
+
truncated[key] = value.map(
|
|
2428
|
+
(item) => typeof item === "object" && item !== null ? truncateObject(item) : item
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2432
|
+
truncated[key] = truncateObject(value);
|
|
2433
|
+
} else if (typeof value === "string" && value.length > 500) {
|
|
2434
|
+
truncated[key] = value.substring(0, 500) + "...";
|
|
2435
|
+
} else {
|
|
2436
|
+
truncated[key] = value;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
return truncated;
|
|
2440
|
+
}
|
|
2441
|
+
var ACTIONABLE_FIELDS = /* @__PURE__ */ new Set([
|
|
2442
|
+
// Market identifiers (required for trading)
|
|
2443
|
+
"conditionId",
|
|
2444
|
+
"tokenId",
|
|
2445
|
+
"marketId",
|
|
2446
|
+
"id",
|
|
2447
|
+
"ticker",
|
|
2448
|
+
// Token info (required for order placement)
|
|
2449
|
+
"token_id",
|
|
2450
|
+
"clobTokenIds",
|
|
2451
|
+
"tokens",
|
|
2452
|
+
// Pricing (required for trading decisions)
|
|
2453
|
+
"price",
|
|
2454
|
+
"probability",
|
|
2455
|
+
"outcomePrices",
|
|
2456
|
+
"bestBid",
|
|
2457
|
+
"bestAsk",
|
|
2458
|
+
// Market identity (for user understanding)
|
|
2459
|
+
"title",
|
|
2460
|
+
"question",
|
|
2461
|
+
"slug",
|
|
2462
|
+
"outcome",
|
|
2463
|
+
"name",
|
|
2464
|
+
// Status info (affects tradability)
|
|
2465
|
+
"active",
|
|
2466
|
+
"closed",
|
|
2467
|
+
"status",
|
|
2468
|
+
"endDate",
|
|
2469
|
+
// Platform (for multi-platform support)
|
|
2470
|
+
"platform"
|
|
2471
|
+
]);
|
|
2472
|
+
var SUMMARY_FIELDS = /* @__PURE__ */ new Set([
|
|
2473
|
+
"volume",
|
|
2474
|
+
"liquidity",
|
|
2475
|
+
"volume24hr"
|
|
2476
|
+
]);
|
|
2477
|
+
var DROP_FIELDS = /* @__PURE__ */ new Set([
|
|
2478
|
+
"description",
|
|
2479
|
+
"rules",
|
|
2480
|
+
"resolutionSource",
|
|
2481
|
+
"image",
|
|
2482
|
+
"icon",
|
|
2483
|
+
"createdAt",
|
|
2484
|
+
"updatedAt",
|
|
2485
|
+
"lastTradePrice",
|
|
2486
|
+
"spread",
|
|
2487
|
+
"acceptingOrders",
|
|
2488
|
+
"acceptingOrdersTimestamp",
|
|
2489
|
+
"minimum_tick_size",
|
|
2490
|
+
"minimum_order_size",
|
|
2491
|
+
"maker_base_fee",
|
|
2492
|
+
"taker_base_fee",
|
|
2493
|
+
"neg_risk",
|
|
2494
|
+
"neg_risk_market_id",
|
|
2495
|
+
"neg_risk_request_id",
|
|
2496
|
+
"notifications_enabled",
|
|
2497
|
+
"is_50_50_outcome",
|
|
2498
|
+
"game_start_time",
|
|
2499
|
+
"seconds_delay",
|
|
2500
|
+
"icon",
|
|
2501
|
+
"fpmm",
|
|
2502
|
+
"rewards",
|
|
2503
|
+
"competitive"
|
|
2504
|
+
]);
|
|
2505
|
+
function truncateObject(obj) {
|
|
2506
|
+
const truncated = {};
|
|
2507
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2508
|
+
if (DROP_FIELDS.has(key)) continue;
|
|
2509
|
+
if (ACTIONABLE_FIELDS.has(key)) {
|
|
2510
|
+
if (typeof value === "string" && value.length > 150) {
|
|
2511
|
+
truncated[key] = value.substring(0, 150) + "...";
|
|
2512
|
+
} else if (Array.isArray(value)) {
|
|
2513
|
+
truncated[key] = value.slice(0, 10).map((item) => {
|
|
2514
|
+
if (typeof item === "object" && item !== null) {
|
|
2515
|
+
return extractTokenInfo(item);
|
|
2516
|
+
}
|
|
2517
|
+
return item;
|
|
2518
|
+
});
|
|
2519
|
+
} else {
|
|
2520
|
+
truncated[key] = value;
|
|
2521
|
+
}
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
if (SUMMARY_FIELDS.has(key)) {
|
|
2525
|
+
if (typeof value === "number" || typeof value === "string") {
|
|
2526
|
+
truncated[key] = value;
|
|
2527
|
+
}
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
if (typeof value !== "object" && JSON.stringify(truncated).length < 800) {
|
|
2531
|
+
truncated[key] = value;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
return truncated;
|
|
2535
|
+
}
|
|
2536
|
+
function extractTokenInfo(token) {
|
|
2537
|
+
return {
|
|
2538
|
+
token_id: token.token_id ?? token.tokenId,
|
|
2539
|
+
outcome: token.outcome ?? token.name,
|
|
2540
|
+
price: token.price ?? token.probability
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
var DEFAULT_SYSTEM_PROMPT = `You are Quantish, an AI coding and trading agent.
|
|
2544
|
+
|
|
2545
|
+
You have two sets of capabilities:
|
|
2546
|
+
|
|
2547
|
+
## Trading Tools (via MCP)
|
|
2548
|
+
You can interact with Polymarket prediction markets:
|
|
2549
|
+
- Check wallet balances and positions
|
|
2550
|
+
- Place, cancel, and manage orders
|
|
2551
|
+
- Transfer funds and claim winnings
|
|
2552
|
+
- Get market prices and orderbook data
|
|
2553
|
+
|
|
2554
|
+
## Coding Tools (local)
|
|
2555
|
+
You can work with the local filesystem:
|
|
2556
|
+
- Read and write files
|
|
2557
|
+
- List directories and search with grep
|
|
2558
|
+
- Run shell commands
|
|
2559
|
+
- Use git for version control
|
|
2560
|
+
|
|
2561
|
+
## Guidelines
|
|
2562
|
+
- Be concise and helpful
|
|
2563
|
+
- When making trades, always confirm details before proceeding
|
|
2564
|
+
- Prices on Polymarket are between 0.01 and 0.99 (probabilities)
|
|
2565
|
+
- Minimum order value is $1
|
|
2566
|
+
- When writing code, follow existing patterns and conventions
|
|
2567
|
+
- For dangerous operations (rm, sudo), explain what you're doing
|
|
2568
|
+
|
|
2569
|
+
You help users build trading bots and agents by combining coding skills with trading capabilities.`;
|
|
2570
|
+
var Agent = class {
|
|
2571
|
+
anthropic;
|
|
2572
|
+
mcpClient;
|
|
2573
|
+
mcpClientManager;
|
|
2574
|
+
config;
|
|
2575
|
+
conversationHistory = [];
|
|
2576
|
+
workingDirectory;
|
|
2577
|
+
sessionCost = 0;
|
|
2578
|
+
// Cumulative cost for this session
|
|
2579
|
+
cumulativeTokenUsage = {
|
|
2580
|
+
inputTokens: 0,
|
|
2581
|
+
outputTokens: 0,
|
|
2582
|
+
cacheCreationInputTokens: 0,
|
|
2583
|
+
cacheReadInputTokens: 0,
|
|
2584
|
+
totalTokens: 0,
|
|
2585
|
+
cost: { inputCost: 0, outputCost: 0, cacheWriteCost: 0, cacheReadCost: 0, totalCost: 0 },
|
|
2586
|
+
sessionCost: 0
|
|
2587
|
+
};
|
|
2588
|
+
constructor(config) {
|
|
2589
|
+
this.config = {
|
|
2590
|
+
enableLocalTools: true,
|
|
2591
|
+
enableMCPTools: true,
|
|
2592
|
+
// Default context editing: clear old tool uses when context exceeds 100k tokens
|
|
2593
|
+
contextEditing: config.contextEditing || [
|
|
2594
|
+
{
|
|
2595
|
+
type: "clear_tool_uses_20250919",
|
|
2596
|
+
trigger: { type: "input_tokens", value: 1e5 },
|
|
2597
|
+
keep: { type: "tool_uses", value: 5 }
|
|
2598
|
+
}
|
|
2599
|
+
],
|
|
2600
|
+
...config
|
|
2601
|
+
};
|
|
2602
|
+
const headers = {};
|
|
2603
|
+
if (this.config.contextEditing && this.config.contextEditing.length > 0) {
|
|
2604
|
+
headers["anthropic-beta"] = "context-management-2025-06-27";
|
|
2605
|
+
}
|
|
2606
|
+
this.anthropic = new Anthropic({
|
|
2607
|
+
apiKey: config.anthropicApiKey,
|
|
2608
|
+
defaultHeaders: Object.keys(headers).length > 0 ? headers : void 0
|
|
2609
|
+
});
|
|
2610
|
+
this.mcpClient = config.mcpClient;
|
|
2611
|
+
this.mcpClientManager = config.mcpClientManager;
|
|
2612
|
+
this.workingDirectory = config.workingDirectory || process.cwd();
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Get all available tools
|
|
2616
|
+
*/
|
|
2617
|
+
async getAllTools() {
|
|
2618
|
+
const tools = [];
|
|
2619
|
+
if (this.config.enableLocalTools) {
|
|
2620
|
+
tools.push(...localTools);
|
|
2621
|
+
}
|
|
2622
|
+
if (this.config.enableMCPTools) {
|
|
2623
|
+
if (this.mcpClientManager) {
|
|
2624
|
+
const mcpTools = await this.mcpClientManager.listAllTools();
|
|
2625
|
+
tools.push(...convertToClaudeTools(mcpTools));
|
|
2626
|
+
} else if (this.mcpClient) {
|
|
2627
|
+
const mcpTools = await this.mcpClient.listTools();
|
|
2628
|
+
tools.push(...convertToClaudeTools(mcpTools));
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return tools;
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* Execute a tool (local or MCP)
|
|
2635
|
+
*/
|
|
2636
|
+
async executeTool(name, args) {
|
|
2637
|
+
if (isLocalTool(name)) {
|
|
2638
|
+
const result = await executeLocalTool(name, args);
|
|
2639
|
+
return {
|
|
2640
|
+
result: result.success ? result.data : { error: result.error },
|
|
2641
|
+
source: "local"
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
if (this.mcpClientManager) {
|
|
2645
|
+
const result = await this.mcpClientManager.callTool(name, args);
|
|
2646
|
+
const source = result.source || "mcp";
|
|
2647
|
+
return {
|
|
2648
|
+
result: result.success ? result.data : { error: result.error },
|
|
2649
|
+
source
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
if (this.mcpClient) {
|
|
2653
|
+
const result = await this.mcpClient.callTool(name, args);
|
|
2654
|
+
return {
|
|
2655
|
+
result: result.success ? result.data : { error: result.error },
|
|
2656
|
+
source: "mcp"
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
return {
|
|
2660
|
+
result: { error: `Unknown tool: ${name}` },
|
|
2661
|
+
source: "local"
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Run the agent with a user message (supports streaming)
|
|
2666
|
+
*/
|
|
2667
|
+
async run(userMessage) {
|
|
2668
|
+
const maxIterations = this.config.maxIterations ?? 15;
|
|
2669
|
+
const model = this.config.model ?? "claude-sonnet-4-5-20250929";
|
|
2670
|
+
const maxTokens = this.config.maxTokens ?? 8192;
|
|
2671
|
+
const systemPrompt = this.config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
2672
|
+
const useStreaming = this.config.streaming ?? true;
|
|
2673
|
+
const allTools = await this.getAllTools();
|
|
2674
|
+
const contextManagement = this.config.contextEditing && this.config.contextEditing.length > 0 ? { edits: this.config.contextEditing } : void 0;
|
|
2675
|
+
const contextMessage = `[Working directory: ${this.workingDirectory}]
|
|
2676
|
+
|
|
2677
|
+
${userMessage}`;
|
|
2678
|
+
this.conversationHistory.push({
|
|
2679
|
+
role: "user",
|
|
2680
|
+
content: contextMessage
|
|
2681
|
+
});
|
|
2682
|
+
const toolCalls = [];
|
|
2683
|
+
let iterations = 0;
|
|
2684
|
+
let finalText = "";
|
|
2685
|
+
while (iterations < maxIterations) {
|
|
2686
|
+
iterations++;
|
|
2687
|
+
this.config.onStreamStart?.();
|
|
2688
|
+
let response;
|
|
2689
|
+
let responseContent = [];
|
|
2690
|
+
let currentText = "";
|
|
2691
|
+
let toolUses = [];
|
|
2692
|
+
if (useStreaming) {
|
|
2693
|
+
const systemWithCache = [
|
|
2694
|
+
{
|
|
2695
|
+
type: "text",
|
|
2696
|
+
text: systemPrompt,
|
|
2697
|
+
cache_control: { type: "ephemeral" }
|
|
2698
|
+
}
|
|
2699
|
+
];
|
|
2700
|
+
const streamOptions = {
|
|
2701
|
+
model,
|
|
2702
|
+
max_tokens: maxTokens,
|
|
2703
|
+
system: systemWithCache,
|
|
2704
|
+
tools: allTools,
|
|
2705
|
+
messages: this.conversationHistory
|
|
2706
|
+
};
|
|
2707
|
+
if (contextManagement) {
|
|
2708
|
+
streamOptions.context_management = contextManagement;
|
|
2709
|
+
}
|
|
2710
|
+
const stream = this.anthropic.messages.stream(streamOptions);
|
|
2711
|
+
for await (const event of stream) {
|
|
2712
|
+
if (event.type === "content_block_delta") {
|
|
2713
|
+
const delta = event.delta;
|
|
2714
|
+
if (delta.type === "text_delta" && delta.text) {
|
|
2715
|
+
currentText += delta.text;
|
|
2716
|
+
finalText += delta.text;
|
|
2717
|
+
this.config.onText?.(delta.text, false);
|
|
2718
|
+
} else if (delta.type === "thinking_delta" && delta.thinking) {
|
|
2719
|
+
this.config.onThinking?.(delta.thinking);
|
|
2720
|
+
} else if (delta.type === "input_json_delta" && delta.partial_json) {
|
|
2721
|
+
}
|
|
2722
|
+
} else if (event.type === "content_block_stop") {
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
response = await stream.finalMessage();
|
|
2726
|
+
responseContent = response.content;
|
|
2727
|
+
this.updateTokenUsage(response.usage);
|
|
2728
|
+
toolUses = response.content.filter(
|
|
2729
|
+
(block) => block.type === "tool_use"
|
|
2730
|
+
);
|
|
2731
|
+
if (currentText) {
|
|
2732
|
+
this.config.onText?.("", true);
|
|
2733
|
+
}
|
|
2734
|
+
} else {
|
|
2735
|
+
const systemWithCache = [
|
|
2736
|
+
{
|
|
2737
|
+
type: "text",
|
|
2738
|
+
text: systemPrompt,
|
|
2739
|
+
cache_control: { type: "ephemeral" }
|
|
2740
|
+
}
|
|
2741
|
+
];
|
|
2742
|
+
const createOptions = {
|
|
2743
|
+
model,
|
|
2744
|
+
max_tokens: maxTokens,
|
|
2745
|
+
system: systemWithCache,
|
|
2746
|
+
tools: allTools,
|
|
2747
|
+
messages: this.conversationHistory
|
|
2748
|
+
};
|
|
2749
|
+
if (contextManagement) {
|
|
2750
|
+
createOptions.context_management = contextManagement;
|
|
2751
|
+
}
|
|
2752
|
+
response = await this.anthropic.messages.create(createOptions);
|
|
2753
|
+
responseContent = response.content;
|
|
2754
|
+
this.updateTokenUsage(response.usage);
|
|
2755
|
+
toolUses = response.content.filter(
|
|
2756
|
+
(block) => block.type === "tool_use"
|
|
2757
|
+
);
|
|
2758
|
+
const textBlocks = response.content.filter(
|
|
2759
|
+
(block) => block.type === "text"
|
|
2760
|
+
);
|
|
2761
|
+
for (const block of textBlocks) {
|
|
2762
|
+
finalText += block.text;
|
|
2763
|
+
this.config.onText?.(block.text, true);
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
this.config.onStreamEnd?.();
|
|
2767
|
+
if (toolUses.length === 0) {
|
|
2768
|
+
this.conversationHistory.push({
|
|
2769
|
+
role: "assistant",
|
|
2770
|
+
content: responseContent
|
|
2771
|
+
});
|
|
2772
|
+
break;
|
|
2773
|
+
}
|
|
2774
|
+
const toolResults = [];
|
|
2775
|
+
for (const toolUse of toolUses) {
|
|
2776
|
+
this.config.onToolCall?.(toolUse.name, toolUse.input);
|
|
2777
|
+
const { result, source } = await this.executeTool(
|
|
2778
|
+
toolUse.name,
|
|
2779
|
+
toolUse.input
|
|
2780
|
+
);
|
|
2781
|
+
const success2 = !(result && typeof result === "object" && "error" in result);
|
|
2782
|
+
this.config.onToolResult?.(toolUse.name, result, success2);
|
|
2783
|
+
toolCalls.push({
|
|
2784
|
+
name: toolUse.name,
|
|
2785
|
+
input: toolUse.input,
|
|
2786
|
+
result,
|
|
2787
|
+
source
|
|
2788
|
+
});
|
|
2789
|
+
toolResults.push({
|
|
2790
|
+
type: "tool_result",
|
|
2791
|
+
tool_use_id: toolUse.id,
|
|
2792
|
+
content: JSON.stringify(result)
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
this.conversationHistory.push({
|
|
2796
|
+
role: "assistant",
|
|
2797
|
+
content: responseContent
|
|
2798
|
+
});
|
|
2799
|
+
this.conversationHistory.push({
|
|
2800
|
+
role: "user",
|
|
2801
|
+
content: toolResults
|
|
2802
|
+
});
|
|
2803
|
+
this.truncateLastToolResults();
|
|
2804
|
+
if (response.stop_reason === "end_turn" && toolUses.length === 0) {
|
|
2805
|
+
break;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
return {
|
|
2809
|
+
text: finalText,
|
|
2810
|
+
toolCalls,
|
|
2811
|
+
iterations,
|
|
2812
|
+
tokenUsage: { ...this.cumulativeTokenUsage }
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Clear conversation history (start fresh)
|
|
2817
|
+
*/
|
|
2818
|
+
clearHistory() {
|
|
2819
|
+
this.conversationHistory = [];
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Get current conversation history
|
|
2823
|
+
*/
|
|
2824
|
+
getHistory() {
|
|
2825
|
+
return [...this.conversationHistory];
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Set working directory
|
|
2829
|
+
*/
|
|
2830
|
+
setWorkingDirectory(dir) {
|
|
2831
|
+
this.workingDirectory = dir;
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Get working directory
|
|
2835
|
+
*/
|
|
2836
|
+
getWorkingDirectory() {
|
|
2837
|
+
return this.workingDirectory;
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Truncate tool results in the last message of conversation history.
|
|
2841
|
+
*
|
|
2842
|
+
* This is called AFTER Claude has seen the full tool results and responded.
|
|
2843
|
+
* We then replace the full results with truncated versions to save context
|
|
2844
|
+
* on future turns. This way:
|
|
2845
|
+
* - Current turn: Claude sees full data, can display everything to user
|
|
2846
|
+
* - Future turns: Only actionable data (IDs, prices) is in context
|
|
2847
|
+
*/
|
|
2848
|
+
truncateLastToolResults() {
|
|
2849
|
+
for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
|
|
2850
|
+
const message = this.conversationHistory[i];
|
|
2851
|
+
if (message.role === "user" && Array.isArray(message.content)) {
|
|
2852
|
+
const toolResults = message.content.filter(
|
|
2853
|
+
(block) => block.type === "tool_result"
|
|
2854
|
+
);
|
|
2855
|
+
if (toolResults.length > 0) {
|
|
2856
|
+
const truncatedContent = message.content.map((block) => {
|
|
2857
|
+
if (block.type === "tool_result" && typeof block.content === "string") {
|
|
2858
|
+
try {
|
|
2859
|
+
const fullResult = JSON.parse(block.content);
|
|
2860
|
+
const truncatedResult = truncateToolResult(fullResult, "unknown");
|
|
2861
|
+
return {
|
|
2862
|
+
...block,
|
|
2863
|
+
content: JSON.stringify(truncatedResult)
|
|
2864
|
+
};
|
|
2865
|
+
} catch {
|
|
2866
|
+
if (block.content.length > MAX_TOOL_RESULT_CHARS) {
|
|
2867
|
+
return {
|
|
2868
|
+
...block,
|
|
2869
|
+
content: block.content.substring(0, MAX_TOOL_RESULT_CHARS) + "... [truncated for context]"
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
return block;
|
|
2875
|
+
});
|
|
2876
|
+
this.conversationHistory[i] = {
|
|
2877
|
+
...message,
|
|
2878
|
+
content: truncatedContent
|
|
2879
|
+
};
|
|
2880
|
+
break;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
/**
|
|
2886
|
+
* Update cumulative token usage from API response
|
|
2887
|
+
*/
|
|
2888
|
+
updateTokenUsage(usage) {
|
|
2889
|
+
const model = this.config.model ?? DEFAULT_MODEL;
|
|
2890
|
+
this.cumulativeTokenUsage.inputTokens = usage.input_tokens;
|
|
2891
|
+
this.cumulativeTokenUsage.outputTokens += usage.output_tokens;
|
|
2892
|
+
this.cumulativeTokenUsage.cacheCreationInputTokens = usage.cache_creation_input_tokens || 0;
|
|
2893
|
+
this.cumulativeTokenUsage.cacheReadInputTokens = usage.cache_read_input_tokens || 0;
|
|
2894
|
+
this.cumulativeTokenUsage.totalTokens = this.cumulativeTokenUsage.inputTokens + this.cumulativeTokenUsage.outputTokens;
|
|
2895
|
+
const callCost = calculateCost(
|
|
2896
|
+
model,
|
|
2897
|
+
usage.input_tokens,
|
|
2898
|
+
usage.output_tokens,
|
|
2899
|
+
usage.cache_creation_input_tokens || 0,
|
|
2900
|
+
usage.cache_read_input_tokens || 0
|
|
2901
|
+
);
|
|
2902
|
+
this.sessionCost += callCost.totalCost;
|
|
2903
|
+
this.cumulativeTokenUsage.cost = callCost;
|
|
2904
|
+
this.cumulativeTokenUsage.sessionCost = this.sessionCost;
|
|
2905
|
+
this.config.onTokenUsage?.(this.cumulativeTokenUsage);
|
|
2906
|
+
}
|
|
2907
|
+
/**
|
|
2908
|
+
* Get current token usage estimate
|
|
2909
|
+
*/
|
|
2910
|
+
getTokenUsage() {
|
|
2911
|
+
return { ...this.cumulativeTokenUsage };
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* Count tokens in current conversation (uses Anthropic's token counting API)
|
|
2915
|
+
*/
|
|
2916
|
+
async countTokens() {
|
|
2917
|
+
const model = this.config.model ?? "claude-sonnet-4-5-20250929";
|
|
2918
|
+
const systemPrompt = this.config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
2919
|
+
const allTools = await this.getAllTools();
|
|
2920
|
+
try {
|
|
2921
|
+
const response = await this.anthropic.messages.countTokens({
|
|
2922
|
+
model,
|
|
2923
|
+
system: systemPrompt,
|
|
2924
|
+
tools: allTools,
|
|
2925
|
+
messages: this.conversationHistory
|
|
2926
|
+
});
|
|
2927
|
+
return response.input_tokens;
|
|
2928
|
+
} catch (error2) {
|
|
2929
|
+
return this.cumulativeTokenUsage.inputTokens;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Reset token usage (e.g., after compaction)
|
|
2934
|
+
*/
|
|
2935
|
+
resetTokenUsage() {
|
|
2936
|
+
this.sessionCost = 0;
|
|
2937
|
+
this.cumulativeTokenUsage = {
|
|
2938
|
+
inputTokens: 0,
|
|
2939
|
+
outputTokens: 0,
|
|
2940
|
+
cacheCreationInputTokens: 0,
|
|
2941
|
+
cacheReadInputTokens: 0,
|
|
2942
|
+
totalTokens: 0,
|
|
2943
|
+
cost: { inputCost: 0, outputCost: 0, cacheWriteCost: 0, cacheReadCost: 0, totalCost: 0 },
|
|
2944
|
+
sessionCost: 0
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Get the current model being used
|
|
2949
|
+
*/
|
|
2950
|
+
getModel() {
|
|
2951
|
+
return this.config.model ?? DEFAULT_MODEL;
|
|
2952
|
+
}
|
|
2953
|
+
/**
|
|
2954
|
+
* Set the model to use for future requests
|
|
2955
|
+
*/
|
|
2956
|
+
setModel(modelIdOrAlias) {
|
|
2957
|
+
const resolvedId = resolveModelId(modelIdOrAlias);
|
|
2958
|
+
if (!resolvedId) {
|
|
2959
|
+
const availableModels = Object.values(MODELS).map((m) => m.name).join(", ");
|
|
2960
|
+
return {
|
|
2961
|
+
success: false,
|
|
2962
|
+
error: `Unknown model: "${modelIdOrAlias}". Available: ${availableModels}`
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
this.config.model = resolvedId;
|
|
2966
|
+
const modelConfig = getModelConfig(resolvedId);
|
|
2967
|
+
return {
|
|
2968
|
+
success: true,
|
|
2969
|
+
model: modelConfig?.displayName ?? resolvedId
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* Get session cost so far
|
|
2974
|
+
*/
|
|
2975
|
+
getSessionCost() {
|
|
2976
|
+
return this.sessionCost;
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* Compact the conversation history to reduce token usage.
|
|
2980
|
+
*
|
|
2981
|
+
* This uses Claude to create a structured summary of the conversation,
|
|
2982
|
+
* then replaces the history with just the summary. This dramatically
|
|
2983
|
+
* reduces token count while preserving important context.
|
|
2984
|
+
*
|
|
2985
|
+
* @returns Object with original/new token counts and the summary
|
|
2986
|
+
*/
|
|
2987
|
+
async compactHistory() {
|
|
2988
|
+
const model = this.config.model ?? "claude-sonnet-4-5-20250929";
|
|
2989
|
+
const systemPrompt = this.config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
2990
|
+
const allTools = await this.getAllTools();
|
|
2991
|
+
if (this.conversationHistory.length < 2) {
|
|
2992
|
+
return {
|
|
2993
|
+
success: false,
|
|
2994
|
+
originalTokenCount: 0,
|
|
2995
|
+
newTokenCount: 0,
|
|
2996
|
+
error: "Conversation too short to compact"
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
try {
|
|
3000
|
+
const result = await compactConversation(
|
|
3001
|
+
this.anthropic,
|
|
3002
|
+
this.conversationHistory,
|
|
3003
|
+
model,
|
|
3004
|
+
systemPrompt,
|
|
3005
|
+
allTools
|
|
3006
|
+
);
|
|
3007
|
+
this.conversationHistory = result.newHistory;
|
|
3008
|
+
this.resetTokenUsage();
|
|
3009
|
+
this.cumulativeTokenUsage.inputTokens = result.newTokens;
|
|
3010
|
+
this.cumulativeTokenUsage.totalTokens = result.newTokens;
|
|
3011
|
+
this.config.onTokenUsage?.(this.cumulativeTokenUsage);
|
|
3012
|
+
return {
|
|
3013
|
+
success: true,
|
|
3014
|
+
summary: result.summary,
|
|
3015
|
+
originalTokenCount: result.originalTokens,
|
|
3016
|
+
newTokenCount: result.newTokens
|
|
3017
|
+
};
|
|
3018
|
+
} catch (error2) {
|
|
3019
|
+
return {
|
|
3020
|
+
success: false,
|
|
3021
|
+
originalTokenCount: this.cumulativeTokenUsage.inputTokens,
|
|
3022
|
+
newTokenCount: this.cumulativeTokenUsage.inputTokens,
|
|
3023
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Set conversation history (useful for restoring state)
|
|
3029
|
+
*/
|
|
3030
|
+
setHistory(history) {
|
|
3031
|
+
this.conversationHistory = history;
|
|
3032
|
+
}
|
|
3033
|
+
};
|
|
3034
|
+
function createAgent(config) {
|
|
3035
|
+
return new Agent(config);
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// src/ui/output.ts
|
|
3039
|
+
import chalk2 from "chalk";
|
|
3040
|
+
import ora from "ora";
|
|
3041
|
+
var BANNER = `
|
|
3042
|
+
${chalk2.yellow(" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ")}${chalk2.hex("#FFD700")("\u2588\u2588\u2557 \u2588\u2588\u2557")}${chalk2.hex("#FFC000")(" \u2588\u2588\u2588\u2588\u2588\u2557 ")}${chalk2.hex("#FFB000")("\u2588\u2588\u2588\u2557 \u2588\u2588\u2557")}${chalk2.hex("#FFA000")("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}${chalk2.hex("#FF9000")("\u2588\u2588\u2557")}${chalk2.hex("#FF8000")("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}${chalk2.hex("#FF7000")("\u2588\u2588\u2557 \u2588\u2588\u2557")}
|
|
3043
|
+
${chalk2.yellow(" \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557")}${chalk2.hex("#FFD700")("\u2588\u2588\u2551 \u2588\u2588\u2551")}${chalk2.hex("#FFC000")("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557")}${chalk2.hex("#FFB000")("\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551")}${chalk2.hex("#FFA000")("\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D")}${chalk2.hex("#FF9000")("\u2588\u2588\u2551")}${chalk2.hex("#FF8000")("\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D")}${chalk2.hex("#FF7000")("\u2588\u2588\u2551 \u2588\u2588\u2551")}
|
|
3044
|
+
${chalk2.yellow(" \u2588\u2588\u2551 \u2588\u2588\u2551")}${chalk2.hex("#FFD700")("\u2588\u2588\u2551 \u2588\u2588\u2551")}${chalk2.hex("#FFC000")("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551")}${chalk2.hex("#FFB000")("\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551")}${chalk2.hex("#FFA000")(" \u2588\u2588\u2551 ")}${chalk2.hex("#FF9000")("\u2588\u2588\u2551")}${chalk2.hex("#FF8000")("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}${chalk2.hex("#FF7000")("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551")}
|
|
3045
|
+
${chalk2.yellow(" \u2588\u2588\u2551\u2584\u2584 \u2588\u2588\u2551")}${chalk2.hex("#FFD700")("\u2588\u2588\u2551 \u2588\u2588\u2551")}${chalk2.hex("#FFC000")("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551")}${chalk2.hex("#FFB000")("\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551")}${chalk2.hex("#FFA000")(" \u2588\u2588\u2551 ")}${chalk2.hex("#FF9000")("\u2588\u2588\u2551")}${chalk2.hex("#FF8000")("\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551")}${chalk2.hex("#FF7000")("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551")}
|
|
3046
|
+
${chalk2.yellow(" \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}${chalk2.hex("#FFD700")("\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}${chalk2.hex("#FFC000")("\u2588\u2588\u2551 \u2588\u2588\u2551")}${chalk2.hex("#FFB000")("\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551")}${chalk2.hex("#FFA000")(" \u2588\u2588\u2551 ")}${chalk2.hex("#FF9000")("\u2588\u2588\u2551")}${chalk2.hex("#FF8000")("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551")}${chalk2.hex("#FF7000")("\u2588\u2588\u2551 \u2588\u2588\u2551")}
|
|
3047
|
+
${chalk2.yellow(" \u255A\u2550\u2550\u2580\u2580\u2550\u255D ")}${chalk2.hex("#FFD700")(" \u255A\u2550\u2550\u2550\u2550\u2550\u255D ")}${chalk2.hex("#FFC000")("\u255A\u2550\u255D \u255A\u2550\u255D")}${chalk2.hex("#FFB000")("\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D")}${chalk2.hex("#FFA000")(" \u255A\u2550\u255D ")}${chalk2.hex("#FF9000")("\u255A\u2550\u255D")}${chalk2.hex("#FF8000")("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}${chalk2.hex("#FF7000")("\u255A\u2550\u255D \u255A\u2550\u255D")}
|
|
3048
|
+
`;
|
|
3049
|
+
var TAGLINE = chalk2.dim(" AI-powered trading agent for Polymarket");
|
|
3050
|
+
function printHeader() {
|
|
3051
|
+
console.log(BANNER);
|
|
3052
|
+
console.log(TAGLINE);
|
|
3053
|
+
console.log();
|
|
3054
|
+
}
|
|
3055
|
+
function printDivider() {
|
|
3056
|
+
console.log(chalk2.dim("\u2500".repeat(40)));
|
|
3057
|
+
}
|
|
3058
|
+
function success(message) {
|
|
3059
|
+
console.log(chalk2.green("\u2713") + " " + message);
|
|
3060
|
+
}
|
|
3061
|
+
function warn(message) {
|
|
3062
|
+
console.log(chalk2.yellow("\u26A0") + " " + message);
|
|
3063
|
+
}
|
|
3064
|
+
function error(message) {
|
|
3065
|
+
console.log(chalk2.red("\u2717") + " " + message);
|
|
3066
|
+
}
|
|
3067
|
+
function toolCall(name, args) {
|
|
3068
|
+
console.log(chalk2.yellow("\u26A1") + " " + chalk2.dim("Calling ") + chalk2.yellow.bold(name));
|
|
3069
|
+
if (args && Object.keys(args).length > 0) {
|
|
3070
|
+
console.log(chalk2.dim(" " + JSON.stringify(args)));
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
function assistant(message) {
|
|
3074
|
+
console.log();
|
|
3075
|
+
console.log(chalk2.yellow("Quantish:"));
|
|
3076
|
+
console.log(message);
|
|
3077
|
+
console.log();
|
|
3078
|
+
}
|
|
3079
|
+
function spinner(text) {
|
|
3080
|
+
return ora({
|
|
3081
|
+
text,
|
|
3082
|
+
color: "yellow"
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
function tableRow(label, value, width = 20) {
|
|
3086
|
+
const paddedLabel = label.padEnd(width);
|
|
3087
|
+
console.log(chalk2.dim(paddedLabel) + value);
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
// src/ui/App.tsx
|
|
3091
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
3092
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
3093
|
+
import TextInput from "ink-text-input";
|
|
3094
|
+
import Spinner from "ink-spinner";
|
|
3095
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3096
|
+
function formatTokenCount(count) {
|
|
3097
|
+
if (count < 1e3) return String(count);
|
|
3098
|
+
if (count < 1e5) return `${(count / 1e3).toFixed(1)}k`;
|
|
3099
|
+
return `${Math.round(count / 1e3)}k`;
|
|
3100
|
+
}
|
|
3101
|
+
function getTokenColor(count) {
|
|
3102
|
+
if (count < 5e4) return "green";
|
|
3103
|
+
if (count < 1e5) return "yellow";
|
|
3104
|
+
return "red";
|
|
3105
|
+
}
|
|
3106
|
+
var SLASH_COMMANDS = [
|
|
3107
|
+
{ cmd: "/help", desc: "Show available commands" },
|
|
3108
|
+
{ cmd: "/clear", desc: "Clear conversation history" },
|
|
3109
|
+
{ cmd: "/compact", desc: "Summarize conversation to save tokens" },
|
|
3110
|
+
{ cmd: "/model", desc: "Switch model (opus, sonnet, haiku)" },
|
|
3111
|
+
{ cmd: "/cost", desc: "Show session cost breakdown" },
|
|
3112
|
+
{ cmd: "/tools", desc: "List available tools" },
|
|
3113
|
+
{ cmd: "/config", desc: "Show configuration info" },
|
|
3114
|
+
{ cmd: "/processes", desc: "List running background processes" },
|
|
3115
|
+
{ cmd: "/stop", desc: "Stop a background process by ID" },
|
|
3116
|
+
{ cmd: "/stopall", desc: "Stop all background processes" },
|
|
3117
|
+
{ cmd: "/exit", desc: "Exit the CLI" }
|
|
3118
|
+
];
|
|
3119
|
+
function formatArgs(args) {
|
|
3120
|
+
const entries = Object.entries(args);
|
|
3121
|
+
if (entries.length === 0) return "()";
|
|
3122
|
+
const formatted = entries.map(([key, value]) => {
|
|
3123
|
+
if (typeof value === "string") {
|
|
3124
|
+
const str = value.length > 50 ? value.slice(0, 50) + "..." : value;
|
|
3125
|
+
return `${key}: "${str}"`;
|
|
3126
|
+
}
|
|
3127
|
+
if (typeof value === "object") {
|
|
3128
|
+
return `${key}: {...}`;
|
|
3129
|
+
}
|
|
3130
|
+
return `${key}: ${String(value)}`;
|
|
3131
|
+
});
|
|
3132
|
+
return `(${formatted.join(", ")})`;
|
|
3133
|
+
}
|
|
3134
|
+
function formatResult(result, maxLength = 200) {
|
|
3135
|
+
if (result === null || result === void 0) return "null";
|
|
3136
|
+
if (typeof result === "string") {
|
|
3137
|
+
return result.length > maxLength ? result.slice(0, maxLength) + "..." : result;
|
|
3138
|
+
}
|
|
3139
|
+
if (typeof result === "object") {
|
|
3140
|
+
const str = JSON.stringify(result, null, 2);
|
|
3141
|
+
return str.length > maxLength ? str.slice(0, maxLength) + "..." : str;
|
|
3142
|
+
}
|
|
3143
|
+
return String(result);
|
|
3144
|
+
}
|
|
3145
|
+
function App({ agent, onExit }) {
|
|
3146
|
+
const { exit } = useApp();
|
|
3147
|
+
const [messages, setMessages] = useState([]);
|
|
3148
|
+
const [input, setInput] = useState("");
|
|
3149
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
3150
|
+
const [currentToolCalls, setCurrentToolCalls] = useState([]);
|
|
3151
|
+
const [streamingText, setStreamingText] = useState("");
|
|
3152
|
+
const [error2, setError] = useState(null);
|
|
3153
|
+
const [thinkingText, setThinkingText] = useState(null);
|
|
3154
|
+
const [isInterrupted, setIsInterrupted] = useState(false);
|
|
3155
|
+
const [tokenUsage, setTokenUsage] = useState({
|
|
3156
|
+
inputTokens: 0,
|
|
3157
|
+
outputTokens: 0,
|
|
3158
|
+
cacheCreationInputTokens: 0,
|
|
3159
|
+
cacheReadInputTokens: 0,
|
|
3160
|
+
totalTokens: 0,
|
|
3161
|
+
cost: { inputCost: 0, outputCost: 0, cacheWriteCost: 0, cacheReadCost: 0, totalCost: 0 },
|
|
3162
|
+
sessionCost: 0
|
|
3163
|
+
});
|
|
3164
|
+
const completedToolCalls = useRef([]);
|
|
3165
|
+
const abortController = useRef(null);
|
|
3166
|
+
const handleSlashCommand = useCallback((command) => {
|
|
3167
|
+
const cmd = command.slice(1).toLowerCase().split(" ")[0];
|
|
3168
|
+
const args = command.slice(cmd.length + 2).trim();
|
|
3169
|
+
switch (cmd) {
|
|
3170
|
+
case "help":
|
|
3171
|
+
setMessages((prev) => [...prev, {
|
|
3172
|
+
role: "system",
|
|
3173
|
+
content: `\u{1F4DA} Available Commands:
|
|
3174
|
+
/clear - Clear conversation history
|
|
3175
|
+
/compact - Summarize conversation (keeps context, saves tokens)
|
|
3176
|
+
/model - Switch model (opus, sonnet, haiku)
|
|
3177
|
+
/cost - Show session cost breakdown
|
|
3178
|
+
/help - Show this help message
|
|
3179
|
+
/tools - List available tools
|
|
3180
|
+
/config - Show configuration info
|
|
3181
|
+
/processes - List running background processes
|
|
3182
|
+
/stop <id> - Stop a background process by ID
|
|
3183
|
+
/stopall - Stop all background processes
|
|
3184
|
+
/exit - Exit the CLI
|
|
3185
|
+
|
|
3186
|
+
\u2328\uFE0F Keyboard Shortcuts:
|
|
3187
|
+
Esc - Interrupt current generation
|
|
3188
|
+
Ctrl+C - Exit (stops all processes)`
|
|
3189
|
+
}]);
|
|
3190
|
+
return true;
|
|
3191
|
+
case "clear":
|
|
3192
|
+
agent.clearHistory();
|
|
3193
|
+
agent.resetTokenUsage();
|
|
3194
|
+
setMessages([]);
|
|
3195
|
+
setCurrentToolCalls([]);
|
|
3196
|
+
setStreamingText("");
|
|
3197
|
+
setError(null);
|
|
3198
|
+
setTokenUsage({
|
|
3199
|
+
inputTokens: 0,
|
|
3200
|
+
outputTokens: 0,
|
|
3201
|
+
cacheCreationInputTokens: 0,
|
|
3202
|
+
cacheReadInputTokens: 0,
|
|
3203
|
+
totalTokens: 0,
|
|
3204
|
+
cost: { inputCost: 0, outputCost: 0, cacheWriteCost: 0, cacheReadCost: 0, totalCost: 0 },
|
|
3205
|
+
sessionCost: 0
|
|
3206
|
+
});
|
|
3207
|
+
setMessages([{ role: "system", content: "\u2728 Conversation cleared." }]);
|
|
3208
|
+
return true;
|
|
3209
|
+
case "compact":
|
|
3210
|
+
setMessages((prev) => [...prev, {
|
|
3211
|
+
role: "system",
|
|
3212
|
+
content: "\u{1F5DC}\uFE0F Compacting conversation..."
|
|
3213
|
+
}]);
|
|
3214
|
+
setIsProcessing(true);
|
|
3215
|
+
agent.compactHistory().then((result2) => {
|
|
3216
|
+
if (result2.success) {
|
|
3217
|
+
const savedTokens = result2.originalTokenCount - result2.newTokenCount;
|
|
3218
|
+
const savedPercent = result2.originalTokenCount > 0 ? Math.round(savedTokens / result2.originalTokenCount * 100) : 0;
|
|
3219
|
+
setMessages((prev) => [...prev, {
|
|
3220
|
+
role: "system",
|
|
3221
|
+
content: `\u2705 Compaction complete!
|
|
3222
|
+
Before: ${formatTokenCount(result2.originalTokenCount)} tokens
|
|
3223
|
+
After: ${formatTokenCount(result2.newTokenCount)} tokens
|
|
3224
|
+
Saved: ${formatTokenCount(savedTokens)} tokens (${savedPercent}%)`
|
|
3225
|
+
}]);
|
|
3226
|
+
} else {
|
|
3227
|
+
setMessages((prev) => [...prev, {
|
|
3228
|
+
role: "system",
|
|
3229
|
+
content: `\u274C Compaction failed: ${result2.error || "Unknown error"}`
|
|
3230
|
+
}]);
|
|
3231
|
+
}
|
|
3232
|
+
setIsProcessing(false);
|
|
3233
|
+
}).catch((err) => {
|
|
3234
|
+
setMessages((prev) => [...prev, {
|
|
3235
|
+
role: "system",
|
|
3236
|
+
content: `\u274C Compaction error: ${err.message || String(err)}`
|
|
3237
|
+
}]);
|
|
3238
|
+
setIsProcessing(false);
|
|
3239
|
+
});
|
|
3240
|
+
return true;
|
|
3241
|
+
case "tools":
|
|
3242
|
+
setMessages((prev) => [...prev, {
|
|
3243
|
+
role: "system",
|
|
3244
|
+
content: '\u{1F527} Run "quantish tools" in your terminal to see all available tools.'
|
|
3245
|
+
}]);
|
|
3246
|
+
return true;
|
|
3247
|
+
case "config":
|
|
3248
|
+
setMessages((prev) => [...prev, {
|
|
3249
|
+
role: "system",
|
|
3250
|
+
content: '\u2699\uFE0F Run "quantish config" to view/export your configuration.\n "quantish config --export" exports as .env format for your bots.'
|
|
3251
|
+
}]);
|
|
3252
|
+
return true;
|
|
3253
|
+
case "processes":
|
|
3254
|
+
case "ps":
|
|
3255
|
+
const processes = processManager.listRunning();
|
|
3256
|
+
if (processes.length === 0) {
|
|
3257
|
+
setMessages((prev) => [...prev, {
|
|
3258
|
+
role: "system",
|
|
3259
|
+
content: "\u{1F4CB} No background processes running."
|
|
3260
|
+
}]);
|
|
3261
|
+
} else {
|
|
3262
|
+
const processLines = processes.map((p) => {
|
|
3263
|
+
const uptime = Math.round((Date.now() - p.startedAt.getTime()) / 1e3);
|
|
3264
|
+
return ` [${p.id}] ${p.name} (PID: ${p.pid}) - ${uptime}s`;
|
|
3265
|
+
}).join("\n");
|
|
3266
|
+
setMessages((prev) => [...prev, {
|
|
3267
|
+
role: "system",
|
|
3268
|
+
content: `\u{1F4CB} Running processes:
|
|
3269
|
+
${processLines}
|
|
3270
|
+
|
|
3271
|
+
Use /stop <id> to stop a process.`
|
|
3272
|
+
}]);
|
|
3273
|
+
}
|
|
3274
|
+
return true;
|
|
3275
|
+
case "stop":
|
|
3276
|
+
if (!args) {
|
|
3277
|
+
setMessages((prev) => [...prev, {
|
|
3278
|
+
role: "system",
|
|
3279
|
+
content: "\u2753 Usage: /stop <process_id>\n Use /processes to see running processes."
|
|
3280
|
+
}]);
|
|
3281
|
+
return true;
|
|
3282
|
+
}
|
|
3283
|
+
const processId = parseInt(args, 10);
|
|
3284
|
+
if (isNaN(processId)) {
|
|
3285
|
+
setMessages((prev) => [...prev, {
|
|
3286
|
+
role: "system",
|
|
3287
|
+
content: `\u274C Invalid process ID: ${args}. Must be a number.`
|
|
3288
|
+
}]);
|
|
3289
|
+
return true;
|
|
3290
|
+
}
|
|
3291
|
+
const processToStop = processManager.get(processId);
|
|
3292
|
+
if (!processToStop) {
|
|
3293
|
+
setMessages((prev) => [...prev, {
|
|
3294
|
+
role: "system",
|
|
3295
|
+
content: `\u274C Process ${processId} not found.`
|
|
3296
|
+
}]);
|
|
3297
|
+
return true;
|
|
3298
|
+
}
|
|
3299
|
+
if (processManager.kill(processId)) {
|
|
3300
|
+
setMessages((prev) => [...prev, {
|
|
3301
|
+
role: "system",
|
|
3302
|
+
content: `\u2705 Stopped process "${processToStop.name}" (ID: ${processId})`
|
|
3303
|
+
}]);
|
|
3304
|
+
} else {
|
|
3305
|
+
setMessages((prev) => [...prev, {
|
|
3306
|
+
role: "system",
|
|
3307
|
+
content: `\u274C Failed to stop process ${processId}`
|
|
3308
|
+
}]);
|
|
3309
|
+
}
|
|
3310
|
+
return true;
|
|
3311
|
+
case "stopall":
|
|
3312
|
+
const runningCount = processManager.runningCount();
|
|
3313
|
+
if (runningCount === 0) {
|
|
3314
|
+
setMessages((prev) => [...prev, {
|
|
3315
|
+
role: "system",
|
|
3316
|
+
content: "\u{1F4CB} No background processes to stop."
|
|
3317
|
+
}]);
|
|
3318
|
+
} else {
|
|
3319
|
+
processManager.killAll();
|
|
3320
|
+
setMessages((prev) => [...prev, {
|
|
3321
|
+
role: "system",
|
|
3322
|
+
content: `\u2705 Stopped ${runningCount} background process${runningCount > 1 ? "es" : ""}.`
|
|
3323
|
+
}]);
|
|
3324
|
+
}
|
|
3325
|
+
return true;
|
|
3326
|
+
case "model":
|
|
3327
|
+
if (!args) {
|
|
3328
|
+
const currentModel = agent.getModel();
|
|
3329
|
+
const modelConfig = getModelConfig(currentModel);
|
|
3330
|
+
const models = listModels();
|
|
3331
|
+
const modelList = models.map((m) => {
|
|
3332
|
+
const isCurrent = m.id === currentModel ? " (current)" : "";
|
|
3333
|
+
return ` ${m.name}${isCurrent} - ${m.description}`;
|
|
3334
|
+
}).join("\n");
|
|
3335
|
+
setMessages((prev) => [...prev, {
|
|
3336
|
+
role: "system",
|
|
3337
|
+
content: `\u{1F916} Current model: ${modelConfig?.displayName || currentModel}
|
|
3338
|
+
|
|
3339
|
+
Available models:
|
|
3340
|
+
${modelList}
|
|
3341
|
+
|
|
3342
|
+
Usage: /model <name> (e.g., /model haiku, /model opus)`
|
|
3343
|
+
}]);
|
|
3344
|
+
return true;
|
|
3345
|
+
}
|
|
3346
|
+
const result = agent.setModel(args);
|
|
3347
|
+
if (result.success) {
|
|
3348
|
+
const newConfig = getModelConfig(agent.getModel());
|
|
3349
|
+
setMessages((prev) => [...prev, {
|
|
3350
|
+
role: "system",
|
|
3351
|
+
content: `\u2705 Switched to ${result.model}
|
|
3352
|
+
${newConfig?.description || ""}`
|
|
3353
|
+
}]);
|
|
3354
|
+
} else {
|
|
3355
|
+
setMessages((prev) => [...prev, {
|
|
3356
|
+
role: "system",
|
|
3357
|
+
content: `\u274C ${result.error}`
|
|
3358
|
+
}]);
|
|
3359
|
+
}
|
|
3360
|
+
return true;
|
|
3361
|
+
case "cost":
|
|
3362
|
+
const usage = agent.getTokenUsage();
|
|
3363
|
+
const sessionCost = agent.getSessionCost();
|
|
3364
|
+
const costBreakdown = usage.cost;
|
|
3365
|
+
setMessages((prev) => [...prev, {
|
|
3366
|
+
role: "system",
|
|
3367
|
+
content: `\u{1F4B0} Session Cost: ${formatCost(sessionCost)}
|
|
3368
|
+
|
|
3369
|
+
Token Usage (current context):
|
|
3370
|
+
Input: ${formatTokenCount(usage.inputTokens)} tokens
|
|
3371
|
+
Output: ${formatTokenCount(usage.outputTokens)} tokens
|
|
3372
|
+
Cache Write: ${formatTokenCount(usage.cacheCreationInputTokens)} tokens
|
|
3373
|
+
Cache Read: ${formatTokenCount(usage.cacheReadInputTokens)} tokens
|
|
3374
|
+
Total: ${formatTokenCount(usage.totalTokens)} tokens
|
|
3375
|
+
|
|
3376
|
+
Last API Call Cost:
|
|
3377
|
+
Input: ${formatCost(costBreakdown.inputCost)}
|
|
3378
|
+
Output: ${formatCost(costBreakdown.outputCost)}
|
|
3379
|
+
Cache Write: ${formatCost(costBreakdown.cacheWriteCost)}
|
|
3380
|
+
Cache Read: ${formatCost(costBreakdown.cacheReadCost)}
|
|
3381
|
+
|
|
3382
|
+
\u{1F4A1} Tip: Use /model haiku for cheaper operations, /compact to reduce context.`
|
|
3383
|
+
}]);
|
|
3384
|
+
return true;
|
|
3385
|
+
case "exit":
|
|
3386
|
+
case "quit":
|
|
3387
|
+
if (processManager.hasRunning()) {
|
|
3388
|
+
processManager.killAll();
|
|
3389
|
+
}
|
|
3390
|
+
onExit?.();
|
|
3391
|
+
exit();
|
|
3392
|
+
return true;
|
|
3393
|
+
default:
|
|
3394
|
+
setMessages((prev) => [...prev, {
|
|
3395
|
+
role: "system",
|
|
3396
|
+
content: `Unknown command: /${cmd}. Type /help for available commands.`
|
|
3397
|
+
}]);
|
|
3398
|
+
return true;
|
|
3399
|
+
}
|
|
3400
|
+
}, [agent, onExit, exit]);
|
|
3401
|
+
const handleSubmit = useCallback(async (value) => {
|
|
3402
|
+
const trimmed = value.trim();
|
|
3403
|
+
if (!trimmed || isProcessing) return;
|
|
3404
|
+
if (trimmed.startsWith("/")) {
|
|
3405
|
+
setInput("");
|
|
3406
|
+
handleSlashCommand(trimmed);
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
if (trimmed.toLowerCase() === "exit" || trimmed.toLowerCase() === "quit") {
|
|
3410
|
+
onExit?.();
|
|
3411
|
+
exit();
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
if (trimmed.toLowerCase() === "clear") {
|
|
3415
|
+
agent.clearHistory();
|
|
3416
|
+
setMessages([]);
|
|
3417
|
+
setInput("");
|
|
3418
|
+
setCurrentToolCalls([]);
|
|
3419
|
+
setStreamingText("");
|
|
3420
|
+
return;
|
|
3421
|
+
}
|
|
3422
|
+
setMessages((prev) => [...prev, { role: "user", content: trimmed }]);
|
|
3423
|
+
setInput("");
|
|
3424
|
+
setIsProcessing(true);
|
|
3425
|
+
setIsInterrupted(false);
|
|
3426
|
+
setError(null);
|
|
3427
|
+
setCurrentToolCalls([]);
|
|
3428
|
+
setStreamingText("");
|
|
3429
|
+
setThinkingText(null);
|
|
3430
|
+
completedToolCalls.current = [];
|
|
3431
|
+
abortController.current = new AbortController();
|
|
3432
|
+
try {
|
|
3433
|
+
const result = await agent.run(trimmed);
|
|
3434
|
+
if (isInterrupted) {
|
|
3435
|
+
setMessages((prev) => [...prev, {
|
|
3436
|
+
role: "system",
|
|
3437
|
+
content: "\u26A1 Generation interrupted by user."
|
|
3438
|
+
}]);
|
|
3439
|
+
} else {
|
|
3440
|
+
setMessages((prev) => {
|
|
3441
|
+
const filtered = prev.filter((m) => !m.isStreaming);
|
|
3442
|
+
return [...filtered, {
|
|
3443
|
+
role: "assistant",
|
|
3444
|
+
content: result.text || "(completed)",
|
|
3445
|
+
toolCalls: result.toolCalls.map((tc) => ({
|
|
3446
|
+
name: tc.name,
|
|
3447
|
+
args: tc.input,
|
|
3448
|
+
result: tc.result,
|
|
3449
|
+
success: !(tc.result && typeof tc.result === "object" && "error" in tc.result),
|
|
3450
|
+
pending: false
|
|
3451
|
+
}))
|
|
3452
|
+
}];
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
setStreamingText("");
|
|
3456
|
+
setCurrentToolCalls([]);
|
|
3457
|
+
} catch (err) {
|
|
3458
|
+
const errorMsg = err.message || String(err);
|
|
3459
|
+
let displayError = errorMsg;
|
|
3460
|
+
if (errorMsg.includes("aborted") || errorMsg.includes("AbortError")) {
|
|
3461
|
+
setMessages((prev) => [...prev, {
|
|
3462
|
+
role: "system",
|
|
3463
|
+
content: "\u26A1 Generation interrupted by user."
|
|
3464
|
+
}]);
|
|
3465
|
+
} else if (errorMsg.includes("credits exhausted")) {
|
|
3466
|
+
displayError = "Anthropic API credits exhausted. Please add credits at console.anthropic.com";
|
|
3467
|
+
setError(displayError);
|
|
3468
|
+
} else if (errorMsg.includes("invalid_api_key") || errorMsg.includes("401")) {
|
|
3469
|
+
displayError = 'Invalid Anthropic API key. Run "quantish init" to reconfigure.';
|
|
3470
|
+
setError(displayError);
|
|
3471
|
+
} else if (errorMsg.includes("rate_limit")) {
|
|
3472
|
+
displayError = "Rate limited by Anthropic API. Please wait a moment and try again.";
|
|
3473
|
+
setError(displayError);
|
|
3474
|
+
} else {
|
|
3475
|
+
setError(displayError);
|
|
3476
|
+
}
|
|
3477
|
+
} finally {
|
|
3478
|
+
setIsProcessing(false);
|
|
3479
|
+
setThinkingText(null);
|
|
3480
|
+
abortController.current = null;
|
|
3481
|
+
}
|
|
3482
|
+
}, [agent, isProcessing, isInterrupted, exit, onExit, handleSlashCommand]);
|
|
3483
|
+
useEffect(() => {
|
|
3484
|
+
const originalConfig = agent.config;
|
|
3485
|
+
agent.config = {
|
|
3486
|
+
...originalConfig,
|
|
3487
|
+
streaming: true,
|
|
3488
|
+
onText: (text, isComplete) => {
|
|
3489
|
+
if (!isComplete) {
|
|
3490
|
+
setStreamingText((prev) => prev + text);
|
|
3491
|
+
}
|
|
3492
|
+
},
|
|
3493
|
+
onThinking: (text) => {
|
|
3494
|
+
setThinkingText((prev) => (prev || "") + text);
|
|
3495
|
+
},
|
|
3496
|
+
onToolCall: (name, args) => {
|
|
3497
|
+
setCurrentToolCalls((prev) => [...prev, {
|
|
3498
|
+
name,
|
|
3499
|
+
args,
|
|
3500
|
+
pending: true
|
|
3501
|
+
}]);
|
|
3502
|
+
},
|
|
3503
|
+
onToolResult: (name, result, success2) => {
|
|
3504
|
+
setCurrentToolCalls(
|
|
3505
|
+
(prev) => prev.map(
|
|
3506
|
+
(tc) => tc.name === name && tc.pending ? { ...tc, result, success: success2, pending: false } : tc
|
|
3507
|
+
)
|
|
3508
|
+
);
|
|
3509
|
+
},
|
|
3510
|
+
onStreamStart: () => {
|
|
3511
|
+
setStreamingText("");
|
|
3512
|
+
},
|
|
3513
|
+
onStreamEnd: () => {
|
|
3514
|
+
},
|
|
3515
|
+
onTokenUsage: (usage) => {
|
|
3516
|
+
setTokenUsage(usage);
|
|
3517
|
+
}
|
|
3518
|
+
};
|
|
3519
|
+
return () => {
|
|
3520
|
+
agent.config = originalConfig;
|
|
3521
|
+
};
|
|
3522
|
+
}, [agent]);
|
|
3523
|
+
useInput((inputChar, key) => {
|
|
3524
|
+
if (key.ctrl && inputChar === "c") {
|
|
3525
|
+
if (processManager.hasRunning()) {
|
|
3526
|
+
const count = processManager.runningCount();
|
|
3527
|
+
processManager.killAll();
|
|
3528
|
+
console.log(`
|
|
3529
|
+
Stopped ${count} background process${count > 1 ? "es" : ""}.`);
|
|
3530
|
+
}
|
|
3531
|
+
onExit?.();
|
|
3532
|
+
exit();
|
|
3533
|
+
}
|
|
3534
|
+
if (key.escape && isProcessing) {
|
|
3535
|
+
setIsInterrupted(true);
|
|
3536
|
+
abortController.current?.abort();
|
|
3537
|
+
setMessages((prev) => [...prev, {
|
|
3538
|
+
role: "system",
|
|
3539
|
+
content: "\u26A1 Interrupting..."
|
|
3540
|
+
}]);
|
|
3541
|
+
}
|
|
3542
|
+
});
|
|
3543
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
3544
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: messages.map((msg, i) => /* @__PURE__ */ jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [
|
|
3545
|
+
msg.role === "user" && /* @__PURE__ */ jsxs(Box, { children: [
|
|
3546
|
+
/* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "You: " }),
|
|
3547
|
+
/* @__PURE__ */ jsx(Text, { children: msg.content })
|
|
3548
|
+
] }),
|
|
3549
|
+
msg.role === "assistant" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
3550
|
+
msg.toolCalls && msg.toolCalls.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: msg.toolCalls.map((tc, j) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [
|
|
3551
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
3552
|
+
/* @__PURE__ */ jsxs(Text, { color: tc.success ? "blue" : "red", children: [
|
|
3553
|
+
tc.success ? "\u2713" : "\u2717",
|
|
3554
|
+
" ",
|
|
3555
|
+
tc.name
|
|
3556
|
+
] }),
|
|
3557
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: formatArgs(tc.args) })
|
|
3558
|
+
] }),
|
|
3559
|
+
tc.result && /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
3560
|
+
"\u2192 ",
|
|
3561
|
+
formatResult(tc.result, 100)
|
|
3562
|
+
] }) })
|
|
3563
|
+
] }, j)) }),
|
|
3564
|
+
msg.content && msg.content !== "(completed)" && /* @__PURE__ */ jsxs(Box, { children: [
|
|
3565
|
+
/* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "Quantish: " }),
|
|
3566
|
+
/* @__PURE__ */ jsx(Text, { wrap: "wrap", children: msg.content })
|
|
3567
|
+
] })
|
|
3568
|
+
] }),
|
|
3569
|
+
msg.role === "system" && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "gray", italic: true, children: msg.content }) })
|
|
3570
|
+
] }, i)) }),
|
|
3571
|
+
currentToolCalls.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, marginLeft: 2, children: currentToolCalls.map((tc, i) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
3572
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
3573
|
+
tc.pending ? /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
|
|
3574
|
+
/* @__PURE__ */ jsx(Spinner, { type: "dots" }),
|
|
3575
|
+
" ",
|
|
3576
|
+
tc.name
|
|
3577
|
+
] }) : /* @__PURE__ */ jsxs(Text, { color: tc.success ? "blue" : "red", children: [
|
|
3578
|
+
tc.success ? "\u2713" : "\u2717",
|
|
3579
|
+
" ",
|
|
3580
|
+
tc.name
|
|
3581
|
+
] }),
|
|
3582
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: formatArgs(tc.args) })
|
|
3583
|
+
] }),
|
|
3584
|
+
!tc.pending && tc.result && /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
3585
|
+
"\u2192 ",
|
|
3586
|
+
formatResult(tc.result, 100)
|
|
3587
|
+
] }) })
|
|
3588
|
+
] }, i)) }),
|
|
3589
|
+
streamingText && /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
3590
|
+
/* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "Quantish: " }),
|
|
3591
|
+
/* @__PURE__ */ jsx(Text, { wrap: "wrap", children: streamingText }),
|
|
3592
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u258A" })
|
|
3593
|
+
] }),
|
|
3594
|
+
thinkingText && /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", italic: true, children: [
|
|
3595
|
+
"\u{1F4AD} ",
|
|
3596
|
+
thinkingText.slice(0, 100),
|
|
3597
|
+
thinkingText.length > 100 ? "..." : ""
|
|
3598
|
+
] }) }),
|
|
3599
|
+
error2 && /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
3600
|
+
"\u274C Error: ",
|
|
3601
|
+
error2
|
|
3602
|
+
] }) }),
|
|
3603
|
+
isProcessing && !streamingText && currentToolCalls.length === 0 && /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
|
|
3604
|
+
/* @__PURE__ */ jsx(Spinner, { type: "dots" }),
|
|
3605
|
+
" Thinking..."
|
|
3606
|
+
] }) }),
|
|
3607
|
+
input.startsWith("/") && !isProcessing && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 2, children: [
|
|
3608
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Commands:" }),
|
|
3609
|
+
SLASH_COMMANDS.filter((c) => c.cmd.startsWith(input.toLowerCase()) || input === "/").slice(0, 5).map((c, i) => /* @__PURE__ */ jsxs(Box, { paddingLeft: 1, children: [
|
|
3610
|
+
/* @__PURE__ */ jsx(Text, { color: c.cmd === input.toLowerCase() ? "yellow" : "gray", children: c.cmd }),
|
|
3611
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
3612
|
+
" - ",
|
|
3613
|
+
c.desc
|
|
3614
|
+
] })
|
|
3615
|
+
] }, i))
|
|
3616
|
+
] }),
|
|
3617
|
+
/* @__PURE__ */ jsx(
|
|
3618
|
+
Box,
|
|
3619
|
+
{
|
|
3620
|
+
borderStyle: "round",
|
|
3621
|
+
borderColor: isProcessing ? "gray" : "yellow",
|
|
3622
|
+
paddingX: 1,
|
|
3623
|
+
marginTop: 1,
|
|
3624
|
+
children: /* @__PURE__ */ jsxs(Box, { children: [
|
|
3625
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", bold: true, children: "\u276F " }),
|
|
3626
|
+
/* @__PURE__ */ jsx(
|
|
3627
|
+
TextInput,
|
|
3628
|
+
{
|
|
3629
|
+
value: input,
|
|
3630
|
+
onChange: setInput,
|
|
3631
|
+
onSubmit: handleSubmit,
|
|
3632
|
+
placeholder: isProcessing ? "Processing..." : "Ask anything or type / for commands"
|
|
3633
|
+
}
|
|
3634
|
+
)
|
|
3635
|
+
] })
|
|
3636
|
+
}
|
|
3637
|
+
),
|
|
3638
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [
|
|
3639
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\u21B5 Send \u2022 Esc interrupt \u2022 /help commands" }),
|
|
3640
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
3641
|
+
tokenUsage.sessionCost > 0 && /* @__PURE__ */ jsx(Text, { color: "cyan", children: formatCost(tokenUsage.sessionCost) }),
|
|
3642
|
+
tokenUsage.totalTokens > 0 && /* @__PURE__ */ jsxs(Text, { color: getTokenColor(tokenUsage.inputTokens), children: [
|
|
3643
|
+
tokenUsage.sessionCost > 0 ? " \u2022 " : "",
|
|
3644
|
+
"~",
|
|
3645
|
+
formatTokenCount(tokenUsage.inputTokens),
|
|
3646
|
+
" tokens",
|
|
3647
|
+
tokenUsage.inputTokens >= 8e4 && " (/compact)"
|
|
3648
|
+
] }),
|
|
3649
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
3650
|
+
tokenUsage.totalTokens > 0 ? " \u2022 " : "",
|
|
3651
|
+
isProcessing ? "\u23F3" : "\u2713",
|
|
3652
|
+
" Ready"
|
|
3653
|
+
] })
|
|
3654
|
+
] })
|
|
3655
|
+
] })
|
|
3656
|
+
] });
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
// src/index.ts
|
|
3660
|
+
var VERSION = "0.1.0";
|
|
3661
|
+
function cleanup() {
|
|
3662
|
+
if (processManager.hasRunning()) {
|
|
3663
|
+
const count = processManager.runningCount();
|
|
3664
|
+
console.log(chalk3.dim(`
|
|
3665
|
+
Stopping ${count} background process${count > 1 ? "es" : ""}...`));
|
|
3666
|
+
processManager.killAll();
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
process.on("SIGINT", () => {
|
|
3670
|
+
cleanup();
|
|
3671
|
+
process.exit(0);
|
|
3672
|
+
});
|
|
3673
|
+
process.on("SIGTERM", () => {
|
|
3674
|
+
cleanup();
|
|
3675
|
+
process.exit(0);
|
|
3676
|
+
});
|
|
3677
|
+
process.on("exit", () => {
|
|
3678
|
+
processManager.killAll();
|
|
3679
|
+
});
|
|
3680
|
+
var program = new Command();
|
|
3681
|
+
program.name("quantish").description("AI coding & trading agent for Polymarket").version(VERSION);
|
|
3682
|
+
program.command("init").description("Configure Quantish CLI with your API keys").action(async () => {
|
|
3683
|
+
await runSetup();
|
|
3684
|
+
});
|
|
3685
|
+
program.command("config").description("View or edit configuration").option("-s, --show", "Show current configuration").option("-c, --clear", "Clear all configuration").option("--path", "Show config file path").option("--export", "Export configuration as .env format").option("--show-keys", "Show full API keys (use with caution)").action(async (options) => {
|
|
3686
|
+
const config = getConfigManager();
|
|
3687
|
+
if (options.path) {
|
|
3688
|
+
console.log(config.getConfigPath());
|
|
3689
|
+
return;
|
|
3690
|
+
}
|
|
3691
|
+
if (options.clear) {
|
|
3692
|
+
config.clear();
|
|
3693
|
+
success("Configuration cleared.");
|
|
3694
|
+
return;
|
|
3695
|
+
}
|
|
3696
|
+
if (options.export) {
|
|
3697
|
+
const all2 = config.getAll();
|
|
3698
|
+
console.log();
|
|
3699
|
+
console.log(chalk3.bold.yellow("# Quantish CLI Configuration"));
|
|
3700
|
+
console.log(chalk3.dim("# Add these to your .env file for your custom agents"));
|
|
3701
|
+
console.log();
|
|
3702
|
+
if (all2.anthropicApiKey) {
|
|
3703
|
+
console.log(`ANTHROPIC_API_KEY=${all2.anthropicApiKey}`);
|
|
3704
|
+
}
|
|
3705
|
+
if (all2.quantishApiKey) {
|
|
3706
|
+
console.log(`QUANTISH_API_KEY=${all2.quantishApiKey}`);
|
|
3707
|
+
}
|
|
3708
|
+
console.log(`QUANTISH_MCP_URL=${all2.mcpServerUrl}`);
|
|
3709
|
+
console.log(`QUANTISH_MODEL=${all2.model || "claude-sonnet-4-5-20250929"}`);
|
|
3710
|
+
console.log();
|
|
3711
|
+
console.log(chalk3.dim("# Discovery MCP (public, read-only market data)"));
|
|
3712
|
+
console.log(`QUANTISH_DISCOVERY_URL=https://quantish.live/mcp`);
|
|
3713
|
+
console.log();
|
|
3714
|
+
console.log(chalk3.yellow("\u26A0\uFE0F Keep these keys secure! Do not commit to git."));
|
|
3715
|
+
console.log();
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
const all = config.getAll();
|
|
3719
|
+
console.log();
|
|
3720
|
+
console.log(chalk3.bold("Quantish Configuration"));
|
|
3721
|
+
printDivider();
|
|
3722
|
+
if (options.showKeys) {
|
|
3723
|
+
tableRow("Anthropic API Key", all.anthropicApiKey || chalk3.dim("Not set"));
|
|
3724
|
+
tableRow("Quantish API Key", all.quantishApiKey || chalk3.dim("Not set"));
|
|
3725
|
+
} else {
|
|
3726
|
+
tableRow("Anthropic API Key", all.anthropicApiKey ? `${all.anthropicApiKey.slice(0, 10)}...` : chalk3.dim("Not set"));
|
|
3727
|
+
tableRow("Quantish API Key", all.quantishApiKey ? `${all.quantishApiKey.slice(0, 12)}...` : chalk3.dim("Not set"));
|
|
3728
|
+
}
|
|
3729
|
+
tableRow("MCP Server URL", all.mcpServerUrl);
|
|
3730
|
+
tableRow("Model", all.model || "claude-sonnet-4-5-20250929");
|
|
3731
|
+
printDivider();
|
|
3732
|
+
console.log(chalk3.dim(`Config file: ${config.getConfigPath()}`));
|
|
3733
|
+
console.log();
|
|
3734
|
+
if (all.quantishApiKey) {
|
|
3735
|
+
console.log(chalk3.green("\u2713 Trading enabled") + chalk3.dim(" - Your wallet credentials are stored securely on the Quantish server."));
|
|
3736
|
+
console.log(chalk3.dim(' Use "quantish config --export" to export keys for your own agents.'));
|
|
3737
|
+
} else {
|
|
3738
|
+
console.log(chalk3.yellow("\u26A0 Trading not enabled") + chalk3.dim(' - Run "quantish init" to set up your wallet.'));
|
|
3739
|
+
}
|
|
3740
|
+
console.log();
|
|
3741
|
+
});
|
|
3742
|
+
program.command("tools").description("List available tools").option("-l, --local", "Show only local tools").option("-d, --discovery", "Show only Discovery MCP tools").option("-t, --trading", "Show only Trading MCP tools").action(async (options) => {
|
|
3743
|
+
console.log();
|
|
3744
|
+
const showAll = !options.local && !options.discovery && !options.trading;
|
|
3745
|
+
if (showAll || options.local) {
|
|
3746
|
+
console.log(chalk3.bold.blue("\u{1F4C1} Local Tools (coding)"));
|
|
3747
|
+
printDivider();
|
|
3748
|
+
for (const tool of localTools) {
|
|
3749
|
+
console.log(chalk3.cyan(` ${tool.name}`));
|
|
3750
|
+
const desc = tool.description || "";
|
|
3751
|
+
console.log(chalk3.dim(` ${desc.slice(0, 80)}${desc.length > 80 ? "..." : ""}`));
|
|
3752
|
+
}
|
|
3753
|
+
console.log();
|
|
3754
|
+
}
|
|
3755
|
+
if (showAll || options.discovery) {
|
|
3756
|
+
console.log(chalk3.bold.green("\u{1F50D} Discovery MCP Tools (market search)"));
|
|
3757
|
+
printDivider();
|
|
3758
|
+
try {
|
|
3759
|
+
const discoveryClient = createMCPClient(
|
|
3760
|
+
DISCOVERY_MCP_URL,
|
|
3761
|
+
DISCOVERY_MCP_PUBLIC_KEY,
|
|
3762
|
+
"discovery"
|
|
3763
|
+
);
|
|
3764
|
+
const discoveryTools = await discoveryClient.listTools();
|
|
3765
|
+
for (const tool of discoveryTools) {
|
|
3766
|
+
console.log(chalk3.green(` ${tool.name}`));
|
|
3767
|
+
const desc = tool.description || "";
|
|
3768
|
+
console.log(chalk3.dim(` ${desc.slice(0, 80)}${desc.length > 80 ? "..." : ""}`));
|
|
3769
|
+
}
|
|
3770
|
+
} catch (error2) {
|
|
3771
|
+
warn("Could not fetch Discovery tools.");
|
|
3772
|
+
}
|
|
3773
|
+
console.log();
|
|
3774
|
+
}
|
|
3775
|
+
if (showAll || options.trading) {
|
|
3776
|
+
const config = getConfigManager();
|
|
3777
|
+
if (config.isTradingEnabled()) {
|
|
3778
|
+
console.log(chalk3.bold.magenta("\u{1F4B0} Trading MCP Tools (wallet & orders)"));
|
|
3779
|
+
printDivider();
|
|
3780
|
+
try {
|
|
3781
|
+
const tradingClient = createMCPClient(
|
|
3782
|
+
config.getTradingMcpUrl(),
|
|
3783
|
+
config.getQuantishApiKey(),
|
|
3784
|
+
"trading"
|
|
3785
|
+
);
|
|
3786
|
+
const tradingTools = await tradingClient.listTools();
|
|
3787
|
+
for (const tool of tradingTools) {
|
|
3788
|
+
console.log(chalk3.magenta(` ${tool.name}`));
|
|
3789
|
+
const desc = tool.description || "";
|
|
3790
|
+
console.log(chalk3.dim(` ${desc.slice(0, 80)}${desc.length > 80 ? "..." : ""}`));
|
|
3791
|
+
}
|
|
3792
|
+
} catch (error2) {
|
|
3793
|
+
warn("Could not fetch Trading tools. Check your API key.");
|
|
3794
|
+
}
|
|
3795
|
+
console.log();
|
|
3796
|
+
} else {
|
|
3797
|
+
console.log(chalk3.dim('\u{1F4B0} Trading MCP: Not configured. Run "quantish init" to enable trading.'));
|
|
3798
|
+
console.log();
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
});
|
|
3802
|
+
program.command("chat").description("Start interactive chat mode").option("--no-mcp", "Disable MCP trading tools").option("--no-local", "Disable local coding tools").action(async (options) => {
|
|
3803
|
+
if (!await ensureConfigured()) {
|
|
3804
|
+
return;
|
|
3805
|
+
}
|
|
3806
|
+
await runInteractiveChat({
|
|
3807
|
+
enableMCP: options.mcp !== false,
|
|
3808
|
+
enableLocal: options.local !== false
|
|
3809
|
+
});
|
|
3810
|
+
});
|
|
3811
|
+
program.option("-p, --prompt <message>", "Send a one-shot prompt").option("-v, --verbose", "Show tool calls and details").option("--no-mcp", "Disable MCP trading tools").option("--no-local", "Disable local coding tools").action(async (options) => {
|
|
3812
|
+
if (options.prompt) {
|
|
3813
|
+
if (!await ensureConfigured()) {
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
await runOneShotPrompt(options.prompt, {
|
|
3817
|
+
verbose: options.verbose,
|
|
3818
|
+
enableMCP: options.mcp !== false,
|
|
3819
|
+
enableLocal: options.local !== false
|
|
3820
|
+
});
|
|
3821
|
+
return;
|
|
3822
|
+
}
|
|
3823
|
+
if (!process.stdin.isTTY) {
|
|
3824
|
+
const input = await readStdin();
|
|
3825
|
+
if (input) {
|
|
3826
|
+
if (!await ensureConfigured()) {
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
await runOneShotPrompt(input, {
|
|
3830
|
+
verbose: options.verbose,
|
|
3831
|
+
enableMCP: options.mcp !== false,
|
|
3832
|
+
enableLocal: options.local !== false
|
|
3833
|
+
});
|
|
3834
|
+
return;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
if (!await ensureConfigured()) {
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
await runInteractiveChat({
|
|
3841
|
+
enableMCP: options.mcp !== false,
|
|
3842
|
+
enableLocal: options.local !== false
|
|
3843
|
+
});
|
|
3844
|
+
});
|
|
3845
|
+
function createMCPManager(options) {
|
|
3846
|
+
if (options.enableMCP === false) {
|
|
3847
|
+
return void 0;
|
|
3848
|
+
}
|
|
3849
|
+
const config = getConfigManager();
|
|
3850
|
+
return createMCPClientManager(
|
|
3851
|
+
DISCOVERY_MCP_URL,
|
|
3852
|
+
DISCOVERY_MCP_PUBLIC_KEY,
|
|
3853
|
+
config.isTradingEnabled() ? config.getTradingMcpUrl() : void 0,
|
|
3854
|
+
config.getQuantishApiKey()
|
|
3855
|
+
);
|
|
3856
|
+
}
|
|
3857
|
+
async function runInteractiveChat(options = {}) {
|
|
3858
|
+
const config = getConfigManager();
|
|
3859
|
+
const mcpClientManager = createMCPManager(options);
|
|
3860
|
+
const agent = createAgent({
|
|
3861
|
+
anthropicApiKey: config.getAnthropicApiKey(),
|
|
3862
|
+
mcpClientManager,
|
|
3863
|
+
model: config.getModel(),
|
|
3864
|
+
enableLocalTools: options.enableLocal !== false,
|
|
3865
|
+
enableMCPTools: options.enableMCP !== false,
|
|
3866
|
+
workingDirectory: process.cwd()
|
|
3867
|
+
});
|
|
3868
|
+
const canUseInk = process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
|
|
3869
|
+
if (canUseInk) {
|
|
3870
|
+
printHeader();
|
|
3871
|
+
const { waitUntilExit } = render(
|
|
3872
|
+
React2.createElement(App, {
|
|
3873
|
+
agent,
|
|
3874
|
+
onExit: () => {
|
|
3875
|
+
console.log(chalk3.dim("Goodbye!"));
|
|
3876
|
+
}
|
|
3877
|
+
}),
|
|
3878
|
+
{
|
|
3879
|
+
exitOnCtrlC: false
|
|
3880
|
+
// We handle Ctrl+C ourselves
|
|
3881
|
+
}
|
|
3882
|
+
);
|
|
3883
|
+
await waitUntilExit();
|
|
3884
|
+
} else {
|
|
3885
|
+
await runReadlineChat(agent, mcpClientManager, options);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
async function runReadlineChat(agent, mcpClientManager, options) {
|
|
3889
|
+
const readline2 = await import("readline");
|
|
3890
|
+
printHeader();
|
|
3891
|
+
const config = getConfigManager();
|
|
3892
|
+
const capabilities = [];
|
|
3893
|
+
if (options.enableLocal !== false) capabilities.push("coding");
|
|
3894
|
+
if (options.enableMCP !== false) {
|
|
3895
|
+
capabilities.push("discovery");
|
|
3896
|
+
if (config.isTradingEnabled()) capabilities.push("trading");
|
|
3897
|
+
}
|
|
3898
|
+
console.log(chalk3.dim(`Capabilities: ${capabilities.join(", ")}`));
|
|
3899
|
+
console.log(chalk3.dim('Type "exit" to quit, "clear" to reset conversation, "tools" to list tools.'));
|
|
3900
|
+
console.log();
|
|
3901
|
+
const rl = readline2.createInterface({
|
|
3902
|
+
input: process.stdin,
|
|
3903
|
+
output: process.stdout,
|
|
3904
|
+
terminal: process.stdin.isTTY ?? false
|
|
3905
|
+
});
|
|
3906
|
+
const promptUser = () => {
|
|
3907
|
+
rl.question(chalk3.yellow("You: "), async (input) => {
|
|
3908
|
+
const trimmed = input.trim();
|
|
3909
|
+
if (!trimmed) {
|
|
3910
|
+
promptUser();
|
|
3911
|
+
return;
|
|
3912
|
+
}
|
|
3913
|
+
if (trimmed.toLowerCase() === "exit" || trimmed.toLowerCase() === "quit") {
|
|
3914
|
+
console.log(chalk3.dim("Goodbye!"));
|
|
3915
|
+
rl.close();
|
|
3916
|
+
return;
|
|
3917
|
+
}
|
|
3918
|
+
if (trimmed.toLowerCase() === "clear") {
|
|
3919
|
+
agent.clearHistory();
|
|
3920
|
+
console.log(chalk3.dim("Conversation cleared."));
|
|
3921
|
+
promptUser();
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
if (trimmed.toLowerCase() === "tools") {
|
|
3925
|
+
console.log(chalk3.dim(`
|
|
3926
|
+
Local tools: ${localTools.map((t) => t.name).join(", ")}`));
|
|
3927
|
+
if (mcpClientManager) {
|
|
3928
|
+
try {
|
|
3929
|
+
const mcpTools = await mcpClientManager.listAllTools();
|
|
3930
|
+
const discoveryTools = mcpTools.filter((t) => t.source === "discovery");
|
|
3931
|
+
const tradingTools = mcpTools.filter((t) => t.source === "trading");
|
|
3932
|
+
console.log(chalk3.dim(`Discovery tools (${discoveryTools.length}): ${discoveryTools.map((t) => t.name).join(", ")}`));
|
|
3933
|
+
if (tradingTools.length > 0) {
|
|
3934
|
+
console.log(chalk3.dim(`Trading tools (${tradingTools.length}): ${tradingTools.map((t) => t.name).join(", ")}`));
|
|
3935
|
+
}
|
|
3936
|
+
} catch {
|
|
3937
|
+
console.log(chalk3.dim("MCP tools: (error fetching)"));
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
console.log();
|
|
3941
|
+
promptUser();
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
const spin = spinner("Thinking...");
|
|
3945
|
+
try {
|
|
3946
|
+
spin.start();
|
|
3947
|
+
const result = await agent.run(trimmed);
|
|
3948
|
+
spin.stop();
|
|
3949
|
+
if (result.text) {
|
|
3950
|
+
assistant(result.text);
|
|
3951
|
+
} else if (result.toolCalls.length > 0) {
|
|
3952
|
+
console.log();
|
|
3953
|
+
const localCount = result.toolCalls.filter((t) => t.source === "local").length;
|
|
3954
|
+
const discoveryCount = result.toolCalls.filter((t) => t.source === "discovery").length;
|
|
3955
|
+
const tradingCount = result.toolCalls.filter((t) => t.source === "trading").length;
|
|
3956
|
+
const summary = [];
|
|
3957
|
+
if (localCount > 0) summary.push(`${localCount} local`);
|
|
3958
|
+
if (discoveryCount > 0) summary.push(`${discoveryCount} discovery`);
|
|
3959
|
+
if (tradingCount > 0) summary.push(`${tradingCount} trading`);
|
|
3960
|
+
console.log(chalk3.cyan("Done.") + chalk3.dim(` (${summary.join(", ")} tool calls)`));
|
|
3961
|
+
console.log();
|
|
3962
|
+
}
|
|
3963
|
+
} catch (error2) {
|
|
3964
|
+
spin.stop();
|
|
3965
|
+
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
3966
|
+
if (errorMsg.includes("credit balance is too low")) {
|
|
3967
|
+
error("Anthropic API credits exhausted. Please add credits at console.anthropic.com");
|
|
3968
|
+
} else if (errorMsg.includes("invalid_api_key") || errorMsg.includes("401")) {
|
|
3969
|
+
error('Invalid Anthropic API key. Run "quantish init" to reconfigure.');
|
|
3970
|
+
} else if (errorMsg.includes("rate_limit")) {
|
|
3971
|
+
error("Rate limited by Anthropic API. Please wait a moment and try again.");
|
|
3972
|
+
} else {
|
|
3973
|
+
error(`Error: ${errorMsg}`);
|
|
3974
|
+
}
|
|
3975
|
+
console.log();
|
|
3976
|
+
}
|
|
3977
|
+
promptUser();
|
|
3978
|
+
});
|
|
3979
|
+
};
|
|
3980
|
+
rl.on("close", () => {
|
|
3981
|
+
process.exit(0);
|
|
3982
|
+
});
|
|
3983
|
+
process.on("SIGINT", () => {
|
|
3984
|
+
console.log(chalk3.dim("\nGoodbye!"));
|
|
3985
|
+
rl.close();
|
|
3986
|
+
process.exit(0);
|
|
3987
|
+
});
|
|
3988
|
+
promptUser();
|
|
3989
|
+
}
|
|
3990
|
+
async function runOneShotPrompt(message, options = {}) {
|
|
3991
|
+
const config = getConfigManager();
|
|
3992
|
+
const mcpClientManager = createMCPManager(options);
|
|
3993
|
+
const agent = createAgent({
|
|
3994
|
+
anthropicApiKey: config.getAnthropicApiKey(),
|
|
3995
|
+
mcpClientManager,
|
|
3996
|
+
model: config.getModel(),
|
|
3997
|
+
enableLocalTools: options.enableLocal !== false,
|
|
3998
|
+
enableMCPTools: options.enableMCP !== false,
|
|
3999
|
+
workingDirectory: process.cwd(),
|
|
4000
|
+
onToolCall: options.verbose ? (name, args) => {
|
|
4001
|
+
toolCall(name, args);
|
|
4002
|
+
} : void 0
|
|
4003
|
+
});
|
|
4004
|
+
const spin = options.verbose ? null : spinner("Processing...");
|
|
4005
|
+
try {
|
|
4006
|
+
spin?.start();
|
|
4007
|
+
const result = await agent.run(message);
|
|
4008
|
+
spin?.stop();
|
|
4009
|
+
if (result.text) {
|
|
4010
|
+
console.log(result.text);
|
|
4011
|
+
}
|
|
4012
|
+
} catch (error2) {
|
|
4013
|
+
spin?.stop();
|
|
4014
|
+
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
4015
|
+
if (errorMsg.includes("credit balance is too low")) {
|
|
4016
|
+
error("Anthropic API credits exhausted. Please add credits at console.anthropic.com");
|
|
4017
|
+
} else if (errorMsg.includes("invalid_api_key") || errorMsg.includes("401")) {
|
|
4018
|
+
error('Invalid Anthropic API key. Run "quantish init" to reconfigure.');
|
|
4019
|
+
} else if (errorMsg.includes("rate_limit")) {
|
|
4020
|
+
error("Rate limited by Anthropic API. Please wait a moment and try again.");
|
|
4021
|
+
} else {
|
|
4022
|
+
error(`Error: ${errorMsg}`);
|
|
4023
|
+
}
|
|
4024
|
+
process.exit(1);
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
async function readStdin() {
|
|
4028
|
+
return new Promise((resolve2) => {
|
|
4029
|
+
let data = "";
|
|
4030
|
+
process.stdin.setEncoding("utf8");
|
|
4031
|
+
process.stdin.on("data", (chunk) => {
|
|
4032
|
+
data += chunk;
|
|
4033
|
+
});
|
|
4034
|
+
process.stdin.on("end", () => {
|
|
4035
|
+
resolve2(data.trim());
|
|
4036
|
+
});
|
|
4037
|
+
setTimeout(() => {
|
|
4038
|
+
resolve2(data.trim());
|
|
4039
|
+
}, 100);
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
program.parse();
|