Merge master into feature/openrouter-support
This commit is contained in:
@@ -31,12 +31,12 @@ REFRESH_INTERVAL_MINUTES=15
|
|||||||
|
|
||||||
# === LLM Layer (optional) ===
|
# === LLM Layer (optional) ===
|
||||||
# Enables AI-enhanced trade ideas and breaking news Telegram alerts.
|
# Enables AI-enhanced trade ideas and breaking news Telegram alerts.
|
||||||
# Provider options: anthropic | openai | gemini | codex | openrouter
|
# Provider options: anthropic | openai | gemini | codex | openrouter | minimax
|
||||||
LLM_PROVIDER=
|
LLM_PROVIDER=
|
||||||
# Not needed for codex (uses ~/.codex/auth.json)
|
# Not needed for codex (uses ~/.codex/auth.json)
|
||||||
LLM_API_KEY=
|
LLM_API_KEY=
|
||||||
# Optional override. Each provider has a sensible default:
|
# Optional override. Each provider has a sensible default:
|
||||||
# anthropic: claude-sonnet-4-6 | openai: gpt-5.4 | gemini: gemini-3.1-pro | codex: gpt-5.3-codex | openrouter: openrouter/auto
|
# anthropic: claude-sonnet-4-6 | openai: gpt-5.4 | gemini: gemini-3.1-pro | codex: gpt-5.3-codex | openrouter: openrouter/auto | minimax: MiniMax-M2.5
|
||||||
LLM_MODEL=
|
LLM_MODEL=
|
||||||
|
|
||||||
# === Telegram Alerts (optional, requires LLM) ===
|
# === Telegram Alerts (optional, requires LLM) ===
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,3 +45,6 @@ npm-debug.log*
|
|||||||
|
|
||||||
# Local maintainer notes
|
# Local maintainer notes
|
||||||
MAINTAINER_DECISIONS.local.md
|
MAINTAINER_DECISIONS.local.md
|
||||||
|
|
||||||
|
# Local deploy config
|
||||||
|
dashboard/public/vercel.json
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
**Your own intelligence terminal. 27 sources. One command. Zero cloud.**
|
**Your own intelligence terminal. 27 sources. One command. Zero cloud.**
|
||||||
|
|
||||||
|
## [Visit The Live Site: crucix.live](https://www.crucix.live/)
|
||||||
|
|
||||||
|
[](https://www.crucix.live/)
|
||||||
|
[](https://www.crucix.live/)
|
||||||
|
|
||||||
[](#quick-start)
|
[](#quick-start)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[-orange)](#architecture)
|
[-orange)](#architecture)
|
||||||
@@ -27,10 +32,15 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
> **Live website:** [https://www.crucix.live/](https://www.crucix.live/)
|
||||||
|
> Explore the public demo first, then clone the repo to run Crucix locally.
|
||||||
|
|
||||||
Crucix pulls satellite fire detection, flight tracking, radiation monitoring, satellite constellation tracking, economic indicators, live market prices, conflict data, sanctions lists, and social sentiment from 27 open-source intelligence feeds — in parallel, every 15 minutes — and renders everything on a single self-contained Jarvis-style dashboard.
|
Crucix pulls satellite fire detection, flight tracking, radiation monitoring, satellite constellation tracking, economic indicators, live market prices, conflict data, sanctions lists, and social sentiment from 27 open-source intelligence feeds — in parallel, every 15 minutes — and renders everything on a single self-contained Jarvis-style dashboard.
|
||||||
|
|
||||||
Hook it up to an LLM and it becomes a **two-way intelligence assistant** — pushing multi-tier alerts to Telegram and Discord when something meaningful changes, responding to commands like `/brief` and `/sweep` from your phone, and generating actionable trade ideas grounded in real cross-domain data. Your own analyst that watches the world while you sleep.
|
Hook it up to an LLM and it becomes a **two-way intelligence assistant** — pushing multi-tier alerts to Telegram and Discord when something meaningful changes, responding to commands like `/brief` and `/sweep` from your phone, and generating actionable trade ideas grounded in real cross-domain data. Your own analyst that watches the world while you sleep.
|
||||||
|
|
||||||
|
Try the live demo first at [https://www.crucix.live/](https://www.crucix.live/), then clone the repo when you want the full local stack.
|
||||||
|
|
||||||
No cloud. No telemetry. No subscriptions. Just `node server.mjs` and you're running.
|
No cloud. No telemetry. No subscriptions. Just `node server.mjs` and you're running.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -50,7 +60,7 @@ It was built for anyone who wants to understand what's actually happening in the
|
|||||||
```bash
|
```bash
|
||||||
# 1. Clone the repo
|
# 1. Clone the repo
|
||||||
git clone https://github.com/calesthio/Crucix.git
|
git clone https://github.com/calesthio/Crucix.git
|
||||||
cd crucix
|
cd Crucix
|
||||||
|
|
||||||
# 2. Install dependencies (just Express)
|
# 2. Install dependencies (just Express)
|
||||||
npm install
|
npm install
|
||||||
@@ -76,7 +86,7 @@ The dashboard opens automatically at `http://localhost:3117` and immediately beg
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/calesthio/Crucix.git
|
git clone https://github.com/calesthio/Crucix.git
|
||||||
cd crucix
|
cd Crucix
|
||||||
cp .env.example .env # add your API keys
|
cp .env.example .env # add your API keys
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
@@ -148,10 +158,10 @@ Alerts are delivered as rich embeds with color-coded sidebars: red for FLASH, ye
|
|||||||
**Optional dependency:** The full bot requires `discord.js`. Install it with `npm install discord.js`. If it's not installed, Crucix automatically falls back to webhook-only mode.
|
**Optional dependency:** The full bot requires `discord.js`. Install it with `npm install discord.js`. If it's not installed, Crucix automatically falls back to webhook-only mode.
|
||||||
|
|
||||||
### Optional LLM Layer
|
### Optional LLM Layer
|
||||||
Connect any of 5 LLM providers for enhanced analysis:
|
Connect any of 6 LLM providers for enhanced analysis:
|
||||||
- **AI trade ideas** — quantitative analyst producing 5-8 actionable ideas citing specific data
|
- **AI trade ideas** — quantitative analyst producing 5-8 actionable ideas citing specific data
|
||||||
- **Smarter alert evaluation** — LLM classifies signals into FLASH/PRIORITY/ROUTINE tiers with cross-domain correlation and confidence scoring
|
- **Smarter alert evaluation** — LLM classifies signals into FLASH/PRIORITY/ROUTINE tiers with cross-domain correlation and confidence scoring
|
||||||
- Providers: Anthropic Claude, OpenAI, Google Gemini, OpenRouter (Unified API), OpenAI Codex (ChatGPT subscription)
|
- Providers: Anthropic Claude, OpenAI, Google Gemini, OpenRouter (Unified API), OpenAI Codex (ChatGPT subscription), MiniMax
|
||||||
- Graceful fallback — when LLM is unavailable, a rule-based engine takes over alert evaluation. LLM failures never crash the sweep cycle.
|
- Graceful fallback — when LLM is unavailable, a rule-based engine takes over alert evaluation. LLM failures never crash the sweep cycle.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -184,7 +194,7 @@ These three unlock the most valuable economic and satellite data. Each takes abo
|
|||||||
|
|
||||||
### LLM Provider (optional, for AI-enhanced ideas)
|
### LLM Provider (optional, for AI-enhanced ideas)
|
||||||
|
|
||||||
Set `LLM_PROVIDER` to one of: `anthropic`, `openai`, `gemini`, `codex`
|
Set `LLM_PROVIDER` to one of: `anthropic`, `openai`, `gemini`, `codex`, `openrouter`, `minimax`
|
||||||
|
|
||||||
| Provider | Key Required | Default Model |
|
| Provider | Key Required | Default Model |
|
||||||
|----------|-------------|---------------|
|
|----------|-------------|---------------|
|
||||||
@@ -193,6 +203,7 @@ Set `LLM_PROVIDER` to one of: `anthropic`, `openai`, `gemini`, `codex`
|
|||||||
| `gemini` | `LLM_API_KEY` | gemini-3.1-pro |
|
| `gemini` | `LLM_API_KEY` | gemini-3.1-pro |
|
||||||
| `openrouter` | `LLM_API_KEY` | openrouter/auto |
|
| `openrouter` | `LLM_API_KEY` | openrouter/auto |
|
||||||
| `codex` | None (uses `~/.codex/auth.json`) | gpt-5.3-codex |
|
| `codex` | None (uses `~/.codex/auth.json`) | gpt-5.3-codex |
|
||||||
|
| `minimax` | `LLM_API_KEY` | MiniMax-M2.5 |
|
||||||
|
|
||||||
For Codex, run `npx @openai/codex login` to authenticate via your ChatGPT subscription.
|
For Codex, run `npx @openai/codex login` to authenticate via your ChatGPT subscription.
|
||||||
|
|
||||||
@@ -269,6 +280,7 @@ crucix/
|
|||||||
│ │ ├── gemini.mjs # Gemini
|
│ │ ├── gemini.mjs # Gemini
|
||||||
│ │ ├── openrouter.mjs # OpenRouter (Unified API)
|
│ │ ├── openrouter.mjs # OpenRouter (Unified API)
|
||||||
│ │ ├── codex.mjs # Codex (ChatGPT subscription)
|
│ │ ├── codex.mjs # Codex (ChatGPT subscription)
|
||||||
|
│ │ ├── minimax.mjs # MiniMax (M2.5, 204K context)
|
||||||
│ │ ├── ideas.mjs # LLM-powered trade idea generation
|
│ │ ├── ideas.mjs # LLM-powered trade idea generation
|
||||||
│ │ └── index.mjs # Factory: createLLMProvider()
|
│ │ └── index.mjs # Factory: createLLMProvider()
|
||||||
│ ├── delta/ # Change tracking between sweeps
|
│ ├── delta/ # Change tracking between sweeps
|
||||||
@@ -370,7 +382,7 @@ All settings are in `.env` with sensible defaults:
|
|||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `PORT` | `3117` | Dashboard server port |
|
| `PORT` | `3117` | Dashboard server port |
|
||||||
| `REFRESH_INTERVAL_MINUTES` | `15` | Auto-refresh interval |
|
| `REFRESH_INTERVAL_MINUTES` | `15` | Auto-refresh interval |
|
||||||
| `LLM_PROVIDER` | disabled | `anthropic`, `openai`, `gemini`, `codex`, or `openrouter` |
|
| `LLM_PROVIDER` | disabled | `anthropic`, `openai`, `gemini`, `codex`, `openrouter`, or `minimax` |
|
||||||
| `LLM_API_KEY` | — | API key (not needed for codex) |
|
| `LLM_API_KEY` | — | API key (not needed for codex) |
|
||||||
| `LLM_MODEL` | per-provider default | Override model selection |
|
| `LLM_MODEL` | per-provider default | Override model selection |
|
||||||
| `TELEGRAM_BOT_TOKEN` | disabled | For Telegram alerts + bot commands |
|
| `TELEGRAM_BOT_TOKEN` | disabled | For Telegram alerts + bot commands |
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default {
|
|||||||
refreshIntervalMinutes: parseInt(process.env.REFRESH_INTERVAL_MINUTES) || 15,
|
refreshIntervalMinutes: parseInt(process.env.REFRESH_INTERVAL_MINUTES) || 15,
|
||||||
|
|
||||||
llm: {
|
llm: {
|
||||||
provider: process.env.LLM_PROVIDER || null, // anthropic | openai | gemini | codex | openrouter
|
provider: process.env.LLM_PROVIDER || null, // anthropic | openai | gemini | codex | openrouter | minimax
|
||||||
apiKey: process.env.LLM_API_KEY || null,
|
apiKey: process.env.LLM_API_KEY || null,
|
||||||
model: process.env.LLM_MODEL || null,
|
model: process.env.LLM_MODEL || null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import { readFileSync, writeFileSync } from 'fs';
|
|||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
|
import config from '../crucix.config.mjs';
|
||||||
|
import { createLLMProvider } from '../lib/llm/index.mjs';
|
||||||
|
import { generateLLMIdeas } from '../lib/llm/ideas.mjs';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const ROOT = join(__dirname, '..');
|
const ROOT = join(__dirname, '..');
|
||||||
@@ -511,11 +514,42 @@ function buildNewsFeed(rssNews, gdeltData, tgUrgent, tgTop) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === CLI Mode: inject into HTML file ===
|
// === CLI Mode: inject into HTML file ===
|
||||||
|
function getCliArg(flag) {
|
||||||
|
const idx = process.argv.indexOf(flag);
|
||||||
|
return idx >= 0 ? process.argv[idx + 1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
async function cliInject() {
|
async function cliInject() {
|
||||||
const data = JSON.parse(readFileSync(join(ROOT, 'runs/latest.json'), 'utf8'));
|
const data = JSON.parse(readFileSync(join(ROOT, 'runs/latest.json'), 'utf8'));
|
||||||
|
const htmlOverride = getCliArg('--html');
|
||||||
|
const shouldOpen = !process.argv.includes('--no-open');
|
||||||
|
|
||||||
console.log('Fetching RSS news feeds...');
|
console.log('Fetching RSS news feeds...');
|
||||||
const V2 = await synthesize(data);
|
const V2 = await synthesize(data);
|
||||||
|
const llmProvider = createLLMProvider(config.llm);
|
||||||
|
|
||||||
|
if (llmProvider?.isConfigured) {
|
||||||
|
try {
|
||||||
|
console.log(`[LLM] Generating ideas via ${llmProvider.name}...`);
|
||||||
|
const llmIdeas = await generateLLMIdeas(llmProvider, V2, null, []);
|
||||||
|
if (llmIdeas?.length) {
|
||||||
|
V2.ideas = llmIdeas;
|
||||||
|
V2.ideasSource = 'llm';
|
||||||
|
console.log(`[LLM] Generated ${llmIdeas.length} ideas`);
|
||||||
|
} else {
|
||||||
|
V2.ideas = [];
|
||||||
|
V2.ideasSource = 'llm-failed';
|
||||||
|
console.log('[LLM] No ideas returned');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
V2.ideas = [];
|
||||||
|
V2.ideasSource = 'llm-failed';
|
||||||
|
console.log('[LLM] Idea generation failed:', err.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
V2.ideas = [];
|
||||||
|
V2.ideasSource = 'disabled';
|
||||||
|
}
|
||||||
console.log(`Generated ${V2.ideas.length} leverageable ideas`);
|
console.log(`Generated ${V2.ideas.length} leverageable ideas`);
|
||||||
|
|
||||||
const json = JSON.stringify(V2);
|
const json = JSON.stringify(V2);
|
||||||
@@ -523,12 +557,15 @@ async function cliInject() {
|
|||||||
console.log('Size:', json.length, 'bytes | Air:', V2.air.length, '| Thermal:', V2.thermal.length,
|
console.log('Size:', json.length, 'bytes | Air:', V2.air.length, '| Thermal:', V2.thermal.length,
|
||||||
'| News:', V2.news.length, '| Ideas:', V2.ideas.length, '| Sources:', V2.health.length);
|
'| News:', V2.news.length, '| Ideas:', V2.ideas.length, '| Sources:', V2.health.length);
|
||||||
|
|
||||||
const htmlPath = join(ROOT, 'dashboard/public/jarvis.html');
|
const htmlPath = htmlOverride || join(ROOT, 'dashboard/public/jarvis.html');
|
||||||
let html = readFileSync(htmlPath, 'utf8');
|
let html = readFileSync(htmlPath, 'utf8');
|
||||||
html = html.replace(/^(let|const) D = .*;\s*$/m, 'let D = ' + json + ';');
|
// Use a replacer function so JSON is inserted literally even if it contains `$`.
|
||||||
|
html = html.replace(/^(let|const) D = .*;\s*$/m, () => 'let D = ' + json + ';');
|
||||||
writeFileSync(htmlPath, html);
|
writeFileSync(htmlPath, html);
|
||||||
console.log('Data injected into jarvis.html!');
|
console.log('Data injected into jarvis.html!');
|
||||||
|
|
||||||
|
if (!shouldOpen) return;
|
||||||
|
|
||||||
// Auto-open dashboard in default browser
|
// Auto-open dashboard in default browser
|
||||||
// NOTE: On Windows, `start` in PowerShell is an alias for Start-Service, not cmd's start.
|
// NOTE: On Windows, `start` in PowerShell is an alias for Start-Service, not cmd's start.
|
||||||
// We must use `cmd /c start ""` to ensure it works in both cmd.exe and PowerShell.
|
// We must use `cmd /c start ""` to ensure it works in both cmd.exe and PowerShell.
|
||||||
@@ -542,7 +579,8 @@ async function cliInject() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run CLI if invoked directly
|
// Run CLI if invoked directly
|
||||||
const isMain = process.argv[1] && fileURLToPath(import.meta.url).includes(process.argv[1].replace(/\\/g, '/'));
|
const isMain = process.argv[1]
|
||||||
|
&& fileURLToPath(import.meta.url).replace(/\\/g, '/') === process.argv[1].replace(/\\/g, '/');
|
||||||
if (isMain) {
|
if (isMain) {
|
||||||
cliInject();
|
await cliInject();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1202,9 +1202,10 @@ function init(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const isServer = location.protocol !== 'file:';
|
const hasInlineData = !!(D && D.meta);
|
||||||
|
const canProbeApi = location.protocol !== 'file:';
|
||||||
|
|
||||||
if (isServer) {
|
if (canProbeApi && !hasInlineData) {
|
||||||
// Server mode: always fetch live data from API (ignore any stale inline D)
|
// Server mode: always fetch live data from API (ignore any stale inline D)
|
||||||
fetch('/api/data')
|
fetch('/api/data')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
@@ -1213,7 +1214,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Should not reach here — server routes to loading.html when no data
|
// Should not reach here — server routes to loading.html when no data
|
||||||
if (D && D.meta) { init(); connectSSE(); }
|
if (D && D.meta) { init(); connectSSE(); }
|
||||||
});
|
});
|
||||||
} else if (D && D.meta) {
|
} else if (hasInlineData) {
|
||||||
// File mode: use inline data
|
// File mode: use inline data
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
services:
|
services:
|
||||||
crucix:
|
crucix:
|
||||||
build: .
|
build: .
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { OpenAIProvider } from './openai.mjs';
|
|||||||
import { OpenRouterProvider } from './openrouter.mjs';
|
import { OpenRouterProvider } from './openrouter.mjs';
|
||||||
import { GeminiProvider } from './gemini.mjs';
|
import { GeminiProvider } from './gemini.mjs';
|
||||||
import { CodexProvider } from './codex.mjs';
|
import { CodexProvider } from './codex.mjs';
|
||||||
|
import { MiniMaxProvider } from './minimax.mjs';
|
||||||
|
|
||||||
export { LLMProvider } from './provider.mjs';
|
export { LLMProvider } from './provider.mjs';
|
||||||
export { AnthropicProvider } from './anthropic.mjs';
|
export { AnthropicProvider } from './anthropic.mjs';
|
||||||
@@ -12,6 +13,7 @@ export { OpenAIProvider } from './openai.mjs';
|
|||||||
export { OpenRouterProvider } from './openrouter.mjs';
|
export { OpenRouterProvider } from './openrouter.mjs';
|
||||||
export { GeminiProvider } from './gemini.mjs';
|
export { GeminiProvider } from './gemini.mjs';
|
||||||
export { CodexProvider } from './codex.mjs';
|
export { CodexProvider } from './codex.mjs';
|
||||||
|
export { MiniMaxProvider } from './minimax.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an LLM provider based on config.
|
* Create an LLM provider based on config.
|
||||||
@@ -34,6 +36,8 @@ export function createLLMProvider(llmConfig) {
|
|||||||
return new GeminiProvider({ apiKey, model });
|
return new GeminiProvider({ apiKey, model });
|
||||||
case 'codex':
|
case 'codex':
|
||||||
return new CodexProvider({ model });
|
return new CodexProvider({ model });
|
||||||
|
case 'minimax':
|
||||||
|
return new MiniMaxProvider({ apiKey, model });
|
||||||
default:
|
default:
|
||||||
console.warn(`[LLM] Unknown provider "${provider}". LLM features disabled.`);
|
console.warn(`[LLM] Unknown provider "${provider}". LLM features disabled.`);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
51
lib/llm/minimax.mjs
Normal file
51
lib/llm/minimax.mjs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// MiniMax Provider — raw fetch, no SDK
|
||||||
|
// Uses MiniMax's OpenAI-compatible Chat Completions API
|
||||||
|
|
||||||
|
import { LLMProvider } from './provider.mjs';
|
||||||
|
|
||||||
|
export class MiniMaxProvider extends LLMProvider {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.name = 'minimax';
|
||||||
|
this.apiKey = config.apiKey;
|
||||||
|
this.model = config.model || 'MiniMax-M2.5';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConfigured() { return !!this.apiKey; }
|
||||||
|
|
||||||
|
async complete(systemPrompt, userMessage, opts = {}) {
|
||||||
|
const res = await fetch('https://api.minimax.io/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: this.model,
|
||||||
|
max_tokens: opts.maxTokens || 4096,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: userMessage },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(opts.timeout || 60000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text().catch(() => '');
|
||||||
|
throw new Error(`MiniMax API ${res.status}: ${err.substring(0, 200)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const text = data.choices?.[0]?.message?.content || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
usage: {
|
||||||
|
inputTokens: data.usage?.prompt_tokens || 0,
|
||||||
|
outputTokens: data.usage?.completion_tokens || 0,
|
||||||
|
},
|
||||||
|
model: data.model || this.model,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
332
package-lock.json
generated
332
package-lock.json
generated
@@ -7,12 +7,221 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "crucix",
|
"name": "crucix",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"license": "ISC",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.1.0"
|
"express": "^5.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22",
|
||||||
|
"npm": ">=10"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"discord.js": "^14.25.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/builders": {
|
||||||
|
"version": "1.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
|
||||||
|
"integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/formatters": "^0.6.2",
|
||||||
|
"@discordjs/util": "^1.2.0",
|
||||||
|
"@sapphire/shapeshift": "^4.0.0",
|
||||||
|
"discord-api-types": "^0.38.33",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"ts-mixer": "^6.0.4",
|
||||||
|
"tslib": "^2.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/collection": {
|
||||||
|
"version": "1.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
|
||||||
|
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/formatters": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"discord-api-types": "^0.38.33"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/rest": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/collection": "^2.1.1",
|
||||||
|
"@discordjs/util": "^1.1.1",
|
||||||
|
"@sapphire/async-queue": "^1.5.3",
|
||||||
|
"@sapphire/snowflake": "^3.5.3",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||||
|
"discord-api-types": "^0.38.16",
|
||||||
|
"magic-bytes.js": "^1.10.0",
|
||||||
|
"tslib": "^2.6.3",
|
||||||
|
"undici": "6.21.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/util": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"discord-api-types": "^0.38.33"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/ws": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/collection": "^2.1.0",
|
||||||
|
"@discordjs/rest": "^2.5.1",
|
||||||
|
"@discordjs/util": "^1.1.0",
|
||||||
|
"@sapphire/async-queue": "^1.5.2",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.2.4",
|
||||||
|
"discord-api-types": "^0.38.1",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"ws": "^8.17.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/async-queue": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/shapeshift": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/snowflake": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
||||||
|
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vladfrangu/async_event_emitter": {
|
||||||
|
"version": "2.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
|
||||||
|
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
@@ -156,6 +365,44 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/discord-api-types": {
|
||||||
|
"version": "0.38.42",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz",
|
||||||
|
"integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"workspaces": [
|
||||||
|
"scripts/actions/documentation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/discord.js": {
|
||||||
|
"version": "14.25.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz",
|
||||||
|
"integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/builders": "^1.13.0",
|
||||||
|
"@discordjs/collection": "1.5.3",
|
||||||
|
"@discordjs/formatters": "^0.6.2",
|
||||||
|
"@discordjs/rest": "^2.6.0",
|
||||||
|
"@discordjs/util": "^1.2.0",
|
||||||
|
"@discordjs/ws": "^1.2.3",
|
||||||
|
"@sapphire/snowflake": "3.5.3",
|
||||||
|
"discord-api-types": "^0.38.33",
|
||||||
|
"fast-deep-equal": "3.1.3",
|
||||||
|
"lodash.snakecase": "4.1.1",
|
||||||
|
"magic-bytes.js": "^1.10.0",
|
||||||
|
"tslib": "^2.6.3",
|
||||||
|
"undici": "6.21.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -273,6 +520,13 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||||
@@ -451,6 +705,27 @@
|
|||||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/lodash.snakecase": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/magic-bytes.js": {
|
||||||
|
"version": "1.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
|
||||||
|
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -788,6 +1063,20 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-mixer": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/type-is": {
|
"node_modules/type-is": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||||
@@ -802,6 +1091,23 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "7.24.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
|
||||||
|
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
|
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
@@ -825,6 +1131,28 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "crucix",
|
"name": "crucix",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"description": "Local intelligence engine — 26 OSINT sources, live dashboard, auto-refresh, optional LLM layer.",
|
"description": "Local intelligence engine - 27 OSINT sources, live dashboard, public demo at crucix.live, auto-refresh, optional LLM layer.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.mjs",
|
"start": "node server.mjs",
|
||||||
@@ -14,7 +14,12 @@
|
|||||||
"clean": "node scripts/clean.mjs",
|
"clean": "node scripts/clean.mjs",
|
||||||
"fresh-start": "npm run clean && npm start"
|
"fresh-start": "npm run clean && npm start"
|
||||||
},
|
},
|
||||||
"keywords": ["osint", "intelligence", "dashboard", "geopolitical"],
|
"keywords": [
|
||||||
|
"osint",
|
||||||
|
"intelligence",
|
||||||
|
"dashboard",
|
||||||
|
"geopolitical"
|
||||||
|
],
|
||||||
"author": "Crucix",
|
"author": "Crucix",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -25,6 +30,7 @@
|
|||||||
"express": "^5.1.0"
|
"express": "^5.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"discord.js": "^14.25.0"
|
"discord.js": "^14.25.1" },
|
||||||
}
|
"overrides": {
|
||||||
|
"undici": "^7.24.4" }
|
||||||
}
|
}
|
||||||
|
|||||||
30
test/llm-minimax-integration.test.mjs
Normal file
30
test/llm-minimax-integration.test.mjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// MiniMax provider — integration test (calls real API)
|
||||||
|
// Requires MINIMAX_API_KEY environment variable
|
||||||
|
// Run: MINIMAX_API_KEY=sk-... node --test test/llm-minimax-integration.test.mjs
|
||||||
|
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { MiniMaxProvider } from '../lib/llm/minimax.mjs';
|
||||||
|
|
||||||
|
const API_KEY = process.env.MINIMAX_API_KEY;
|
||||||
|
|
||||||
|
describe('MiniMax integration', { skip: !API_KEY && 'MINIMAX_API_KEY not set' }, () => {
|
||||||
|
it('should complete a prompt with MiniMax-M2.5', async () => {
|
||||||
|
const provider = new MiniMaxProvider({ apiKey: API_KEY, model: 'MiniMax-M2.5' });
|
||||||
|
assert.equal(provider.isConfigured, true);
|
||||||
|
|
||||||
|
const result = await provider.complete(
|
||||||
|
'You are a helpful assistant. Respond in exactly one sentence.',
|
||||||
|
'What is 2+2?',
|
||||||
|
{ maxTokens: 128, timeout: 30000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(result.text.length > 0, 'Response text should not be empty');
|
||||||
|
assert.ok(result.usage.inputTokens > 0, 'Should report input tokens');
|
||||||
|
assert.ok(result.usage.outputTokens > 0, 'Should report output tokens');
|
||||||
|
assert.ok(result.model, 'Should report model name');
|
||||||
|
console.log(` Response: ${result.text}`);
|
||||||
|
console.log(` Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out`);
|
||||||
|
console.log(` Model: ${result.model}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
144
test/llm-minimax.test.mjs
Normal file
144
test/llm-minimax.test.mjs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// MiniMax provider — unit tests
|
||||||
|
// Uses Node.js built-in test runner (node:test) — no extra dependencies
|
||||||
|
|
||||||
|
import { describe, it, mock, beforeEach } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { MiniMaxProvider } from '../lib/llm/minimax.mjs';
|
||||||
|
import { createLLMProvider } from '../lib/llm/index.mjs';
|
||||||
|
|
||||||
|
// ─── Unit Tests ───
|
||||||
|
|
||||||
|
describe('MiniMaxProvider', () => {
|
||||||
|
it('should set defaults correctly', () => {
|
||||||
|
const provider = new MiniMaxProvider({ apiKey: 'sk-test' });
|
||||||
|
assert.equal(provider.name, 'minimax');
|
||||||
|
assert.equal(provider.model, 'MiniMax-M2.5');
|
||||||
|
assert.equal(provider.isConfigured, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept custom model', () => {
|
||||||
|
const provider = new MiniMaxProvider({ apiKey: 'sk-test', model: 'MiniMax-M2.5-highspeed' });
|
||||||
|
assert.equal(provider.model, 'MiniMax-M2.5-highspeed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report not configured without API key', () => {
|
||||||
|
const provider = new MiniMaxProvider({});
|
||||||
|
assert.equal(provider.isConfigured, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on API error', async () => {
|
||||||
|
const provider = new MiniMaxProvider({ apiKey: 'sk-test' });
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = mock.fn(() =>
|
||||||
|
Promise.resolve({ ok: false, status: 401, text: () => Promise.resolve('Unauthorized') })
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
() => provider.complete('system', 'user'),
|
||||||
|
(err) => {
|
||||||
|
assert.match(err.message, /MiniMax API 401/);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse successful response', async () => {
|
||||||
|
const provider = new MiniMaxProvider({ apiKey: 'sk-test' });
|
||||||
|
const mockResponse = {
|
||||||
|
choices: [{ message: { content: 'Hello from MiniMax' } }],
|
||||||
|
usage: { prompt_tokens: 10, completion_tokens: 5 },
|
||||||
|
model: 'MiniMax-M2.5',
|
||||||
|
};
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = mock.fn(() =>
|
||||||
|
Promise.resolve({ ok: true, json: () => Promise.resolve(mockResponse) })
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const result = await provider.complete('You are helpful.', 'Say hello');
|
||||||
|
assert.equal(result.text, 'Hello from MiniMax');
|
||||||
|
assert.equal(result.usage.inputTokens, 10);
|
||||||
|
assert.equal(result.usage.outputTokens, 5);
|
||||||
|
assert.equal(result.model, 'MiniMax-M2.5');
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send correct request format', async () => {
|
||||||
|
const provider = new MiniMaxProvider({ apiKey: 'sk-test-key', model: 'MiniMax-M2.5' });
|
||||||
|
let capturedUrl, capturedOpts;
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = mock.fn((url, opts) => {
|
||||||
|
capturedUrl = url;
|
||||||
|
capturedOpts = opts;
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
choices: [{ message: { content: 'ok' } }],
|
||||||
|
usage: { prompt_tokens: 1, completion_tokens: 1 },
|
||||||
|
model: 'MiniMax-M2.5',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await provider.complete('system prompt', 'user message', { maxTokens: 2048 });
|
||||||
|
assert.equal(capturedUrl, 'https://api.minimax.io/v1/chat/completions');
|
||||||
|
assert.equal(capturedOpts.method, 'POST');
|
||||||
|
const headers = capturedOpts.headers;
|
||||||
|
assert.equal(headers['Content-Type'], 'application/json');
|
||||||
|
assert.equal(headers['Authorization'], 'Bearer sk-test-key');
|
||||||
|
const body = JSON.parse(capturedOpts.body);
|
||||||
|
assert.equal(body.model, 'MiniMax-M2.5');
|
||||||
|
assert.equal(body.max_tokens, 2048);
|
||||||
|
assert.equal(body.messages[0].role, 'system');
|
||||||
|
assert.equal(body.messages[0].content, 'system prompt');
|
||||||
|
assert.equal(body.messages[1].role, 'user');
|
||||||
|
assert.equal(body.messages[1].content, 'user message');
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty response gracefully', async () => {
|
||||||
|
const provider = new MiniMaxProvider({ apiKey: 'sk-test' });
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = mock.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ choices: [], usage: {} }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const result = await provider.complete('sys', 'user');
|
||||||
|
assert.equal(result.text, '');
|
||||||
|
assert.equal(result.usage.inputTokens, 0);
|
||||||
|
assert.equal(result.usage.outputTokens, 0);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Factory Tests ───
|
||||||
|
|
||||||
|
describe('createLLMProvider — minimax', () => {
|
||||||
|
it('should create MiniMaxProvider for provider=minimax', () => {
|
||||||
|
const provider = createLLMProvider({ provider: 'minimax', apiKey: 'sk-test', model: null });
|
||||||
|
assert.ok(provider instanceof MiniMaxProvider);
|
||||||
|
assert.equal(provider.name, 'minimax');
|
||||||
|
assert.equal(provider.isConfigured, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be case-insensitive', () => {
|
||||||
|
const provider = createLLMProvider({ provider: 'MiniMax', apiKey: 'sk-test', model: null });
|
||||||
|
assert.ok(provider instanceof MiniMaxProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for empty provider', () => {
|
||||||
|
const provider = createLLMProvider({ provider: null, apiKey: 'sk-test', model: null });
|
||||||
|
assert.equal(provider, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user