morpheus-cli 0.1.6 → 0.1.8
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/README.md +7 -0
- package/dist/channels/telegram.js +5 -0
- package/dist/cli/index.js +4 -0
- package/dist/config/schemas.js +16 -7
- package/dist/http/api.js +11 -0
- package/dist/runtime/__tests__/agent_persistence.test.js +39 -33
- package/dist/runtime/agent.js +14 -1
- package/dist/runtime/memory/sqlite.js +53 -6
- package/dist/runtime/providers/factory.js +3 -2
- package/dist/runtime/tools/analytics-tools.js +30 -0
- package/dist/runtime/tools/factory.js +9 -9
- package/dist/types/stats.js +1 -0
- package/dist/ui/assets/index-CMGsLiNG.css +1 -0
- package/dist/ui/assets/index-DpbRiL07.js +50 -0
- package/dist/ui/index.html +2 -2
- package/package.json +5 -2
- package/dist/ui/assets/index-4kQpg2wK.js +0 -50
- package/dist/ui/assets/index-CwL7mn36.css +0 -1
package/README.md
CHANGED
|
@@ -115,6 +115,9 @@ When enabled:
|
|
|
115
115
|
### 🧩 MCP Support (Model Context Protocol)
|
|
116
116
|
Full integration with [Model Context Protocol](https://modelcontextprotocol.io/), allowing Morpheus to use standardized tools from any MCP-compatible server.
|
|
117
117
|
|
|
118
|
+
### 📊 Usage Analytics
|
|
119
|
+
Track your token usage across different providers and models directly from the Web UI. View detailed breakdowns of input/output tokens and message counts to monitor costs and activity.
|
|
120
|
+
|
|
118
121
|
### 🎙️ Audio Transcription (Telegram)
|
|
119
122
|
Send voice messages directly to the Telegram bot. Morpheus will:
|
|
120
123
|
1. Transcribe the audio using **Google Gemini**.
|
|
@@ -213,6 +216,10 @@ Morpheus supports external tools via **MCP (Model Context Protocol)**. Configure
|
|
|
213
216
|
"COOLIFY_URL": "https://app.coolify.io",
|
|
214
217
|
"COOLIFY_TOKEN": "your-token"
|
|
215
218
|
}
|
|
219
|
+
},
|
|
220
|
+
"coingecko": {
|
|
221
|
+
"transport": "http",
|
|
222
|
+
"url": "https://mcps.mnunes.xyz/coingecko/mcp"
|
|
216
223
|
}
|
|
217
224
|
}
|
|
218
225
|
```
|
|
@@ -7,6 +7,7 @@ import os from 'os';
|
|
|
7
7
|
import { ConfigManager } from '../config/manager.js';
|
|
8
8
|
import { DisplayManager } from '../runtime/display.js';
|
|
9
9
|
import { AudioAgent } from '../runtime/audio-agent.js';
|
|
10
|
+
import { convert } from 'telegram-markdown-v2';
|
|
10
11
|
export class TelegramAdapter {
|
|
11
12
|
bot = null;
|
|
12
13
|
isConnected = false;
|
|
@@ -22,6 +23,10 @@ export class TelegramAdapter {
|
|
|
22
23
|
this.display.log('Telegram adapter already connected.', { source: 'Telegram', level: 'warning' });
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
26
|
+
const escapeMarkdownV2 = (text) => {
|
|
27
|
+
// Regex matches all special characters requiring escaping
|
|
28
|
+
return convert(text);
|
|
29
|
+
};
|
|
25
30
|
try {
|
|
26
31
|
this.display.log('Connecting to Telegram...', { source: 'Telegram' });
|
|
27
32
|
this.bot = new Telegraf(token);
|
package/dist/cli/index.js
CHANGED
|
@@ -40,3 +40,7 @@ export async function cli() {
|
|
|
40
40
|
program.addCommand(doctorCommand);
|
|
41
41
|
program.parse(process.argv);
|
|
42
42
|
}
|
|
43
|
+
// Support direct execution via tsx
|
|
44
|
+
if (import.meta.url.startsWith('file:') && (process.argv[1]?.endsWith('index.ts') || process.argv[1]?.endsWith('cli/index.js'))) {
|
|
45
|
+
cli();
|
|
46
|
+
}
|
package/dist/config/schemas.js
CHANGED
|
@@ -45,13 +45,22 @@ export const ConfigSchema = z.object({
|
|
|
45
45
|
retention: z.string().default(DEFAULT_CONFIG.logging.retention),
|
|
46
46
|
}).default(DEFAULT_CONFIG.logging),
|
|
47
47
|
});
|
|
48
|
-
export const MCPServerConfigSchema = z.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
export const MCPServerConfigSchema = z.discriminatedUnion('transport', [
|
|
49
|
+
z.object({
|
|
50
|
+
transport: z.literal('stdio'),
|
|
51
|
+
command: z.string().min(1, 'Command is required for stdio transport'),
|
|
52
|
+
args: z.array(z.string()).optional().default([]),
|
|
53
|
+
env: z.record(z.string(), z.string()).optional().default({}),
|
|
54
|
+
_comment: z.string().optional(),
|
|
55
|
+
}),
|
|
56
|
+
z.object({
|
|
57
|
+
transport: z.literal('http'),
|
|
58
|
+
url: z.string().url('Valid URL is required for http transport'),
|
|
59
|
+
args: z.array(z.string()).optional().default([]),
|
|
60
|
+
env: z.record(z.string(), z.string()).optional().default({}),
|
|
61
|
+
_comment: z.string().optional(),
|
|
62
|
+
}),
|
|
63
|
+
]);
|
|
55
64
|
export const MCPConfigFileSchema = z.record(z.string(), z.union([
|
|
56
65
|
MCPServerConfigSchema,
|
|
57
66
|
z.string(),
|
package/dist/http/api.js
CHANGED
|
@@ -51,6 +51,17 @@ export function createApiRouter() {
|
|
|
51
51
|
res.status(500).json({ error: error.message });
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
|
+
router.get('/stats/usage/grouped', async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const history = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
57
|
+
const stats = await history.getUsageStatsByProviderAndModel();
|
|
58
|
+
history.close();
|
|
59
|
+
res.json(stats);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
res.status(500).json({ error: error.message });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
54
65
|
// Calculate diff between two objects
|
|
55
66
|
const getDiff = (obj1, obj2, prefix = '') => {
|
|
56
67
|
const changes = [];
|
|
@@ -6,51 +6,45 @@ import { AIMessage } from "@langchain/core/messages";
|
|
|
6
6
|
import * as fs from "fs-extra";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { tmpdir } from "os";
|
|
9
|
-
import {
|
|
9
|
+
import { ToolsFactory } from "../tools/factory.js";
|
|
10
10
|
vi.mock("../providers/factory.js");
|
|
11
|
+
vi.mock("../tools/factory.js");
|
|
11
12
|
describe("Agent Persistence Integration", () => {
|
|
12
13
|
let agent;
|
|
13
14
|
let testDbPath;
|
|
15
|
+
let tempDir;
|
|
14
16
|
const mockProvider = {
|
|
15
17
|
invoke: vi.fn(),
|
|
16
18
|
};
|
|
17
19
|
beforeEach(async () => {
|
|
18
20
|
vi.resetAllMocks();
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
if (fs.existsSync(defaultDbPath)) {
|
|
22
|
-
try {
|
|
23
|
-
const Database = (await import("better-sqlite3")).default;
|
|
24
|
-
const db = new Database(defaultDbPath);
|
|
25
|
-
db.exec("DELETE FROM messages");
|
|
26
|
-
db.close();
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
// Ignore errors if database doesn't exist or is corrupted
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
// Create a temporary test database path
|
|
33
|
-
const tempDir = path.join(tmpdir(), "morpheus-test-agent", Date.now().toString());
|
|
21
|
+
// Create a unique temporary test database path for each test to avoid interference
|
|
22
|
+
tempDir = path.join(tmpdir(), "morpheus-test-agent", Date.now().toString() + Math.random().toString(36).substring(7));
|
|
34
23
|
testDbPath = path.join(tempDir, "short-memory.db");
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
25
|
+
return {
|
|
26
|
+
messages: [...messages, new AIMessage("Test response")]
|
|
27
|
+
};
|
|
28
|
+
});
|
|
38
29
|
vi.mocked(ProviderFactory.create).mockResolvedValue(mockProvider);
|
|
39
|
-
|
|
30
|
+
vi.mocked(ToolsFactory.create).mockResolvedValue([]);
|
|
31
|
+
agent = new Agent(DEFAULT_CONFIG, { databasePath: testDbPath });
|
|
40
32
|
});
|
|
41
33
|
afterEach(async () => {
|
|
42
|
-
// Clean up test
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
// Clean up temporary test directory
|
|
35
|
+
if (fs.existsSync(tempDir)) {
|
|
36
|
+
try {
|
|
37
|
+
fs.removeSync(tempDir);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
// Ignore removal errors if file is locked
|
|
41
|
+
}
|
|
47
42
|
}
|
|
48
43
|
});
|
|
49
44
|
describe("Database File Creation", () => {
|
|
50
45
|
it("should create database file on initialization", async () => {
|
|
51
46
|
await agent.initialize();
|
|
52
|
-
|
|
53
|
-
expect(fs.existsSync(defaultDbPath)).toBe(true);
|
|
47
|
+
expect(fs.existsSync(testDbPath)).toBe(true);
|
|
54
48
|
});
|
|
55
49
|
});
|
|
56
50
|
describe("Message Persistence", () => {
|
|
@@ -70,8 +64,8 @@ describe("Agent Persistence Integration", () => {
|
|
|
70
64
|
await agent.chat("Remember this message");
|
|
71
65
|
const firstHistory = await agent.getHistory();
|
|
72
66
|
expect(firstHistory).toHaveLength(2);
|
|
73
|
-
// Simulate restart: create new agent instance
|
|
74
|
-
const agent2 = new Agent(DEFAULT_CONFIG);
|
|
67
|
+
// Simulate restart: create new agent instance with SAME database path
|
|
68
|
+
const agent2 = new Agent(DEFAULT_CONFIG, { databasePath: testDbPath });
|
|
75
69
|
await agent2.initialize();
|
|
76
70
|
// Verify history was restored
|
|
77
71
|
const restoredHistory = await agent2.getHistory();
|
|
@@ -86,7 +80,11 @@ describe("Agent Persistence Integration", () => {
|
|
|
86
80
|
// First conversation
|
|
87
81
|
await agent.chat("First message");
|
|
88
82
|
// Second conversation
|
|
89
|
-
mockProvider.invoke.
|
|
83
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
84
|
+
return {
|
|
85
|
+
messages: [...messages, new AIMessage("Second response")]
|
|
86
|
+
};
|
|
87
|
+
});
|
|
90
88
|
await agent.chat("Second message");
|
|
91
89
|
// Verify all messages are persisted
|
|
92
90
|
const history = await agent.getHistory();
|
|
@@ -118,7 +116,11 @@ describe("Agent Persistence Integration", () => {
|
|
|
118
116
|
await agent.chat("Old message");
|
|
119
117
|
await agent.clearMemory();
|
|
120
118
|
// Add new message
|
|
121
|
-
mockProvider.invoke.
|
|
119
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
120
|
+
return {
|
|
121
|
+
messages: [...messages, new AIMessage("New response")]
|
|
122
|
+
};
|
|
123
|
+
});
|
|
122
124
|
await agent.chat("New message");
|
|
123
125
|
// Verify only new messages exist
|
|
124
126
|
const history = await agent.getHistory();
|
|
@@ -133,11 +135,15 @@ describe("Agent Persistence Integration", () => {
|
|
|
133
135
|
// First message
|
|
134
136
|
await agent.chat("My name is Alice");
|
|
135
137
|
// Second message (should have context)
|
|
136
|
-
mockProvider.invoke.
|
|
138
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
139
|
+
return {
|
|
140
|
+
messages: [...messages, new AIMessage("Hello Alice!")]
|
|
141
|
+
};
|
|
142
|
+
});
|
|
137
143
|
await agent.chat("What is my name?");
|
|
138
144
|
// Verify the provider was called with full history
|
|
139
145
|
const lastCall = mockProvider.invoke.mock.calls[1];
|
|
140
|
-
const messagesPassedToLLM = lastCall[0];
|
|
146
|
+
const messagesPassedToLLM = lastCall[0].messages;
|
|
141
147
|
// Should include: system message, previous user message, previous AI response, new user message
|
|
142
148
|
expect(messagesPassedToLLM.length).toBeGreaterThanOrEqual(4);
|
|
143
149
|
// Check that context includes the original message
|
package/dist/runtime/agent.js
CHANGED
|
@@ -10,8 +10,10 @@ export class Agent {
|
|
|
10
10
|
config;
|
|
11
11
|
history;
|
|
12
12
|
display = DisplayManager.getInstance();
|
|
13
|
-
|
|
13
|
+
databasePath;
|
|
14
|
+
constructor(config, overrides) {
|
|
14
15
|
this.config = config || ConfigManager.getInstance().get();
|
|
16
|
+
this.databasePath = overrides?.databasePath;
|
|
15
17
|
}
|
|
16
18
|
async initialize() {
|
|
17
19
|
if (!this.config.llm) {
|
|
@@ -32,6 +34,7 @@ export class Agent {
|
|
|
32
34
|
// Initialize persistent memory with SQLite
|
|
33
35
|
this.history = new SQLiteChatMessageHistory({
|
|
34
36
|
sessionId: "default",
|
|
37
|
+
databasePath: this.databasePath,
|
|
35
38
|
limit: this.config.memory?.limit || 100, // Fallback purely defensive if config type allows optional
|
|
36
39
|
});
|
|
37
40
|
}
|
|
@@ -52,6 +55,11 @@ export class Agent {
|
|
|
52
55
|
try {
|
|
53
56
|
this.display.log('Processing message...', { source: 'Agent' });
|
|
54
57
|
const userMessage = new HumanMessage(message);
|
|
58
|
+
// Inject provider/model metadata for persistence
|
|
59
|
+
userMessage.provider_metadata = {
|
|
60
|
+
provider: this.config.llm.provider,
|
|
61
|
+
model: this.config.llm.model
|
|
62
|
+
};
|
|
55
63
|
// Attach extra usage (e.g. from Audio) to the user message to be persisted
|
|
56
64
|
if (extraUsage) {
|
|
57
65
|
userMessage.usage_metadata = extraUsage;
|
|
@@ -134,6 +142,11 @@ export class Agent {
|
|
|
134
142
|
await this.history.addMessage(userMessage);
|
|
135
143
|
// Persist all new intermediate tool calls and responses
|
|
136
144
|
for (const msg of newGeneratedMessages) {
|
|
145
|
+
// Inject provider/model metadata search interactors
|
|
146
|
+
msg.provider_metadata = {
|
|
147
|
+
provider: this.config.llm.provider,
|
|
148
|
+
model: this.config.llm.model
|
|
149
|
+
};
|
|
137
150
|
await this.history.addMessage(msg);
|
|
138
151
|
}
|
|
139
152
|
this.display.log('Response generated.', { source: 'Agent' });
|
|
@@ -86,7 +86,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
86
86
|
input_tokens INTEGER,
|
|
87
87
|
output_tokens INTEGER,
|
|
88
88
|
total_tokens INTEGER,
|
|
89
|
-
cache_read_tokens INTEGER
|
|
89
|
+
cache_read_tokens INTEGER,
|
|
90
|
+
provider TEXT,
|
|
91
|
+
model TEXT
|
|
90
92
|
);
|
|
91
93
|
|
|
92
94
|
CREATE INDEX IF NOT EXISTS idx_messages_session_id
|
|
@@ -109,12 +111,16 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
109
111
|
'input_tokens',
|
|
110
112
|
'output_tokens',
|
|
111
113
|
'total_tokens',
|
|
112
|
-
'cache_read_tokens'
|
|
114
|
+
'cache_read_tokens',
|
|
115
|
+
'provider',
|
|
116
|
+
'model'
|
|
113
117
|
];
|
|
118
|
+
const integerColumns = new Set(['input_tokens', 'output_tokens', 'total_tokens', 'cache_read_tokens']);
|
|
114
119
|
for (const col of newColumns) {
|
|
115
120
|
if (!columns.has(col)) {
|
|
116
121
|
try {
|
|
117
|
-
|
|
122
|
+
const type = integerColumns.has(col) ? 'INTEGER' : 'TEXT';
|
|
123
|
+
this.db.exec(`ALTER TABLE messages ADD COLUMN ${col} ${type}`);
|
|
118
124
|
}
|
|
119
125
|
catch (e) {
|
|
120
126
|
// Ignore error if column already exists (race condition or check failed)
|
|
@@ -134,7 +140,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
134
140
|
async getMessages() {
|
|
135
141
|
try {
|
|
136
142
|
// Fetch new columns
|
|
137
|
-
const stmt = this.db.prepare("SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
|
|
143
|
+
const stmt = this.db.prepare("SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
|
|
138
144
|
const rows = stmt.all(this.sessionId, this.limit);
|
|
139
145
|
return rows.map((row) => {
|
|
140
146
|
let msg;
|
|
@@ -145,6 +151,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
145
151
|
total_tokens: row.total_tokens || 0,
|
|
146
152
|
input_token_details: row.cache_read_tokens ? { cache_read: row.cache_read_tokens } : undefined
|
|
147
153
|
} : undefined;
|
|
154
|
+
// Reconstruct provider metadata
|
|
155
|
+
const provider_metadata = row.provider ? {
|
|
156
|
+
provider: row.provider,
|
|
157
|
+
model: row.model || "unknown"
|
|
158
|
+
} : undefined;
|
|
148
159
|
switch (row.type) {
|
|
149
160
|
case "human":
|
|
150
161
|
msg = new HumanMessage(row.content);
|
|
@@ -190,6 +201,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
190
201
|
if (usage_metadata) {
|
|
191
202
|
msg.usage_metadata = usage_metadata;
|
|
192
203
|
}
|
|
204
|
+
if (provider_metadata) {
|
|
205
|
+
msg.provider_metadata = provider_metadata;
|
|
206
|
+
}
|
|
193
207
|
return msg;
|
|
194
208
|
});
|
|
195
209
|
}
|
|
@@ -237,6 +251,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
237
251
|
const outputTokens = usage?.output_tokens ?? null;
|
|
238
252
|
const totalTokens = usage?.total_tokens ?? null;
|
|
239
253
|
const cacheReadTokens = usage?.input_token_details?.cache_read ?? usage?.cache_read_tokens ?? null;
|
|
254
|
+
// Extract provider metadata
|
|
255
|
+
const provider = anyMsg.provider_metadata?.provider ?? null;
|
|
256
|
+
const model = anyMsg.provider_metadata?.model ?? null;
|
|
240
257
|
// Handle special content serialization for Tools
|
|
241
258
|
let finalContent = "";
|
|
242
259
|
if (type === 'ai' && (message.tool_calls?.length ?? 0) > 0) {
|
|
@@ -259,8 +276,8 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
259
276
|
? message.content
|
|
260
277
|
: JSON.stringify(message.content);
|
|
261
278
|
}
|
|
262
|
-
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
263
|
-
stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens);
|
|
279
|
+
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
280
|
+
stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens, provider, model);
|
|
264
281
|
}
|
|
265
282
|
catch (error) {
|
|
266
283
|
// Check for specific SQLite errors
|
|
@@ -294,6 +311,36 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
294
311
|
throw new Error(`Failed to get usage stats: ${error}`);
|
|
295
312
|
}
|
|
296
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Retrieves aggregated usage statistics grouped by provider and model.
|
|
316
|
+
*/
|
|
317
|
+
async getUsageStatsByProviderAndModel() {
|
|
318
|
+
try {
|
|
319
|
+
const stmt = this.db.prepare(`SELECT
|
|
320
|
+
provider,
|
|
321
|
+
COALESCE(model, 'unknown') as model,
|
|
322
|
+
SUM(input_tokens) as totalInputTokens,
|
|
323
|
+
SUM(output_tokens) as totalOutputTokens,
|
|
324
|
+
SUM(total_tokens) as totalTokens,
|
|
325
|
+
COUNT(*) as messageCount
|
|
326
|
+
FROM messages
|
|
327
|
+
WHERE provider IS NOT NULL
|
|
328
|
+
GROUP BY provider, COALESCE(model, 'unknown')
|
|
329
|
+
ORDER BY provider, model`);
|
|
330
|
+
const rows = stmt.all();
|
|
331
|
+
return rows.map((row) => ({
|
|
332
|
+
provider: row.provider,
|
|
333
|
+
model: row.model,
|
|
334
|
+
totalInputTokens: row.totalInputTokens || 0,
|
|
335
|
+
totalOutputTokens: row.totalOutputTokens || 0,
|
|
336
|
+
totalTokens: row.totalTokens || 0,
|
|
337
|
+
messageCount: row.messageCount || 0
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
throw new Error(`Failed to get grouped usage stats: ${error}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
297
344
|
/**
|
|
298
345
|
* Clears all messages for the current session from the database.
|
|
299
346
|
*/
|
|
@@ -7,7 +7,7 @@ import { createAgent, createMiddleware } from "langchain";
|
|
|
7
7
|
// import { MultiServerMCPClient, } from "@langchain/mcp-adapters"; // REMOVED
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { DisplayManager } from "../display.js";
|
|
10
|
-
import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool } from "../tools/index.js";
|
|
10
|
+
import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool, ProviderModelUsageTool } from "../tools/index.js";
|
|
11
11
|
export class ProviderFactory {
|
|
12
12
|
static async create(config, tools = []) {
|
|
13
13
|
let display = DisplayManager.getInstance();
|
|
@@ -72,7 +72,8 @@ export class ProviderFactory {
|
|
|
72
72
|
ConfigUpdateTool,
|
|
73
73
|
DiagnosticTool,
|
|
74
74
|
MessageCountTool,
|
|
75
|
-
TokenUsageTool
|
|
75
|
+
TokenUsageTool,
|
|
76
|
+
ProviderModelUsageTool
|
|
76
77
|
];
|
|
77
78
|
return createAgent({
|
|
78
79
|
model: model,
|
|
@@ -34,6 +34,36 @@ export const MessageCountTool = tool(async ({ timeRange }) => {
|
|
|
34
34
|
}).optional(),
|
|
35
35
|
}),
|
|
36
36
|
});
|
|
37
|
+
// Tool for querying token usage grouped by provider and model
|
|
38
|
+
export const ProviderModelUsageTool = tool(async () => {
|
|
39
|
+
try {
|
|
40
|
+
const db = new Database(dbPath);
|
|
41
|
+
const query = `
|
|
42
|
+
SELECT
|
|
43
|
+
provider,
|
|
44
|
+
COALESCE(model, 'unknown') as model,
|
|
45
|
+
SUM(input_tokens) as totalInputTokens,
|
|
46
|
+
SUM(output_tokens) as totalOutputTokens,
|
|
47
|
+
SUM(total_tokens) as totalTokens,
|
|
48
|
+
COUNT(*) as messageCount
|
|
49
|
+
FROM messages
|
|
50
|
+
WHERE provider IS NOT NULL
|
|
51
|
+
GROUP BY provider, COALESCE(model, 'unknown')
|
|
52
|
+
ORDER BY provider, model
|
|
53
|
+
`;
|
|
54
|
+
const results = db.prepare(query).all();
|
|
55
|
+
db.close();
|
|
56
|
+
return JSON.stringify(results);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error("Error in ProviderModelUsageTool:", error);
|
|
60
|
+
return JSON.stringify({ error: `Failed to get provider usage stats: ${error.message}` });
|
|
61
|
+
}
|
|
62
|
+
}, {
|
|
63
|
+
name: "provider_model_usage",
|
|
64
|
+
description: "Returns token usage statistics grouped by provider and model.",
|
|
65
|
+
schema: z.object({}),
|
|
66
|
+
});
|
|
37
67
|
// Tool for querying token usage statistics from the database
|
|
38
68
|
export const TokenUsageTool = tool(async ({ timeRange }) => {
|
|
39
69
|
try {
|
|
@@ -53,15 +53,15 @@ export class ToolsFactory {
|
|
|
53
53
|
mcpServers: mcpServers,
|
|
54
54
|
onConnectionError: "ignore",
|
|
55
55
|
// log the MCP client's internal events
|
|
56
|
-
beforeToolCall: ({ serverName, name, args }) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
},
|
|
60
|
-
// log the results of tool calls
|
|
61
|
-
afterToolCall: (res) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
56
|
+
// beforeToolCall: ({ serverName, name, args }) => {
|
|
57
|
+
// display.log(`MCP Tool Call - Server: ${serverName}, Tool: ${name}, Args: ${JSON.stringify(args)}`, { source: 'MCPServer' });
|
|
58
|
+
// return;
|
|
59
|
+
// },
|
|
60
|
+
// // log the results of tool calls
|
|
61
|
+
// afterToolCall: (res) => {
|
|
62
|
+
// display.log(`MCP Tool Result - ${JSON.stringify(res)}`, { source: 'MCPServer' });
|
|
63
|
+
// return;
|
|
64
|
+
// }
|
|
65
65
|
});
|
|
66
66
|
try {
|
|
67
67
|
const tools = await client.getTools();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:Courier New,Courier,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));font-family:Courier New,Courier,monospace;--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}body:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(0 143 17 / var(--tw-text-opacity, 1))}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}:is(.dark *)::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}:is(.dark *)::-webkit-scrollbar-thumb{--tw-bg-opacity: 1;background-color:rgb(0 59 0 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}:is(.dark *)::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(0 143 17 / var(--tw-bg-opacity, 1))}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.static{position:static}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.top-0{top:0}.z-10{z-index:10}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mt-1{margin-top:.25rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-11{width:2.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-full{width:100%}.max-w-4xl{max-width:56rem}.max-w-6xl{max-width:72rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.translate-x-1{--tw-translate-x: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-6{--tw-translate-x: 1.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity, 1))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.border{border-width:1px}.border-x{border-left-width:1px;border-right-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-green-500{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.border-green-800{--tw-border-opacity: 1;border-color:rgb(22 101 52 / var(--tw-border-opacity, 1))}.border-matrix-highlight{--tw-border-opacity: 1;border-color:rgb(0 255 65 / var(--tw-border-opacity, 1))}.border-matrix-primary{--tw-border-opacity: 1;border-color:rgb(0 59 0 / var(--tw-border-opacity, 1))}.border-matrix-primary\/50{border-color:#003b0080}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-500\/50{border-color:#ef444480}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.bg-green-900\/20{background-color:#14532d33}.bg-matrix-base{--tw-bg-opacity: 1;background-color:rgb(13 2 8 / var(--tw-bg-opacity, 1))}.bg-matrix-base\/50{background-color:#0d020880}.bg-matrix-highlight{--tw-bg-opacity: 1;background-color:rgb(0 255 65 / var(--tw-bg-opacity, 1))}.bg-matrix-highlight\/10{background-color:#00ff411a}.bg-matrix-primary{--tw-bg-opacity: 1;background-color:rgb(0 59 0 / var(--tw-bg-opacity, 1))}.bg-matrix-primary\/20{background-color:#003b0033}.bg-matrix-primary\/50{background-color:#003b0080}.bg-red-900\/10{background-color:#7f1d1d1a}.bg-red-900\/20{background-color:#7f1d1d33}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1))}.bg-zinc-950{--tw-bg-opacity: 1;background-color:rgb(12 12 12 / var(--tw-bg-opacity, 1))}.bg-zinc-950\/50{background-color:#0c0c0c80}.bg-\[linear-gradient\(rgba\(18\,16\,16\,0\)_50\%\,rgba\(0\,0\,0\,0\.1\)_50\%\)\,linear-gradient\(90deg\,rgba\(0\,255\,0\,0\.03\)\,rgba\(0\,255\,0\,0\.01\)\)\]{background-image:linear-gradient(#12101000 50%,#0000001a 50%),linear-gradient(90deg,#00ff0008,#00ff0003)}.bg-\[length\:100\%_2px\,3px_100\%\]{background-size:100% 2px,3px 100%}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-px{padding-bottom:1px}.pl-4{padding-left:1rem}.pr-4{padding-right:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:Courier New,Courier,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tracking-tighter{letter-spacing:-.05em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-matrix-highlight{--tw-text-opacity: 1;color:rgb(0 255 65 / var(--tw-text-opacity, 1))}.text-matrix-highlight\/50{color:#00ff4180}.text-matrix-highlight\/80{color:#00ff41cc}.text-matrix-primary{--tw-text-opacity: 1;color:rgb(0 59 0 / var(--tw-text-opacity, 1))}.text-matrix-secondary{--tw-text-opacity: 1;color:rgb(0 143 17 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.placeholder-green-900::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(20 83 45 / var(--tw-placeholder-opacity, 1))}.placeholder-green-900::placeholder{--tw-placeholder-opacity: 1;color:rgb(20 83 45 / var(--tw-placeholder-opacity, 1))}.placeholder-matrix-secondary\/50::-moz-placeholder{color:#008f1180}.placeholder-matrix-secondary\/50::placeholder{color:#008f1180}.opacity-0{opacity:0}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow-\[0_0_15px_rgba\(34\,197\,94\,0\.3\)\]{--tw-shadow: 0 0 15px rgba(34,197,94,.3);--tw-shadow-colored: 0 0 15px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.hover\:border-matrix-highlight:hover{--tw-border-opacity: 1;border-color:rgb(0 255 65 / var(--tw-border-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:bg-matrix-highlight\/90:hover{background-color:#00ff41e6}.hover\:bg-matrix-secondary:hover{--tw-bg-opacity: 1;background-color:rgb(0 143 17 / var(--tw-bg-opacity, 1))}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:bg-zinc-900:hover{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1))}.hover\:text-matrix-highlight:hover{--tw-text-opacity: 1;color:rgb(0 255 65 / var(--tw-text-opacity, 1))}.hover\:text-red-600:hover{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.focus\:border-green-500:focus{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.focus\:border-matrix-highlight:focus{--tw-border-opacity: 1;border-color:rgb(0 255 65 / var(--tw-border-opacity, 1))}.focus\:border-red-500:focus{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-matrix-highlight:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(0 255 65 / var(--tw-ring-opacity, 1))}.focus\:ring-red-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus\:ring-offset-black:focus{--tw-ring-offset-color: #000}.group:hover .group-hover\:text-matrix-highlight{--tw-text-opacity: 1;color:rgb(0 255 65 / var(--tw-text-opacity, 1))}.dark\:divide-matrix-primary\/30:is(.dark *)>:not([hidden])~:not([hidden]){border-color:#003b004d}.dark\:border-matrix-primary:is(.dark *){--tw-border-opacity: 1;border-color:rgb(0 59 0 / var(--tw-border-opacity, 1))}.dark\:bg-black:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.dark\:bg-black\/40:is(.dark *){background-color:#0006}.dark\:bg-matrix-primary:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(0 59 0 / var(--tw-bg-opacity, 1))}.dark\:bg-zinc-900\/50:is(.dark *){background-color:#18181b80}.dark\:bg-zinc-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(12 12 12 / var(--tw-bg-opacity, 1))}.dark\:text-gray-300:is(.dark *){--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:text-matrix-highlight:is(.dark *){--tw-text-opacity: 1;color:rgb(0 255 65 / var(--tw-text-opacity, 1))}.dark\:text-matrix-secondary:is(.dark *){--tw-text-opacity: 1;color:rgb(0 143 17 / var(--tw-text-opacity, 1))}.dark\:opacity-20:is(.dark *){opacity:.2}.dark\:hover\:bg-matrix-primary\/10:hover:is(.dark *){background-color:#003b001a}.dark\:hover\:bg-matrix-primary\/50:hover:is(.dark *){background-color:#003b0080}.dark\:hover\:bg-red-900\/20:hover:is(.dark *){background-color:#7f1d1d33}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
|