@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.
Files changed (67) hide show
  1. package/README.md +91 -0
  2. package/bin/albacli +31 -0
  3. package/dist/api/ExtensionAPI.d.ts +103 -0
  4. package/dist/api/ExtensionAPI.js +15 -0
  5. package/dist/api/Registry.d.ts +54 -0
  6. package/dist/api/Registry.js +103 -0
  7. package/dist/cli.d.ts +6 -0
  8. package/dist/cli.js +230 -0
  9. package/dist/index.d.ts +41 -0
  10. package/dist/index.js +41 -0
  11. package/dist/loader.d.ts +16 -0
  12. package/dist/loader.js +89 -0
  13. package/dist/mcp/entry.d.ts +2 -0
  14. package/dist/mcp/entry.js +71 -0
  15. package/dist/mcp/server.d.ts +31 -0
  16. package/dist/mcp/server.js +128 -0
  17. package/dist/models/CostTracker.d.ts +67 -0
  18. package/dist/models/CostTracker.js +151 -0
  19. package/dist/models/ModelRegistry.d.ts +159 -0
  20. package/dist/models/ModelRegistry.js +500 -0
  21. package/dist/models/index.d.ts +5 -0
  22. package/dist/models/index.js +3 -0
  23. package/dist/runner/AgentRunner.d.ts +88 -0
  24. package/dist/runner/AgentRunner.js +473 -0
  25. package/dist/runner/ModelClient.d.ts +97 -0
  26. package/dist/runner/ModelClient.js +350 -0
  27. package/dist/runner/SwarmRouter.d.ts +64 -0
  28. package/dist/runner/SwarmRouter.js +216 -0
  29. package/dist/runner/ToolDispatcher.d.ts +29 -0
  30. package/dist/runner/ToolDispatcher.js +168 -0
  31. package/dist/scheduler/AgentScheduler.d.ts +119 -0
  32. package/dist/scheduler/AgentScheduler.js +263 -0
  33. package/dist/server-entry.d.ts +11 -0
  34. package/dist/server-entry.js +15 -0
  35. package/dist/session/ContextStore.d.ts +96 -0
  36. package/dist/session/ContextStore.js +207 -0
  37. package/dist/session/GoalManager.d.ts +101 -0
  38. package/dist/session/GoalManager.js +167 -0
  39. package/dist/session/MemoryStore.d.ts +48 -0
  40. package/dist/session/MemoryStore.js +167 -0
  41. package/dist/session/SessionManager.d.ts +64 -0
  42. package/dist/session/SessionManager.js +193 -0
  43. package/dist/telemetry/Tracer.d.ts +48 -0
  44. package/dist/telemetry/Tracer.js +102 -0
  45. package/dist/tools/MarketSentiment.d.ts +166 -0
  46. package/dist/tools/MarketSentiment.js +209 -0
  47. package/dist/tools/NewsSentiment.d.ts +67 -0
  48. package/dist/tools/NewsSentiment.js +226 -0
  49. package/dist/tools/PriceFeed.d.ts +105 -0
  50. package/dist/tools/PriceFeed.js +282 -0
  51. package/dist/tools/TechnicalAnalysis.d.ts +110 -0
  52. package/dist/tools/TechnicalAnalysis.js +357 -0
  53. package/dist/tools/index.d.ts +7 -0
  54. package/dist/tools/index.js +4 -0
  55. package/dist/tui/App.d.ts +17 -0
  56. package/dist/tui/App.js +225 -0
  57. package/dist/tui/ModelSelector.d.ts +12 -0
  58. package/dist/tui/ModelSelector.js +87 -0
  59. package/dist/tui/REPL.d.ts +24 -0
  60. package/dist/tui/REPL.js +51 -0
  61. package/dist/tui/StatusBar.d.ts +14 -0
  62. package/dist/tui/StatusBar.js +14 -0
  63. package/dist/tui/theme.d.ts +30 -0
  64. package/dist/tui/theme.js +41 -0
  65. package/dist/util/safeLog.d.ts +3 -0
  66. package/dist/util/safeLog.js +21 -0
  67. 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
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=entry.d.ts.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