@chrysb/alphaclaw 0.4.3 → 0.4.4

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.
@@ -0,0 +1,82 @@
1
+ const kTokensPerMillion = 1_000_000;
2
+ const kLongContextThresholdTokens = 200_000;
3
+ const kGlobalModelPricing = {
4
+ "claude-opus-4-6": {
5
+ input: (tokens) => (tokens > kLongContextThresholdTokens ? 10.0 : 5.0),
6
+ output: (tokens) => (tokens > kLongContextThresholdTokens ? 37.5 : 25.0),
7
+ },
8
+ "claude-sonnet-4-6": { input: 3.0, output: 15.0 },
9
+ "claude-haiku-4-6": { input: 0.8, output: 4.0 },
10
+ "gpt-5": { input: 1.25, output: 10.0 },
11
+ "gpt-5.1-codex": { input: 2.5, output: 10.0 },
12
+ "gpt-5.3-codex": { input: 2.5, output: 10.0 },
13
+ "gpt-4.1": { input: 2.0, output: 8.0 },
14
+ "gpt-4o": { input: 2.5, output: 10.0 },
15
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
16
+ "gemini-3-pro-preview": { input: 2.0, output: 12.0 },
17
+ "gemini-3-flash-preview": { input: 0.5, output: 3.0 },
18
+ "gemini-2.0-flash": { input: 0.1, output: 0.4 },
19
+ };
20
+
21
+ const toInt = (value, fallbackValue = 0) => {
22
+ const parsed = Number.parseInt(String(value ?? ""), 10);
23
+ return Number.isFinite(parsed) ? parsed : fallbackValue;
24
+ };
25
+
26
+ const resolvePricing = (model) => {
27
+ const normalized = String(model || "").toLowerCase();
28
+ if (!normalized) return null;
29
+ const exact = kGlobalModelPricing[normalized];
30
+ if (exact) return exact;
31
+ const matchKey = Object.keys(kGlobalModelPricing).find((key) =>
32
+ normalized.includes(key),
33
+ );
34
+ return matchKey ? kGlobalModelPricing[matchKey] : null;
35
+ };
36
+
37
+ const resolvePerMillionRate = (rate, tokens) => {
38
+ if (typeof rate === "function") {
39
+ return Number(rate(toInt(tokens)));
40
+ }
41
+ return Number(rate || 0);
42
+ };
43
+
44
+ const deriveCostBreakdown = ({
45
+ inputTokens = 0,
46
+ outputTokens = 0,
47
+ cacheReadTokens = 0,
48
+ cacheWriteTokens = 0,
49
+ model = "",
50
+ }) => {
51
+ const pricing = resolvePricing(model);
52
+ if (!pricing) {
53
+ return {
54
+ inputCost: 0,
55
+ outputCost: 0,
56
+ cacheReadCost: 0,
57
+ cacheWriteCost: 0,
58
+ totalCost: 0,
59
+ pricingFound: false,
60
+ };
61
+ }
62
+ const inputRate = resolvePerMillionRate(pricing.input, inputTokens);
63
+ const outputRate = resolvePerMillionRate(pricing.output, outputTokens);
64
+ const inputCost = (inputTokens / kTokensPerMillion) * inputRate;
65
+ const outputCost = (outputTokens / kTokensPerMillion) * outputRate;
66
+ const cacheReadCost = 0;
67
+ const cacheWriteRate = resolvePerMillionRate(pricing.input, cacheWriteTokens);
68
+ const cacheWriteCost = (cacheWriteTokens / kTokensPerMillion) * cacheWriteRate;
69
+ return {
70
+ inputCost,
71
+ outputCost,
72
+ cacheReadCost,
73
+ cacheWriteCost,
74
+ totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,
75
+ pricingFound: true,
76
+ };
77
+ };
78
+
79
+ module.exports = {
80
+ kGlobalModelPricing,
81
+ deriveCostBreakdown,
82
+ };
@@ -0,0 +1,87 @@
1
+ const safeAlterTable = (database, sql) => {
2
+ try {
3
+ database.exec(sql);
4
+ } catch (err) {
5
+ const message = String(err?.message || "").toLowerCase();
6
+ if (!message.includes("duplicate column name")) throw err;
7
+ }
8
+ };
9
+
10
+ const ensureSchema = (database) => {
11
+ database.exec("PRAGMA journal_mode=WAL;");
12
+ database.exec("PRAGMA synchronous=NORMAL;");
13
+ database.exec("PRAGMA busy_timeout=5000;");
14
+ database.exec(`
15
+ CREATE TABLE IF NOT EXISTS usage_events (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ timestamp INTEGER NOT NULL,
18
+ session_id TEXT,
19
+ session_key TEXT,
20
+ run_id TEXT,
21
+ provider TEXT NOT NULL,
22
+ model TEXT NOT NULL,
23
+ input_tokens INTEGER NOT NULL DEFAULT 0,
24
+ output_tokens INTEGER NOT NULL DEFAULT 0,
25
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
26
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
27
+ total_tokens INTEGER NOT NULL DEFAULT 0
28
+ );
29
+ `);
30
+ database.exec(`
31
+ CREATE INDEX IF NOT EXISTS idx_usage_events_ts
32
+ ON usage_events(timestamp DESC);
33
+ `);
34
+ database.exec(`
35
+ CREATE INDEX IF NOT EXISTS idx_usage_events_session
36
+ ON usage_events(session_id);
37
+ `);
38
+ safeAlterTable(
39
+ database,
40
+ "ALTER TABLE usage_events ADD COLUMN session_key TEXT;",
41
+ );
42
+ database.exec(`
43
+ CREATE INDEX IF NOT EXISTS idx_usage_events_session_key
44
+ ON usage_events(session_key);
45
+ `);
46
+ database.exec(`
47
+ CREATE TABLE IF NOT EXISTS usage_daily (
48
+ date TEXT NOT NULL,
49
+ model TEXT NOT NULL,
50
+ provider TEXT,
51
+ input_tokens INTEGER NOT NULL DEFAULT 0,
52
+ output_tokens INTEGER NOT NULL DEFAULT 0,
53
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
54
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
55
+ total_tokens INTEGER NOT NULL DEFAULT 0,
56
+ turn_count INTEGER NOT NULL DEFAULT 0,
57
+ PRIMARY KEY (date, model)
58
+ );
59
+ `);
60
+ database.exec(`
61
+ CREATE TABLE IF NOT EXISTS tool_events (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ timestamp INTEGER NOT NULL,
64
+ session_id TEXT,
65
+ session_key TEXT,
66
+ tool_name TEXT NOT NULL,
67
+ success INTEGER NOT NULL DEFAULT 1,
68
+ duration_ms INTEGER
69
+ );
70
+ `);
71
+ database.exec(`
72
+ CREATE INDEX IF NOT EXISTS idx_tool_events_session
73
+ ON tool_events(session_id);
74
+ `);
75
+ safeAlterTable(
76
+ database,
77
+ "ALTER TABLE tool_events ADD COLUMN session_key TEXT;",
78
+ );
79
+ database.exec(`
80
+ CREATE INDEX IF NOT EXISTS idx_tool_events_session_key
81
+ ON tool_events(session_key);
82
+ `);
83
+ };
84
+
85
+ module.exports = {
86
+ ensureSchema,
87
+ };
@@ -0,0 +1,217 @@
1
+ const {
2
+ kDefaultSessionLimit,
3
+ kMaxSessionLimit,
4
+ coerceInt,
5
+ clampInt,
6
+ getUsageMetricsFromEventRow,
7
+ } = require("./shared");
8
+
9
+ const getSessionsList = ({ database, limit = kDefaultSessionLimit } = {}) => {
10
+ const safeLimit = clampInt(limit, 1, kMaxSessionLimit, kDefaultSessionLimit);
11
+ const rows = database
12
+ .prepare(`
13
+ SELECT
14
+ COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) AS session_ref,
15
+ MAX(session_key) AS session_key,
16
+ MAX(session_id) AS session_id,
17
+ MIN(timestamp) AS first_activity_ms,
18
+ MAX(timestamp) AS last_activity_ms,
19
+ COUNT(*) AS turn_count,
20
+ SUM(input_tokens) AS input_tokens,
21
+ SUM(output_tokens) AS output_tokens,
22
+ SUM(cache_read_tokens) AS cache_read_tokens,
23
+ SUM(cache_write_tokens) AS cache_write_tokens,
24
+ SUM(total_tokens) AS total_tokens
25
+ FROM usage_events
26
+ WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) IS NOT NULL
27
+ GROUP BY session_ref
28
+ ORDER BY last_activity_ms DESC
29
+ LIMIT $limit
30
+ `)
31
+ .all({ $limit: safeLimit });
32
+ return rows.map((row) => {
33
+ const eventRows = database
34
+ .prepare(`
35
+ SELECT
36
+ model,
37
+ input_tokens,
38
+ output_tokens,
39
+ cache_read_tokens,
40
+ cache_write_tokens,
41
+ total_tokens
42
+ FROM usage_events
43
+ WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
44
+ `)
45
+ .all({ $sessionRef: row.session_ref });
46
+ let totalCost = 0;
47
+ const modelTokenTotals = new Map();
48
+ for (const eventRow of eventRows) {
49
+ const metrics = getUsageMetricsFromEventRow(eventRow);
50
+ totalCost += metrics.totalCost;
51
+ const model = String(eventRow.model || "");
52
+ modelTokenTotals.set(model, (modelTokenTotals.get(model) || 0) + metrics.totalTokens);
53
+ }
54
+ const dominantModel = Array.from(modelTokenTotals.entries())
55
+ .sort((a, b) => b[1] - a[1])[0]?.[0] || "";
56
+ return {
57
+ sessionId: row.session_ref,
58
+ sessionKey: String(row.session_key || ""),
59
+ rawSessionId: String(row.session_id || ""),
60
+ firstActivityMs: coerceInt(row.first_activity_ms),
61
+ lastActivityMs: coerceInt(row.last_activity_ms),
62
+ durationMs: Math.max(
63
+ 0,
64
+ coerceInt(row.last_activity_ms) - coerceInt(row.first_activity_ms),
65
+ ),
66
+ turnCount: coerceInt(row.turn_count),
67
+ inputTokens: coerceInt(row.input_tokens),
68
+ outputTokens: coerceInt(row.output_tokens),
69
+ cacheReadTokens: coerceInt(row.cache_read_tokens),
70
+ cacheWriteTokens: coerceInt(row.cache_write_tokens),
71
+ totalTokens: coerceInt(row.total_tokens),
72
+ totalCost,
73
+ dominantModel,
74
+ };
75
+ });
76
+ };
77
+
78
+ const getSessionDetail = ({ database, sessionId }) => {
79
+ const safeSessionRef = String(sessionId || "").trim();
80
+ if (!safeSessionRef) return null;
81
+ const summaryRow = database
82
+ .prepare(`
83
+ SELECT
84
+ MAX(session_key) AS session_key,
85
+ MAX(session_id) AS session_id,
86
+ MIN(timestamp) AS first_activity_ms,
87
+ MAX(timestamp) AS last_activity_ms,
88
+ COUNT(*) AS turn_count,
89
+ SUM(input_tokens) AS input_tokens,
90
+ SUM(output_tokens) AS output_tokens,
91
+ SUM(cache_read_tokens) AS cache_read_tokens,
92
+ SUM(cache_write_tokens) AS cache_write_tokens,
93
+ SUM(total_tokens) AS total_tokens
94
+ FROM usage_events
95
+ WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
96
+ `)
97
+ .get({ $sessionRef: safeSessionRef });
98
+ if (!summaryRow || !coerceInt(summaryRow.turn_count)) return null;
99
+
100
+ const modelEvents = database
101
+ .prepare(`
102
+ SELECT
103
+ provider,
104
+ model,
105
+ input_tokens,
106
+ output_tokens,
107
+ cache_read_tokens,
108
+ cache_write_tokens,
109
+ total_tokens
110
+ FROM usage_events
111
+ WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
112
+ `)
113
+ .all({ $sessionRef: safeSessionRef });
114
+ const byProviderModel = new Map();
115
+ for (const eventRow of modelEvents) {
116
+ const provider = String(eventRow.provider || "unknown");
117
+ const model = String(eventRow.model || "unknown");
118
+ const mapKey = `${provider}\u0000${model}`;
119
+ if (!byProviderModel.has(mapKey)) {
120
+ byProviderModel.set(mapKey, {
121
+ provider,
122
+ model,
123
+ turnCount: 0,
124
+ inputTokens: 0,
125
+ outputTokens: 0,
126
+ cacheReadTokens: 0,
127
+ cacheWriteTokens: 0,
128
+ totalTokens: 0,
129
+ totalCost: 0,
130
+ inputCost: 0,
131
+ outputCost: 0,
132
+ cacheReadCost: 0,
133
+ cacheWriteCost: 0,
134
+ pricingFound: false,
135
+ });
136
+ }
137
+ const aggregate = byProviderModel.get(mapKey);
138
+ const metrics = getUsageMetricsFromEventRow(eventRow);
139
+ aggregate.turnCount += 1;
140
+ aggregate.inputTokens += metrics.inputTokens;
141
+ aggregate.outputTokens += metrics.outputTokens;
142
+ aggregate.cacheReadTokens += metrics.cacheReadTokens;
143
+ aggregate.cacheWriteTokens += metrics.cacheWriteTokens;
144
+ aggregate.totalTokens += metrics.totalTokens;
145
+ aggregate.totalCost += metrics.totalCost;
146
+ aggregate.inputCost += metrics.inputCost;
147
+ aggregate.outputCost += metrics.outputCost;
148
+ aggregate.cacheReadCost += metrics.cacheReadCost;
149
+ aggregate.cacheWriteCost += metrics.cacheWriteCost;
150
+ aggregate.pricingFound = aggregate.pricingFound || metrics.pricingFound;
151
+ }
152
+ const modelRows = Array.from(byProviderModel.values()).sort(
153
+ (a, b) => b.totalTokens - a.totalTokens,
154
+ );
155
+
156
+ const toolRows = database
157
+ .prepare(`
158
+ SELECT
159
+ tool_name,
160
+ COUNT(*) AS call_count,
161
+ SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) AS success_count,
162
+ SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS error_count,
163
+ AVG(duration_ms) AS avg_duration_ms,
164
+ MIN(duration_ms) AS min_duration_ms,
165
+ MAX(duration_ms) AS max_duration_ms
166
+ FROM tool_events
167
+ WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
168
+ GROUP BY tool_name
169
+ ORDER BY call_count DESC
170
+ `)
171
+ .all({ $sessionRef: safeSessionRef })
172
+ .map((row) => {
173
+ const callCount = coerceInt(row.call_count);
174
+ const successCount = coerceInt(row.success_count);
175
+ const errorCount = coerceInt(row.error_count);
176
+ return {
177
+ toolName: row.tool_name,
178
+ callCount,
179
+ successCount,
180
+ errorCount,
181
+ errorRate: callCount > 0 ? errorCount / callCount : 0,
182
+ avgDurationMs: Number(row.avg_duration_ms || 0),
183
+ minDurationMs: coerceInt(row.min_duration_ms),
184
+ maxDurationMs: coerceInt(row.max_duration_ms),
185
+ };
186
+ });
187
+
188
+ const firstActivityMs = coerceInt(summaryRow.first_activity_ms);
189
+ const lastActivityMs = coerceInt(summaryRow.last_activity_ms);
190
+ const totalCost = modelRows.reduce(
191
+ (sum, modelRow) => sum + Number(modelRow.totalCost || 0),
192
+ 0,
193
+ );
194
+
195
+ return {
196
+ sessionId: safeSessionRef,
197
+ sessionKey: String(summaryRow.session_key || ""),
198
+ rawSessionId: String(summaryRow.session_id || ""),
199
+ firstActivityMs,
200
+ lastActivityMs,
201
+ durationMs: Math.max(0, lastActivityMs - firstActivityMs),
202
+ turnCount: coerceInt(summaryRow.turn_count),
203
+ inputTokens: coerceInt(summaryRow.input_tokens),
204
+ outputTokens: coerceInt(summaryRow.output_tokens),
205
+ cacheReadTokens: coerceInt(summaryRow.cache_read_tokens),
206
+ cacheWriteTokens: coerceInt(summaryRow.cache_write_tokens),
207
+ totalTokens: coerceInt(summaryRow.total_tokens),
208
+ totalCost,
209
+ modelBreakdown: modelRows,
210
+ toolUsage: toolRows,
211
+ };
212
+ };
213
+
214
+ module.exports = {
215
+ getSessionsList,
216
+ getSessionDetail,
217
+ };
@@ -0,0 +1,139 @@
1
+ const { deriveCostBreakdown } = require("./pricing");
2
+
3
+ const kDefaultSessionLimit = 50;
4
+ const kMaxSessionLimit = 200;
5
+ const kDefaultDays = 30;
6
+ const kDefaultMaxPoints = 100;
7
+ const kMaxMaxPoints = 1000;
8
+ const kDayMs = 24 * 60 * 60 * 1000;
9
+ const kUtcTimeZone = "UTC";
10
+ const kDayKeyFormatterCache = new Map();
11
+
12
+ const coerceInt = (value, fallbackValue = 0) => {
13
+ const parsed = Number.parseInt(String(value ?? ""), 10);
14
+ return Number.isFinite(parsed) ? parsed : fallbackValue;
15
+ };
16
+
17
+ const clampInt = (value, minValue, maxValue, fallbackValue) =>
18
+ Math.min(maxValue, Math.max(minValue, coerceInt(value, fallbackValue)));
19
+
20
+ const normalizeTimeZone = (value) => {
21
+ const raw = String(value || "").trim();
22
+ if (!raw) return kUtcTimeZone;
23
+ try {
24
+ new Intl.DateTimeFormat("en-US", { timeZone: raw });
25
+ return raw;
26
+ } catch {
27
+ return kUtcTimeZone;
28
+ }
29
+ };
30
+
31
+ const getDayKeyFormatter = (timeZone) => {
32
+ if (kDayKeyFormatterCache.has(timeZone)) {
33
+ return kDayKeyFormatterCache.get(timeZone);
34
+ }
35
+ const formatter = new Intl.DateTimeFormat("en-US", {
36
+ timeZone,
37
+ year: "numeric",
38
+ month: "2-digit",
39
+ day: "2-digit",
40
+ });
41
+ kDayKeyFormatterCache.set(timeZone, formatter);
42
+ return formatter;
43
+ };
44
+
45
+ const toTimeZoneDayKey = (timestampMs, timeZone) => {
46
+ const parts = getDayKeyFormatter(timeZone).formatToParts(new Date(timestampMs));
47
+ const year = parts.find((part) => part.type === "year")?.value || "0000";
48
+ const month = parts.find((part) => part.type === "month")?.value || "01";
49
+ const day = parts.find((part) => part.type === "day")?.value || "01";
50
+ return `${year}-${month}-${day}`;
51
+ };
52
+
53
+ const toDayKey = (timestampMs) => new Date(timestampMs).toISOString().slice(0, 10);
54
+
55
+ const getPeriodRange = (days, timeZone = kUtcTimeZone) => {
56
+ const now = Date.now();
57
+ const safeDays = clampInt(days, 1, 3650, kDefaultDays);
58
+ const startMs = now - safeDays * kDayMs;
59
+ const normalizedTimeZone = normalizeTimeZone(timeZone);
60
+ const startDay = normalizedTimeZone === kUtcTimeZone
61
+ ? toDayKey(startMs)
62
+ : toTimeZoneDayKey(startMs, normalizedTimeZone);
63
+ return { now, safeDays, startDay, timeZone: normalizedTimeZone };
64
+ };
65
+
66
+ const getUsageMetricsFromEventRow = (row) => {
67
+ const inputTokens = coerceInt(row.input_tokens);
68
+ const outputTokens = coerceInt(row.output_tokens);
69
+ const cacheReadTokens = coerceInt(row.cache_read_tokens);
70
+ const cacheWriteTokens = coerceInt(row.cache_write_tokens);
71
+ const totalTokens =
72
+ coerceInt(row.total_tokens) ||
73
+ inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
74
+ const cost = deriveCostBreakdown({
75
+ inputTokens,
76
+ outputTokens,
77
+ cacheReadTokens,
78
+ cacheWriteTokens,
79
+ model: row.model,
80
+ });
81
+ return {
82
+ inputTokens,
83
+ outputTokens,
84
+ cacheReadTokens,
85
+ cacheWriteTokens,
86
+ totalTokens,
87
+ ...cost,
88
+ };
89
+ };
90
+
91
+ const parseAgentAndSourceFromSessionRef = (sessionRef) => {
92
+ const raw = String(sessionRef || "").trim();
93
+ if (!raw) {
94
+ return { agent: "unknown", source: "chat" };
95
+ }
96
+ const parts = raw.split(":");
97
+ const agent =
98
+ parts[0] === "agent" && String(parts[1] || "").trim()
99
+ ? String(parts[1] || "").trim()
100
+ : "unknown";
101
+ const source = parts.includes("hook")
102
+ ? "hooks"
103
+ : parts.includes("cron")
104
+ ? "cron"
105
+ : "chat";
106
+ return { agent, source };
107
+ };
108
+
109
+ const downsamplePoints = (points, maxPoints) => {
110
+ if (points.length <= maxPoints) return points;
111
+ const stride = Math.ceil(points.length / maxPoints);
112
+ const sampled = [];
113
+ for (let index = 0; index < points.length; index += stride) {
114
+ sampled.push(points[index]);
115
+ }
116
+ const lastPoint = points[points.length - 1];
117
+ if (sampled[sampled.length - 1]?.timestamp !== lastPoint.timestamp) {
118
+ sampled.push(lastPoint);
119
+ }
120
+ return sampled;
121
+ };
122
+
123
+ module.exports = {
124
+ kDefaultSessionLimit,
125
+ kMaxSessionLimit,
126
+ kDefaultDays,
127
+ kDefaultMaxPoints,
128
+ kMaxMaxPoints,
129
+ kDayMs,
130
+ kUtcTimeZone,
131
+ coerceInt,
132
+ clampInt,
133
+ toTimeZoneDayKey,
134
+ toDayKey,
135
+ getPeriodRange,
136
+ getUsageMetricsFromEventRow,
137
+ parseAgentAndSourceFromSessionRef,
138
+ downsamplePoints,
139
+ };