@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/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();