moon-iq 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,1005 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command, Option } from "commander";
5
+
6
+ // src/auth.ts
7
+ import { execSync } from "child_process";
8
+ import * as crypto from "crypto";
9
+ import * as fs from "fs";
10
+ import * as http from "http";
11
+ import * as os from "os";
12
+ import * as path from "path";
13
+ function generateCodeVerifier() {
14
+ return crypto.randomBytes(32).toString("base64url");
15
+ }
16
+ function generateCodeChallenge(verifier) {
17
+ return crypto.createHash("sha256").update(verifier).digest("base64url");
18
+ }
19
+ var CONFIG_DIR = path.join(os.homedir(), ".mooniq");
20
+ var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
21
+ var CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
22
+ var CALLBACK_PORT = 3847;
23
+ var CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
24
+ var DEFAULT_CONFIG = {
25
+ baseUrl: "https://moon-iq.com",
26
+ clientId: "mooniq_zin3s5jz3olzkdfxpmbeaogv"
27
+ };
28
+ function ensureConfigDir() {
29
+ if (!fs.existsSync(CONFIG_DIR)) {
30
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
31
+ }
32
+ }
33
+ function getConfig() {
34
+ if (!fs.existsSync(CONFIG_PATH)) {
35
+ return null;
36
+ }
37
+ try {
38
+ const data = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
39
+ if (!data.baseUrl || !data.clientId) {
40
+ return null;
41
+ }
42
+ return data;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function getConfigOrDefault(baseUrl) {
48
+ const fromFile = getConfig();
49
+ if (fromFile) return fromFile;
50
+ const config = {
51
+ ...DEFAULT_CONFIG,
52
+ baseUrl: baseUrl ?? DEFAULT_CONFIG.baseUrl
53
+ };
54
+ saveConfig(config);
55
+ return config;
56
+ }
57
+ function getCredentials() {
58
+ if (!fs.existsSync(CREDENTIALS_PATH)) {
59
+ return null;
60
+ }
61
+ try {
62
+ const data = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf-8"));
63
+ if (!data.accessToken || !data.baseUrl) {
64
+ return null;
65
+ }
66
+ return data;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ function saveCredentials(creds) {
72
+ ensureConfigDir();
73
+ fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), "utf-8");
74
+ }
75
+ function saveConfig(config) {
76
+ ensureConfigDir();
77
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
78
+ }
79
+ function clearCredentials() {
80
+ if (fs.existsSync(CREDENTIALS_PATH)) {
81
+ fs.unlinkSync(CREDENTIALS_PATH);
82
+ }
83
+ }
84
+ function openBrowser(url) {
85
+ const platform = process.platform;
86
+ const command = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
87
+ execSync(command);
88
+ }
89
+ async function login(config) {
90
+ const state = crypto.randomUUID();
91
+ const usePkce = !config.clientSecret;
92
+ let codeVerifier;
93
+ if (usePkce) {
94
+ codeVerifier = generateCodeVerifier();
95
+ }
96
+ let resolveCallback;
97
+ const codePromise = new Promise((resolve) => {
98
+ resolveCallback = resolve;
99
+ });
100
+ let callbackSocket = null;
101
+ const server = http.createServer((req, res) => {
102
+ const url = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
103
+ if (url.pathname === "/callback") {
104
+ const code2 = url.searchParams.get("code");
105
+ const error = url.searchParams.get("error");
106
+ const errorDesc = url.searchParams.get("error_description");
107
+ if (error) {
108
+ res.writeHead(200, { "Content-Type": "text/html" });
109
+ res.end(
110
+ `<!DOCTYPE html><html><head><meta charset="utf-8"><title>OAuth Error</title></head><body style="font-family:system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:2rem;text-align:center;background:#fef2f2"><h1 style="color:#b91c1c;margin:0 0 1rem">OAuth Error</h1><p style="color:#991b1b;margin:0">${error}: ${errorDesc ?? "Unknown error"}</p></body></html>`
111
+ );
112
+ resolveCallback("");
113
+ setTimeout(() => server.close(), 2e3);
114
+ } else if (code2) {
115
+ callbackSocket = res.socket ?? req.socket;
116
+ res.writeHead(200, { "Content-Type": "text/html" });
117
+ res.end(
118
+ `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Success</title></head><body style="font-family:system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:2rem;text-align:center;background:#f0fdf4"><h1 style="color:#166534;margin:0 0 1rem;font-size:1.5rem">\u2713 Success!</h1><p style="color:#15803d;margin:0;font-size:1.1rem">You can close this page and return to the terminal.</p></body></html>`
119
+ );
120
+ resolveCallback(code2);
121
+ } else {
122
+ res.writeHead(200, { "Content-Type": "text/html" });
123
+ res.end(
124
+ `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Error</title></head><body style="font-family:system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:2rem;text-align:center;background:#fef2f2"><h1 style="color:#b91c1c;margin:0 0 1rem">Error</h1><p style="color:#991b1b;margin:0">No authorization code received.</p></body></html>`
125
+ );
126
+ resolveCallback("");
127
+ setTimeout(() => server.close(), 2e3);
128
+ }
129
+ } else {
130
+ res.writeHead(204);
131
+ res.end();
132
+ }
133
+ });
134
+ await new Promise((resolve, reject) => {
135
+ server.on("error", (err) => {
136
+ if (err.code === "EADDRINUSE") {
137
+ reject(
138
+ new Error(
139
+ `Port ${CALLBACK_PORT} is in use. Close the other process or run: lsof -i :${CALLBACK_PORT}`
140
+ )
141
+ );
142
+ } else {
143
+ reject(err);
144
+ }
145
+ });
146
+ server.listen(CALLBACK_PORT, "0.0.0.0", () => resolve());
147
+ });
148
+ console.log(
149
+ `Waiting for callback at http://localhost:${CALLBACK_PORT}/callback`
150
+ );
151
+ const authorizeParams = new URLSearchParams({
152
+ client_id: config.clientId,
153
+ redirect_uri: CALLBACK_URL,
154
+ response_type: "code",
155
+ scope: "profile email",
156
+ state
157
+ });
158
+ if (usePkce && codeVerifier) {
159
+ authorizeParams.set("code_challenge", generateCodeChallenge(codeVerifier));
160
+ authorizeParams.set("code_challenge_method", "S256");
161
+ }
162
+ const authorizeUrl = `${config.baseUrl}/api/oauth/authorize?${authorizeParams.toString()}`;
163
+ console.log("Opening browser for authorization...");
164
+ console.log("(Make sure you're logged in to Moon IQ in your browser)\n");
165
+ openBrowser(authorizeUrl);
166
+ const code = await codePromise;
167
+ if (!code) {
168
+ throw new Error("No authorization code received");
169
+ }
170
+ const tokenParams = {
171
+ grant_type: "authorization_code",
172
+ code,
173
+ client_id: config.clientId,
174
+ redirect_uri: CALLBACK_URL
175
+ };
176
+ if (config.clientSecret) {
177
+ tokenParams.client_secret = config.clientSecret;
178
+ }
179
+ if (usePkce && codeVerifier) {
180
+ tokenParams.code_verifier = codeVerifier;
181
+ }
182
+ const tokenResponse = await fetch(`${config.baseUrl}/api/oauth/token`, {
183
+ method: "POST",
184
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
185
+ body: new URLSearchParams(tokenParams)
186
+ });
187
+ if (!tokenResponse.ok) {
188
+ const err = await tokenResponse.json().catch(() => ({}));
189
+ const msg = err.error_description ?? err.error ?? tokenResponse.statusText;
190
+ throw new Error(`Token exchange failed: ${msg}`);
191
+ }
192
+ const tokens = await tokenResponse.json();
193
+ const expiresAt = Date.now() + (tokens.expires_in ?? 3600) * 1e3;
194
+ const creds = {
195
+ accessToken: tokens.access_token,
196
+ refreshToken: tokens.refresh_token ?? null,
197
+ expiresAt,
198
+ baseUrl: config.baseUrl
199
+ };
200
+ saveCredentials(creds);
201
+ server.close();
202
+ if (callbackSocket && "destroy" in callbackSocket) {
203
+ callbackSocket.destroy();
204
+ }
205
+ return creds;
206
+ }
207
+ async function refreshCredentials(creds, config) {
208
+ if (!creds.refreshToken) {
209
+ throw new Error("No refresh token available");
210
+ }
211
+ const refreshParams = {
212
+ grant_type: "refresh_token",
213
+ refresh_token: creds.refreshToken,
214
+ client_id: config.clientId
215
+ };
216
+ if (config.clientSecret) {
217
+ refreshParams.client_secret = config.clientSecret;
218
+ }
219
+ const tokenResponse = await fetch(`${config.baseUrl}/api/oauth/token`, {
220
+ method: "POST",
221
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
222
+ body: new URLSearchParams(refreshParams)
223
+ });
224
+ if (!tokenResponse.ok) {
225
+ throw new Error("Token refresh failed");
226
+ }
227
+ const tokens = await tokenResponse.json();
228
+ const expiresAt = Date.now() + (tokens.expires_in ?? 3600) * 1e3;
229
+ const newCreds = {
230
+ accessToken: tokens.access_token,
231
+ refreshToken: tokens.refresh_token ?? creds.refreshToken,
232
+ expiresAt,
233
+ baseUrl: config.baseUrl
234
+ };
235
+ saveCredentials(newCreds);
236
+ return newCreds;
237
+ }
238
+ var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
239
+ async function getValidToken() {
240
+ const creds = getCredentials();
241
+ if (!creds) return null;
242
+ const config = getConfig();
243
+ if (!config || config.baseUrl !== creds.baseUrl) return null;
244
+ if (Date.now() >= creds.expiresAt - TOKEN_EXPIRY_BUFFER_MS) {
245
+ if (creds.refreshToken) {
246
+ try {
247
+ const newCreds = await refreshCredentials(creds, config);
248
+ return newCreds.accessToken;
249
+ } catch {
250
+ return null;
251
+ }
252
+ }
253
+ return null;
254
+ }
255
+ return creds.accessToken;
256
+ }
257
+
258
+ // src/generated/client.ts
259
+ var TOOL_NAMES = [
260
+ "iron_account_create",
261
+ "iron_account_retrieve",
262
+ "iron_offramp_create",
263
+ "iron_offramp_initiate",
264
+ "iron_offramp_list",
265
+ "iron_offramp_retrieve",
266
+ "iron_onramp_create",
267
+ "iron_onramp_list",
268
+ "iron_onramp_retrieve",
269
+ "lending_account_retrieve",
270
+ "lending_deposit",
271
+ "lending_rate_retrieve",
272
+ "lending_recommend",
273
+ "lending_withdraw",
274
+ "moonit_buy",
275
+ "moonit_buy_recommend",
276
+ "moonit_sell",
277
+ "moonit_sell_recommend",
278
+ "moonit_token_list",
279
+ "moonpay_buy",
280
+ "rhythm_token_list",
281
+ "rhythm_token_trending_list",
282
+ "rhythm_trade_list",
283
+ "token_balance_list",
284
+ "token_balance_retrieve",
285
+ "token_buy",
286
+ "token_digest_retrieve",
287
+ "token_limit_buy",
288
+ "token_limit_buy_recommend",
289
+ "token_limit_order_list",
290
+ "token_limit_recommend",
291
+ "token_limit_sell",
292
+ "token_limit_sell_recommend",
293
+ "token_recurring_buy",
294
+ "token_recurring_order_list",
295
+ "token_recurring_recommend",
296
+ "token_recurring_sell",
297
+ "token_retrieve",
298
+ "token_search",
299
+ "token_sell",
300
+ "token_swap",
301
+ "token_swap_recommend",
302
+ "token_trending_list",
303
+ "user_retrieve",
304
+ "user_update"
305
+ ];
306
+ var TOOL_DEFINITIONS = [
307
+ {
308
+ "name": "iron_account_create",
309
+ "description": "Create a new Iron account for the user.",
310
+ "options": []
311
+ },
312
+ {
313
+ "name": "iron_account_retrieve",
314
+ "description": "Get the user's Iron account information.",
315
+ "options": []
316
+ },
317
+ {
318
+ "name": "iron_offramp_create",
319
+ "description": "Create a new offramp for the user to convert crypto to fiat",
320
+ "options": [
321
+ {
322
+ "name": "name",
323
+ "type": "string",
324
+ "description": "The name of the offramp",
325
+ "required": true
326
+ },
327
+ {
328
+ "name": "fiat",
329
+ "type": "string",
330
+ "description": "The fiat currency to convert to",
331
+ "required": true
332
+ },
333
+ {
334
+ "name": "stablecoin",
335
+ "type": "string",
336
+ "description": "The stablecoin token to convert from",
337
+ "required": true
338
+ }
339
+ ]
340
+ },
341
+ {
342
+ "name": "iron_offramp_initiate",
343
+ "description": "Initiate an offramp by sending stablecoin to the offramp's deposit account. The stablecoin will be converted to fiat and deposited into your registered bank account. This process is asynchronous and may take some time for funds to appear in your bank account.",
344
+ "options": []
345
+ },
346
+ {
347
+ "name": "iron_offramp_list",
348
+ "description": "Get all offramps for the user",
349
+ "options": []
350
+ },
351
+ {
352
+ "name": "iron_offramp_retrieve",
353
+ "description": "Get an offramp for the user",
354
+ "options": [
355
+ {
356
+ "name": "offrampId",
357
+ "type": "string",
358
+ "description": "The ID of the offramp to retrieve",
359
+ "required": true
360
+ }
361
+ ]
362
+ },
363
+ {
364
+ "name": "iron_onramp_create",
365
+ "description": "Create an onramp to convert fiat currency to a stablecoin",
366
+ "options": [
367
+ {
368
+ "name": "name",
369
+ "type": "string",
370
+ "description": "The name of the onramp",
371
+ "required": true
372
+ },
373
+ {
374
+ "name": "fiat",
375
+ "type": "string",
376
+ "description": "The fiat currency to convert from",
377
+ "required": true
378
+ },
379
+ {
380
+ "name": "stablecoin",
381
+ "type": "string",
382
+ "description": "The stablecoin token to convert to",
383
+ "required": true
384
+ }
385
+ ]
386
+ },
387
+ {
388
+ "name": "iron_onramp_list",
389
+ "description": "Get all onramps for the user",
390
+ "options": [
391
+ {
392
+ "name": "status",
393
+ "type": "string",
394
+ "description": "Status of the onramps to get",
395
+ "required": true
396
+ }
397
+ ]
398
+ },
399
+ {
400
+ "name": "iron_onramp_retrieve",
401
+ "description": "Get an onramp for the user",
402
+ "options": [
403
+ {
404
+ "name": "onrampId",
405
+ "type": "string",
406
+ "description": "The ID of the onramp to retrieve",
407
+ "required": true
408
+ }
409
+ ]
410
+ },
411
+ {
412
+ "name": "lending_account_retrieve",
413
+ "description": "Get account details for a lending account by wallet address, including balances, interest earned, and withdrawal limits",
414
+ "options": []
415
+ },
416
+ {
417
+ "name": "lending_deposit",
418
+ "description": "Deposit USDC into your lending account to earn interest.",
419
+ "options": []
420
+ },
421
+ {
422
+ "name": "lending_rate_retrieve",
423
+ "description": "Get current and historical interest rates for lending USDC",
424
+ "options": []
425
+ },
426
+ {
427
+ "name": "lending_recommend",
428
+ "description": "Analyze user's lending account and wallet balances to recommend either a deposit or withdraw action with appropriate parameters. Understands queries like 'should I deposit more?', 'recommend a withdrawal', or 'what should I do with my lending account?'.",
429
+ "options": [
430
+ {
431
+ "name": "query",
432
+ "type": "string",
433
+ "description": "Natural language query about lending recommendations. Examples: 'should I deposit more?', 'recommend a withdrawal', 'what should I do with my lending account?'.",
434
+ "required": true
435
+ }
436
+ ]
437
+ },
438
+ {
439
+ "name": "lending_withdraw",
440
+ "description": "Withdraw USDC from your lending account.",
441
+ "options": []
442
+ },
443
+ {
444
+ "name": "moonit_buy",
445
+ "description": "Buy tokens on Moonit platform using bonding curve. This tool converts USDC to SOL, then uses the SOL as collateral to buy Moonit tokens. It builds, signs, and executes the transaction. Note: This tool requires SOL for transaction fees.",
446
+ "options": []
447
+ },
448
+ {
449
+ "name": "moonit_buy_recommend",
450
+ "description": "Parse natural language queries into structured buy parameters for Moonit on Solana. Understands queries like 'buy 100 USDC worth of Moonit token', 'buy $50 worth of token ABC', or 'buy all my USDC worth of Moonit token XYZ'. Returns the recommended buy parameters ready to use with moonit_buy.",
451
+ "options": [
452
+ {
453
+ "name": "query",
454
+ "type": "string",
455
+ "description": "Natural language description of the desired buy transaction on Moonit. Examples: 'buy 100 USDC worth of Moonit token', 'buy $50 worth of token ABC', 'buy all my USDC worth of Moonit token XYZ'. The agent will parse token and amount from this query.",
456
+ "required": true
457
+ }
458
+ ]
459
+ },
460
+ {
461
+ "name": "moonit_sell",
462
+ "description": "Sell tokens on Moonit platform using bonding curve. This tool sells Moonit tokens to get SOL, then converts SOL to USDC. It builds, signs, and executes the transaction. Note: This tool requires SOL for transaction fees.",
463
+ "options": []
464
+ },
465
+ {
466
+ "name": "moonit_sell_recommend",
467
+ "description": "Parse natural language queries into structured sell parameters for Moonit on Solana. Understands queries like 'sell 100 SANTAMOON tokens', 'sell all my Moonit tokens', or 'sell 50% of my token ABC'. Returns the recommended sell parameters ready to use with moonit_sell.",
468
+ "options": [
469
+ {
470
+ "name": "query",
471
+ "type": "string",
472
+ "description": "Natural language description of the desired sell transaction on Moonit. Examples: 'sell 100 SANTAMOON tokens', 'sell all my Moonit tokens', 'sell 50% of my token ABC'. The agent will parse token and amount from this query.",
473
+ "required": true
474
+ }
475
+ ]
476
+ },
477
+ {
478
+ "name": "moonit_token_list",
479
+ "description": "Get the most popular Moonit tokens sorted by market capitalization. Moonit is a token launch platform by DEX Screener x Helio offering bonding curve mechanics, daily LP rewards, and graduation to Raydium/Meteora DEXs.",
480
+ "options": []
481
+ },
482
+ {
483
+ "name": "moonpay_buy",
484
+ "description": "Buy a token using Moonpay. Specify either from.amount (USD to spend) or to.amount (tokens to buy), not both.",
485
+ "options": [
486
+ {
487
+ "name": "from",
488
+ "type": "string",
489
+ "description": "Payment currency and amount to spend",
490
+ "required": true
491
+ },
492
+ {
493
+ "name": "to",
494
+ "type": "string",
495
+ "description": "Destination chain, currency, and token amount",
496
+ "required": true
497
+ }
498
+ ]
499
+ },
500
+ {
501
+ "name": "rhythm_token_list",
502
+ "description": "List tokens from Rhythm",
503
+ "options": []
504
+ },
505
+ {
506
+ "name": "rhythm_token_trending_list",
507
+ "description": "Get trending tokens from Rhythm",
508
+ "options": [
509
+ {
510
+ "name": "type",
511
+ "type": "string",
512
+ "description": "Type of trending tokens to get",
513
+ "required": true,
514
+ "choices": [
515
+ "holding_strong",
516
+ "survivor",
517
+ "legacies"
518
+ ]
519
+ }
520
+ ]
521
+ },
522
+ {
523
+ "name": "rhythm_trade_list",
524
+ "description": "Get whale trades from Rhythm discover",
525
+ "options": []
526
+ },
527
+ {
528
+ "name": "token_balance_list",
529
+ "description": "List all token balances held in a wallet, including amount owned, current value in USD, and token details. Shows the complete token portfolio.",
530
+ "options": [
531
+ {
532
+ "name": "wallet",
533
+ "type": "string",
534
+ "description": "Wallet address to check token balances for",
535
+ "required": true
536
+ },
537
+ {
538
+ "name": "chain",
539
+ "type": "string",
540
+ "description": "Blockchain to check balances on (e.g. 'solana', 'ethereum', 'base')",
541
+ "required": true
542
+ }
543
+ ]
544
+ },
545
+ {
546
+ "name": "token_balance_retrieve",
547
+ "description": "Get the balance of a specific token in a wallet, including the amount owned, current USD value, and token price. Returns zero balance if token is not held.",
548
+ "options": [
549
+ {
550
+ "name": "wallet",
551
+ "type": "string",
552
+ "description": "Wallet address to check the balance for",
553
+ "required": true
554
+ },
555
+ {
556
+ "name": "token",
557
+ "type": "string",
558
+ "description": "Token address to check the balance of",
559
+ "required": true
560
+ },
561
+ {
562
+ "name": "chain",
563
+ "type": "string",
564
+ "description": "Blockchain where the token and wallet exist",
565
+ "required": true
566
+ }
567
+ ]
568
+ },
569
+ {
570
+ "name": "token_buy",
571
+ "description": "Swap the specified amount of USDC for a token.",
572
+ "options": []
573
+ },
574
+ {
575
+ "name": "token_digest_retrieve",
576
+ "description": "Retrieve a token digest by token and chain.",
577
+ "options": [
578
+ {
579
+ "name": "token",
580
+ "type": "string",
581
+ "description": "Token address",
582
+ "required": true
583
+ },
584
+ {
585
+ "name": "chain",
586
+ "type": "string",
587
+ "description": "Blockchain network where the token exists",
588
+ "required": true
589
+ }
590
+ ]
591
+ },
592
+ {
593
+ "name": "token_limit_buy",
594
+ "description": "Create a limit buy order to swap USDC for a token at a specific price.",
595
+ "options": []
596
+ },
597
+ {
598
+ "name": "token_limit_buy_recommend",
599
+ "description": "Parse natural language queries into structured limit buy parameters. Understands queries like 'buy BONK at $0.00001', 'limit buy 100 USDC worth of SOL when price reaches $150', or 'buy BONK at $0.00001 with 50 USDC'. Returns the recommended limit buy parameters ready to use with token_limit_buy.",
600
+ "options": [
601
+ {
602
+ "name": "query",
603
+ "type": "string",
604
+ "description": "Natural language description of the desired limit buy order. Examples: 'buy BONK at $0.00001', 'limit buy 100 USDC worth of SOL when price reaches $150', 'buy BONK at $0.00001 with 50 USDC'. The agent will parse token, amount, and price from this query.",
605
+ "required": true
606
+ }
607
+ ]
608
+ },
609
+ {
610
+ "name": "token_limit_order_list",
611
+ "description": "Get limit orders for the authenticated user.",
612
+ "options": []
613
+ },
614
+ {
615
+ "name": "token_limit_recommend",
616
+ "description": "Parse natural language queries into structured limit order parameters (buy or sell). Understands queries like 'buy BONK at $0.00001', 'sell BONK at $0.00002', 'limit buy 100 USDC worth of SOL when price reaches $150', or 'sell all my BONK at $0.00002'. Automatically determines if the request is a buy or sell order and returns the appropriate parameters ready to use with token_limit_buy or token_limit_sell.",
617
+ "options": [
618
+ {
619
+ "name": "query",
620
+ "type": "string",
621
+ "description": "Natural language description of the desired limit order (buy or sell). Examples: 'buy BONK at $0.00001', 'sell BONK at $0.00002', 'limit buy 100 USDC worth of SOL when price reaches $150', 'sell all my BONK at $0.00002'. The agent will parse the order type (buy/sell), token, amount, and price from this query.",
622
+ "required": true
623
+ }
624
+ ]
625
+ },
626
+ {
627
+ "name": "token_limit_sell",
628
+ "description": "Create a limit sell order to swap a token for USDC at a specific price.",
629
+ "options": []
630
+ },
631
+ {
632
+ "name": "token_limit_sell_recommend",
633
+ "description": "Parse natural language queries into structured limit sell parameters. Understands queries like 'sell BONK at $0.00002', 'limit sell 100 SOL when price reaches $200', or 'sell all my BONK at $0.00002'. Returns the recommended limit sell parameters ready to use with token_limit_sell.",
634
+ "options": [
635
+ {
636
+ "name": "query",
637
+ "type": "string",
638
+ "description": "Natural language description of the desired limit sell order. Examples: 'sell BONK at $0.00002', 'limit sell 100 SOL when price reaches $200', 'sell all my BONK at $0.00002'. The agent will parse token, amount, and price from this query.",
639
+ "required": true
640
+ }
641
+ ]
642
+ },
643
+ {
644
+ "name": "token_recurring_buy",
645
+ "description": "Create a recurring buy order (DCA) to swap USDC for a token at regular intervals.",
646
+ "options": []
647
+ },
648
+ {
649
+ "name": "token_recurring_order_list",
650
+ "description": "Get recurring orders.",
651
+ "options": []
652
+ },
653
+ {
654
+ "name": "token_recurring_recommend",
655
+ "description": "Parse natural language queries into structured recurring order (DCA) parameters (buy or sell). Understands queries like 'buy BONK daily with 50 USDC for 10 days', 'sell BONK daily, 100 tokens per order for 10 days', 'DCA 100 USDC into SOL weekly for a month', or 'recurring sell SOL every day, 0.5 SOL, 20 orders'. Automatically determines if the request is a buy or sell order and returns the appropriate parameters ready to use with token_recurring_buy or token_recurring_sell.",
656
+ "options": [
657
+ {
658
+ "name": "query",
659
+ "type": "string",
660
+ "description": "Natural language description of the desired recurring order (buy or sell). Examples: 'buy BONK daily with 50 USDC for 10 days', 'sell BONK daily, 100 tokens per order for 10 days', 'DCA 100 USDC into SOL weekly for a month', 'recurring sell SOL every day, 0.5 SOL, 20 orders'. The agent will parse the order type (buy/sell), token, amount, interval, and numberOfOrders from this query.",
661
+ "required": true
662
+ }
663
+ ]
664
+ },
665
+ {
666
+ "name": "token_recurring_sell",
667
+ "description": "Create a recurring sell order (DCA) to swap a token for USDC at regular intervals.",
668
+ "options": []
669
+ },
670
+ {
671
+ "name": "token_retrieve",
672
+ "description": "Get detailed token information including market data, price changes, volume, trades, and holder statistics by token address",
673
+ "options": [
674
+ {
675
+ "name": "token",
676
+ "type": "string",
677
+ "description": "Address of the token to retrieve",
678
+ "required": true
679
+ },
680
+ {
681
+ "name": "chain",
682
+ "type": "string",
683
+ "description": "Blockchain network where the token exists",
684
+ "required": true
685
+ }
686
+ ]
687
+ },
688
+ {
689
+ "name": "token_search",
690
+ "description": "Search for tokens by name, symbol, or address. Returns matching tokens with their market data including price, volume, and liquidity.",
691
+ "options": [
692
+ {
693
+ "name": "query",
694
+ "type": "string",
695
+ "description": "Search term - can be token name (e.g. 'Bitcoin'), symbol (e.g. 'BTC'), or partial match",
696
+ "required": true
697
+ },
698
+ {
699
+ "name": "chain",
700
+ "type": "string",
701
+ "description": "Blockchain to search on (e.g. 'solana', 'ethereum', 'base')",
702
+ "required": true
703
+ },
704
+ {
705
+ "name": "limit",
706
+ "type": "number",
707
+ "description": "Maximum number of results to return (optional, defaults to 5)",
708
+ "required": false
709
+ }
710
+ ]
711
+ },
712
+ {
713
+ "name": "token_sell",
714
+ "description": "Swap the specified amount of tokens for USDC.",
715
+ "options": []
716
+ },
717
+ {
718
+ "name": "token_swap",
719
+ "description": "Swap the specified amount of input token for output token.",
720
+ "options": []
721
+ },
722
+ {
723
+ "name": "token_swap_recommend",
724
+ "description": "Parse natural language queries into structured swap parameters for swapping tokens on Solana. Understands queries like 'swap 100 USDC for SOL', 'swap all my USDC for BONK', or '50 SOL to USDC'. Returns the recommended swap parameters ready to use with token_swap.",
725
+ "options": [
726
+ {
727
+ "name": "query",
728
+ "type": "string",
729
+ "description": "Natural language description of the desired swap on Solana. Examples: 'swap 100 USDC for SOL', 'swap all my USDC for BONK', '50 SOL to USDC', 'convert my SOL to USDC'. The agent will parse tokens and amounts from this query.",
730
+ "required": true
731
+ }
732
+ ]
733
+ },
734
+ {
735
+ "name": "token_trending_list",
736
+ "description": "Get currently trending tokens on a blockchain, sorted by popularity rank, trading volume, or liquidity. Perfect for discovering what's hot right now.",
737
+ "options": []
738
+ },
739
+ {
740
+ "name": "user_retrieve",
741
+ "description": "Retrieve the currently authenticated user from the session context.",
742
+ "options": []
743
+ },
744
+ {
745
+ "name": "user_update",
746
+ "description": "Update user fields in the database. This is a PUT-style update requiring all updatable fields (developer, firstName, lastName, username) to be sent. You must first call user_retrieve to get the current user state, then update with all four fields.",
747
+ "options": [
748
+ {
749
+ "name": "developer",
750
+ "type": "boolean",
751
+ "description": "Whether the user is a developer",
752
+ "required": true
753
+ },
754
+ {
755
+ "name": "firstName",
756
+ "type": "string",
757
+ "description": "The user's first name, or null to clear it",
758
+ "required": false
759
+ },
760
+ {
761
+ "name": "lastName",
762
+ "type": "string",
763
+ "description": "The user's last name, or null to clear it",
764
+ "required": false
765
+ },
766
+ {
767
+ "name": "username",
768
+ "type": "string",
769
+ "description": "The user's username (3-20 lowercase letters, numbers, and underscores, must start with a letter), or null to clear it",
770
+ "required": false
771
+ }
772
+ ]
773
+ }
774
+ ];
775
+
776
+ // src/client.ts
777
+ async function callToolWithToken(baseUrl, token, toolName, params) {
778
+ const res = await fetch(`${baseUrl}/api/tools/${toolName}`, {
779
+ method: "POST",
780
+ headers: {
781
+ "Content-Type": "application/json",
782
+ Authorization: `Bearer ${token}`
783
+ },
784
+ body: JSON.stringify(params)
785
+ });
786
+ const data = await res.json();
787
+ return { data, status: res.status };
788
+ }
789
+ async function callToolWithAuth(baseUrl, toolName, params) {
790
+ let token = await getValidToken();
791
+ if (!token) {
792
+ throw new Error(
793
+ "Not logged in. Run `mooniq login` first and ensure ~/.mooniq/config.json has baseUrl and clientId."
794
+ );
795
+ }
796
+ let result = await callToolWithToken(baseUrl, token, toolName, params);
797
+ if (result.status === 401) {
798
+ const creds = getCredentials();
799
+ const config = getConfig();
800
+ if (creds?.refreshToken && config && config.baseUrl === creds.baseUrl) {
801
+ try {
802
+ const newCreds = await refreshCredentials(creds, config);
803
+ result = await callToolWithToken(
804
+ baseUrl,
805
+ newCreds.accessToken,
806
+ toolName,
807
+ params
808
+ );
809
+ } catch {
810
+ }
811
+ }
812
+ }
813
+ if (result.status < 200 || result.status >= 300) {
814
+ const err = result.data;
815
+ throw new Error(err?.error ?? "Tool call failed");
816
+ }
817
+ return result.data;
818
+ }
819
+
820
+ // src/index.ts
821
+ var DEFAULT_BASE_URL = "https://moon-iq.com";
822
+ var program = new Command();
823
+ program.name("mooniq").description("Moon IQ CLI").version("0.1.0");
824
+ program.command("login").description("Log in to Moon IQ via OAuth").option(
825
+ "-b, --base-url <url>",
826
+ "Base URL (default: https://moon-iq.com or from ~/.mooniq/config.json)"
827
+ ).action(async (options) => {
828
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
829
+ const config = getConfigOrDefault(baseUrl);
830
+ try {
831
+ await login({ ...config, baseUrl });
832
+ console.log("Logged in successfully.");
833
+ } catch (error) {
834
+ console.error("Login failed:", error.message);
835
+ process.exit(1);
836
+ }
837
+ });
838
+ program.command("logout").description("Log out and clear stored credentials").action(() => {
839
+ clearCredentials();
840
+ console.log("Logged out.");
841
+ });
842
+ program.command("whoami").description("Show current user info").action(async () => {
843
+ let token = await getValidToken();
844
+ if (!token) {
845
+ console.error("Not logged in. Run `mooniq login` first.");
846
+ process.exit(1);
847
+ }
848
+ const creds = getCredentials();
849
+ const config = getConfig();
850
+ const baseUrl = config?.baseUrl ?? creds?.baseUrl ?? DEFAULT_BASE_URL;
851
+ let res = await fetch(`${baseUrl}/api/oauth/userinfo`, {
852
+ headers: { Authorization: `Bearer ${token}` }
853
+ });
854
+ if (res.status === 401 && creds?.refreshToken && config && config.baseUrl === creds.baseUrl) {
855
+ try {
856
+ const newCreds = await refreshCredentials(creds, config);
857
+ res = await fetch(`${baseUrl}/api/oauth/userinfo`, {
858
+ headers: { Authorization: `Bearer ${newCreds.accessToken}` }
859
+ });
860
+ } catch {
861
+ }
862
+ }
863
+ if (!res.ok) {
864
+ console.error("Failed to fetch user info:", res.status);
865
+ process.exit(1);
866
+ }
867
+ const userinfo = await res.json();
868
+ console.log(JSON.stringify(userinfo, null, 2));
869
+ });
870
+ program.command("run <tool_name>").description("Run a tool by name. Use --input for JSON params.").option(
871
+ "-i, --input <json>",
872
+ "Input parameters as JSON (omit for empty params)"
873
+ ).option(
874
+ "-b, --base-url <url>",
875
+ "Base URL (default: from credentials or https://moon-iq.com)"
876
+ ).addHelpText(
877
+ "after",
878
+ `
879
+ Examples:
880
+ mooniq run user_retrieve # no params
881
+ mooniq run token_search --input '{"query":"SOL","chain":"solana"}' # with params
882
+ `
883
+ ).action(async (toolName, options) => {
884
+ const toolNames = TOOL_NAMES;
885
+ if (!toolNames.includes(toolName)) {
886
+ console.error(`Unknown tool: ${toolName}`);
887
+ console.error(`Available tools: ${toolNames.slice(0, 10).join(", ")}...`);
888
+ process.exit(1);
889
+ }
890
+ let params = {};
891
+ if (options.input) {
892
+ try {
893
+ params = JSON.parse(options.input);
894
+ } catch {
895
+ console.error("Invalid JSON in --input");
896
+ process.exit(1);
897
+ }
898
+ }
899
+ const creds = getCredentials();
900
+ const config = getConfig();
901
+ const baseUrl = options.baseUrl ?? config?.baseUrl ?? creds?.baseUrl ?? DEFAULT_BASE_URL;
902
+ try {
903
+ const result = await callToolWithAuth(baseUrl, toolName, params);
904
+ console.log(JSON.stringify(result, null, 2));
905
+ } catch (error) {
906
+ console.error("Tool call failed:", error.message);
907
+ process.exit(1);
908
+ }
909
+ });
910
+ program.command("tools").description("List available tools").action(() => {
911
+ TOOL_NAMES.forEach((name) => console.log(name));
912
+ });
913
+ function buildToolCommands(program2) {
914
+ const root = { children: /* @__PURE__ */ new Map() };
915
+ for (const tool of TOOL_DEFINITIONS) {
916
+ const parts = tool.name.split("_");
917
+ let current = root;
918
+ for (let i = 0; i < parts.length; i++) {
919
+ const part = parts[i];
920
+ if (!current.children.has(part)) {
921
+ current.children.set(part, { children: /* @__PURE__ */ new Map() });
922
+ }
923
+ current = current.children.get(part);
924
+ if (i === parts.length - 1) {
925
+ current.tool = tool;
926
+ }
927
+ }
928
+ }
929
+ function createCommands(node, parent, path2) {
930
+ for (const [name, child] of node.children) {
931
+ const currentPath = [...path2, name];
932
+ if (child.tool) {
933
+ const cmd = parent.command(name).description(child.tool.description || `Run ${child.tool.name}`);
934
+ for (const opt of child.tool.options) {
935
+ const optionFlag = createOptionFlag(opt);
936
+ const option = new Option(optionFlag, opt.description);
937
+ if (opt.choices) {
938
+ option.choices(opt.choices);
939
+ }
940
+ if (opt.required) {
941
+ cmd.addOption(option);
942
+ } else {
943
+ cmd.addOption(option);
944
+ }
945
+ }
946
+ cmd.option(
947
+ "-b, --base-url <url>",
948
+ "Base URL (default: from credentials or https://moon-iq.com)"
949
+ );
950
+ const toolName = child.tool.name;
951
+ cmd.action(async (options) => {
952
+ const params = buildParams(child.tool.options, options);
953
+ const creds = getCredentials();
954
+ const config = getConfig();
955
+ const baseUrl = options.baseUrl ?? config?.baseUrl ?? creds?.baseUrl ?? DEFAULT_BASE_URL;
956
+ try {
957
+ const result = await callToolWithAuth(baseUrl, toolName, params);
958
+ console.log(JSON.stringify(result, null, 2));
959
+ } catch (error) {
960
+ console.error("Tool call failed:", error.message);
961
+ process.exit(1);
962
+ }
963
+ });
964
+ if (child.children.size > 0) {
965
+ createCommands(child, cmd, currentPath);
966
+ }
967
+ } else if (child.children.size > 0) {
968
+ const cmd = parent.command(name).description(`${name} commands`);
969
+ createCommands(child, cmd, currentPath);
970
+ }
971
+ }
972
+ }
973
+ createCommands(root, program2, []);
974
+ }
975
+ function createOptionFlag(opt) {
976
+ const kebabName = opt.name.replace(/([A-Z])/g, "-$1").toLowerCase();
977
+ if (opt.type === "boolean") {
978
+ return `--${kebabName}`;
979
+ }
980
+ const valuePlaceholder = `<${opt.name}>`;
981
+ return `--${kebabName} ${valuePlaceholder}`;
982
+ }
983
+ function buildParams(toolOptions, cmdOptions) {
984
+ const params = {};
985
+ for (const opt of toolOptions) {
986
+ const kebabName = opt.name.replace(/([A-Z])/g, "-$1").toLowerCase();
987
+ const camelName = kebabName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
988
+ const value = cmdOptions[camelName] ?? cmdOptions[opt.name];
989
+ if (value !== void 0) {
990
+ if (opt.type === "number" && typeof value === "string") {
991
+ params[opt.name] = parseFloat(value);
992
+ } else if (opt.type === "boolean") {
993
+ params[opt.name] = value === true || value === "true";
994
+ } else {
995
+ params[opt.name] = value;
996
+ }
997
+ } else if (!opt.required) {
998
+ params[opt.name] = null;
999
+ }
1000
+ }
1001
+ return params;
1002
+ }
1003
+ buildToolCommands(program);
1004
+ program.parse();
1005
+ //# sourceMappingURL=index.js.map