@hasna/economy 0.2.36 → 0.2.38
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 +1 -1
- package/dist/cli/index.js +148 -90
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +136 -78
- package/dist/ingest/codex.d.ts +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/mcp/index.js +136 -78
- package/dist/otel/index.js +30 -7
- package/dist/server/index.js +136 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ economy sync --recalculate
|
|
|
102
102
|
economy sync --backfill-machine
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
Full sync also imports active project metadata from `@hasna/projects` when the registry is available.
|
|
105
|
+
Full sync also imports active project metadata from `@hasna/projects` when the registry is available. The Codex source reads both legacy `~/.codex/state_5.sqlite` and current Codewith `~/.codewith/state_5.sqlite` usage stores by default; explicit `HASNA_ECONOMY_CODEX_DB_PATH` and `HASNA_ECONOMY_CODEWITH_DB_PATH` values override those locations.
|
|
106
106
|
|
|
107
107
|
Account attribution is automatic when `@hasna/accounts` has a matching active, applied, or env-dir profile for the agent. Account identity is the email address plus coding agent, so `work@example.com` under Codex and Claude is reported as two accounts. You can also force attribution for a process with `ECONOMY_ACCOUNT=tool:name` or agent-specific overrides such as `ECONOMY_CODEX_ACCOUNT=codex:work`.
|
|
108
108
|
|
package/dist/cli/index.js
CHANGED
|
@@ -565,17 +565,40 @@ __export(exports_database, {
|
|
|
565
565
|
clearBillingRange: () => clearBillingRange
|
|
566
566
|
});
|
|
567
567
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
568
|
+
import { execFileSync } from "child_process";
|
|
568
569
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
569
|
-
import { hostname } from "os";
|
|
570
|
+
import { hostname, platform } from "os";
|
|
570
571
|
import { homedir } from "os";
|
|
571
572
|
import { join } from "path";
|
|
573
|
+
function normalizeMachineId(value) {
|
|
574
|
+
const id = value?.trim().toLowerCase().split(".")[0];
|
|
575
|
+
return id && id.length > 0 ? id : null;
|
|
576
|
+
}
|
|
577
|
+
function macHostMachineId() {
|
|
578
|
+
if (platform() !== "darwin")
|
|
579
|
+
return null;
|
|
580
|
+
for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
|
|
581
|
+
try {
|
|
582
|
+
const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
|
|
583
|
+
encoding: "utf8",
|
|
584
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
585
|
+
timeout: 1000
|
|
586
|
+
});
|
|
587
|
+
const id = normalizeMachineId(value);
|
|
588
|
+
if (id && id !== "mac" && id !== "localhost")
|
|
589
|
+
return id;
|
|
590
|
+
} catch {}
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
572
594
|
function getMachineId() {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
595
|
+
const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
|
|
596
|
+
if (envMachine)
|
|
597
|
+
return envMachine;
|
|
598
|
+
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
599
|
+
if (hostMachine === "mac" || hostMachine === "localhost")
|
|
600
|
+
return macHostMachineId() ?? hostMachine;
|
|
601
|
+
return hostMachine;
|
|
579
602
|
}
|
|
580
603
|
function getDataDir() {
|
|
581
604
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
@@ -2766,8 +2789,40 @@ function codexDbPath() {
|
|
|
2766
2789
|
function codexConfigPath() {
|
|
2767
2790
|
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
2768
2791
|
}
|
|
2769
|
-
function
|
|
2770
|
-
|
|
2792
|
+
function codewithDbPath() {
|
|
2793
|
+
return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? DEFAULT_CODEWITH_DB_PATH;
|
|
2794
|
+
}
|
|
2795
|
+
function codewithConfigPath() {
|
|
2796
|
+
return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? DEFAULT_CODEWITH_CONFIG_PATH;
|
|
2797
|
+
}
|
|
2798
|
+
function codexSources() {
|
|
2799
|
+
const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
|
|
2800
|
+
const explicitCodewithPath = process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"];
|
|
2801
|
+
const sources = [{
|
|
2802
|
+
label: "Codex",
|
|
2803
|
+
dbPath: codexDbPath(),
|
|
2804
|
+
configPath: codexConfigPath(),
|
|
2805
|
+
sessionPrefix: "codex",
|
|
2806
|
+
stateSource: "codex"
|
|
2807
|
+
}];
|
|
2808
|
+
if (!explicitCodexPath || explicitCodewithPath) {
|
|
2809
|
+
sources.push({
|
|
2810
|
+
label: "Codewith",
|
|
2811
|
+
dbPath: codewithDbPath(),
|
|
2812
|
+
configPath: codewithConfigPath(),
|
|
2813
|
+
sessionPrefix: "codex-codewith",
|
|
2814
|
+
stateSource: "codex-codewith"
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
const seen = new Set;
|
|
2818
|
+
return sources.filter((source) => {
|
|
2819
|
+
if (seen.has(source.dbPath))
|
|
2820
|
+
return false;
|
|
2821
|
+
seen.add(source.dbPath);
|
|
2822
|
+
return true;
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
function readCodexModel(configPath = codexConfigPath()) {
|
|
2771
2826
|
if (!existsSync5(configPath))
|
|
2772
2827
|
return "gpt-5-codex";
|
|
2773
2828
|
try {
|
|
@@ -2885,94 +2940,97 @@ function fallbackEvents(totalTokens) {
|
|
|
2885
2940
|
}];
|
|
2886
2941
|
}
|
|
2887
2942
|
async function ingestCodex(db, verbose = false) {
|
|
2888
|
-
const dbPath = codexDbPath();
|
|
2889
|
-
if (!existsSync5(dbPath)) {
|
|
2890
|
-
if (verbose)
|
|
2891
|
-
console.log("Codex DB not found:", dbPath);
|
|
2892
|
-
return { sessions: 0, requests: 0 };
|
|
2893
|
-
}
|
|
2894
2943
|
const machineId = getMachineId();
|
|
2895
|
-
let codexDb = null;
|
|
2896
2944
|
let ingested = 0;
|
|
2897
2945
|
let requests = 0;
|
|
2898
2946
|
const account = await resolveAccountForAgent("codex");
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
if (
|
|
2947
|
+
for (const source of codexSources()) {
|
|
2948
|
+
if (!existsSync5(source.dbPath)) {
|
|
2949
|
+
if (verbose)
|
|
2950
|
+
console.log(`${source.label} DB not found:`, source.dbPath);
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
let codexDb = null;
|
|
2954
|
+
try {
|
|
2955
|
+
codexDb = openCodexDb(source.dbPath, verbose);
|
|
2956
|
+
if (!codexDb)
|
|
2909
2957
|
continue;
|
|
2910
|
-
const
|
|
2911
|
-
const
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
request_count: 0,
|
|
2925
|
-
machine_id: machineId
|
|
2926
|
-
}, account));
|
|
2927
|
-
const events = readTokenEvents(thread.rollout_path);
|
|
2928
|
-
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2929
|
-
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2930
|
-
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2931
|
-
tokenEvents.forEach((event, index) => {
|
|
2932
|
-
const usage = event.usage;
|
|
2933
|
-
const inputTotal = usage.input_tokens ?? 0;
|
|
2934
|
-
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2935
|
-
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2936
|
-
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2937
|
-
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2938
|
-
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2939
|
-
const requestId = `${sessionId}-${index}`;
|
|
2940
|
-
upsertRequest(db, withAccount({
|
|
2941
|
-
id: requestId,
|
|
2958
|
+
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
2959
|
+
for (const thread of threads) {
|
|
2960
|
+
const model = thread.model ?? readCodexModel(source.configPath);
|
|
2961
|
+
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
2962
|
+
const processed = getIngestState(db, source.stateSource, thread.id);
|
|
2963
|
+
if (processed === stateValue)
|
|
2964
|
+
continue;
|
|
2965
|
+
const projectPath = thread.cwd ?? "";
|
|
2966
|
+
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
2967
|
+
const sessionId = `${source.sessionPrefix}-${thread.id}`;
|
|
2968
|
+
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
2969
|
+
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
2970
|
+
upsertSession(db, withAccount({
|
|
2971
|
+
id: sessionId,
|
|
2942
2972
|
agent: "codex",
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2951
|
-
duration_ms: 0,
|
|
2952
|
-
timestamp,
|
|
2953
|
-
source_request_id: requestId,
|
|
2973
|
+
project_path: projectPath,
|
|
2974
|
+
project_name: projectName,
|
|
2975
|
+
started_at: startedAt,
|
|
2976
|
+
ended_at: endedAt,
|
|
2977
|
+
total_cost_usd: 0,
|
|
2978
|
+
total_tokens: 0,
|
|
2979
|
+
request_count: 0,
|
|
2954
2980
|
machine_id: machineId
|
|
2955
2981
|
}, account));
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2982
|
+
const events = readTokenEvents(thread.rollout_path);
|
|
2983
|
+
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2984
|
+
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2985
|
+
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2986
|
+
tokenEvents.forEach((event, index) => {
|
|
2987
|
+
const usage = event.usage;
|
|
2988
|
+
const inputTotal = usage.input_tokens ?? 0;
|
|
2989
|
+
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2990
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2991
|
+
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2992
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2993
|
+
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2994
|
+
const requestId = `${sessionId}-${index}`;
|
|
2995
|
+
upsertRequest(db, withAccount({
|
|
2996
|
+
id: requestId,
|
|
2997
|
+
agent: "codex",
|
|
2998
|
+
session_id: sessionId,
|
|
2999
|
+
model,
|
|
3000
|
+
input_tokens: inputTokens,
|
|
3001
|
+
output_tokens: outputTokens,
|
|
3002
|
+
cache_read_tokens: cacheReadTokens,
|
|
3003
|
+
cache_create_tokens: 0,
|
|
3004
|
+
cost_usd: costUsd,
|
|
3005
|
+
cost_basis: defaultCostBasisForAgent("codex"),
|
|
3006
|
+
duration_ms: 0,
|
|
3007
|
+
timestamp,
|
|
3008
|
+
source_request_id: requestId,
|
|
3009
|
+
machine_id: machineId
|
|
3010
|
+
}, account));
|
|
3011
|
+
requests++;
|
|
3012
|
+
});
|
|
3013
|
+
rollupSession(db, sessionId);
|
|
3014
|
+
setIngestState(db, source.stateSource, thread.id, stateValue);
|
|
3015
|
+
ingested++;
|
|
3016
|
+
if (verbose)
|
|
3017
|
+
console.log(`${source.label} session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
|
|
3018
|
+
}
|
|
3019
|
+
} finally {
|
|
3020
|
+
codexDb?.close();
|
|
2963
3021
|
}
|
|
2964
|
-
} finally {
|
|
2965
|
-
codexDb?.close();
|
|
2966
3022
|
}
|
|
2967
3023
|
return { sessions: ingested, requests };
|
|
2968
3024
|
}
|
|
2969
|
-
var DEFAULT_CODEX_DB_PATH, DEFAULT_CODEX_CONFIG_PATH, CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
3025
|
+
var DEFAULT_CODEX_DB_PATH, DEFAULT_CODEX_CONFIG_PATH, DEFAULT_CODEWITH_DB_PATH, DEFAULT_CODEWITH_CONFIG_PATH, CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
2970
3026
|
var init_codex = __esm(() => {
|
|
2971
3027
|
init_database();
|
|
2972
3028
|
init_pricing();
|
|
2973
3029
|
init_accounts();
|
|
2974
3030
|
DEFAULT_CODEX_DB_PATH = join7(homedir4(), ".codex", "state_5.sqlite");
|
|
2975
3031
|
DEFAULT_CODEX_CONFIG_PATH = join7(homedir4(), ".codex", "config.toml");
|
|
3032
|
+
DEFAULT_CODEWITH_DB_PATH = join7(homedir4(), ".codewith", "state_5.sqlite");
|
|
3033
|
+
DEFAULT_CODEWITH_CONFIG_PATH = join7(homedir4(), ".codewith", "config.toml");
|
|
2976
3034
|
});
|
|
2977
3035
|
|
|
2978
3036
|
// src/ingest/gemini.ts
|
|
@@ -4390,14 +4448,14 @@ var init_watch_paths = __esm(() => {
|
|
|
4390
4448
|
});
|
|
4391
4449
|
|
|
4392
4450
|
// src/cli/commands/notification.ts
|
|
4393
|
-
import { execFileSync } from "child_process";
|
|
4451
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
4394
4452
|
function appleScriptString(value) {
|
|
4395
4453
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
4396
4454
|
}
|
|
4397
4455
|
function notificationScript(title, body) {
|
|
4398
4456
|
return `display notification ${appleScriptString(body)} with title ${appleScriptString(title)}`;
|
|
4399
4457
|
}
|
|
4400
|
-
function sendNotification(title, body, execFile =
|
|
4458
|
+
function sendNotification(title, body, execFile = execFileSync2) {
|
|
4401
4459
|
try {
|
|
4402
4460
|
execFile("osascript", ["-e", notificationScript(title, body)], { stdio: "ignore" });
|
|
4403
4461
|
} catch {}
|
|
@@ -5038,7 +5096,7 @@ __export(exports_menubar, {
|
|
|
5038
5096
|
menubarInstall: () => menubarInstall
|
|
5039
5097
|
});
|
|
5040
5098
|
import chalk6 from "chalk";
|
|
5041
|
-
import { execFileSync as
|
|
5099
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
5042
5100
|
import { cpSync, existsSync as existsSync16, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5043
5101
|
import { tmpdir, arch } from "os";
|
|
5044
5102
|
import { join as join13 } from "path";
|
|
@@ -5050,7 +5108,7 @@ function isInstalled() {
|
|
|
5050
5108
|
}
|
|
5051
5109
|
function isRunning() {
|
|
5052
5110
|
try {
|
|
5053
|
-
const result =
|
|
5111
|
+
const result = execFileSync3("pgrep", ["-x", "EconomyBar"], { stdio: "pipe" }).toString().trim();
|
|
5054
5112
|
return result.length > 0;
|
|
5055
5113
|
} catch {
|
|
5056
5114
|
return false;
|
|
@@ -5101,12 +5159,12 @@ async function menubarInstall(opts) {
|
|
|
5101
5159
|
try {
|
|
5102
5160
|
rmSync(extractDir, { recursive: true, force: true });
|
|
5103
5161
|
mkdirSync4(extractDir, { recursive: true });
|
|
5104
|
-
|
|
5162
|
+
execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
|
|
5105
5163
|
if (isInstalled())
|
|
5106
5164
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
5107
5165
|
cpSync(join13(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
|
|
5108
5166
|
try {
|
|
5109
|
-
|
|
5167
|
+
execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
|
|
5110
5168
|
} catch {}
|
|
5111
5169
|
rmSync(zipPath, { force: true });
|
|
5112
5170
|
rmSync(extractDir, { recursive: true, force: true });
|
|
@@ -5117,7 +5175,7 @@ async function menubarInstall(opts) {
|
|
|
5117
5175
|
}
|
|
5118
5176
|
console.log(chalk6.cyan("\u2192 Launching Economy Bar..."));
|
|
5119
5177
|
try {
|
|
5120
|
-
|
|
5178
|
+
execFileSync3("open", [APP_PATH], { stdio: "ignore" });
|
|
5121
5179
|
console.log(chalk6.bold.green(`
|
|
5122
5180
|
\u2713 Economy Bar is running in your menu bar!`));
|
|
5123
5181
|
console.log(chalk6.dim(" Make sure economy serve is running: economy serve"));
|
|
@@ -5132,8 +5190,8 @@ function menubarUninstall() {
|
|
|
5132
5190
|
}
|
|
5133
5191
|
if (isRunning()) {
|
|
5134
5192
|
try {
|
|
5135
|
-
|
|
5136
|
-
|
|
5193
|
+
execFileSync3("osascript", ["-e", 'quit app "Economy Bar"'], { stdio: "ignore" });
|
|
5194
|
+
execFileSync3("sleep", ["1"], { stdio: "ignore" });
|
|
5137
5195
|
} catch {}
|
|
5138
5196
|
}
|
|
5139
5197
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
@@ -5144,7 +5202,7 @@ function menubarStart() {
|
|
|
5144
5202
|
console.error(chalk6.red("Economy Bar is not installed. Run: economy menubar install"));
|
|
5145
5203
|
process.exit(1);
|
|
5146
5204
|
}
|
|
5147
|
-
|
|
5205
|
+
execFileSync3("open", [APP_PATH], { stdio: "ignore" });
|
|
5148
5206
|
console.log(chalk6.green("\u2713 Economy Bar launched"));
|
|
5149
5207
|
}
|
|
5150
5208
|
function menubarStop() {
|
|
@@ -5153,7 +5211,7 @@ function menubarStop() {
|
|
|
5153
5211
|
return;
|
|
5154
5212
|
}
|
|
5155
5213
|
try {
|
|
5156
|
-
|
|
5214
|
+
execFileSync3("osascript", ["-e", 'quit app "Economy Bar"'], { stdio: "ignore" });
|
|
5157
5215
|
console.log(chalk6.green("\u2713 Economy Bar stopped"));
|
|
5158
5216
|
} catch {
|
|
5159
5217
|
console.log(chalk6.yellow("Could not quit Economy Bar gracefully"));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAMxD,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,MAAM,EACN,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,MAAM,EACN,aAAa,EACd,MAAM,mBAAmB,CAAA;AAyB1B,wBAAgB,YAAY,IAAI,MAAM,CAMrC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAkBnC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAIlC;AAED,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,QAAQ,CAgBxE;AA4QD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAuBrE;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAkBzE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CA2BnE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,aAAkB,GAAG,cAAc,EAAE,CAuBxF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,SAAK,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAKvF;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,WAAW,CAmC7G;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAUlE;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAAc,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CA4E5G;AA2CD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAAc,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE,CA8EhH;AA+FD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAAc,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE,CA+EhH;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,SAAK,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAUvI;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAiB7I;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAKzE;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAI5E;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAG3D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9D;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAU/D;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,EAAE,CAElD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,YAAY,EAAE,CA2B9D;AAID,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;IACzC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI;IACtC,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;IACpB,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CASzD;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,EAAE,CAE9C;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,GAAG,UAAU,EAAE,CA6B1D;AAID,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGvF;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE7F;AAID,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAEhF;AAID,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAA;IACzC,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,GAAG,IAAI,CAKxE;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAExG;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAY5H;AAID,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAAc,GAAG,WAAW,EAAE,CA4ChF;AAID,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,cAAc,GAAG,IAAI,CAexE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAElF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAE/D;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAEpE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAkBvO;AAID,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,mBAAmB,EAAE,YAAY,GAAG,IAAI,CASpG;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,mBAAmB,EAAE,YAAY,EAAE,CAE1F;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAEjE;AAID,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,IAAI,CAAC,OAAO,mBAAmB,EAAE,aAAa,EAAE,IAAI,GAAG,YAAY,CAAC,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAChH,IAAI,CAON;AAED,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,QAAQ,EACZ,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3D,OAAO,mBAAmB,EAAE,aAAa,EAAE,CAQ7C;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,mBAAmB,EAAE,eAAe,EAAE,CAE/F;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAqBnD"}
|
package/dist/index.js
CHANGED
|
@@ -514,17 +514,40 @@ var init_pricing = __esm(() => {
|
|
|
514
514
|
|
|
515
515
|
// src/db/database.ts
|
|
516
516
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
517
|
+
import { execFileSync } from "child_process";
|
|
517
518
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
518
|
-
import { hostname } from "os";
|
|
519
|
+
import { hostname, platform } from "os";
|
|
519
520
|
import { homedir } from "os";
|
|
520
521
|
import { join } from "path";
|
|
522
|
+
function normalizeMachineId(value) {
|
|
523
|
+
const id = value?.trim().toLowerCase().split(".")[0];
|
|
524
|
+
return id && id.length > 0 ? id : null;
|
|
525
|
+
}
|
|
526
|
+
function macHostMachineId() {
|
|
527
|
+
if (platform() !== "darwin")
|
|
528
|
+
return null;
|
|
529
|
+
for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
|
|
530
|
+
try {
|
|
531
|
+
const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
|
|
532
|
+
encoding: "utf8",
|
|
533
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
534
|
+
timeout: 1000
|
|
535
|
+
});
|
|
536
|
+
const id = normalizeMachineId(value);
|
|
537
|
+
if (id && id !== "mac" && id !== "localhost")
|
|
538
|
+
return id;
|
|
539
|
+
} catch {}
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
521
543
|
function getMachineId() {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
544
|
+
const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
|
|
545
|
+
if (envMachine)
|
|
546
|
+
return envMachine;
|
|
547
|
+
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
548
|
+
if (hostMachine === "mac" || hostMachine === "localhost")
|
|
549
|
+
return macHostMachineId() ?? hostMachine;
|
|
550
|
+
return hostMachine;
|
|
528
551
|
}
|
|
529
552
|
function getDataDir() {
|
|
530
553
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
@@ -2496,6 +2519,8 @@ import { join as join4, basename as basename2 } from "path";
|
|
|
2496
2519
|
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
2497
2520
|
var DEFAULT_CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
|
|
2498
2521
|
var DEFAULT_CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
|
|
2522
|
+
var DEFAULT_CODEWITH_DB_PATH = join4(homedir3(), ".codewith", "state_5.sqlite");
|
|
2523
|
+
var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir3(), ".codewith", "config.toml");
|
|
2499
2524
|
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
2500
2525
|
function codexDbPath() {
|
|
2501
2526
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
@@ -2503,8 +2528,40 @@ function codexDbPath() {
|
|
|
2503
2528
|
function codexConfigPath() {
|
|
2504
2529
|
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
2505
2530
|
}
|
|
2506
|
-
function
|
|
2507
|
-
|
|
2531
|
+
function codewithDbPath() {
|
|
2532
|
+
return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? DEFAULT_CODEWITH_DB_PATH;
|
|
2533
|
+
}
|
|
2534
|
+
function codewithConfigPath() {
|
|
2535
|
+
return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? DEFAULT_CODEWITH_CONFIG_PATH;
|
|
2536
|
+
}
|
|
2537
|
+
function codexSources() {
|
|
2538
|
+
const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
|
|
2539
|
+
const explicitCodewithPath = process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"];
|
|
2540
|
+
const sources = [{
|
|
2541
|
+
label: "Codex",
|
|
2542
|
+
dbPath: codexDbPath(),
|
|
2543
|
+
configPath: codexConfigPath(),
|
|
2544
|
+
sessionPrefix: "codex",
|
|
2545
|
+
stateSource: "codex"
|
|
2546
|
+
}];
|
|
2547
|
+
if (!explicitCodexPath || explicitCodewithPath) {
|
|
2548
|
+
sources.push({
|
|
2549
|
+
label: "Codewith",
|
|
2550
|
+
dbPath: codewithDbPath(),
|
|
2551
|
+
configPath: codewithConfigPath(),
|
|
2552
|
+
sessionPrefix: "codex-codewith",
|
|
2553
|
+
stateSource: "codex-codewith"
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
const seen = new Set;
|
|
2557
|
+
return sources.filter((source) => {
|
|
2558
|
+
if (seen.has(source.dbPath))
|
|
2559
|
+
return false;
|
|
2560
|
+
seen.add(source.dbPath);
|
|
2561
|
+
return true;
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
function readCodexModel(configPath = codexConfigPath()) {
|
|
2508
2565
|
if (!existsSync5(configPath))
|
|
2509
2566
|
return "gpt-5-codex";
|
|
2510
2567
|
try {
|
|
@@ -2622,84 +2679,85 @@ function fallbackEvents(totalTokens) {
|
|
|
2622
2679
|
}];
|
|
2623
2680
|
}
|
|
2624
2681
|
async function ingestCodex(db, verbose = false) {
|
|
2625
|
-
const dbPath = codexDbPath();
|
|
2626
|
-
if (!existsSync5(dbPath)) {
|
|
2627
|
-
if (verbose)
|
|
2628
|
-
console.log("Codex DB not found:", dbPath);
|
|
2629
|
-
return { sessions: 0, requests: 0 };
|
|
2630
|
-
}
|
|
2631
2682
|
const machineId = getMachineId();
|
|
2632
|
-
let codexDb = null;
|
|
2633
2683
|
let ingested = 0;
|
|
2634
2684
|
let requests = 0;
|
|
2635
2685
|
const account = await resolveAccountForAgent("codex");
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
if (
|
|
2686
|
+
for (const source of codexSources()) {
|
|
2687
|
+
if (!existsSync5(source.dbPath)) {
|
|
2688
|
+
if (verbose)
|
|
2689
|
+
console.log(`${source.label} DB not found:`, source.dbPath);
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
let codexDb = null;
|
|
2693
|
+
try {
|
|
2694
|
+
codexDb = openCodexDb(source.dbPath, verbose);
|
|
2695
|
+
if (!codexDb)
|
|
2646
2696
|
continue;
|
|
2647
|
-
const
|
|
2648
|
-
const
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
request_count: 0,
|
|
2662
|
-
machine_id: machineId
|
|
2663
|
-
}, account));
|
|
2664
|
-
const events = readTokenEvents(thread.rollout_path);
|
|
2665
|
-
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2666
|
-
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2667
|
-
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2668
|
-
tokenEvents.forEach((event, index) => {
|
|
2669
|
-
const usage = event.usage;
|
|
2670
|
-
const inputTotal = usage.input_tokens ?? 0;
|
|
2671
|
-
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2672
|
-
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2673
|
-
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2674
|
-
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2675
|
-
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2676
|
-
const requestId = `${sessionId}-${index}`;
|
|
2677
|
-
upsertRequest(db, withAccount({
|
|
2678
|
-
id: requestId,
|
|
2697
|
+
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
2698
|
+
for (const thread of threads) {
|
|
2699
|
+
const model = thread.model ?? readCodexModel(source.configPath);
|
|
2700
|
+
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
2701
|
+
const processed = getIngestState(db, source.stateSource, thread.id);
|
|
2702
|
+
if (processed === stateValue)
|
|
2703
|
+
continue;
|
|
2704
|
+
const projectPath = thread.cwd ?? "";
|
|
2705
|
+
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
2706
|
+
const sessionId = `${source.sessionPrefix}-${thread.id}`;
|
|
2707
|
+
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
2708
|
+
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
2709
|
+
upsertSession(db, withAccount({
|
|
2710
|
+
id: sessionId,
|
|
2679
2711
|
agent: "codex",
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2688
|
-
duration_ms: 0,
|
|
2689
|
-
timestamp,
|
|
2690
|
-
source_request_id: requestId,
|
|
2712
|
+
project_path: projectPath,
|
|
2713
|
+
project_name: projectName,
|
|
2714
|
+
started_at: startedAt,
|
|
2715
|
+
ended_at: endedAt,
|
|
2716
|
+
total_cost_usd: 0,
|
|
2717
|
+
total_tokens: 0,
|
|
2718
|
+
request_count: 0,
|
|
2691
2719
|
machine_id: machineId
|
|
2692
2720
|
}, account));
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2721
|
+
const events = readTokenEvents(thread.rollout_path);
|
|
2722
|
+
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2723
|
+
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2724
|
+
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2725
|
+
tokenEvents.forEach((event, index) => {
|
|
2726
|
+
const usage = event.usage;
|
|
2727
|
+
const inputTotal = usage.input_tokens ?? 0;
|
|
2728
|
+
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2729
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2730
|
+
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2731
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2732
|
+
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2733
|
+
const requestId = `${sessionId}-${index}`;
|
|
2734
|
+
upsertRequest(db, withAccount({
|
|
2735
|
+
id: requestId,
|
|
2736
|
+
agent: "codex",
|
|
2737
|
+
session_id: sessionId,
|
|
2738
|
+
model,
|
|
2739
|
+
input_tokens: inputTokens,
|
|
2740
|
+
output_tokens: outputTokens,
|
|
2741
|
+
cache_read_tokens: cacheReadTokens,
|
|
2742
|
+
cache_create_tokens: 0,
|
|
2743
|
+
cost_usd: costUsd,
|
|
2744
|
+
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2745
|
+
duration_ms: 0,
|
|
2746
|
+
timestamp,
|
|
2747
|
+
source_request_id: requestId,
|
|
2748
|
+
machine_id: machineId
|
|
2749
|
+
}, account));
|
|
2750
|
+
requests++;
|
|
2751
|
+
});
|
|
2752
|
+
rollupSession(db, sessionId);
|
|
2753
|
+
setIngestState(db, source.stateSource, thread.id, stateValue);
|
|
2754
|
+
ingested++;
|
|
2755
|
+
if (verbose)
|
|
2756
|
+
console.log(`${source.label} session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
|
|
2757
|
+
}
|
|
2758
|
+
} finally {
|
|
2759
|
+
codexDb?.close();
|
|
2700
2760
|
}
|
|
2701
|
-
} finally {
|
|
2702
|
-
codexDb?.close();
|
|
2703
2761
|
}
|
|
2704
2762
|
return { sessions: ingested, requests };
|
|
2705
2763
|
}
|
package/dist/ingest/codex.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SqliteAdapter as Database } from '@hasna/cloud';
|
|
2
|
-
declare function readCodexModel(): string;
|
|
2
|
+
declare function readCodexModel(configPath?: string): string;
|
|
3
3
|
export declare function ingestCodex(db: Database, verbose?: boolean): Promise<{
|
|
4
4
|
sessions: number;
|
|
5
5
|
requests: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA4F7D,iBAAS,cAAc,CAAC,UAAU,SAAoB,GAAG,MAAM,CAS9D;AAwGD,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA+FhH;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -515,17 +515,40 @@ var init_pricing = __esm(() => {
|
|
|
515
515
|
|
|
516
516
|
// src/db/database.ts
|
|
517
517
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
518
|
+
import { execFileSync } from "child_process";
|
|
518
519
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
519
|
-
import { hostname } from "os";
|
|
520
|
+
import { hostname, platform } from "os";
|
|
520
521
|
import { homedir } from "os";
|
|
521
522
|
import { join } from "path";
|
|
523
|
+
function normalizeMachineId(value) {
|
|
524
|
+
const id = value?.trim().toLowerCase().split(".")[0];
|
|
525
|
+
return id && id.length > 0 ? id : null;
|
|
526
|
+
}
|
|
527
|
+
function macHostMachineId() {
|
|
528
|
+
if (platform() !== "darwin")
|
|
529
|
+
return null;
|
|
530
|
+
for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
|
|
531
|
+
try {
|
|
532
|
+
const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
|
|
533
|
+
encoding: "utf8",
|
|
534
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
535
|
+
timeout: 1000
|
|
536
|
+
});
|
|
537
|
+
const id = normalizeMachineId(value);
|
|
538
|
+
if (id && id !== "mac" && id !== "localhost")
|
|
539
|
+
return id;
|
|
540
|
+
} catch {}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
522
544
|
function getMachineId() {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
545
|
+
const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
|
|
546
|
+
if (envMachine)
|
|
547
|
+
return envMachine;
|
|
548
|
+
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
549
|
+
if (hostMachine === "mac" || hostMachine === "localhost")
|
|
550
|
+
return macHostMachineId() ?? hostMachine;
|
|
551
|
+
return hostMachine;
|
|
529
552
|
}
|
|
530
553
|
function getDataDir() {
|
|
531
554
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
@@ -2240,6 +2263,8 @@ import { join as join3, basename as basename2 } from "path";
|
|
|
2240
2263
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
2241
2264
|
var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
2242
2265
|
var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
2266
|
+
var DEFAULT_CODEWITH_DB_PATH = join3(homedir3(), ".codewith", "state_5.sqlite");
|
|
2267
|
+
var DEFAULT_CODEWITH_CONFIG_PATH = join3(homedir3(), ".codewith", "config.toml");
|
|
2243
2268
|
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
2244
2269
|
function codexDbPath() {
|
|
2245
2270
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
@@ -2247,8 +2272,40 @@ function codexDbPath() {
|
|
|
2247
2272
|
function codexConfigPath() {
|
|
2248
2273
|
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
2249
2274
|
}
|
|
2250
|
-
function
|
|
2251
|
-
|
|
2275
|
+
function codewithDbPath() {
|
|
2276
|
+
return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? DEFAULT_CODEWITH_DB_PATH;
|
|
2277
|
+
}
|
|
2278
|
+
function codewithConfigPath() {
|
|
2279
|
+
return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? DEFAULT_CODEWITH_CONFIG_PATH;
|
|
2280
|
+
}
|
|
2281
|
+
function codexSources() {
|
|
2282
|
+
const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
|
|
2283
|
+
const explicitCodewithPath = process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"];
|
|
2284
|
+
const sources = [{
|
|
2285
|
+
label: "Codex",
|
|
2286
|
+
dbPath: codexDbPath(),
|
|
2287
|
+
configPath: codexConfigPath(),
|
|
2288
|
+
sessionPrefix: "codex",
|
|
2289
|
+
stateSource: "codex"
|
|
2290
|
+
}];
|
|
2291
|
+
if (!explicitCodexPath || explicitCodewithPath) {
|
|
2292
|
+
sources.push({
|
|
2293
|
+
label: "Codewith",
|
|
2294
|
+
dbPath: codewithDbPath(),
|
|
2295
|
+
configPath: codewithConfigPath(),
|
|
2296
|
+
sessionPrefix: "codex-codewith",
|
|
2297
|
+
stateSource: "codex-codewith"
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
const seen = new Set;
|
|
2301
|
+
return sources.filter((source) => {
|
|
2302
|
+
if (seen.has(source.dbPath))
|
|
2303
|
+
return false;
|
|
2304
|
+
seen.add(source.dbPath);
|
|
2305
|
+
return true;
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
function readCodexModel(configPath = codexConfigPath()) {
|
|
2252
2309
|
if (!existsSync3(configPath))
|
|
2253
2310
|
return "gpt-5-codex";
|
|
2254
2311
|
try {
|
|
@@ -2366,84 +2423,85 @@ function fallbackEvents(totalTokens) {
|
|
|
2366
2423
|
}];
|
|
2367
2424
|
}
|
|
2368
2425
|
async function ingestCodex(db, verbose = false) {
|
|
2369
|
-
const dbPath = codexDbPath();
|
|
2370
|
-
if (!existsSync3(dbPath)) {
|
|
2371
|
-
if (verbose)
|
|
2372
|
-
console.log("Codex DB not found:", dbPath);
|
|
2373
|
-
return { sessions: 0, requests: 0 };
|
|
2374
|
-
}
|
|
2375
2426
|
const machineId = getMachineId();
|
|
2376
|
-
let codexDb = null;
|
|
2377
2427
|
let ingested = 0;
|
|
2378
2428
|
let requests = 0;
|
|
2379
2429
|
const account = await resolveAccountForAgent("codex");
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
if (
|
|
2430
|
+
for (const source of codexSources()) {
|
|
2431
|
+
if (!existsSync3(source.dbPath)) {
|
|
2432
|
+
if (verbose)
|
|
2433
|
+
console.log(`${source.label} DB not found:`, source.dbPath);
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
let codexDb = null;
|
|
2437
|
+
try {
|
|
2438
|
+
codexDb = openCodexDb(source.dbPath, verbose);
|
|
2439
|
+
if (!codexDb)
|
|
2390
2440
|
continue;
|
|
2391
|
-
const
|
|
2392
|
-
const
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
request_count: 0,
|
|
2406
|
-
machine_id: machineId
|
|
2407
|
-
}, account));
|
|
2408
|
-
const events = readTokenEvents(thread.rollout_path);
|
|
2409
|
-
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2410
|
-
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2411
|
-
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2412
|
-
tokenEvents.forEach((event, index) => {
|
|
2413
|
-
const usage = event.usage;
|
|
2414
|
-
const inputTotal = usage.input_tokens ?? 0;
|
|
2415
|
-
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2416
|
-
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2417
|
-
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2418
|
-
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2419
|
-
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2420
|
-
const requestId = `${sessionId}-${index}`;
|
|
2421
|
-
upsertRequest(db, withAccount({
|
|
2422
|
-
id: requestId,
|
|
2441
|
+
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
2442
|
+
for (const thread of threads) {
|
|
2443
|
+
const model = thread.model ?? readCodexModel(source.configPath);
|
|
2444
|
+
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
2445
|
+
const processed = getIngestState(db, source.stateSource, thread.id);
|
|
2446
|
+
if (processed === stateValue)
|
|
2447
|
+
continue;
|
|
2448
|
+
const projectPath = thread.cwd ?? "";
|
|
2449
|
+
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
2450
|
+
const sessionId = `${source.sessionPrefix}-${thread.id}`;
|
|
2451
|
+
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
2452
|
+
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
2453
|
+
upsertSession(db, withAccount({
|
|
2454
|
+
id: sessionId,
|
|
2423
2455
|
agent: "codex",
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2432
|
-
duration_ms: 0,
|
|
2433
|
-
timestamp,
|
|
2434
|
-
source_request_id: requestId,
|
|
2456
|
+
project_path: projectPath,
|
|
2457
|
+
project_name: projectName,
|
|
2458
|
+
started_at: startedAt,
|
|
2459
|
+
ended_at: endedAt,
|
|
2460
|
+
total_cost_usd: 0,
|
|
2461
|
+
total_tokens: 0,
|
|
2462
|
+
request_count: 0,
|
|
2435
2463
|
machine_id: machineId
|
|
2436
2464
|
}, account));
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2465
|
+
const events = readTokenEvents(thread.rollout_path);
|
|
2466
|
+
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2467
|
+
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2468
|
+
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2469
|
+
tokenEvents.forEach((event, index) => {
|
|
2470
|
+
const usage = event.usage;
|
|
2471
|
+
const inputTotal = usage.input_tokens ?? 0;
|
|
2472
|
+
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2473
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2474
|
+
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2475
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2476
|
+
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2477
|
+
const requestId = `${sessionId}-${index}`;
|
|
2478
|
+
upsertRequest(db, withAccount({
|
|
2479
|
+
id: requestId,
|
|
2480
|
+
agent: "codex",
|
|
2481
|
+
session_id: sessionId,
|
|
2482
|
+
model,
|
|
2483
|
+
input_tokens: inputTokens,
|
|
2484
|
+
output_tokens: outputTokens,
|
|
2485
|
+
cache_read_tokens: cacheReadTokens,
|
|
2486
|
+
cache_create_tokens: 0,
|
|
2487
|
+
cost_usd: costUsd,
|
|
2488
|
+
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2489
|
+
duration_ms: 0,
|
|
2490
|
+
timestamp,
|
|
2491
|
+
source_request_id: requestId,
|
|
2492
|
+
machine_id: machineId
|
|
2493
|
+
}, account));
|
|
2494
|
+
requests++;
|
|
2495
|
+
});
|
|
2496
|
+
rollupSession(db, sessionId);
|
|
2497
|
+
setIngestState(db, source.stateSource, thread.id, stateValue);
|
|
2498
|
+
ingested++;
|
|
2499
|
+
if (verbose)
|
|
2500
|
+
console.log(`${source.label} session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
|
|
2501
|
+
}
|
|
2502
|
+
} finally {
|
|
2503
|
+
codexDb?.close();
|
|
2444
2504
|
}
|
|
2445
|
-
} finally {
|
|
2446
|
-
codexDb?.close();
|
|
2447
2505
|
}
|
|
2448
2506
|
return { sessions: ingested, requests };
|
|
2449
2507
|
}
|
package/dist/otel/index.js
CHANGED
|
@@ -515,17 +515,40 @@ var init_pricing = __esm(() => {
|
|
|
515
515
|
|
|
516
516
|
// src/db/database.ts
|
|
517
517
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
518
|
+
import { execFileSync } from "child_process";
|
|
518
519
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
519
|
-
import { hostname } from "os";
|
|
520
|
+
import { hostname, platform } from "os";
|
|
520
521
|
import { homedir } from "os";
|
|
521
522
|
import { join } from "path";
|
|
523
|
+
function normalizeMachineId(value) {
|
|
524
|
+
const id = value?.trim().toLowerCase().split(".")[0];
|
|
525
|
+
return id && id.length > 0 ? id : null;
|
|
526
|
+
}
|
|
527
|
+
function macHostMachineId() {
|
|
528
|
+
if (platform() !== "darwin")
|
|
529
|
+
return null;
|
|
530
|
+
for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
|
|
531
|
+
try {
|
|
532
|
+
const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
|
|
533
|
+
encoding: "utf8",
|
|
534
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
535
|
+
timeout: 1000
|
|
536
|
+
});
|
|
537
|
+
const id = normalizeMachineId(value);
|
|
538
|
+
if (id && id !== "mac" && id !== "localhost")
|
|
539
|
+
return id;
|
|
540
|
+
} catch {}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
522
544
|
function getMachineId() {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
545
|
+
const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
|
|
546
|
+
if (envMachine)
|
|
547
|
+
return envMachine;
|
|
548
|
+
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
549
|
+
if (hostMachine === "mac" || hostMachine === "localhost")
|
|
550
|
+
return macHostMachineId() ?? hostMachine;
|
|
551
|
+
return hostMachine;
|
|
529
552
|
}
|
|
530
553
|
function getDataDir() {
|
|
531
554
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
package/dist/server/index.js
CHANGED
|
@@ -515,17 +515,40 @@ var init_pricing = __esm(() => {
|
|
|
515
515
|
|
|
516
516
|
// src/db/database.ts
|
|
517
517
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
518
|
+
import { execFileSync } from "child_process";
|
|
518
519
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
519
|
-
import { hostname } from "os";
|
|
520
|
+
import { hostname, platform } from "os";
|
|
520
521
|
import { homedir } from "os";
|
|
521
522
|
import { join } from "path";
|
|
523
|
+
function normalizeMachineId(value) {
|
|
524
|
+
const id = value?.trim().toLowerCase().split(".")[0];
|
|
525
|
+
return id && id.length > 0 ? id : null;
|
|
526
|
+
}
|
|
527
|
+
function macHostMachineId() {
|
|
528
|
+
if (platform() !== "darwin")
|
|
529
|
+
return null;
|
|
530
|
+
for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
|
|
531
|
+
try {
|
|
532
|
+
const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
|
|
533
|
+
encoding: "utf8",
|
|
534
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
535
|
+
timeout: 1000
|
|
536
|
+
});
|
|
537
|
+
const id = normalizeMachineId(value);
|
|
538
|
+
if (id && id !== "mac" && id !== "localhost")
|
|
539
|
+
return id;
|
|
540
|
+
} catch {}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
522
544
|
function getMachineId() {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
545
|
+
const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
|
|
546
|
+
if (envMachine)
|
|
547
|
+
return envMachine;
|
|
548
|
+
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
549
|
+
if (hostMachine === "mac" || hostMachine === "localhost")
|
|
550
|
+
return macHostMachineId() ?? hostMachine;
|
|
551
|
+
return hostMachine;
|
|
529
552
|
}
|
|
530
553
|
function getDataDir() {
|
|
531
554
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
@@ -2698,6 +2721,8 @@ import { join as join3, basename as basename2 } from "path";
|
|
|
2698
2721
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
2699
2722
|
var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
2700
2723
|
var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
2724
|
+
var DEFAULT_CODEWITH_DB_PATH = join3(homedir3(), ".codewith", "state_5.sqlite");
|
|
2725
|
+
var DEFAULT_CODEWITH_CONFIG_PATH = join3(homedir3(), ".codewith", "config.toml");
|
|
2701
2726
|
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
2702
2727
|
function codexDbPath() {
|
|
2703
2728
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
@@ -2705,8 +2730,40 @@ function codexDbPath() {
|
|
|
2705
2730
|
function codexConfigPath() {
|
|
2706
2731
|
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
2707
2732
|
}
|
|
2708
|
-
function
|
|
2709
|
-
|
|
2733
|
+
function codewithDbPath() {
|
|
2734
|
+
return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? DEFAULT_CODEWITH_DB_PATH;
|
|
2735
|
+
}
|
|
2736
|
+
function codewithConfigPath() {
|
|
2737
|
+
return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? DEFAULT_CODEWITH_CONFIG_PATH;
|
|
2738
|
+
}
|
|
2739
|
+
function codexSources() {
|
|
2740
|
+
const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
|
|
2741
|
+
const explicitCodewithPath = process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"];
|
|
2742
|
+
const sources = [{
|
|
2743
|
+
label: "Codex",
|
|
2744
|
+
dbPath: codexDbPath(),
|
|
2745
|
+
configPath: codexConfigPath(),
|
|
2746
|
+
sessionPrefix: "codex",
|
|
2747
|
+
stateSource: "codex"
|
|
2748
|
+
}];
|
|
2749
|
+
if (!explicitCodexPath || explicitCodewithPath) {
|
|
2750
|
+
sources.push({
|
|
2751
|
+
label: "Codewith",
|
|
2752
|
+
dbPath: codewithDbPath(),
|
|
2753
|
+
configPath: codewithConfigPath(),
|
|
2754
|
+
sessionPrefix: "codex-codewith",
|
|
2755
|
+
stateSource: "codex-codewith"
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
const seen = new Set;
|
|
2759
|
+
return sources.filter((source) => {
|
|
2760
|
+
if (seen.has(source.dbPath))
|
|
2761
|
+
return false;
|
|
2762
|
+
seen.add(source.dbPath);
|
|
2763
|
+
return true;
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
function readCodexModel(configPath = codexConfigPath()) {
|
|
2710
2767
|
if (!existsSync3(configPath))
|
|
2711
2768
|
return "gpt-5-codex";
|
|
2712
2769
|
try {
|
|
@@ -2824,84 +2881,85 @@ function fallbackEvents(totalTokens) {
|
|
|
2824
2881
|
}];
|
|
2825
2882
|
}
|
|
2826
2883
|
async function ingestCodex(db, verbose = false) {
|
|
2827
|
-
const dbPath = codexDbPath();
|
|
2828
|
-
if (!existsSync3(dbPath)) {
|
|
2829
|
-
if (verbose)
|
|
2830
|
-
console.log("Codex DB not found:", dbPath);
|
|
2831
|
-
return { sessions: 0, requests: 0 };
|
|
2832
|
-
}
|
|
2833
2884
|
const machineId = getMachineId();
|
|
2834
|
-
let codexDb = null;
|
|
2835
2885
|
let ingested = 0;
|
|
2836
2886
|
let requests = 0;
|
|
2837
2887
|
const account = await resolveAccountForAgent("codex");
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
if (
|
|
2888
|
+
for (const source of codexSources()) {
|
|
2889
|
+
if (!existsSync3(source.dbPath)) {
|
|
2890
|
+
if (verbose)
|
|
2891
|
+
console.log(`${source.label} DB not found:`, source.dbPath);
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
let codexDb = null;
|
|
2895
|
+
try {
|
|
2896
|
+
codexDb = openCodexDb(source.dbPath, verbose);
|
|
2897
|
+
if (!codexDb)
|
|
2848
2898
|
continue;
|
|
2849
|
-
const
|
|
2850
|
-
const
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
request_count: 0,
|
|
2864
|
-
machine_id: machineId
|
|
2865
|
-
}, account));
|
|
2866
|
-
const events = readTokenEvents(thread.rollout_path);
|
|
2867
|
-
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2868
|
-
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2869
|
-
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2870
|
-
tokenEvents.forEach((event, index) => {
|
|
2871
|
-
const usage = event.usage;
|
|
2872
|
-
const inputTotal = usage.input_tokens ?? 0;
|
|
2873
|
-
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2874
|
-
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2875
|
-
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2876
|
-
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2877
|
-
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2878
|
-
const requestId = `${sessionId}-${index}`;
|
|
2879
|
-
upsertRequest(db, withAccount({
|
|
2880
|
-
id: requestId,
|
|
2899
|
+
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
2900
|
+
for (const thread of threads) {
|
|
2901
|
+
const model = thread.model ?? readCodexModel(source.configPath);
|
|
2902
|
+
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
2903
|
+
const processed = getIngestState(db, source.stateSource, thread.id);
|
|
2904
|
+
if (processed === stateValue)
|
|
2905
|
+
continue;
|
|
2906
|
+
const projectPath = thread.cwd ?? "";
|
|
2907
|
+
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
2908
|
+
const sessionId = `${source.sessionPrefix}-${thread.id}`;
|
|
2909
|
+
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
2910
|
+
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
2911
|
+
upsertSession(db, withAccount({
|
|
2912
|
+
id: sessionId,
|
|
2881
2913
|
agent: "codex",
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2890
|
-
duration_ms: 0,
|
|
2891
|
-
timestamp,
|
|
2892
|
-
source_request_id: requestId,
|
|
2914
|
+
project_path: projectPath,
|
|
2915
|
+
project_name: projectName,
|
|
2916
|
+
started_at: startedAt,
|
|
2917
|
+
ended_at: endedAt,
|
|
2918
|
+
total_cost_usd: 0,
|
|
2919
|
+
total_tokens: 0,
|
|
2920
|
+
request_count: 0,
|
|
2893
2921
|
machine_id: machineId
|
|
2894
2922
|
}, account));
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2923
|
+
const events = readTokenEvents(thread.rollout_path);
|
|
2924
|
+
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2925
|
+
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2926
|
+
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2927
|
+
tokenEvents.forEach((event, index) => {
|
|
2928
|
+
const usage = event.usage;
|
|
2929
|
+
const inputTotal = usage.input_tokens ?? 0;
|
|
2930
|
+
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2931
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2932
|
+
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2933
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2934
|
+
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2935
|
+
const requestId = `${sessionId}-${index}`;
|
|
2936
|
+
upsertRequest(db, withAccount({
|
|
2937
|
+
id: requestId,
|
|
2938
|
+
agent: "codex",
|
|
2939
|
+
session_id: sessionId,
|
|
2940
|
+
model,
|
|
2941
|
+
input_tokens: inputTokens,
|
|
2942
|
+
output_tokens: outputTokens,
|
|
2943
|
+
cache_read_tokens: cacheReadTokens,
|
|
2944
|
+
cache_create_tokens: 0,
|
|
2945
|
+
cost_usd: costUsd,
|
|
2946
|
+
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2947
|
+
duration_ms: 0,
|
|
2948
|
+
timestamp,
|
|
2949
|
+
source_request_id: requestId,
|
|
2950
|
+
machine_id: machineId
|
|
2951
|
+
}, account));
|
|
2952
|
+
requests++;
|
|
2953
|
+
});
|
|
2954
|
+
rollupSession(db, sessionId);
|
|
2955
|
+
setIngestState(db, source.stateSource, thread.id, stateValue);
|
|
2956
|
+
ingested++;
|
|
2957
|
+
if (verbose)
|
|
2958
|
+
console.log(`${source.label} session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
|
|
2959
|
+
}
|
|
2960
|
+
} finally {
|
|
2961
|
+
codexDb?.close();
|
|
2902
2962
|
}
|
|
2903
|
-
} finally {
|
|
2904
|
-
codexDb?.close();
|
|
2905
2963
|
}
|
|
2906
2964
|
return { sessions: ingested, requests };
|
|
2907
2965
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/economy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.38",
|
|
4
4
|
"description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, Gemini, OpenCode, Cursor, Pi, and Hermes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|