@hasna/economy 0.5.2 → 0.5.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.
- package/CHANGELOG.md +12 -0
- package/README.md +4 -2
- package/dist/chunks/{billing-bkpxy4kv.js → billing-22az60hy.js} +2 -2
- package/dist/chunks/{config-j9mv3ehk.js → config-hsp7ndhj.js} +2 -2
- package/dist/chunks/{index-npsjjv96.js → index-9arv0b1j.js} +162 -36
- package/dist/chunks/{index-hesxq90g.js → index-9wa1f2kv.js} +1 -1
- package/dist/chunks/{index-zpvea08j.js → index-c7h2rxx1.js} +38 -12
- package/dist/chunks/{index-9d2aky06.js → index-ct3zdefz.js} +3 -3
- package/dist/chunks/{index-k540hfww.js → index-dk5rq1rq.js} +1 -1
- package/dist/chunks/{index-ez9thsr7.js → index-f5jv6q5q.js} +38 -12
- package/dist/chunks/{index-dj3tctqh.js → index-n1wwqxfe.js} +14 -13
- package/dist/chunks/{index-kzwqw3ve.js → index-nheq2nrz.js} +2 -2
- package/dist/chunks/{index-2hwzmfgv.js → index-vajzfzgv.js} +38 -12
- package/dist/chunks/{open-projects-6z4pq6mx.js → open-projects-8qm46m3d.js} +1 -1
- package/dist/chunks/{pricing-vf2pycmh.js → pricing-462f8xkm.js} +1 -1
- package/dist/chunks/{pricing-f80p81gr.js → pricing-k9wsjwbz.js} +1 -1
- package/dist/chunks/{pricing-y21mcjsa.js → pricing-vxejj1qa.js} +1 -1
- package/dist/chunks/{serve-p2yarhm8.js → serve-ra46ms69.js} +18 -10
- package/dist/chunks/{sqlite-store-st99garx.js → sqlite-store-13nhr1z0.js} +2 -2
- package/dist/chunks/{sqlite-store-501f32ts.js → sqlite-store-gamn65xp.js} +2 -2
- package/dist/chunks/{sqlite-store-fq4natnf.js → sqlite-store-ht6frztj.js} +2 -2
- package/dist/chunks/{tui-x64hjfrh.js → tui-a1tvcw2j.js} +4 -4
- package/dist/chunks/{watch-bxstjpbx.js → watch-rvd3dn6r.js} +5 -5
- package/dist/chunks/{webhooks-k4fh2t9c.js → webhooks-nw4daw9y.js} +20 -9
- package/dist/cli/index.js +27 -27
- package/dist/db/database.d.ts +2 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/index.js +15 -14
- package/dist/lib/accounts-store.d.ts +25 -1
- package/dist/lib/accounts-store.d.ts.map +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/analytics.d.ts +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/periods.d.ts +20 -1
- package/dist/lib/periods.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +3 -3
- package/dist/lib/spikes.d.ts.map +1 -1
- package/dist/lib/subscriptions-registry.d.ts +26 -0
- package/dist/lib/subscriptions-registry.d.ts.map +1 -0
- package/dist/lib/test-keychain-fixture.d.ts +3 -2
- package/dist/lib/test-keychain-fixture.d.ts.map +1 -1
- package/dist/mcp/index.js +245 -118
- package/dist/otel/index.js +56 -27
- package/dist/server/index.js +246 -77
- package/dist/server/service-operations.d.ts.map +1 -1
- package/dist/server/service-worker.js +237 -68
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1118,12 +1118,32 @@ function meteredBudgetAmounts(groups, limit, threshold) {
|
|
|
1118
1118
|
function ymd(date2) {
|
|
1119
1119
|
return date2.toISOString().substring(0, 10);
|
|
1120
1120
|
}
|
|
1121
|
-
function
|
|
1121
|
+
function resolveReportingInterval(interval, now = new Date) {
|
|
1122
1122
|
if (!Number.isFinite(now.getTime()))
|
|
1123
1123
|
throw new RangeError("Invalid reporting clock");
|
|
1124
|
-
const start = new Date(
|
|
1124
|
+
const start = new Date(now);
|
|
1125
|
+
start.setUTCHours(0, 0, 0, 0);
|
|
1125
1126
|
const end = new Date(start);
|
|
1126
|
-
|
|
1127
|
+
if (interval.kind === "rolling") {
|
|
1128
|
+
if (!Number.isSafeInteger(interval.count) || interval.count < 1)
|
|
1129
|
+
throw new RangeError("Invalid reporting window");
|
|
1130
|
+
if (interval.unit === "hour") {
|
|
1131
|
+
start.setTime(now.getTime() - interval.count * 3600000);
|
|
1132
|
+
if (!Number.isFinite(start.getTime()))
|
|
1133
|
+
throw new RangeError("Invalid reporting window");
|
|
1134
|
+
return { time_zone: "UTC", start: start.toISOString(), end_exclusive: now.toISOString() };
|
|
1135
|
+
}
|
|
1136
|
+
if (interval.unit !== "day")
|
|
1137
|
+
throw new RangeError("Invalid reporting unit");
|
|
1138
|
+
start.setUTCDate(start.getUTCDate() - interval.count + 1);
|
|
1139
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
1140
|
+
if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()))
|
|
1141
|
+
throw new RangeError("Invalid reporting window");
|
|
1142
|
+
return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
|
|
1143
|
+
}
|
|
1144
|
+
if (interval.kind !== "calendar")
|
|
1145
|
+
throw new RangeError("Invalid reporting interval");
|
|
1146
|
+
switch (interval.period) {
|
|
1127
1147
|
case "today":
|
|
1128
1148
|
end.setUTCDate(end.getUTCDate() + 1);
|
|
1129
1149
|
break;
|
|
@@ -1146,11 +1166,15 @@ function calendarPeriodBounds(period, now = new Date) {
|
|
|
1146
1166
|
end.setUTCFullYear(end.getUTCFullYear() + 1);
|
|
1147
1167
|
break;
|
|
1148
1168
|
case "all":
|
|
1149
|
-
return { time_zone: "UTC",
|
|
1169
|
+
return { time_zone: "UTC", start: null, end_exclusive: null };
|
|
1150
1170
|
default:
|
|
1151
1171
|
throw new RangeError("Invalid reporting period");
|
|
1152
1172
|
}
|
|
1153
|
-
return { time_zone: "UTC",
|
|
1173
|
+
return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
|
|
1174
|
+
}
|
|
1175
|
+
function calendarPeriodBounds(period, now = new Date) {
|
|
1176
|
+
const bounds = resolveReportingInterval({ kind: "calendar", period }, now);
|
|
1177
|
+
return { time_zone: bounds.time_zone, start_date: bounds.start, end_date_exclusive: bounds.end_exclusive };
|
|
1154
1178
|
}
|
|
1155
1179
|
function calendarPeriodWhere(period, column, now = new Date) {
|
|
1156
1180
|
if (!/^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/i.test(column)) {
|
|
@@ -2209,20 +2233,22 @@ function queryAccountBreakdownSince(db, since) {
|
|
|
2209
2233
|
ORDER BY api_equivalent_usd DESC
|
|
2210
2234
|
`).all(since, since);
|
|
2211
2235
|
}
|
|
2212
|
-
function queryDailyBreakdown(db, days = 30, machine) {
|
|
2236
|
+
function queryDailyBreakdown(db, days = 30, machine, now = new Date) {
|
|
2237
|
+
const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
|
|
2213
2238
|
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
2214
|
-
const params = machine ? [
|
|
2239
|
+
const params = machine ? [bounds.start, bounds.end_exclusive, machine] : [bounds.start, bounds.end_exclusive];
|
|
2215
2240
|
return db.prepare(`
|
|
2216
2241
|
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
2217
2242
|
FROM economy_effective_requests
|
|
2218
|
-
WHERE timestamp >=
|
|
2243
|
+
WHERE timestamp >= ? AND timestamp < ?${machineClause}
|
|
2219
2244
|
GROUP BY DATE(timestamp), agent
|
|
2220
2245
|
ORDER BY date ASC
|
|
2221
2246
|
`).all(...params);
|
|
2222
2247
|
}
|
|
2223
|
-
function queryHourlyBreakdown(db, machine, hours) {
|
|
2224
|
-
const
|
|
2225
|
-
const
|
|
2248
|
+
function queryHourlyBreakdown(db, machine, hours, now = new Date) {
|
|
2249
|
+
const bounds = resolveReportingInterval(hours == null ? { kind: "calendar", period: "today" } : { kind: "rolling", unit: "hour", count: hours }, now);
|
|
2250
|
+
const clauses = hours == null ? ["timestamp >= ? AND timestamp < ?"] : ["STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) >= ? AND STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) < ?"];
|
|
2251
|
+
const params = [bounds.start, bounds.end_exclusive];
|
|
2226
2252
|
if (machine) {
|
|
2227
2253
|
clauses.push("machine_id = ?");
|
|
2228
2254
|
params.push(machine);
|
|
@@ -3143,15 +3169,16 @@ function translateSqliteDates(sql) {
|
|
|
3143
3169
|
s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
|
|
3144
3170
|
s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
|
|
3145
3171
|
s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
|
|
3172
|
+
s = s.replace(/STRFTIME\('%Y-%m-%dT%H:%M:%fZ',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, col) => `to_char(CAST("${col}" AS timestamptz) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`);
|
|
3146
3173
|
const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
|
|
3147
3174
|
s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt, col) => {
|
|
3148
3175
|
const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt);
|
|
3149
|
-
return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
|
|
3176
|
+
return `to_char(CAST("${col}" AS timestamptz) AT TIME ZONE 'UTC', '${pgFmt}')`;
|
|
3150
3177
|
});
|
|
3151
3178
|
s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
|
|
3152
3179
|
s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
|
|
3153
3180
|
s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
|
|
3154
|
-
s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${
|
|
3181
|
+
s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\s*\)/gi, (_m, col) => `substr(${col.split(".").map((part) => `"${part}"`).join(".")}, 1, 10)`);
|
|
3155
3182
|
return s;
|
|
3156
3183
|
}
|
|
3157
3184
|
function resolveWorkerPath() {
|
|
@@ -4029,12 +4056,21 @@ var init_config = __esm(() => {
|
|
|
4029
4056
|
function detectCostSpikes(dailyTotals, opts) {
|
|
4030
4057
|
const windowDays = opts?.windowDays ?? 7;
|
|
4031
4058
|
const multiplier = opts?.multiplier ?? 2;
|
|
4059
|
+
if (!Number.isSafeInteger(windowDays) || windowDays < 1)
|
|
4060
|
+
throw new RangeError("Invalid spike window");
|
|
4061
|
+
if (!Number.isFinite(multiplier) || multiplier <= 0)
|
|
4062
|
+
throw new RangeError("Invalid spike multiplier");
|
|
4032
4063
|
const sorted = [...dailyTotals].sort((a, b) => a.date.localeCompare(b.date));
|
|
4033
4064
|
const spikes = [];
|
|
4034
4065
|
for (let i = windowDays;i < sorted.length; i++) {
|
|
4035
4066
|
const window = sorted.slice(i - windowDays, i);
|
|
4036
|
-
const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
|
|
4037
4067
|
const current = sorted[i];
|
|
4068
|
+
const previous = new Date(current.date + "T00:00:00.000Z");
|
|
4069
|
+
previous.setUTCDate(previous.getUTCDate() - 1);
|
|
4070
|
+
const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: windowDays }, previous);
|
|
4071
|
+
if (window[0].date !== bounds.start || window[window.length - 1].date !== previous.toISOString().slice(0, 10) || new Set(window.map((day) => day.date)).size !== windowDays)
|
|
4072
|
+
continue;
|
|
4073
|
+
const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
|
|
4038
4074
|
if (avg > 0 && current.cost_usd > avg * multiplier) {
|
|
4039
4075
|
spikes.push({
|
|
4040
4076
|
date: current.date,
|
|
@@ -4046,21 +4082,23 @@ function detectCostSpikes(dailyTotals, opts) {
|
|
|
4046
4082
|
}
|
|
4047
4083
|
return spikes;
|
|
4048
4084
|
}
|
|
4049
|
-
function queryRecentSpikes(db, days = 14) {
|
|
4085
|
+
function queryRecentSpikes(db, days = 14, now = new Date) {
|
|
4086
|
+
const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
|
|
4050
4087
|
const rows = db.prepare(`
|
|
4051
4088
|
SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
4052
4089
|
FROM economy_effective_requests
|
|
4053
|
-
WHERE timestamp >=
|
|
4090
|
+
WHERE timestamp >= ? AND timestamp < ?
|
|
4054
4091
|
GROUP BY DATE(timestamp)
|
|
4055
4092
|
ORDER BY date ASC
|
|
4056
|
-
`).all(
|
|
4093
|
+
`).all(bounds.start, bounds.end_exclusive);
|
|
4057
4094
|
return detectCostSpikes(rows);
|
|
4058
4095
|
}
|
|
4059
|
-
function getTodaySpike(db) {
|
|
4060
|
-
const today =
|
|
4061
|
-
const spikes = queryRecentSpikes(db, 14);
|
|
4096
|
+
function getTodaySpike(db, now = new Date) {
|
|
4097
|
+
const today = resolveReportingInterval({ kind: "calendar", period: "today" }, now).start;
|
|
4098
|
+
const spikes = queryRecentSpikes(db, 14, now);
|
|
4062
4099
|
return spikes.find((s) => s.date === today) ?? null;
|
|
4063
4100
|
}
|
|
4101
|
+
var init_spikes = () => {};
|
|
4064
4102
|
|
|
4065
4103
|
// src/lib/webhooks.ts
|
|
4066
4104
|
var exports_webhooks = {};
|
|
@@ -4127,6 +4165,7 @@ async function fireWebhook(url, payload) {
|
|
|
4127
4165
|
var init_webhooks = __esm(() => {
|
|
4128
4166
|
init_config();
|
|
4129
4167
|
init_database();
|
|
4168
|
+
init_spikes();
|
|
4130
4169
|
});
|
|
4131
4170
|
|
|
4132
4171
|
// src/db/pg-migrate.ts
|
|
@@ -4581,7 +4620,7 @@ __export(exports_legacy_plan, {
|
|
|
4581
4620
|
});
|
|
4582
4621
|
import { createHash as createHash6 } from "crypto";
|
|
4583
4622
|
import { closeSync, constants, fstatSync, openSync, readSync } from "fs";
|
|
4584
|
-
import { isAbsolute } from "path";
|
|
4623
|
+
import { isAbsolute as isAbsolute2 } from "path";
|
|
4585
4624
|
function object(value, fields, label) {
|
|
4586
4625
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== fields.sort().join(","))
|
|
4587
4626
|
throw Error(`${label} has unsupported or missing fields`);
|
|
@@ -4634,7 +4673,7 @@ function parseLegacySelection(value) {
|
|
|
4634
4673
|
return { schema: mode.selection, items };
|
|
4635
4674
|
}
|
|
4636
4675
|
function readLegacySelectionFile(path) {
|
|
4637
|
-
if (!
|
|
4676
|
+
if (!isAbsolute2(path) || path.includes("\x00"))
|
|
4638
4677
|
throw Error("--input must be an absolute path to a regular JSON file");
|
|
4639
4678
|
let fd;
|
|
4640
4679
|
try {
|
|
@@ -4669,7 +4708,7 @@ function readLegacySelectionFile(path) {
|
|
|
4669
4708
|
}
|
|
4670
4709
|
}
|
|
4671
4710
|
function parseLegacyPlanOptions(args) {
|
|
4672
|
-
if (args.length !== 2 || args[0] !== "--input" || !args[1] || !
|
|
4711
|
+
if (args.length !== 2 || args[0] !== "--input" || !args[1] || !isAbsolute2(args[1]) || args[1].includes("\x00"))
|
|
4673
4712
|
throw Error("legacy-plan accepts exactly --input <absolute JSON file>");
|
|
4674
4713
|
return args[1];
|
|
4675
4714
|
}
|
|
@@ -4798,7 +4837,7 @@ __export(exports_legacy_preservation, {
|
|
|
4798
4837
|
});
|
|
4799
4838
|
import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
|
|
4800
4839
|
import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, lstatSync, openSync as openSync2, readSync as readSync2, realpathSync, writeSync } from "fs";
|
|
4801
|
-
import { basename as basename4, dirname as dirname4, isAbsolute as
|
|
4840
|
+
import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
|
|
4802
4841
|
function parseLegacyPlan(value) {
|
|
4803
4842
|
const p = record(value);
|
|
4804
4843
|
const mode = legacyMode("plan", p.schema);
|
|
@@ -4949,7 +4988,7 @@ async function preserveLegacyOriginals(client, input) {
|
|
|
4949
4988
|
});
|
|
4950
4989
|
}
|
|
4951
4990
|
function readLegacyBytes(path, maxBytes = MAX_BYTES) {
|
|
4952
|
-
if (!
|
|
4991
|
+
if (!isAbsolute3(path) || path.includes("\x00"))
|
|
4953
4992
|
fail("Artifact path must be absolute");
|
|
4954
4993
|
let fd;
|
|
4955
4994
|
try {
|
|
@@ -4988,7 +5027,7 @@ function readLegacyArtifact(path, maxBytes = MAX_BYTES) {
|
|
|
4988
5027
|
}
|
|
4989
5028
|
function writeLegacyCapsule(path, input) {
|
|
4990
5029
|
const capsule = parseLegacyCapsule(input);
|
|
4991
|
-
if (!
|
|
5030
|
+
if (!isAbsolute3(path) || path.includes("\x00") || realpathSync(dirname4(path)) !== dirname4(path))
|
|
4992
5031
|
fail("Output requires an absolute path with a real parent directory");
|
|
4993
5032
|
const parent = lstatSync(dirname4(path));
|
|
4994
5033
|
if (!parent.isDirectory() || parent.uid !== process.getuid?.() || (parent.mode & 63) !== 0)
|
|
@@ -5102,7 +5141,7 @@ async function rehearseLegacyRestore(client, input, scope = "transaction") {
|
|
|
5102
5141
|
}
|
|
5103
5142
|
function parseLegacyPreservationOptions(command, args) {
|
|
5104
5143
|
const preserve = command === "legacy-preserve";
|
|
5105
|
-
if (!preserve && command !== "legacy-rehearse" || args.length !== (preserve ? 4 : 2) || args[0] !== "--input" || !args[1] || !
|
|
5144
|
+
if (!preserve && command !== "legacy-rehearse" || args.length !== (preserve ? 4 : 2) || args[0] !== "--input" || !args[1] || !isAbsolute3(args[1]) || preserve && (args[2] !== "--output" || !args[3] || !isAbsolute3(args[3])) || args.some((x) => x.includes("\x00")))
|
|
5106
5145
|
fail("Expected " + command + " --input <absolute JSON file>" + (preserve ? " --output <new file in an owner-only directory>" : ""));
|
|
5107
5146
|
return { input: args[1], ...preserve ? { output: args[3] } : {} };
|
|
5108
5147
|
}
|
|
@@ -5151,7 +5190,7 @@ __export(exports_legacy_operations, {
|
|
|
5151
5190
|
LegacyOperationInputError: () => LegacyOperationInputError
|
|
5152
5191
|
});
|
|
5153
5192
|
import { createHash as createHash8 } from "crypto";
|
|
5154
|
-
import { isAbsolute as
|
|
5193
|
+
import { isAbsolute as isAbsolute4 } from "path";
|
|
5155
5194
|
function parseLegacyIntent(action, value) {
|
|
5156
5195
|
try {
|
|
5157
5196
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -5396,7 +5435,7 @@ function parseLegacyOperationOptions(command, args) {
|
|
|
5396
5435
|
const apply = command === "legacy-apply";
|
|
5397
5436
|
if (!apply && command !== "legacy-rollback")
|
|
5398
5437
|
fail2("Unknown legacy operation or arguments");
|
|
5399
|
-
if (args.length !== (apply ? 4 : 2) || args[0] !== "--input" || !args[1] || !
|
|
5438
|
+
if (args.length !== (apply ? 4 : 2) || args[0] !== "--input" || !args[1] || !isAbsolute4(args[1]) || apply && (args[2] !== "--capsule" || !args[3] || !isAbsolute4(args[3])) || args.some((s) => s.includes("\x00")))
|
|
5400
5439
|
fail2("Expected " + command + " --input <absolute intent JSON>" + (apply ? " --capsule <absolute preservation JSON>" : ""));
|
|
5401
5440
|
return { input: args[1], ...apply ? { capsule: args[3] } : {} };
|
|
5402
5441
|
}
|
|
@@ -5490,7 +5529,7 @@ function isAuthorizedRequest(req, path) {
|
|
|
5490
5529
|
// package.json
|
|
5491
5530
|
var package_default = {
|
|
5492
5531
|
name: "@hasna/economy",
|
|
5493
|
-
version: "0.5.
|
|
5532
|
+
version: "0.5.4",
|
|
5494
5533
|
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
5495
5534
|
repository: {
|
|
5496
5535
|
type: "git",
|
|
@@ -8780,7 +8819,7 @@ import { join as join5, basename } from "path";
|
|
|
8780
8819
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
8781
8820
|
import { homedir as homedir2 } from "os";
|
|
8782
8821
|
import { join as join4 } from "path";
|
|
8783
|
-
import { z } from "zod";
|
|
8822
|
+
import { z as z2 } from "zod";
|
|
8784
8823
|
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
8785
8824
|
|
|
8786
8825
|
// src/lib/cloud-storage.ts
|
|
@@ -8799,33 +8838,138 @@ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
|
8799
8838
|
var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
8800
8839
|
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
8801
8840
|
|
|
8802
|
-
// src/lib/
|
|
8803
|
-
|
|
8804
|
-
|
|
8841
|
+
// src/lib/subscriptions-registry.ts
|
|
8842
|
+
import { execFile } from "child_process";
|
|
8843
|
+
import { promisify } from "util";
|
|
8844
|
+
import { isAbsolute } from "path";
|
|
8845
|
+
import { z } from "zod";
|
|
8846
|
+
var execute = promisify(execFile);
|
|
8847
|
+
var readSubscriptionsMetadata = async (args, env) => {
|
|
8848
|
+
let stdout;
|
|
8849
|
+
try {
|
|
8850
|
+
const result = await execute("subscriptions", args, {
|
|
8851
|
+
env,
|
|
8852
|
+
encoding: "utf8",
|
|
8853
|
+
timeout: 30000,
|
|
8854
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
8855
|
+
windowsHide: true
|
|
8856
|
+
});
|
|
8857
|
+
stdout = result.stdout;
|
|
8858
|
+
} catch (error) {
|
|
8859
|
+
const code = error?.code;
|
|
8860
|
+
if (code === "ENOENT")
|
|
8861
|
+
throw new Error("subscriptions: CLI not found on PATH; install the Subscriptions CLI to resolve station account metadata");
|
|
8862
|
+
const status = typeof code === "number" && Number.isInteger(code) ? `exit ${code}` : "execution limit or transport failure";
|
|
8863
|
+
throw new Error(`subscriptions: read-only metadata command failed (${status}); verify the installed CLI and its hosted credentials`);
|
|
8864
|
+
}
|
|
8865
|
+
try {
|
|
8866
|
+
return JSON.parse(stdout);
|
|
8867
|
+
} catch {
|
|
8868
|
+
throw new Error("subscriptions: metadata command returned invalid JSON");
|
|
8869
|
+
}
|
|
8870
|
+
};
|
|
8871
|
+
var nonempty = z.string().min(1);
|
|
8805
8872
|
var profileSchema = z.object({
|
|
8806
|
-
name:
|
|
8807
|
-
tool:
|
|
8873
|
+
name: nonempty,
|
|
8874
|
+
tool: nonempty,
|
|
8808
8875
|
email: z.string().optional(),
|
|
8809
8876
|
displayName: z.string().optional(),
|
|
8810
|
-
dir:
|
|
8877
|
+
dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
|
|
8878
|
+
createdAt: nonempty,
|
|
8879
|
+
lastUsedAt: z.string().optional(),
|
|
8811
8880
|
description: z.string().optional(),
|
|
8812
|
-
|
|
8813
|
-
|
|
8881
|
+
active: z.boolean(),
|
|
8882
|
+
applied: z.boolean(),
|
|
8883
|
+
switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
|
|
8884
|
+
});
|
|
8885
|
+
var toolsSchema = z.array(z.object({
|
|
8886
|
+
id: nonempty,
|
|
8887
|
+
label: nonempty,
|
|
8888
|
+
envVar: nonempty,
|
|
8889
|
+
defaultDir: nonempty,
|
|
8890
|
+
bin: nonempty
|
|
8891
|
+
}));
|
|
8892
|
+
function unique(rows, identity) {
|
|
8893
|
+
if (new Set(rows.map(identity)).size !== rows.length) {
|
|
8894
|
+
throw new Error("subscriptions: duplicate identity in metadata response");
|
|
8895
|
+
}
|
|
8896
|
+
return rows;
|
|
8897
|
+
}
|
|
8898
|
+
|
|
8899
|
+
class SubscriptionsRegistry {
|
|
8900
|
+
read;
|
|
8901
|
+
transport = "api";
|
|
8902
|
+
registry = "subscriptions";
|
|
8903
|
+
env;
|
|
8904
|
+
profiles;
|
|
8905
|
+
tools;
|
|
8906
|
+
constructor(baseUrl, env, read = readSubscriptionsMetadata) {
|
|
8907
|
+
this.read = read;
|
|
8908
|
+
this.env = { ...env, HASNA_SUBSCRIPTIONS_API_URL: baseUrl.replace(/\/v1\/?$/, "") };
|
|
8909
|
+
}
|
|
8910
|
+
allProfiles() {
|
|
8911
|
+
return this.profiles ??= this.read(["list", "--json"], this.env).then((raw) => {
|
|
8912
|
+
const parsed = z.array(profileSchema).safeParse(raw);
|
|
8913
|
+
if (!parsed.success)
|
|
8914
|
+
throw new Error("subscriptions: invalid profile metadata response");
|
|
8915
|
+
return unique(parsed.data, (row) => JSON.stringify([row.tool, row.name]));
|
|
8916
|
+
});
|
|
8917
|
+
}
|
|
8918
|
+
async listProfiles(tool) {
|
|
8919
|
+
const rows = await this.allProfiles();
|
|
8920
|
+
return rows.filter((row) => !tool || row.tool === tool);
|
|
8921
|
+
}
|
|
8922
|
+
async findProfile(name, tool) {
|
|
8923
|
+
const matches = (await this.listProfiles(tool)).filter((row) => row.name === name);
|
|
8924
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
8925
|
+
}
|
|
8926
|
+
async appliedProfile(tool) {
|
|
8927
|
+
const matches = (await this.allProfiles()).filter((row) => row.tool === tool && row.applied);
|
|
8928
|
+
if (matches.length > 1)
|
|
8929
|
+
throw new Error("subscriptions: ambiguous station applied-profile binding");
|
|
8930
|
+
return matches[0];
|
|
8931
|
+
}
|
|
8932
|
+
async currentProfile(_tool) {
|
|
8933
|
+
return;
|
|
8934
|
+
}
|
|
8935
|
+
async listTools() {
|
|
8936
|
+
return this.tools ??= this.read(["tools", "list", "--json"], this.env).then((raw) => {
|
|
8937
|
+
const parsed = toolsSchema.safeParse(raw);
|
|
8938
|
+
if (!parsed.success)
|
|
8939
|
+
throw new Error("subscriptions: invalid tool metadata response");
|
|
8940
|
+
return unique(parsed.data, (row) => row.id);
|
|
8941
|
+
});
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8944
|
+
|
|
8945
|
+
// src/lib/accounts-store.ts
|
|
8946
|
+
var ACCOUNTS_APP = "accounts";
|
|
8947
|
+
var profileNameSchema = z2.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
|
|
8948
|
+
var profileSchema2 = z2.object({
|
|
8949
|
+
name: z2.string(),
|
|
8950
|
+
tool: z2.string(),
|
|
8951
|
+
email: z2.string().optional(),
|
|
8952
|
+
displayName: z2.string().optional(),
|
|
8953
|
+
dir: z2.string(),
|
|
8954
|
+
description: z2.string().optional(),
|
|
8955
|
+
createdAt: z2.string(),
|
|
8956
|
+
lastUsedAt: z2.string().optional(),
|
|
8957
|
+
switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
|
|
8814
8958
|
});
|
|
8815
|
-
var toolDefSchema =
|
|
8816
|
-
id:
|
|
8817
|
-
label:
|
|
8818
|
-
envVar:
|
|
8819
|
-
defaultDir:
|
|
8820
|
-
bin:
|
|
8959
|
+
var toolDefSchema = z2.object({
|
|
8960
|
+
id: z2.string(),
|
|
8961
|
+
label: z2.string(),
|
|
8962
|
+
envVar: z2.string(),
|
|
8963
|
+
defaultDir: z2.string(),
|
|
8964
|
+
bin: z2.string()
|
|
8821
8965
|
});
|
|
8822
|
-
var storeSchema =
|
|
8823
|
-
version:
|
|
8824
|
-
current:
|
|
8825
|
-
applied:
|
|
8826
|
-
toolLocks:
|
|
8827
|
-
profiles:
|
|
8828
|
-
tools:
|
|
8966
|
+
var storeSchema = z2.object({
|
|
8967
|
+
version: z2.literal(1),
|
|
8968
|
+
current: z2.record(z2.string(), z2.string()).default({}),
|
|
8969
|
+
applied: z2.record(z2.string(), z2.string()).default({}),
|
|
8970
|
+
toolLocks: z2.record(z2.string(), z2.string()).default({}),
|
|
8971
|
+
profiles: z2.array(profileSchema2).default([]),
|
|
8972
|
+
tools: z2.array(toolDefSchema).default([])
|
|
8829
8973
|
});
|
|
8830
8974
|
function localRegistryAllowed(env = process.env) {
|
|
8831
8975
|
return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => {
|
|
@@ -9107,25 +9251,34 @@ class ApiStore {
|
|
|
9107
9251
|
}
|
|
9108
9252
|
}
|
|
9109
9253
|
function resolveStore(env = process.env, options = {}) {
|
|
9254
|
+
const legacy = resolveRegistryTransport(ACCOUNTS_APP, env, options.credentials);
|
|
9255
|
+
if (legacy)
|
|
9256
|
+
return new ApiStore(legacy.baseUrl, legacy.apiKey);
|
|
9257
|
+
const current = resolveRegistryTransport("subscriptions", env, options.subscriptionsCredentials);
|
|
9258
|
+
if (current)
|
|
9259
|
+
return new SubscriptionsRegistry(current.baseUrl, env, options.readSubscriptionsMetadata);
|
|
9260
|
+
return unconfiguredStore(env);
|
|
9261
|
+
}
|
|
9262
|
+
function resolveRegistryTransport(app, env, credentials) {
|
|
9110
9263
|
const clientEnv = env;
|
|
9111
|
-
const credential = resolveCredential(
|
|
9264
|
+
const credential = resolveCredential(app, clientEnv, credentials);
|
|
9112
9265
|
if (credential === null) {
|
|
9113
9266
|
try {
|
|
9114
|
-
resolveClientTransport(
|
|
9267
|
+
resolveClientTransport(app, clientEnv, credentials ? { credentials } : {});
|
|
9115
9268
|
} catch (err) {
|
|
9116
9269
|
if (isNoAccountsCredentialConfigurationError(err))
|
|
9117
|
-
return
|
|
9270
|
+
return null;
|
|
9118
9271
|
throw err;
|
|
9119
9272
|
}
|
|
9120
|
-
return
|
|
9273
|
+
return null;
|
|
9121
9274
|
}
|
|
9122
9275
|
if (credential.tier === "pointer") {
|
|
9123
|
-
throw new Error(
|
|
9276
|
+
throw new Error(`${app}: ${credential.source} names a secrets-vault item, and the registry adapter resolves ` + "credentials synchronously, so it cannot complete the pointer per request. It is a deliberate " + "selection and is not resolved around. Use a literal tier instead \u2014 the Keychain item " + `hasna.credentials.${app}.api-key, ~/.hasna/${app}/config/credentials, or HASNA_${app.toUpperCase()}_API_KEY.`);
|
|
9124
9277
|
}
|
|
9125
|
-
const resolution = resolveClientTransport(
|
|
9126
|
-
credentials: { ...
|
|
9278
|
+
const resolution = resolveClientTransport(app, clientEnv, {
|
|
9279
|
+
credentials: { ...credentials, apiKey: credential.apiKey }
|
|
9127
9280
|
});
|
|
9128
|
-
return
|
|
9281
|
+
return { baseUrl: resolution.baseUrl, apiKey: credential.apiKey };
|
|
9129
9282
|
}
|
|
9130
9283
|
function isNoAccountsCredentialConfigurationError(error) {
|
|
9131
9284
|
if (typeof error !== "object" || error === null)
|
|
@@ -9142,10 +9295,10 @@ function unconfiguredStore(env) {
|
|
|
9142
9295
|
return new NoRegistryStore;
|
|
9143
9296
|
}
|
|
9144
9297
|
function accountsLocalModeNotice() {
|
|
9145
|
-
return `accounts: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014
|
|
9298
|
+
return `accounts: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 neither Accounts nor Subscriptions credentials resolved, so ` + "attribution reads the on-box JSON registry (~/.hasna/accounts/accounts.json) instead of accounts-serve. " + "For hosted attribution, configure the Subscriptions CLI through its normal sign-in or credential resolver.";
|
|
9146
9299
|
}
|
|
9147
9300
|
function accountsRefusalNotice() {
|
|
9148
|
-
return "accounts:
|
|
9301
|
+
return "accounts: neither Accounts nor Subscriptions credentials resolved (legacy hasna.credentials.accounts.api-key, " + "~/.hasna/accounts/config/credentials, HASNA_ACCOUNTS_API_KEY; current hasna.credentials.subscriptions.api-key, " + "~/.hasna/subscriptions/config/credentials, HASNA_SUBSCRIPTIONS_API_KEY), so spend attribution is omitted. The " + `on-box registry (~/.hasna/accounts/accounts.json) is NOT read without ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 \u2014 ` + "economy fails closed instead of attributing hosted spend from an on-box file.";
|
|
9149
9302
|
}
|
|
9150
9303
|
var accountsLocalNoticePrinted = false;
|
|
9151
9304
|
function announceAccountsLocalMode() {
|
|
@@ -9233,7 +9386,10 @@ async function profileForEnvDir(store, tool, env) {
|
|
|
9233
9386
|
if (!configuredDir)
|
|
9234
9387
|
return null;
|
|
9235
9388
|
const normalized = normalizeDir(configuredDir);
|
|
9236
|
-
|
|
9389
|
+
const matches = (await store.listProfiles(tool.id)).filter((profile) => normalizeDir(profile.dir) === normalized);
|
|
9390
|
+
if (matches.length > 1)
|
|
9391
|
+
throw new Error("account attribution: multiple profiles match the configured directory");
|
|
9392
|
+
return matches[0] ?? null;
|
|
9237
9393
|
}
|
|
9238
9394
|
async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
9239
9395
|
const override = envOverride(agent, env);
|
|
@@ -9247,8 +9403,15 @@ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
|
9247
9403
|
continue;
|
|
9248
9404
|
const envProfile = await profileForEnvDir(store, tool, env);
|
|
9249
9405
|
if (envProfile)
|
|
9250
|
-
return fromProfile(envProfile, "env");
|
|
9251
|
-
|
|
9406
|
+
return envProfile.switchedAway ? null : fromProfile(envProfile, "env");
|
|
9407
|
+
if (store.registry === "subscriptions" && env[tool.envVar])
|
|
9408
|
+
return null;
|
|
9409
|
+
if (store.appliedProfile) {
|
|
9410
|
+
const applied = await store.appliedProfile(toolId);
|
|
9411
|
+
if (applied)
|
|
9412
|
+
return applied.switchedAway ? null : fromProfile(applied, "applied");
|
|
9413
|
+
}
|
|
9414
|
+
const appliedName = store.registry === "subscriptions" ? undefined : appliedProfileName(toolId);
|
|
9252
9415
|
if (appliedName) {
|
|
9253
9416
|
const applied = await store.findProfile(appliedName, toolId);
|
|
9254
9417
|
if (applied)
|
|
@@ -11629,7 +11792,8 @@ function buildBrief(db, opts = {}) {
|
|
|
11629
11792
|
}
|
|
11630
11793
|
|
|
11631
11794
|
// src/lib/analytics.ts
|
|
11632
|
-
function queryProjectDetail(db, nameOrPath) {
|
|
11795
|
+
function queryProjectDetail(db, nameOrPath, now = new Date) {
|
|
11796
|
+
const dailyBounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: 14 }, now);
|
|
11633
11797
|
const like = `%${nameOrPath}%`;
|
|
11634
11798
|
const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
|
|
11635
11799
|
FROM economy_effective_sessions
|
|
@@ -11643,8 +11807,8 @@ function queryProjectDetail(db, nameOrPath) {
|
|
|
11643
11807
|
const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
11644
11808
|
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
11645
11809
|
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
11646
|
-
AND r.timestamp >=
|
|
11647
|
-
GROUP BY date ORDER BY date ASC`).all(like, like);
|
|
11810
|
+
AND r.timestamp >= ? AND r.timestamp < ?
|
|
11811
|
+
GROUP BY date ORDER BY date ASC`).all(like, like, dailyBounds.start, dailyBounds.end_exclusive);
|
|
11648
11812
|
const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
11649
11813
|
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
11650
11814
|
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
@@ -11683,18 +11847,17 @@ function queryRangeStats(db, from, to) {
|
|
|
11683
11847
|
return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
|
|
11684
11848
|
}
|
|
11685
11849
|
function queryForecast(db, now = new Date) {
|
|
11686
|
-
const
|
|
11687
|
-
const
|
|
11688
|
-
const
|
|
11689
|
-
const
|
|
11850
|
+
const month = resolveReportingInterval({ kind: "calendar", period: "month" }, now);
|
|
11851
|
+
const recent = resolveReportingInterval({ kind: "rolling", unit: "day", count: 7 }, now);
|
|
11852
|
+
const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
|
|
11853
|
+
const dayOfMonth = now.getUTCDate();
|
|
11854
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(month.start, recent.end_exclusive);
|
|
11690
11855
|
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
11691
11856
|
const projected = dailyAvg * daysInMonth;
|
|
11692
|
-
const
|
|
11693
|
-
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
11694
|
-
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
11857
|
+
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(recent.start, recent.end_exclusive);
|
|
11695
11858
|
const last7DailyAvg = last7.cost / 7;
|
|
11696
11859
|
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
|
|
11697
|
-
FROM economy_effective_requests WHERE
|
|
11860
|
+
FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ? GROUP BY date ORDER BY cost_usd ASC`).all(month.start, recent.end_exclusive);
|
|
11698
11861
|
return {
|
|
11699
11862
|
day_of_month: dayOfMonth,
|
|
11700
11863
|
days_in_month: daysInMonth,
|
|
@@ -11871,7 +12034,13 @@ function createDataHandler(db, actorId) {
|
|
|
11871
12034
|
if (path === "/api/daily" && method === "GET") {
|
|
11872
12035
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
11873
12036
|
const machine = url.searchParams.get("machine") ?? undefined;
|
|
11874
|
-
|
|
12037
|
+
const now = new Date;
|
|
12038
|
+
try {
|
|
12039
|
+
resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
|
|
12040
|
+
} catch {
|
|
12041
|
+
return err("days must be a positive integer within the supported UTC date range");
|
|
12042
|
+
}
|
|
12043
|
+
return ok(queryDailyBreakdown(db, days, machine, now));
|
|
11875
12044
|
}
|
|
11876
12045
|
if (path === "/api/hourly" && method === "GET") {
|
|
11877
12046
|
const machine = url.searchParams.get("machine") ?? undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-operations.d.ts","sourceRoot":"","sources":["../../src/server/service-operations.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAuCxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"service-operations.d.ts","sourceRoot":"","sources":["../../src/server/service-operations.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAuCxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAgkB5E,gHAAgH;AAChH,wBAAsB,uBAAuB,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAe/G"}
|