@albalink/agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/bin/albacli +31 -0
- package/dist/api/ExtensionAPI.d.ts +103 -0
- package/dist/api/ExtensionAPI.js +15 -0
- package/dist/api/Registry.d.ts +54 -0
- package/dist/api/Registry.js +103 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +230 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +41 -0
- package/dist/loader.d.ts +16 -0
- package/dist/loader.js +89 -0
- package/dist/mcp/entry.d.ts +2 -0
- package/dist/mcp/entry.js +71 -0
- package/dist/mcp/server.d.ts +31 -0
- package/dist/mcp/server.js +128 -0
- package/dist/models/CostTracker.d.ts +67 -0
- package/dist/models/CostTracker.js +151 -0
- package/dist/models/ModelRegistry.d.ts +159 -0
- package/dist/models/ModelRegistry.js +500 -0
- package/dist/models/index.d.ts +5 -0
- package/dist/models/index.js +3 -0
- package/dist/runner/AgentRunner.d.ts +88 -0
- package/dist/runner/AgentRunner.js +473 -0
- package/dist/runner/ModelClient.d.ts +97 -0
- package/dist/runner/ModelClient.js +350 -0
- package/dist/runner/SwarmRouter.d.ts +64 -0
- package/dist/runner/SwarmRouter.js +216 -0
- package/dist/runner/ToolDispatcher.d.ts +29 -0
- package/dist/runner/ToolDispatcher.js +168 -0
- package/dist/scheduler/AgentScheduler.d.ts +119 -0
- package/dist/scheduler/AgentScheduler.js +263 -0
- package/dist/server-entry.d.ts +11 -0
- package/dist/server-entry.js +15 -0
- package/dist/session/ContextStore.d.ts +96 -0
- package/dist/session/ContextStore.js +207 -0
- package/dist/session/GoalManager.d.ts +101 -0
- package/dist/session/GoalManager.js +167 -0
- package/dist/session/MemoryStore.d.ts +48 -0
- package/dist/session/MemoryStore.js +167 -0
- package/dist/session/SessionManager.d.ts +64 -0
- package/dist/session/SessionManager.js +193 -0
- package/dist/telemetry/Tracer.d.ts +48 -0
- package/dist/telemetry/Tracer.js +102 -0
- package/dist/tools/MarketSentiment.d.ts +166 -0
- package/dist/tools/MarketSentiment.js +209 -0
- package/dist/tools/NewsSentiment.d.ts +67 -0
- package/dist/tools/NewsSentiment.js +226 -0
- package/dist/tools/PriceFeed.d.ts +105 -0
- package/dist/tools/PriceFeed.js +282 -0
- package/dist/tools/TechnicalAnalysis.d.ts +110 -0
- package/dist/tools/TechnicalAnalysis.js +357 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +4 -0
- package/dist/tui/App.d.ts +17 -0
- package/dist/tui/App.js +225 -0
- package/dist/tui/ModelSelector.d.ts +12 -0
- package/dist/tui/ModelSelector.js +87 -0
- package/dist/tui/REPL.d.ts +24 -0
- package/dist/tui/REPL.js +51 -0
- package/dist/tui/StatusBar.d.ts +14 -0
- package/dist/tui/StatusBar.js +14 -0
- package/dist/tui/theme.d.ts +30 -0
- package/dist/tui/theme.js +41 -0
- package/dist/util/safeLog.d.ts +3 -0
- package/dist/util/safeLog.js +21 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @alba/agent — public API
|
|
3
|
+
*
|
|
4
|
+
* Extension files import from here:
|
|
5
|
+
* import { Type } from "@alba/agent"
|
|
6
|
+
* import type { ExtensionAPI } from "@alba/agent"
|
|
7
|
+
*/
|
|
8
|
+
// Re-export TypeBox Type builder — drop-in for Pi's @earendil-works/pi-ai
|
|
9
|
+
export { Type } from "@sinclair/typebox";
|
|
10
|
+
// text() helper — same as what Pi provides
|
|
11
|
+
export { text } from "./api/ExtensionAPI.js";
|
|
12
|
+
// Registry (for advanced use / testing)
|
|
13
|
+
export { Registry } from "./api/Registry.js";
|
|
14
|
+
// Runner components
|
|
15
|
+
export { AgentRunner } from "./runner/AgentRunner.js";
|
|
16
|
+
export { ModelClient, resolveModelConfig } from "./runner/ModelClient.js";
|
|
17
|
+
export { ToolDispatcher } from "./runner/ToolDispatcher.js";
|
|
18
|
+
// Loader (for embedding the agent in other tools)
|
|
19
|
+
export { loadExtension } from "./loader.js";
|
|
20
|
+
// Theme
|
|
21
|
+
export { makeTheme, T, ALBA_COLORS } from "./tui/theme.js";
|
|
22
|
+
// Model intelligence
|
|
23
|
+
export { ModelRegistry, modelRegistry, classifyModel } from "./models/ModelRegistry.js";
|
|
24
|
+
export { CostTracker } from "./models/CostTracker.js";
|
|
25
|
+
// Tools
|
|
26
|
+
export { fullAnalysis, rsi, macd, ema, sma, bollingerBands, atr, getCandlesTool, getCandlesParams } from "./tools/TechnicalAnalysis.js";
|
|
27
|
+
export { PriceFeed, priceFeed, getPricesTool, topMoversTool, marketOverviewTool } from "./tools/PriceFeed.js";
|
|
28
|
+
export { NewsFeed, newsFeed, getNewsTool, scoreSentiment } from "./tools/NewsSentiment.js";
|
|
29
|
+
export { getFearGreedTool, fundingRatesParams, getFundingRatesTool, getBtcMempoolTool, getDefiTvlTool, getSolanaStatsTool, } from "./tools/MarketSentiment.js";
|
|
30
|
+
// Session / Memory / Goals / Context
|
|
31
|
+
export { SessionManager } from "./session/SessionManager.js";
|
|
32
|
+
export { MemoryStore, memoryStore } from "./session/MemoryStore.js";
|
|
33
|
+
export { GoalManager, goalManager } from "./session/GoalManager.js";
|
|
34
|
+
export { ContextStore, contextStore } from "./session/ContextStore.js";
|
|
35
|
+
// MCP server
|
|
36
|
+
export { MCPServer } from "./mcp/server.js";
|
|
37
|
+
// Scheduler
|
|
38
|
+
export { AgentScheduler, agentScheduler } from "./scheduler/AgentScheduler.js";
|
|
39
|
+
// Telemetry
|
|
40
|
+
export { Tracer } from "./telemetry/Tracer.js";
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loader.ts — loads an extension file and wires it to a Registry.
|
|
3
|
+
*
|
|
4
|
+
* Implements the full ExtensionAPI including Pi compat methods:
|
|
5
|
+
* ui.setStatus, ui.setTheme, ui.setHeader, ctx.hasUI,
|
|
6
|
+
* session_shutdown, before_agent_start
|
|
7
|
+
*/
|
|
8
|
+
import { Registry } from "./api/Registry.js";
|
|
9
|
+
export interface LoaderOptions {
|
|
10
|
+
onStatusUpdate?: (key: string, value: string) => void;
|
|
11
|
+
onNotify?: (message: string) => void;
|
|
12
|
+
/** Called when the extension calls ui.showModelSelector(query). */
|
|
13
|
+
onShowModelSelector?: (query?: string) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function loadExtension(extensionPath: string, registry: Registry, opts?: LoaderOptions): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=loader.d.ts.map
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loader.ts — loads an extension file and wires it to a Registry.
|
|
3
|
+
*
|
|
4
|
+
* Implements the full ExtensionAPI including Pi compat methods:
|
|
5
|
+
* ui.setStatus, ui.setTheme, ui.setHeader, ctx.hasUI,
|
|
6
|
+
* session_shutdown, before_agent_start
|
|
7
|
+
*/
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { resolve, extname } from "node:path";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { makeTheme } from "./tui/theme.js";
|
|
12
|
+
export async function loadExtension(extensionPath, registry, opts = {}) {
|
|
13
|
+
const abs = resolve(extensionPath);
|
|
14
|
+
if (!existsSync(abs)) {
|
|
15
|
+
throw new Error(`Extension file not found: ${abs}`);
|
|
16
|
+
}
|
|
17
|
+
const theme = makeTheme();
|
|
18
|
+
let _notify = opts.onNotify ?? ((_msg) => { });
|
|
19
|
+
let _setStatus = opts.onStatusUpdate ?? ((_k, _v) => { });
|
|
20
|
+
let _showModelSelector = opts.onShowModelSelector ?? ((_q) => { });
|
|
21
|
+
const ui = {
|
|
22
|
+
notify(message) { _notify(message); },
|
|
23
|
+
setStatus(key, value) { _setStatus(key, value); },
|
|
24
|
+
setTheme(_name) { },
|
|
25
|
+
setHeader(_factory) { },
|
|
26
|
+
showModelSelector(query) { _showModelSelector(query); },
|
|
27
|
+
theme,
|
|
28
|
+
};
|
|
29
|
+
const api = {
|
|
30
|
+
registerCommand(name, def) {
|
|
31
|
+
registry.addCommand(name, def);
|
|
32
|
+
},
|
|
33
|
+
registerTool(def) {
|
|
34
|
+
registry.addTool(def);
|
|
35
|
+
},
|
|
36
|
+
registerSkill(def) {
|
|
37
|
+
registry.addSkill(def);
|
|
38
|
+
},
|
|
39
|
+
on(event, handler) {
|
|
40
|
+
registry.addHook(event, handler);
|
|
41
|
+
},
|
|
42
|
+
setSystemPrompt(prompt) {
|
|
43
|
+
registry.setSystemPrompt(prompt);
|
|
44
|
+
},
|
|
45
|
+
ui,
|
|
46
|
+
};
|
|
47
|
+
const ext = extname(abs).toLowerCase();
|
|
48
|
+
let mod;
|
|
49
|
+
if (ext === ".ts") {
|
|
50
|
+
// In packaged (dist) mode Node.js cannot natively import TypeScript.
|
|
51
|
+
const jsPath = abs.replace(/\.ts$/, ".js");
|
|
52
|
+
if (existsSync(jsPath)) {
|
|
53
|
+
mod = await import(pathToFileURL(jsPath).href);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
throw new Error(`Cannot import TypeScript extension "${abs}" in packaged mode.\n` +
|
|
57
|
+
`Ensure the extension is compiled or run: npm run build`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
mod = await import(pathToFileURL(abs).href);
|
|
62
|
+
}
|
|
63
|
+
const fn = mod.default ?? mod;
|
|
64
|
+
if (typeof fn !== "function") {
|
|
65
|
+
throw new Error(`Extension must export a default function. Got: ${typeof fn} from ${abs}`);
|
|
66
|
+
}
|
|
67
|
+
// Intercept console.log/error/warn during extension load so that any
|
|
68
|
+
// stray prints in the extension (e.g. loadSkills logging) are routed
|
|
69
|
+
// through ui.notify instead of raw stdout writes that corrupt the TUI.
|
|
70
|
+
const _origLog = console.log;
|
|
71
|
+
const _origError = console.error;
|
|
72
|
+
const _origWarn = console.warn;
|
|
73
|
+
const _extLog = (...args) => {
|
|
74
|
+
const msg = args.map(a => (typeof a === "string" ? a : String(a))).join(" ");
|
|
75
|
+
ui.notify(msg);
|
|
76
|
+
};
|
|
77
|
+
console.log = _extLog;
|
|
78
|
+
console.error = _extLog;
|
|
79
|
+
console.warn = _extLog;
|
|
80
|
+
try {
|
|
81
|
+
await fn(api);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
console.log = _origLog;
|
|
85
|
+
console.error = _origError;
|
|
86
|
+
console.warn = _origWarn;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server entry — bootstraps the Registry with built-in tools
|
|
3
|
+
* and starts the MCP stdio server.
|
|
4
|
+
*/
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { config as loadDotenv } from "dotenv";
|
|
9
|
+
import { Registry } from "../api/Registry.js";
|
|
10
|
+
import { loadExtension } from "../loader.js";
|
|
11
|
+
import { MCPServer } from "./server.js";
|
|
12
|
+
import { modelRegistry } from "../models/ModelRegistry.js";
|
|
13
|
+
import { getPricesTool, topMoversTool, marketOverviewTool, getPricesParams, topMoversParams, marketOverviewParams, priceFeed, } from "../tools/PriceFeed.js";
|
|
14
|
+
import { getNewsTool, getNewsParams, newsFeed } from "../tools/NewsSentiment.js";
|
|
15
|
+
import { getCandlesParams, getCandlesTool, analyzeTAParams, fullAnalysis } from "../tools/TechnicalAnalysis.js";
|
|
16
|
+
import { getFearGreedTool, fearGreedParams, getFundingRatesTool, fundingRatesParams, getBtcMempoolTool, btcMempoolParams, getDefiTvlTool, defiTvlParams, getSolanaStatsTool, solanaStatsParams, } from "../tools/MarketSentiment.js";
|
|
17
|
+
const ALBA_HOME = process.env.ALBA_HOME ?? join(homedir(), ".alba");
|
|
18
|
+
const envPath = join(ALBA_HOME, ".env");
|
|
19
|
+
if (existsSync(envPath))
|
|
20
|
+
loadDotenv({ path: envPath, override: false });
|
|
21
|
+
const registry = new Registry();
|
|
22
|
+
// Load optional extension from --extension arg
|
|
23
|
+
const extIdx = process.argv.indexOf("--extension");
|
|
24
|
+
const extPath = extIdx >= 0 ? process.argv[extIdx + 1] : null;
|
|
25
|
+
if (extPath && existsSync(extPath)) {
|
|
26
|
+
try {
|
|
27
|
+
await loadExtension(extPath, registry);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
process.stderr.write(`[MCP] Extension load failed: ${e}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Register all built-in tools
|
|
34
|
+
await modelRegistry.initialise();
|
|
35
|
+
const mk = modelRegistry;
|
|
36
|
+
registry.addTool({ name: "list_models", label: "List Models", description: "Search available AI models.", parameters: mk["listModelsParams"], execute: (id, p) => mk["listModelsTool"](id, p) });
|
|
37
|
+
registry.addTool({ name: "get_prices", label: "Get Prices", description: "Get live crypto prices.", parameters: getPricesParams, execute: (id, p) => getPricesTool(id, p) });
|
|
38
|
+
registry.addTool({ name: "get_top_movers", label: "Top Movers", description: "Top 24h price movers.", parameters: topMoversParams, execute: (id, p) => topMoversTool(id, p) });
|
|
39
|
+
registry.addTool({ name: "market_overview", label: "Market Overview", description: "Aggregated market data.", parameters: marketOverviewParams, execute: () => marketOverviewTool() });
|
|
40
|
+
registry.addTool({ name: "get_news", label: "Get News", description: "Crypto news + sentiment.", parameters: getNewsParams, execute: (id, p) => getNewsTool(id, p) });
|
|
41
|
+
registry.addTool({ name: "get_candles", label: "Get Candles", description: "OHLCV + TA analysis from Binance.", parameters: getCandlesParams, execute: (id, p) => getCandlesTool(id, p) });
|
|
42
|
+
registry.addTool({ name: "get_fear_greed", label: "Fear & Greed", description: "Crypto fear & greed index.", parameters: fearGreedParams, execute: (id, p) => getFearGreedTool(id, p) });
|
|
43
|
+
registry.addTool({ name: "get_funding_rates", label: "Funding Rates", description: "Perp funding rates.", parameters: fundingRatesParams, execute: (id, p) => getFundingRatesTool(id, p) });
|
|
44
|
+
registry.addTool({ name: "get_btc_mempool", label: "BTC Mempool", description: "BTC mempool + fees.", parameters: btcMempoolParams, execute: () => getBtcMempoolTool() });
|
|
45
|
+
registry.addTool({ name: "get_defi_tvl", label: "DeFi TVL", description: "DeFiLlama TVL.", parameters: defiTvlParams, execute: (id, p) => getDefiTvlTool(id, p) });
|
|
46
|
+
registry.addTool({ name: "get_solana_stats", label: "Solana Stats", description: "Solana TPS + health.", parameters: solanaStatsParams, execute: () => getSolanaStatsTool() });
|
|
47
|
+
registry.addTool({
|
|
48
|
+
name: "analyze_ta", label: "Technical Analysis", description: "RSI, MACD, Bollinger on price arrays.",
|
|
49
|
+
parameters: analyzeTAParams,
|
|
50
|
+
execute: async (_id, p) => {
|
|
51
|
+
const params = p;
|
|
52
|
+
const closes = params.prices;
|
|
53
|
+
const candles = closes.map((c, i) => ({
|
|
54
|
+
timestamp: i, open: c, high: (params.highs?.[i] ?? c), low: (params.lows?.[i] ?? c), close: c, volume: (params.volumes?.[i] ?? 0),
|
|
55
|
+
}));
|
|
56
|
+
const results = fullAnalysis(candles);
|
|
57
|
+
const text = results.map(r => {
|
|
58
|
+
const s = r.signal === "bullish" ? "🟢" : r.signal === "bearish" ? "🔴" : "⚪";
|
|
59
|
+
return `${s} ${r.indicator}: ${typeof r.value === "number" ? r.value.toFixed(2) : String(r.value)}`;
|
|
60
|
+
}).join("\n");
|
|
61
|
+
return { content: [{ type: "text", text: `Technical Analysis:\n${text}` }], details: {} };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
// Start price + news feeds
|
|
65
|
+
priceFeed.track("btc", "eth", "sol", "bnb", "matic", "arb", "op", "avax");
|
|
66
|
+
priceFeed.start();
|
|
67
|
+
newsFeed.start();
|
|
68
|
+
// Start MCP server
|
|
69
|
+
const server = new MCPServer(registry);
|
|
70
|
+
await server.run();
|
|
71
|
+
//# sourceMappingURL=entry.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCPServer — Model Context Protocol server over stdio. (#28)
|
|
3
|
+
*
|
|
4
|
+
* Exposes all ALBA registered tools as MCP tools so they can be used
|
|
5
|
+
* by Claude Desktop, Cursor, Continue, and any MCP-compatible client.
|
|
6
|
+
*
|
|
7
|
+
* Protocol: JSON-RPC 2.0 over stdin/stdout (MCP stdio transport).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* alba-mcp # exposes built-in tools
|
|
11
|
+
* alba-mcp --extension ./my.ts # includes extension tools
|
|
12
|
+
*
|
|
13
|
+
* Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
|
|
14
|
+
* {
|
|
15
|
+
* "mcpServers": {
|
|
16
|
+
* "alba": {
|
|
17
|
+
* "command": "alba-mcp",
|
|
18
|
+
* "env": { "OPENROUTER_API_KEY": "sk-or-..." }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
import type { Registry } from "../api/Registry.js";
|
|
24
|
+
export declare class MCPServer {
|
|
25
|
+
private registry;
|
|
26
|
+
constructor(registry: Registry);
|
|
27
|
+
run(): Promise<void>;
|
|
28
|
+
private respond;
|
|
29
|
+
private handle;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCPServer — Model Context Protocol server over stdio. (#28)
|
|
3
|
+
*
|
|
4
|
+
* Exposes all ALBA registered tools as MCP tools so they can be used
|
|
5
|
+
* by Claude Desktop, Cursor, Continue, and any MCP-compatible client.
|
|
6
|
+
*
|
|
7
|
+
* Protocol: JSON-RPC 2.0 over stdin/stdout (MCP stdio transport).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* alba-mcp # exposes built-in tools
|
|
11
|
+
* alba-mcp --extension ./my.ts # includes extension tools
|
|
12
|
+
*
|
|
13
|
+
* Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
|
|
14
|
+
* {
|
|
15
|
+
* "mcpServers": {
|
|
16
|
+
* "alba": {
|
|
17
|
+
* "command": "alba-mcp",
|
|
18
|
+
* "env": { "OPENROUTER_API_KEY": "sk-or-..." }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
import { createInterface } from "node:readline";
|
|
24
|
+
export class MCPServer {
|
|
25
|
+
registry;
|
|
26
|
+
constructor(registry) {
|
|
27
|
+
this.registry = registry;
|
|
28
|
+
}
|
|
29
|
+
async run() {
|
|
30
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
31
|
+
// MCP uses newline-delimited JSON-RPC 2.0
|
|
32
|
+
rl.on("line", async (line) => {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (!trimmed)
|
|
35
|
+
return;
|
|
36
|
+
let req;
|
|
37
|
+
try {
|
|
38
|
+
req = JSON.parse(trimmed);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
this.respond({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const response = await this.handle(req);
|
|
45
|
+
this.respond(response);
|
|
46
|
+
});
|
|
47
|
+
rl.on("close", () => process.exit(0));
|
|
48
|
+
// MCP servers send an initialization notification on stderr
|
|
49
|
+
process.stderr.write("[ALBA MCP] Server ready\n");
|
|
50
|
+
}
|
|
51
|
+
respond(res) {
|
|
52
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
53
|
+
}
|
|
54
|
+
async handle(req) {
|
|
55
|
+
try {
|
|
56
|
+
switch (req.method) {
|
|
57
|
+
case "initialize":
|
|
58
|
+
return {
|
|
59
|
+
jsonrpc: "2.0", id: req.id,
|
|
60
|
+
result: {
|
|
61
|
+
protocolVersion: "2024-11-05",
|
|
62
|
+
capabilities: { tools: {} },
|
|
63
|
+
serverInfo: { name: "alba", version: "0.1.5" },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
case "notifications/initialized":
|
|
67
|
+
// No response needed for notifications
|
|
68
|
+
return { jsonrpc: "2.0", id: req.id, result: null };
|
|
69
|
+
case "tools/list":
|
|
70
|
+
return {
|
|
71
|
+
jsonrpc: "2.0", id: req.id,
|
|
72
|
+
result: {
|
|
73
|
+
tools: this.registry.listTools().map(t => ({
|
|
74
|
+
name: t.name,
|
|
75
|
+
description: t.description,
|
|
76
|
+
inputSchema: {
|
|
77
|
+
...t.parameters,
|
|
78
|
+
type: "object", // MCP requires explicit type
|
|
79
|
+
},
|
|
80
|
+
})),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
case "tools/call": {
|
|
84
|
+
const { name, arguments: args } = (req.params ?? {});
|
|
85
|
+
if (!name) {
|
|
86
|
+
return { jsonrpc: "2.0", id: req.id, error: { code: -32602, message: "Missing tool name" } };
|
|
87
|
+
}
|
|
88
|
+
const tool = this.registry.getTool(name);
|
|
89
|
+
if (!tool) {
|
|
90
|
+
return { jsonrpc: "2.0", id: req.id, error: { code: -32601, message: `Tool not found: ${name}` } };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const result = await tool.execute("mcp", (args ?? {}));
|
|
94
|
+
return {
|
|
95
|
+
jsonrpc: "2.0", id: req.id,
|
|
96
|
+
result: {
|
|
97
|
+
content: result.content.map(c => ({ type: "text", text: c.text })),
|
|
98
|
+
isError: false,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
104
|
+
return {
|
|
105
|
+
jsonrpc: "2.0", id: req.id,
|
|
106
|
+
result: {
|
|
107
|
+
content: [{ type: "text", text: `Tool error: ${msg}` }],
|
|
108
|
+
isError: true,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
case "ping":
|
|
114
|
+
return { jsonrpc: "2.0", id: req.id, result: {} };
|
|
115
|
+
default:
|
|
116
|
+
return {
|
|
117
|
+
jsonrpc: "2.0", id: req.id,
|
|
118
|
+
error: { code: -32601, message: `Method not found: ${req.method}` },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
124
|
+
return { jsonrpc: "2.0", id: req.id, error: { code: -32603, message: `Internal error: ${msg}` } };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CostTracker — tracks per-session and lifetime token usage and cost.
|
|
3
|
+
*
|
|
4
|
+
* Uses OpenRouter response headers (x-credit-used) when available,
|
|
5
|
+
* otherwise estimates from the model registry's pricing data.
|
|
6
|
+
* Emits budget warnings and provides cost reports via tool/command.
|
|
7
|
+
*/
|
|
8
|
+
import type { ModelRegistry } from "./ModelRegistry.js";
|
|
9
|
+
export interface UsageEntry {
|
|
10
|
+
model: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
promptTokens: number;
|
|
13
|
+
completionTokens: number;
|
|
14
|
+
/** Estimated cost in nano-dollars (billionths of a dollar) */
|
|
15
|
+
estimatedCost: number;
|
|
16
|
+
/** Duration in ms */
|
|
17
|
+
duration: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SessionUsage {
|
|
20
|
+
promptTokens: number;
|
|
21
|
+
completionTokens: number;
|
|
22
|
+
estimatedCost: number;
|
|
23
|
+
calls: number;
|
|
24
|
+
errors: number;
|
|
25
|
+
}
|
|
26
|
+
export interface LifetimeUsage {
|
|
27
|
+
promptTokens: number;
|
|
28
|
+
completionTokens: number;
|
|
29
|
+
estimatedCost: number;
|
|
30
|
+
calls: number;
|
|
31
|
+
modelsUsed: Set<string>;
|
|
32
|
+
}
|
|
33
|
+
export declare class CostTracker {
|
|
34
|
+
private modelReg;
|
|
35
|
+
private session;
|
|
36
|
+
private lifetime;
|
|
37
|
+
private budgetSession;
|
|
38
|
+
private budgetLifetime;
|
|
39
|
+
private _albaHome;
|
|
40
|
+
private budgetWarned;
|
|
41
|
+
constructor(modelReg: ModelRegistry);
|
|
42
|
+
record(modelId: string, promptTokens: number, completionTokens: number, duration: number): void;
|
|
43
|
+
recordError(): void;
|
|
44
|
+
private maybeWarnBudget;
|
|
45
|
+
getBudgetStatus(): {
|
|
46
|
+
session: SessionUsage;
|
|
47
|
+
lifetime: LifetimeUsage;
|
|
48
|
+
warnings: string[];
|
|
49
|
+
};
|
|
50
|
+
/** True if the next call would exceed the session budget. */
|
|
51
|
+
wouldExceed(estimatedCallCost: number): boolean;
|
|
52
|
+
private loadLifetime;
|
|
53
|
+
saveLifetime(): void;
|
|
54
|
+
private formatNano;
|
|
55
|
+
statusLine(): string;
|
|
56
|
+
report(): string;
|
|
57
|
+
readonly costReportParams: import("@sinclair/typebox").TObject<{}>;
|
|
58
|
+
costReportTool(): Promise<{
|
|
59
|
+
content: Array<{
|
|
60
|
+
type: "text";
|
|
61
|
+
text: string;
|
|
62
|
+
}>;
|
|
63
|
+
details: Record<string, unknown>;
|
|
64
|
+
}>;
|
|
65
|
+
resetSession(): void;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=CostTracker.d.ts.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CostTracker — tracks per-session and lifetime token usage and cost.
|
|
3
|
+
*
|
|
4
|
+
* Uses OpenRouter response headers (x-credit-used) when available,
|
|
5
|
+
* otherwise estimates from the model registry's pricing data.
|
|
6
|
+
* Emits budget warnings and provides cost reports via tool/command.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { Type } from "@sinclair/typebox";
|
|
12
|
+
const ALBA_HOME = process.env.ALBA_HOME ?? join(homedir(), ".alba");
|
|
13
|
+
const COST_FILE = join(ALBA_HOME, "usage.json");
|
|
14
|
+
// ── CostTracker class ─────────────────────────────────────────────────────────
|
|
15
|
+
const DEFAULT_BUDGET_PER_SESSION = 1_000_000_000_000; // $1.00 in nano-dollars
|
|
16
|
+
const DEFAULT_BUDGET_LIFETIME = 25_000_000_000_000; // $25.00
|
|
17
|
+
export class CostTracker {
|
|
18
|
+
modelReg;
|
|
19
|
+
session = { promptTokens: 0, completionTokens: 0, estimatedCost: 0, calls: 0, errors: 0 };
|
|
20
|
+
lifetime = { promptTokens: 0, completionTokens: 0, estimatedCost: 0, calls: 0, modelsUsed: new Set() };
|
|
21
|
+
budgetSession;
|
|
22
|
+
budgetLifetime;
|
|
23
|
+
_albaHome;
|
|
24
|
+
budgetWarned = false;
|
|
25
|
+
constructor(modelReg) {
|
|
26
|
+
this.modelReg = modelReg;
|
|
27
|
+
this.budgetSession = parseInt(process.env.ALBA_BUDGET_SESSION ?? String(DEFAULT_BUDGET_PER_SESSION));
|
|
28
|
+
this.budgetLifetime = parseInt(process.env.ALBA_BUDGET_LIFETIME ?? String(DEFAULT_BUDGET_LIFETIME));
|
|
29
|
+
this._albaHome = ALBA_HOME;
|
|
30
|
+
this.loadLifetime();
|
|
31
|
+
}
|
|
32
|
+
// ── Recording ─────────────────────────────────────────────────────────────
|
|
33
|
+
record(modelId, promptTokens, completionTokens, duration) {
|
|
34
|
+
const tm = this.modelReg.getModelEntry(modelId);
|
|
35
|
+
const costPer1K = tm?.costPer1K ?? 50_000; // fallback: 50 nano-dollars/1K (=$0.00005/1K)
|
|
36
|
+
const estimatedCost = Math.round(costPer1K * (promptTokens + completionTokens) / 1000);
|
|
37
|
+
const entry = { model: modelId, timestamp: Date.now(), promptTokens, completionTokens, estimatedCost, duration };
|
|
38
|
+
// Session
|
|
39
|
+
this.session.promptTokens += promptTokens;
|
|
40
|
+
this.session.completionTokens += completionTokens;
|
|
41
|
+
this.session.estimatedCost += estimatedCost;
|
|
42
|
+
this.session.calls++;
|
|
43
|
+
// Lifetime
|
|
44
|
+
this.lifetime.promptTokens += promptTokens;
|
|
45
|
+
this.lifetime.completionTokens += completionTokens;
|
|
46
|
+
this.lifetime.estimatedCost += estimatedCost;
|
|
47
|
+
this.lifetime.calls++;
|
|
48
|
+
this.lifetime.modelsUsed.add(modelId);
|
|
49
|
+
this.maybeWarnBudget();
|
|
50
|
+
}
|
|
51
|
+
recordError() {
|
|
52
|
+
this.session.errors++;
|
|
53
|
+
this.lifetime.calls++;
|
|
54
|
+
}
|
|
55
|
+
// ── Budget ────────────────────────────────────────────────────────────────
|
|
56
|
+
maybeWarnBudget() {
|
|
57
|
+
if (this.session.estimatedCost > this.budgetSession * 0.8 && !this.budgetWarned) {
|
|
58
|
+
this.budgetWarned = true;
|
|
59
|
+
// Budget warning is surfaced via getBudgetStatus() — callers can check
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
getBudgetStatus() {
|
|
63
|
+
const warnings = [];
|
|
64
|
+
const pctSession = this.session.estimatedCost / this.budgetSession * 100;
|
|
65
|
+
const pctLifetime = this.lifetime.estimatedCost / this.budgetLifetime * 100;
|
|
66
|
+
if (pctSession >= 90)
|
|
67
|
+
warnings.push(`⚠ Session budget at ${pctSession.toFixed(0)}% — near limit`);
|
|
68
|
+
else if (pctSession >= 75)
|
|
69
|
+
warnings.push(`⚡ Session budget at ${pctSession.toFixed(0)}%`);
|
|
70
|
+
if (pctLifetime >= 90)
|
|
71
|
+
warnings.push(`⚠ Lifetime budget at ${pctLifetime.toFixed(0)}% — near limit`);
|
|
72
|
+
else if (pctLifetime >= 75)
|
|
73
|
+
warnings.push(`⚡ Lifetime budget at ${pctLifetime.toFixed(0)}%`);
|
|
74
|
+
return { session: { ...this.session }, lifetime: { ...this.lifetime, modelsUsed: new Set(this.lifetime.modelsUsed) }, warnings };
|
|
75
|
+
}
|
|
76
|
+
/** True if the next call would exceed the session budget. */
|
|
77
|
+
wouldExceed(estimatedCallCost) {
|
|
78
|
+
return this.session.estimatedCost + estimatedCallCost > this.budgetSession;
|
|
79
|
+
}
|
|
80
|
+
// ── Persistence ────────────────────────────────────────────────────────────
|
|
81
|
+
loadLifetime() {
|
|
82
|
+
try {
|
|
83
|
+
mkdirSync(this._albaHome, { recursive: true });
|
|
84
|
+
if (!existsSync(COST_FILE))
|
|
85
|
+
return;
|
|
86
|
+
const raw = JSON.parse(readFileSync(COST_FILE, "utf-8"));
|
|
87
|
+
this.lifetime.promptTokens = raw.promptTokens ?? 0;
|
|
88
|
+
this.lifetime.completionTokens = raw.completionTokens ?? 0;
|
|
89
|
+
this.lifetime.estimatedCost = raw.estimatedCost ?? 0;
|
|
90
|
+
this.lifetime.calls = raw.calls ?? 0;
|
|
91
|
+
this.lifetime.modelsUsed = new Set(raw.modelsUsed ?? []);
|
|
92
|
+
}
|
|
93
|
+
catch { /* best effort */ }
|
|
94
|
+
}
|
|
95
|
+
saveLifetime() {
|
|
96
|
+
try {
|
|
97
|
+
mkdirSync(this._albaHome, { recursive: true });
|
|
98
|
+
writeFileSync(COST_FILE, JSON.stringify({
|
|
99
|
+
promptTokens: this.lifetime.promptTokens,
|
|
100
|
+
completionTokens: this.lifetime.completionTokens,
|
|
101
|
+
estimatedCost: this.lifetime.estimatedCost,
|
|
102
|
+
calls: this.lifetime.calls,
|
|
103
|
+
modelsUsed: [...this.lifetime.modelsUsed],
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
catch { /* best effort */ }
|
|
107
|
+
}
|
|
108
|
+
// ── Formatting ─────────────────────────────────────────────────────────────
|
|
109
|
+
formatNano(cost) {
|
|
110
|
+
return `$${(cost / 1_000_000_000).toFixed(4)}`;
|
|
111
|
+
}
|
|
112
|
+
statusLine() {
|
|
113
|
+
return `${this.formatNano(this.session.estimatedCost)} · ${this.session.calls}calls`;
|
|
114
|
+
}
|
|
115
|
+
report() {
|
|
116
|
+
const s = this.session;
|
|
117
|
+
return [
|
|
118
|
+
`Session: ${s.calls} calls · ${s.errors} errors · ${this.formatNano(s.estimatedCost)}`,
|
|
119
|
+
` Prompt: ${s.promptTokens.toLocaleString()} tokens`,
|
|
120
|
+
` Completion: ${s.completionTokens.toLocaleString()} tokens`,
|
|
121
|
+
`Lifetime: ${this.lifetime.calls} calls · ${this.formatNano(this.lifetime.estimatedCost)}`,
|
|
122
|
+
` Models used: ${this.lifetime.modelsUsed.size}`,
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
// ── Tool: cost_report ──────────────────────────────────────────────────────
|
|
126
|
+
costReportParams = Type.Object({});
|
|
127
|
+
async costReportTool() {
|
|
128
|
+
const status = this.getBudgetStatus();
|
|
129
|
+
let text = this.report();
|
|
130
|
+
if (status.warnings.length > 0) {
|
|
131
|
+
text += "\n\n" + status.warnings.join("\n");
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text }],
|
|
135
|
+
details: {
|
|
136
|
+
session_cost_nano: status.session.estimatedCost,
|
|
137
|
+
lifetime_cost_nano: status.lifetime.estimatedCost,
|
|
138
|
+
session_calls: status.session.calls,
|
|
139
|
+
lifetime_calls: status.lifetime.calls,
|
|
140
|
+
models_used: status.lifetime.modelsUsed.size,
|
|
141
|
+
warnings: status.warnings,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// ── Reset ──────────────────────────────────────────────────────────────────
|
|
146
|
+
resetSession() {
|
|
147
|
+
this.session = { promptTokens: 0, completionTokens: 0, estimatedCost: 0, calls: 0, errors: 0 };
|
|
148
|
+
this.budgetWarned = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=CostTracker.js.map
|