@vizzor/cli 0.10.0 → 0.10.5

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 CHANGED
@@ -62,13 +62,13 @@ var init_schema = __esm({
62
62
  api: z.object({
63
63
  port: z.number().default(3e3),
64
64
  host: z.string().default("0.0.0.0"),
65
- enableAuth: z.boolean().default(false),
66
- corsOrigin: z.string().default("*")
65
+ enableAuth: z.boolean().default(true),
66
+ corsOrigin: z.string().default("http://localhost:3000")
67
67
  }).default(() => ({
68
68
  port: 3e3,
69
69
  host: "0.0.0.0",
70
- enableAuth: false,
71
- corsOrigin: "*"
70
+ enableAuth: true,
71
+ corsOrigin: "http://localhost:3000"
72
72
  })),
73
73
  n8n: z.object({
74
74
  enabled: z.boolean().default(false),
@@ -82,7 +82,7 @@ var init_schema = __esm({
82
82
  });
83
83
 
84
84
  // src/config/loader.ts
85
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
85
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
86
86
  import { join } from "path";
87
87
  import { homedir } from "os";
88
88
  import yaml from "yaml";
@@ -209,7 +209,8 @@ function saveConfigValue(key, value) {
209
209
  raw[key] = value;
210
210
  }
211
211
  vizzorConfigSchema.parse(raw);
212
- writeFileSync(configPath, yaml.stringify(raw), "utf-8");
212
+ writeFileSync(configPath, yaml.stringify(raw), { encoding: "utf-8", mode: 384 });
213
+ chmodSync(configPath, 384);
213
214
  cachedConfig = null;
214
215
  loadConfig();
215
216
  }
@@ -250,6 +251,20 @@ var init_loader = __esm({
250
251
  }
251
252
  });
252
253
 
254
+ // src/utils/validate.ts
255
+ function assertValidAddress(address) {
256
+ if (!EVM_ADDRESS_RE.test(address)) {
257
+ throw new Error(`Invalid address format: expected 0x followed by 40 hex characters`);
258
+ }
259
+ }
260
+ var EVM_ADDRESS_RE;
261
+ var init_validate = __esm({
262
+ "src/utils/validate.ts"() {
263
+ "use strict";
264
+ EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
265
+ }
266
+ });
267
+
253
268
  // src/chains/evm/abi/erc20.ts
254
269
  var erc20Abi;
255
270
  var init_erc20 = __esm({
@@ -600,6 +615,7 @@ var CHAIN_MAP, EvmAdapter;
600
615
  var init_adapter = __esm({
601
616
  "src/chains/evm/adapter.ts"() {
602
617
  "use strict";
618
+ init_validate();
603
619
  init_erc20();
604
620
  init_etherscan();
605
621
  init_constants();
@@ -651,18 +667,23 @@ var init_adapter = __esm({
651
667
  isConnected() {
652
668
  return this.client !== null;
653
669
  }
670
+ /** Validate and cast to viem Address. */
671
+ toAddress(address) {
672
+ assertValidAddress(address);
673
+ return this.toAddress(address);
674
+ }
654
675
  // ── Balances ────────────────────────────────────────────────────────────
655
676
  async getBalance(address) {
656
677
  const client2 = this.requireClient();
657
- return client2.getBalance({ address });
678
+ return client2.getBalance({ address: this.toAddress(address) });
658
679
  }
659
680
  async getTokenBalance(address, tokenAddress) {
660
681
  const client2 = this.requireClient();
661
682
  const balance = await client2.readContract({
662
- address: tokenAddress,
683
+ address: this.toAddress(tokenAddress),
663
684
  abi: erc20Abi,
664
685
  functionName: "balanceOf",
665
- args: [address]
686
+ args: [this.toAddress(address)]
666
687
  });
667
688
  return balance;
668
689
  }
@@ -687,13 +708,13 @@ var init_adapter = __esm({
687
708
  // ── Contracts ───────────────────────────────────────────────────────────
688
709
  async getContractCode(address) {
689
710
  const client2 = this.requireClient();
690
- const code = await client2.getCode({ address });
711
+ const code = await client2.getCode({ address: this.toAddress(address) });
691
712
  return code ?? "0x";
692
713
  }
693
714
  async readContract(address, abi, functionName, args2) {
694
715
  const client2 = this.requireClient();
695
716
  return client2.readContract({
696
- address,
717
+ address: this.toAddress(address),
697
718
  abi,
698
719
  functionName,
699
720
  args: args2
@@ -702,7 +723,7 @@ var init_adapter = __esm({
702
723
  async getContractEvents(address, abi, eventName, options) {
703
724
  const client2 = this.requireClient();
704
725
  const logs = await client2.getContractEvents({
705
- address,
726
+ address: this.toAddress(address),
706
727
  abi,
707
728
  eventName,
708
729
  fromBlock: options?.fromBlock,
@@ -720,7 +741,7 @@ var init_adapter = __esm({
720
741
  // ── Tokens ──────────────────────────────────────────────────────────────
721
742
  async getTokenInfo(address) {
722
743
  const client2 = this.requireClient();
723
- const tokenAddr = address;
744
+ const tokenAddr = this.toAddress(address);
724
745
  const [name, symbol, decimals, totalSupply] = await Promise.all([
725
746
  client2.readContract({
726
747
  address: tokenAddr,
@@ -1342,6 +1363,7 @@ __export(scan_exports, {
1342
1363
  import chalk from "chalk";
1343
1364
  import ora from "ora";
1344
1365
  async function handleScan(project, options) {
1366
+ assertValidAddress(project);
1345
1367
  const spinner = ora(`Scanning ${project} on ${options.chain}...`).start();
1346
1368
  try {
1347
1369
  const adapter = getAdapter(options.chain);
@@ -1396,6 +1418,7 @@ var init_scan = __esm({
1396
1418
  init_loader();
1397
1419
  init_project_analyzer();
1398
1420
  init_risk_scorer();
1421
+ init_validate();
1399
1422
  }
1400
1423
  });
1401
1424
 
@@ -1718,7 +1741,7 @@ async function fetchJson2(url) {
1718
1741
  }
1719
1742
  async function fetchTickerPrice(symbol) {
1720
1743
  const pair = resolvePair(symbol);
1721
- const data = await fetchJson2(`${BASE_URL3}/ticker/24hr?symbol=${pair}`);
1744
+ const data = await fetchJson2(`${BASE_URL3}/ticker/24hr?symbol=${encodeURIComponent(pair)}`);
1722
1745
  return {
1723
1746
  symbol: symbol.toUpperCase(),
1724
1747
  price: parseFloat(data.lastPrice),
@@ -1728,7 +1751,7 @@ async function fetchTickerPrice(symbol) {
1728
1751
  async function fetchKlines(symbol, interval, limit = 100) {
1729
1752
  const pair = resolvePair(symbol);
1730
1753
  const data = await fetchJson2(
1731
- `${BASE_URL3}/klines?symbol=${pair}&interval=${interval}&limit=${limit}`
1754
+ `${BASE_URL3}/klines?symbol=${encodeURIComponent(pair)}&interval=${interval}&limit=${limit}`
1732
1755
  );
1733
1756
  return data.map((k) => ({
1734
1757
  openTime: Number(k[0]),
@@ -1744,7 +1767,7 @@ async function fetchKlines(symbol, interval, limit = 100) {
1744
1767
  }
1745
1768
  async function fetchFundingRate(symbol) {
1746
1769
  const pair = resolvePair(symbol);
1747
- const data = await fetchJson2(`${FUTURES_URL}/premiumIndex?symbol=${pair}`);
1770
+ const data = await fetchJson2(`${FUTURES_URL}/premiumIndex?symbol=${encodeURIComponent(pair)}`);
1748
1771
  const item = data[0];
1749
1772
  if (!item) throw new Error(`No funding data for ${pair}`);
1750
1773
  return {
@@ -1758,9 +1781,11 @@ async function fetchOpenInterest(symbol) {
1758
1781
  const pair = resolvePair(symbol);
1759
1782
  const [oiData, tickerData] = await Promise.all([
1760
1783
  fetchJson2(
1761
- `${FUTURES_URL}/openInterest?symbol=${pair}`
1784
+ `${FUTURES_URL}/openInterest?symbol=${encodeURIComponent(pair)}`
1762
1785
  ),
1763
- fetchJson2(`${FUTURES_URL}/ticker/price?symbol=${pair}`)
1786
+ fetchJson2(
1787
+ `${FUTURES_URL}/ticker/price?symbol=${encodeURIComponent(pair)}`
1788
+ )
1764
1789
  ]);
1765
1790
  const oi = parseFloat(oiData.openInterest);
1766
1791
  const price = parseFloat(tickerData.lastPrice);
@@ -1790,7 +1815,7 @@ async function fetchTopGainersLosers(limit = 10) {
1790
1815
  async function isValidSymbol(symbol) {
1791
1816
  try {
1792
1817
  const pair = resolvePair(symbol);
1793
- await fetchJson2(`${BASE_URL3}/ticker/price?symbol=${pair}`);
1818
+ await fetchJson2(`${BASE_URL3}/ticker/price?symbol=${encodeURIComponent(pair)}`);
1794
1819
  return true;
1795
1820
  } catch {
1796
1821
  return false;
@@ -2811,6 +2836,7 @@ __export(track_exports, {
2811
2836
  import chalk3 from "chalk";
2812
2837
  import ora3 from "ora";
2813
2838
  async function handleTrack(wallet, options) {
2839
+ assertValidAddress(wallet);
2814
2840
  const spinner = ora3(`Analyzing wallet ${wallet.slice(0, 10)}... on ${options.chain}`).start();
2815
2841
  try {
2816
2842
  const adapter = getAdapter(options.chain);
@@ -2871,6 +2897,7 @@ var init_track = __esm({
2871
2897
  init_registry();
2872
2898
  init_loader();
2873
2899
  init_wallet_analyzer();
2900
+ init_validate();
2874
2901
  }
2875
2902
  });
2876
2903
 
@@ -3163,6 +3190,7 @@ __export(audit_exports, {
3163
3190
  import chalk5 from "chalk";
3164
3191
  import ora5 from "ora";
3165
3192
  async function handleAudit(contract, options) {
3193
+ assertValidAddress(contract);
3166
3194
  const spinner = ora5(`Auditing contract ${contract.slice(0, 10)}... on ${options.chain}`).start();
3167
3195
  try {
3168
3196
  const adapter = getAdapter(options.chain);
@@ -3240,6 +3268,7 @@ var init_audit = __esm({
3240
3268
  init_registry();
3241
3269
  init_loader();
3242
3270
  init_contract_auditor();
3271
+ init_validate();
3243
3272
  }
3244
3273
  });
3245
3274
 
@@ -4292,7 +4321,9 @@ async function fetchJson6(url) {
4292
4321
  }
4293
4322
  async function checkTokenSecurity(contractAddress, chain) {
4294
4323
  const chainId = resolveChainId(chain);
4295
- const data = await fetchJson6(`${BASE_URL6}/token_security/${chainId}?contract_addresses=${contractAddress.toLowerCase()}`);
4324
+ const data = await fetchJson6(
4325
+ `${BASE_URL6}/token_security/${encodeURIComponent(chainId)}?contract_addresses=${encodeURIComponent(contractAddress.toLowerCase())}`
4326
+ );
4296
4327
  if (data.code !== 1) return null;
4297
4328
  const addr = contractAddress.toLowerCase();
4298
4329
  const raw = data.result[addr];
@@ -4347,7 +4378,9 @@ async function checkTokenSecurity(contractAddress, chain) {
4347
4378
  }
4348
4379
  async function checkAddressSecurity(address, chain) {
4349
4380
  const chainId = resolveChainId(chain);
4350
- const data = await fetchJson6(`${BASE_URL6}/address_security/${chainId}?address=${address.toLowerCase()}`);
4381
+ const data = await fetchJson6(
4382
+ `${BASE_URL6}/address_security/${encodeURIComponent(chainId)}?address=${encodeURIComponent(address.toLowerCase())}`
4383
+ );
4351
4384
  if (data.code !== 1) return null;
4352
4385
  const raw = data.result;
4353
4386
  const toBool = (v) => v === "1" || v === 1 || v === true;
@@ -4379,6 +4412,77 @@ var init_goplus = __esm({
4379
4412
  }
4380
4413
  });
4381
4414
 
4415
+ // src/ai/sanitize.ts
4416
+ function sanitizeExternalData(text, maxLen = LIMITS.generic) {
4417
+ let cleaned = text;
4418
+ for (const pattern of INJECTION_PATTERNS) {
4419
+ cleaned = cleaned.replace(pattern, "[FILTERED]");
4420
+ }
4421
+ cleaned = cleaned.replace(/^#{1,6}\s+/gm, "");
4422
+ cleaned = cleaned.replace(/```[\s\S]*?```/g, "[CODE]");
4423
+ if (cleaned.length > maxLen) {
4424
+ cleaned = cleaned.slice(0, maxLen) + "...";
4425
+ }
4426
+ return cleaned;
4427
+ }
4428
+ function sanitizeTokenName(name) {
4429
+ return sanitizeExternalData(name, LIMITS.tokenName);
4430
+ }
4431
+ function sanitizeHeadline(headline) {
4432
+ return sanitizeExternalData(headline, LIMITS.headline);
4433
+ }
4434
+ function wrapUntrustedData(label, data) {
4435
+ return `[BEGIN EXTERNAL DATA: ${label}]
4436
+ ${data}
4437
+ [END EXTERNAL DATA: ${label}]`;
4438
+ }
4439
+ function sanitizeToolResult(result) {
4440
+ if (typeof result === "string") {
4441
+ return sanitizeExternalData(result);
4442
+ }
4443
+ if (Array.isArray(result)) {
4444
+ return result.map(sanitizeToolResult);
4445
+ }
4446
+ if (result !== null && typeof result === "object") {
4447
+ const sanitized = {};
4448
+ for (const [key, value] of Object.entries(result)) {
4449
+ sanitized[key] = sanitizeToolResult(value);
4450
+ }
4451
+ return sanitized;
4452
+ }
4453
+ return result;
4454
+ }
4455
+ var INJECTION_PATTERNS, LIMITS;
4456
+ var init_sanitize = __esm({
4457
+ "src/ai/sanitize.ts"() {
4458
+ "use strict";
4459
+ INJECTION_PATTERNS = [
4460
+ /ignore\s+(all\s+)?previous\s+instructions/gi,
4461
+ /ignore\s+(all\s+)?above/gi,
4462
+ /you\s+are\s+now/gi,
4463
+ /system\s*:/gi,
4464
+ /assistant\s*:/gi,
4465
+ /\[system\]/gi,
4466
+ /\[instruction\]/gi,
4467
+ /<\/?system>/gi,
4468
+ /repeat\s+(everything|the\s+prompt|your\s+instructions)/gi,
4469
+ /reveal\s+(your|the)\s+(prompt|instructions|config)/gi,
4470
+ /forget\s+(everything|your\s+instructions)/gi,
4471
+ /override\s+(previous|your|all)/gi,
4472
+ /new\s+instructions?\s*:/gi,
4473
+ /\bdo\s+not\s+follow\b/gi,
4474
+ /\bact\s+as\b/gi,
4475
+ /\brole\s*:\s*(system|assistant)\b/gi
4476
+ ];
4477
+ LIMITS = {
4478
+ tokenName: 60,
4479
+ headline: 250,
4480
+ description: 500,
4481
+ generic: 1e3
4482
+ };
4483
+ }
4484
+ });
4485
+
4382
4486
  // src/ai/context-injector.ts
4383
4487
  async function buildContextBlock(userMessage) {
4384
4488
  const lower = userMessage.toLowerCase();
@@ -4601,7 +4705,8 @@ async function buildContextBlock(userMessage) {
4601
4705
  }
4602
4706
  output.push(...buildInstructions(queryType, hasSpecificToken, isComplexQuery));
4603
4707
  output.push("");
4604
- return output.join("\n");
4708
+ const raw = output.join("\n");
4709
+ return wrapUntrustedData("MARKET_CONTEXT", raw);
4605
4710
  }
4606
4711
  function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
4607
4712
  const base2 = [
@@ -4685,7 +4790,7 @@ async function fetchTokenData(tokens, address) {
4685
4790
  const pair = pairs[0];
4686
4791
  if (pair) {
4687
4792
  lines.push(
4688
- `${pair.baseToken.name} (${pair.baseToken.symbol}) on ${pair.chainId}:`,
4793
+ `${sanitizeTokenName(pair.baseToken.name)} (${sanitizeTokenName(pair.baseToken.symbol)}) on ${pair.chainId}:`,
4689
4794
  ` Price: $${pair.priceUsd ?? "?"}`,
4690
4795
  ` 24h Volume: $${(pair.volume?.h24 ?? 0).toLocaleString()}`,
4691
4796
  ` Liquidity: $${(pair.liquidity?.usd ?? 0).toLocaleString()}`,
@@ -4754,7 +4859,7 @@ async function fetchTokenData(tokens, address) {
4754
4859
  const pair = pairs[0];
4755
4860
  if (pair) {
4756
4861
  lines.push(
4757
- `${pair.baseToken.name} (${pair.baseToken.symbol}) on ${pair.chainId}:`,
4862
+ `${sanitizeTokenName(pair.baseToken.name)} (${sanitizeTokenName(pair.baseToken.symbol)}) on ${pair.chainId}:`,
4758
4863
  ` Price: $${pair.priceUsd ?? "?"}`,
4759
4864
  ` 24h Volume: $${(pair.volume?.h24 ?? 0).toLocaleString()}`,
4760
4865
  ` 24h Change: ${(pair.priceChange?.h24 ?? 0) > 0 ? "+" : ""}${(pair.priceChange?.h24 ?? 0).toFixed(2)}%`,
@@ -4773,7 +4878,7 @@ async function fetchTrendingData() {
4773
4878
  const lines = ["## Trending Tokens (live)"];
4774
4879
  for (const t of trending.slice(0, 10)) {
4775
4880
  lines.push(
4776
- `- ${t.name} (${t.symbol}) on ${t.chain}: $${t.priceUsd} | 24h: ${t.priceChange24h > 0 ? "+" : ""}${t.priceChange24h.toFixed(1)}% | Vol: $${t.volume24h.toLocaleString()} [${t.source}]`
4881
+ `- ${sanitizeTokenName(t.name)} (${sanitizeTokenName(t.symbol)}) on ${t.chain}: $${t.priceUsd} | 24h: ${t.priceChange24h > 0 ? "+" : ""}${t.priceChange24h.toFixed(1)}% | Vol: $${t.volume24h.toLocaleString()} [${t.source}]`
4777
4882
  );
4778
4883
  }
4779
4884
  return lines.join("\n");
@@ -4797,7 +4902,9 @@ async function fetchNewsData(symbol) {
4797
4902
  const n = headlines[i];
4798
4903
  const ml = mlResults[i];
4799
4904
  const label = ml ? `${ml.sentiment.toUpperCase()} (${(ml.confidence * 100).toFixed(0)}%)` : n.sentiment.toUpperCase();
4800
- lines.push(`- [${label}] ${n.title} (${n.source.title}, ${n.publishedAt})`);
4905
+ lines.push(
4906
+ `- [${label}] ${sanitizeHeadline(n.title)} (${sanitizeTokenName(n.source.title)}, ${n.publishedAt})`
4907
+ );
4801
4908
  }
4802
4909
  const avgScore = mlResults.reduce((s, r) => s + r.score, 0) / mlResults.length;
4803
4910
  const avgSentiment = avgScore > 0.2 ? "BULLISH" : avgScore < -0.2 ? "BEARISH" : "NEUTRAL";
@@ -4810,7 +4917,7 @@ ML Aggregate Sentiment: ${avgSentiment} (score: ${avgScore.toFixed(3)})`);
4810
4917
  }
4811
4918
  for (const n of headlines) {
4812
4919
  lines.push(
4813
- `- [${n.sentiment.toUpperCase()}] ${n.title} (${n.source.title}, ${n.publishedAt})`
4920
+ `- [${n.sentiment.toUpperCase()}] ${sanitizeHeadline(n.title)} (${sanitizeTokenName(n.source.title)}, ${n.publishedAt})`
4814
4921
  );
4815
4922
  }
4816
4923
  return lines.join("\n");
@@ -4850,7 +4957,7 @@ async function fetchRaisesData() {
4850
4957
  const amount = r.amount ? r.amount >= 1e9 ? `$${(r.amount / 1e9).toFixed(1)}B` : r.amount >= 1e6 ? `$${(r.amount / 1e6).toFixed(1)}M` : r.amount >= 1e3 ? `$${(r.amount / 1e3).toFixed(0)}K` : `$${r.amount.toLocaleString()}` : "undisclosed";
4851
4958
  const date = new Date(r.date * 1e3).toISOString().split("T")[0];
4852
4959
  lines.push(
4853
- `- ${r.name} \u2014 ${r.round} (${amount}) on ${r.chains.join(", ") || "multi-chain"} [${date}]${r.leadInvestors.length > 0 ? ` Led by: ${r.leadInvestors.join(", ")}` : ""}`
4960
+ `- ${sanitizeTokenName(r.name)} \u2014 ${sanitizeExternalData(r.round, 50)} (${amount}) on ${r.chains.join(", ") || "multi-chain"} [${date}]${r.leadInvestors.length > 0 ? ` Led by: ${r.leadInvestors.map((inv) => sanitizeTokenName(inv)).join(", ")}` : ""}`
4854
4961
  );
4855
4962
  }
4856
4963
  return lines.join("\n");
@@ -4865,7 +4972,9 @@ async function fetchPumpData() {
4865
4972
  const lines = ["## Latest Pump.fun Launches (Solana)"];
4866
4973
  for (const c of coins) {
4867
4974
  const mcap = c.usd_market_cap ? `$${c.usd_market_cap.toFixed(0)}` : "?";
4868
- lines.push(`- ${c.name} (${c.symbol}) \u2014 MC: ${mcap} | Replies: ${c.reply_count}`);
4975
+ lines.push(
4976
+ `- ${sanitizeTokenName(c.name)} (${sanitizeTokenName(c.symbol)}) \u2014 MC: ${mcap} | Replies: ${c.reply_count}`
4977
+ );
4869
4978
  }
4870
4979
  return lines.join("\n");
4871
4980
  } catch {
@@ -5785,6 +5894,7 @@ var init_context_injector = __esm({
5785
5894
  init_loader();
5786
5895
  init_constants();
5787
5896
  init_client();
5897
+ init_sanitize();
5788
5898
  PRICE_KEYWORDS = ["price", "worth", "cost", "value", "how much"];
5789
5899
  TRENDING_KEYWORDS = ["trending", "hot", "popular", "top", "best", "hype"];
5790
5900
  NEWS_KEYWORDS = ["news", "latest", "update", "happening", "announcement"];
@@ -6524,7 +6634,12 @@ function getCached(key) {
6524
6634
  getDb().prepare("DELETE FROM cache WHERE key = ?").run(key);
6525
6635
  return null;
6526
6636
  }
6527
- return JSON.parse(row.value);
6637
+ try {
6638
+ return JSON.parse(row.value);
6639
+ } catch {
6640
+ getDb().prepare("DELETE FROM cache WHERE key = ?").run(key);
6641
+ return null;
6642
+ }
6528
6643
  }
6529
6644
  function setCache(key, value, ttlSeconds) {
6530
6645
  const now = Math.floor(Date.now() / 1e3);
@@ -6556,12 +6671,14 @@ var init_engine = __esm({
6556
6671
  logger = createLogger("agent-engine");
6557
6672
  AgentEngine = class {
6558
6673
  timer = null;
6674
+ running = false;
6559
6675
  state;
6560
6676
  strategy;
6561
6677
  constructor(config2) {
6562
6678
  this.strategy = getStrategy(config2.strategy);
6679
+ const interval = Math.max(30, config2.interval);
6563
6680
  this.state = {
6564
- config: config2,
6681
+ config: { ...config2, interval },
6565
6682
  status: "idle",
6566
6683
  lastCycle: null,
6567
6684
  cycleCount: 0,
@@ -6575,20 +6692,30 @@ var init_engine = __esm({
6575
6692
  if (this.state.status === "running") return;
6576
6693
  this.state.status = "running";
6577
6694
  this.state.error = null;
6695
+ this.running = true;
6578
6696
  logger.info(`Agent ${this.state.config.name} started (${this.strategy.name})`);
6579
- void this.runCycle();
6580
- this.timer = setInterval(() => {
6581
- void this.runCycle();
6582
- }, this.state.config.interval * 1e3);
6697
+ void this.scheduleNextCycle(true);
6583
6698
  }
6584
6699
  stop() {
6700
+ this.running = false;
6585
6701
  if (this.timer) {
6586
- clearInterval(this.timer);
6702
+ clearTimeout(this.timer);
6587
6703
  this.timer = null;
6588
6704
  }
6589
6705
  this.state.status = "stopped";
6590
6706
  logger.info(`Agent ${this.state.config.name} stopped`);
6591
6707
  }
6708
+ async scheduleNextCycle(immediate = false) {
6709
+ if (!this.running) return;
6710
+ if (!immediate) {
6711
+ await new Promise((resolve4) => {
6712
+ this.timer = setTimeout(resolve4, this.state.config.interval * 1e3);
6713
+ });
6714
+ }
6715
+ if (!this.running) return;
6716
+ await this.runCycle();
6717
+ void this.scheduleNextCycle(false);
6718
+ }
6592
6719
  async runCycle() {
6593
6720
  for (const symbol of this.state.config.pairs) {
6594
6721
  try {
@@ -7011,11 +7138,15 @@ function deleteAgent(id) {
7011
7138
  function startAgent(id) {
7012
7139
  const config2 = getAgentById(id);
7013
7140
  if (!config2) throw new Error(`Agent not found: ${id}`);
7014
- let engine = engines.get(id);
7015
- if (!engine) {
7016
- engine = new AgentEngine(config2);
7017
- engines.set(id, engine);
7141
+ const existing = engines.get(id);
7142
+ if (existing) {
7143
+ const state = existing.getState();
7144
+ if (state.status === "running") {
7145
+ return state;
7146
+ }
7018
7147
  }
7148
+ const engine = new AgentEngine(config2);
7149
+ engines.set(id, engine);
7019
7150
  engine.start();
7020
7151
  return engine.getState();
7021
7152
  }
@@ -7052,23 +7183,43 @@ function logDecision(result) {
7052
7183
  JSON.stringify(result.signals),
7053
7184
  result.timestamp
7054
7185
  );
7186
+ pruneCounter++;
7187
+ if (pruneCounter >= 100) {
7188
+ pruneCounter = 0;
7189
+ pruneDecisions(result.agentId, 1e3);
7190
+ }
7191
+ }
7192
+ function pruneDecisions(agentId, keep = 1e3) {
7193
+ ensureAgentTables();
7194
+ const result = getDb().prepare(
7195
+ `DELETE FROM agent_decisions WHERE agent_id = ? AND id NOT IN (
7196
+ SELECT id FROM agent_decisions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?
7197
+ )`
7198
+ ).run(agentId, agentId, keep);
7199
+ return result.changes;
7055
7200
  }
7056
7201
  function getRecentDecisions(agentId, limit = 20) {
7057
7202
  ensureAgentTables();
7058
7203
  const rows = getDb().prepare(`SELECT * FROM agent_decisions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`).all(agentId, limit);
7059
- return rows.map((r) => ({
7060
- agentId: r.agent_id,
7061
- symbol: r.symbol,
7062
- timestamp: r.created_at,
7063
- signals: JSON.parse(r.signals),
7064
- decision: {
7065
- action: r.action,
7066
- confidence: r.confidence,
7067
- reasoning: JSON.parse(r.reasoning)
7204
+ return rows.map((r) => {
7205
+ try {
7206
+ return {
7207
+ agentId: r.agent_id,
7208
+ symbol: r.symbol,
7209
+ timestamp: r.created_at,
7210
+ signals: JSON.parse(r.signals),
7211
+ decision: {
7212
+ action: r.action,
7213
+ confidence: r.confidence,
7214
+ reasoning: JSON.parse(r.reasoning)
7215
+ }
7216
+ };
7217
+ } catch {
7218
+ return null;
7068
7219
  }
7069
- }));
7220
+ }).filter((r) => r !== null);
7070
7221
  }
7071
- var STRATEGIES, engines;
7222
+ var STRATEGIES, engines, pruneCounter;
7072
7223
  var init_manager = __esm({
7073
7224
  "src/core/agent/manager.ts"() {
7074
7225
  "use strict";
@@ -7083,6 +7234,7 @@ var init_manager = __esm({
7083
7234
  "ml-adaptive": mlAdaptiveStrategy
7084
7235
  };
7085
7236
  engines = /* @__PURE__ */ new Map();
7237
+ pruneCounter = 0;
7086
7238
  }
7087
7239
  });
7088
7240
 
@@ -7549,6 +7701,10 @@ var init_store_factory = __esm({
7549
7701
 
7550
7702
  // src/ai/tool-handler.ts
7551
7703
  async function handleTool(name, input) {
7704
+ const raw = await handleToolUnsafe(name, input);
7705
+ return sanitizeToolResult(raw);
7706
+ }
7707
+ async function handleToolUnsafe(name, input) {
7552
7708
  const params = input;
7553
7709
  switch (name) {
7554
7710
  case "get_token_info": {
@@ -8248,6 +8404,7 @@ var init_tool_handler = __esm({
8248
8404
  init_client();
8249
8405
  init_feature_engineer();
8250
8406
  init_store_factory();
8407
+ init_sanitize();
8251
8408
  }
8252
8409
  });
8253
8410
 
@@ -9288,13 +9445,16 @@ async function startDiscordBot() {
9288
9445
  client2.on("messageCreate", async (message) => {
9289
9446
  if (message.author.bot) return;
9290
9447
  if (!client2.user || !message.mentions.has(client2.user)) return;
9291
- const text = message.content.replace(/<@!?\d+>/g, "").trim();
9448
+ let text = message.content.replace(/<@!?\d+>/g, "").trim();
9292
9449
  if (!text) {
9293
9450
  await message.reply(
9294
9451
  "Mention me with a question! e.g. `@Vizzor what is BTC price?`\nOr use slash commands: `/scan` `/trends` `/track` `/ico` `/audit` `/help`"
9295
9452
  );
9296
9453
  return;
9297
9454
  }
9455
+ if (text.length > 2e3) {
9456
+ text = text.slice(0, 2e3);
9457
+ }
9298
9458
  await message.reply("\u{1F52E} Analyzing...");
9299
9459
  try {
9300
9460
  const response = await analyze(buildChatSystemPrompt(), text, VIZZOR_TOOLS);
@@ -9964,8 +10124,11 @@ async function startTelegramBot() {
9964
10124
  bot.use(rateLimitMiddleware);
9965
10125
  registerCommands(bot);
9966
10126
  bot.on("message:text", async (ctx) => {
9967
- const text = ctx.message.text;
10127
+ let text = ctx.message.text;
9968
10128
  if (text.startsWith("/")) return;
10129
+ if (text.length > 4e3) {
10130
+ text = text.slice(0, 4e3);
10131
+ }
9969
10132
  await ctx.reply("\u{1F52E} Analyzing...");
9970
10133
  try {
9971
10134
  const response = await analyze(buildChatSystemPrompt(), text, VIZZOR_TOOLS);
@@ -10543,19 +10706,26 @@ var init_keys2 = __esm({
10543
10706
  });
10544
10707
 
10545
10708
  // src/api/auth/middleware.ts
10709
+ import { timingSafeEqual } from "crypto";
10546
10710
  async function authMiddleware(request, reply) {
10547
- if (PUBLIC_PATHS.some((p) => request.url === p) || request.url.startsWith("/docs/")) {
10711
+ if (PUBLIC_PATHS.some((p) => request.url === p)) {
10712
+ return;
10713
+ }
10714
+ if (request.url === "/docs" || request.url.startsWith("/docs/")) {
10715
+ if (process.env["NODE_ENV"] === "production") {
10716
+ return reply.status(404).send({ error: "Not found" });
10717
+ }
10548
10718
  return;
10549
10719
  }
10550
10720
  const apiKey = request.headers["x-api-key"];
10551
- if (!apiKey) {
10721
+ if (!apiKey || apiKey.length > 256) {
10552
10722
  return reply.status(401).send({
10553
10723
  error: "Unauthorized",
10554
- message: "Missing X-API-Key header"
10724
+ message: "Missing or invalid X-API-Key header"
10555
10725
  });
10556
10726
  }
10557
10727
  const keyHash = hashApiKey(apiKey);
10558
- const valid = await validateKey2(keyHash);
10728
+ const valid = validateKey2(keyHash);
10559
10729
  if (!valid) {
10560
10730
  return reply.status(403).send({
10561
10731
  error: "Forbidden",
@@ -10563,20 +10733,43 @@ async function authMiddleware(request, reply) {
10563
10733
  });
10564
10734
  }
10565
10735
  }
10566
- async function validateKey2(keyHash) {
10567
- const store = getStoreInstance();
10568
- if (!store) return false;
10569
- const cached = await store.getCached(`apikey:${keyHash}`);
10570
- if (cached !== null) return cached.valid;
10571
- return true;
10736
+ function validateKey2(keyHash) {
10737
+ try {
10738
+ const db2 = getDb();
10739
+ db2.exec(`
10740
+ CREATE TABLE IF NOT EXISTS api_keys (
10741
+ id TEXT PRIMARY KEY,
10742
+ label TEXT NOT NULL,
10743
+ key_hash TEXT NOT NULL UNIQUE,
10744
+ key_prefix TEXT NOT NULL,
10745
+ rate_limit INTEGER NOT NULL DEFAULT 100,
10746
+ created_at INTEGER NOT NULL,
10747
+ revoked_at INTEGER
10748
+ )
10749
+ `);
10750
+ const row = db2.prepare("SELECT key_hash FROM api_keys WHERE revoked_at IS NULL").all();
10751
+ if (row.length === 0) {
10752
+ return false;
10753
+ }
10754
+ const inputBuf = Buffer.from(keyHash, "hex");
10755
+ for (const r of row) {
10756
+ const storedBuf = Buffer.from(r.key_hash, "hex");
10757
+ if (inputBuf.length === storedBuf.length && timingSafeEqual(inputBuf, storedBuf)) {
10758
+ return true;
10759
+ }
10760
+ }
10761
+ return false;
10762
+ } catch {
10763
+ return false;
10764
+ }
10572
10765
  }
10573
10766
  var PUBLIC_PATHS;
10574
10767
  var init_middleware = __esm({
10575
10768
  "src/api/auth/middleware.ts"() {
10576
10769
  "use strict";
10577
10770
  init_keys2();
10578
- init_store_factory();
10579
- PUBLIC_PATHS = ["/health", "/docs", "/docs/"];
10771
+ init_cache();
10772
+ PUBLIC_PATHS = ["/health"];
10580
10773
  }
10581
10774
  });
10582
10775
 
@@ -10613,7 +10806,11 @@ import swagger from "@fastify/swagger";
10613
10806
  import swaggerUi from "@fastify/swagger-ui";
10614
10807
  async function startApiServer(options) {
10615
10808
  const server = Fastify({ logger: false });
10616
- await server.register(cors, { origin: true });
10809
+ const isProd = process.env["NODE_ENV"] === "production";
10810
+ const origin = options.corsOrigin ?? "http://localhost:3000";
10811
+ await server.register(cors, {
10812
+ origin: isProd ? origin : true
10813
+ });
10617
10814
  await server.register(rateLimit, {
10618
10815
  max: 100,
10619
10816
  timeWindow: "1 minute"
@@ -10623,7 +10820,7 @@ async function startApiServer(options) {
10623
10820
  info: {
10624
10821
  title: "Vizzor API",
10625
10822
  description: "AI-powered crypto intelligence REST API",
10626
- version: "0.7.0"
10823
+ version: "0.10.5"
10627
10824
  },
10628
10825
  servers: [{ url: `http://${options.host}:${options.port}` }],
10629
10826
  components: {
@@ -10637,25 +10834,36 @@ async function startApiServer(options) {
10637
10834
  }
10638
10835
  }
10639
10836
  });
10640
- await server.register(swaggerUi, {
10641
- routePrefix: "/docs"
10642
- });
10643
- if (options.enableAuth) {
10837
+ if (!isProd) {
10838
+ await server.register(swaggerUi, {
10839
+ routePrefix: "/docs"
10840
+ });
10841
+ }
10842
+ if (options.enableAuth !== false) {
10644
10843
  server.addHook("onRequest", authMiddleware);
10844
+ } else if (isProd) {
10845
+ log3.warn("API authentication is DISABLED in production \u2014 this is insecure");
10645
10846
  }
10646
10847
  server.setErrorHandler(errorHandler);
10647
- server.get("/health", async () => ({
10648
- status: "ok",
10649
- version: "0.7.0",
10650
- uptime: process.uptime(),
10651
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
10652
- }));
10848
+ server.get("/health", async () => {
10849
+ if (isProd) {
10850
+ return { status: "ok" };
10851
+ }
10852
+ return {
10853
+ status: "ok",
10854
+ version: "0.10.5",
10855
+ uptime: process.uptime(),
10856
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10857
+ };
10858
+ });
10653
10859
  await server.register(registerMarketRoutes, { prefix: "/v1/market" });
10654
10860
  await server.register(registerAnalysisRoutes, { prefix: "/v1/analysis" });
10655
10861
  await server.register(registerSecurityRoutes, { prefix: "/v1/security" });
10656
10862
  await server.listen({ port: options.port, host: options.host });
10657
10863
  log3.info(`Vizzor API listening on ${options.host}:${options.port}`);
10658
- log3.info(`OpenAPI docs at http://${options.host}:${options.port}/docs`);
10864
+ if (!isProd) {
10865
+ log3.info(`OpenAPI docs at http://${options.host}:${options.port}/docs`);
10866
+ }
10659
10867
  }
10660
10868
  var log3;
10661
10869
  var init_server = __esm({