@hasna/economy 0.5.3 → 0.5.5
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 +7 -3
- package/dist/chunks/{index-s9mvven6.js → index-61c0vz9p.js} +223 -100
- package/dist/chunks/{index-bn04v378.js → index-7k1xt8mj.js} +1 -1
- package/dist/chunks/{index-1vz1arz5.js → index-pnnqdv3r.js} +1 -1
- package/dist/chunks/{index-mhrkwvtk.js → index-z9psed59.js} +1 -1
- package/dist/chunks/{serve-8yqe7nhs.js → serve-zp5a3qzw.js} +2 -2
- package/dist/chunks/{tui-fe94bvp9.js → tui-2yea642b.js} +3 -3
- package/dist/chunks/{watch-ct8dvrpr.js → watch-811vncjz.js} +3 -3
- package/dist/cli/index.js +10 -10
- package/dist/index.js +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- 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/paths.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 +290 -167
- package/dist/otel/index.js +87 -65
- package/dist/server/index.js +231 -110
- package/dist/server/service-worker.js +222 -101
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -4620,7 +4620,7 @@ __export(exports_legacy_plan, {
|
|
|
4620
4620
|
});
|
|
4621
4621
|
import { createHash as createHash6 } from "crypto";
|
|
4622
4622
|
import { closeSync, constants, fstatSync, openSync, readSync } from "fs";
|
|
4623
|
-
import { isAbsolute } from "path";
|
|
4623
|
+
import { isAbsolute as isAbsolute2 } from "path";
|
|
4624
4624
|
function object(value, fields, label) {
|
|
4625
4625
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== fields.sort().join(","))
|
|
4626
4626
|
throw Error(`${label} has unsupported or missing fields`);
|
|
@@ -4673,7 +4673,7 @@ function parseLegacySelection(value) {
|
|
|
4673
4673
|
return { schema: mode.selection, items };
|
|
4674
4674
|
}
|
|
4675
4675
|
function readLegacySelectionFile(path) {
|
|
4676
|
-
if (!
|
|
4676
|
+
if (!isAbsolute2(path) || path.includes("\x00"))
|
|
4677
4677
|
throw Error("--input must be an absolute path to a regular JSON file");
|
|
4678
4678
|
let fd;
|
|
4679
4679
|
try {
|
|
@@ -4708,7 +4708,7 @@ function readLegacySelectionFile(path) {
|
|
|
4708
4708
|
}
|
|
4709
4709
|
}
|
|
4710
4710
|
function parseLegacyPlanOptions(args) {
|
|
4711
|
-
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"))
|
|
4712
4712
|
throw Error("legacy-plan accepts exactly --input <absolute JSON file>");
|
|
4713
4713
|
return args[1];
|
|
4714
4714
|
}
|
|
@@ -4837,7 +4837,7 @@ __export(exports_legacy_preservation, {
|
|
|
4837
4837
|
});
|
|
4838
4838
|
import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
|
|
4839
4839
|
import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, lstatSync, openSync as openSync2, readSync as readSync2, realpathSync, writeSync } from "fs";
|
|
4840
|
-
import { basename as basename4, dirname as dirname4, isAbsolute as
|
|
4840
|
+
import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
|
|
4841
4841
|
function parseLegacyPlan(value) {
|
|
4842
4842
|
const p = record(value);
|
|
4843
4843
|
const mode = legacyMode("plan", p.schema);
|
|
@@ -4988,7 +4988,7 @@ async function preserveLegacyOriginals(client, input) {
|
|
|
4988
4988
|
});
|
|
4989
4989
|
}
|
|
4990
4990
|
function readLegacyBytes(path, maxBytes = MAX_BYTES) {
|
|
4991
|
-
if (!
|
|
4991
|
+
if (!isAbsolute3(path) || path.includes("\x00"))
|
|
4992
4992
|
fail("Artifact path must be absolute");
|
|
4993
4993
|
let fd;
|
|
4994
4994
|
try {
|
|
@@ -5027,7 +5027,7 @@ function readLegacyArtifact(path, maxBytes = MAX_BYTES) {
|
|
|
5027
5027
|
}
|
|
5028
5028
|
function writeLegacyCapsule(path, input) {
|
|
5029
5029
|
const capsule = parseLegacyCapsule(input);
|
|
5030
|
-
if (!
|
|
5030
|
+
if (!isAbsolute3(path) || path.includes("\x00") || realpathSync(dirname4(path)) !== dirname4(path))
|
|
5031
5031
|
fail("Output requires an absolute path with a real parent directory");
|
|
5032
5032
|
const parent = lstatSync(dirname4(path));
|
|
5033
5033
|
if (!parent.isDirectory() || parent.uid !== process.getuid?.() || (parent.mode & 63) !== 0)
|
|
@@ -5141,7 +5141,7 @@ async function rehearseLegacyRestore(client, input, scope = "transaction") {
|
|
|
5141
5141
|
}
|
|
5142
5142
|
function parseLegacyPreservationOptions(command, args) {
|
|
5143
5143
|
const preserve = command === "legacy-preserve";
|
|
5144
|
-
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")))
|
|
5145
5145
|
fail("Expected " + command + " --input <absolute JSON file>" + (preserve ? " --output <new file in an owner-only directory>" : ""));
|
|
5146
5146
|
return { input: args[1], ...preserve ? { output: args[3] } : {} };
|
|
5147
5147
|
}
|
|
@@ -5190,7 +5190,7 @@ __export(exports_legacy_operations, {
|
|
|
5190
5190
|
LegacyOperationInputError: () => LegacyOperationInputError
|
|
5191
5191
|
});
|
|
5192
5192
|
import { createHash as createHash8 } from "crypto";
|
|
5193
|
-
import { isAbsolute as
|
|
5193
|
+
import { isAbsolute as isAbsolute4 } from "path";
|
|
5194
5194
|
function parseLegacyIntent(action, value) {
|
|
5195
5195
|
try {
|
|
5196
5196
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -5435,7 +5435,7 @@ function parseLegacyOperationOptions(command, args) {
|
|
|
5435
5435
|
const apply = command === "legacy-apply";
|
|
5436
5436
|
if (!apply && command !== "legacy-rollback")
|
|
5437
5437
|
fail2("Unknown legacy operation or arguments");
|
|
5438
|
-
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")))
|
|
5439
5439
|
fail2("Expected " + command + " --input <absolute intent JSON>" + (apply ? " --capsule <absolute preservation JSON>" : ""));
|
|
5440
5440
|
return { input: args[1], ...apply ? { capsule: args[3] } : {} };
|
|
5441
5441
|
}
|
|
@@ -5529,7 +5529,7 @@ function isAuthorizedRequest(req, path) {
|
|
|
5529
5529
|
// package.json
|
|
5530
5530
|
var package_default = {
|
|
5531
5531
|
name: "@hasna/economy",
|
|
5532
|
-
version: "0.5.
|
|
5532
|
+
version: "0.5.5",
|
|
5533
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",
|
|
5534
5534
|
repository: {
|
|
5535
5535
|
type: "git",
|
|
@@ -8812,14 +8812,14 @@ function sourceTimeMillis(value) {
|
|
|
8812
8812
|
init_database();
|
|
8813
8813
|
init_pricing();
|
|
8814
8814
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
8815
|
-
import { homedir as
|
|
8816
|
-
import { join as
|
|
8815
|
+
import { homedir as homedir4 } from "os";
|
|
8816
|
+
import { join as join6, basename } from "path";
|
|
8817
8817
|
|
|
8818
8818
|
// src/lib/accounts-store.ts
|
|
8819
8819
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
8820
8820
|
import { homedir as homedir2 } from "os";
|
|
8821
8821
|
import { join as join4 } from "path";
|
|
8822
|
-
import { z } from "zod";
|
|
8822
|
+
import { z as z2 } from "zod";
|
|
8823
8823
|
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
8824
8824
|
|
|
8825
8825
|
// src/lib/cloud-storage.ts
|
|
@@ -8838,33 +8838,138 @@ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
|
8838
8838
|
var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
8839
8839
|
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
8840
8840
|
|
|
8841
|
-
// src/lib/
|
|
8842
|
-
|
|
8843
|
-
|
|
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);
|
|
8844
8872
|
var profileSchema = z.object({
|
|
8845
|
-
name:
|
|
8846
|
-
tool:
|
|
8873
|
+
name: nonempty,
|
|
8874
|
+
tool: nonempty,
|
|
8847
8875
|
email: z.string().optional(),
|
|
8848
8876
|
displayName: z.string().optional(),
|
|
8849
|
-
dir:
|
|
8877
|
+
dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
|
|
8878
|
+
createdAt: nonempty,
|
|
8879
|
+
lastUsedAt: z.string().optional(),
|
|
8850
8880
|
description: z.string().optional(),
|
|
8851
|
-
|
|
8852
|
-
|
|
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()
|
|
8853
8958
|
});
|
|
8854
|
-
var toolDefSchema =
|
|
8855
|
-
id:
|
|
8856
|
-
label:
|
|
8857
|
-
envVar:
|
|
8858
|
-
defaultDir:
|
|
8859
|
-
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()
|
|
8860
8965
|
});
|
|
8861
|
-
var storeSchema =
|
|
8862
|
-
version:
|
|
8863
|
-
current:
|
|
8864
|
-
applied:
|
|
8865
|
-
toolLocks:
|
|
8866
|
-
profiles:
|
|
8867
|
-
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([])
|
|
8868
8973
|
});
|
|
8869
8974
|
function localRegistryAllowed(env = process.env) {
|
|
8870
8975
|
return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => {
|
|
@@ -9146,25 +9251,34 @@ class ApiStore {
|
|
|
9146
9251
|
}
|
|
9147
9252
|
}
|
|
9148
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) {
|
|
9149
9263
|
const clientEnv = env;
|
|
9150
|
-
const credential = resolveCredential(
|
|
9264
|
+
const credential = resolveCredential(app, clientEnv, credentials);
|
|
9151
9265
|
if (credential === null) {
|
|
9152
9266
|
try {
|
|
9153
|
-
resolveClientTransport(
|
|
9267
|
+
resolveClientTransport(app, clientEnv, credentials ? { credentials } : {});
|
|
9154
9268
|
} catch (err) {
|
|
9155
9269
|
if (isNoAccountsCredentialConfigurationError(err))
|
|
9156
|
-
return
|
|
9270
|
+
return null;
|
|
9157
9271
|
throw err;
|
|
9158
9272
|
}
|
|
9159
|
-
return
|
|
9273
|
+
return null;
|
|
9160
9274
|
}
|
|
9161
9275
|
if (credential.tier === "pointer") {
|
|
9162
|
-
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.`);
|
|
9163
9277
|
}
|
|
9164
|
-
const resolution = resolveClientTransport(
|
|
9165
|
-
credentials: { ...
|
|
9278
|
+
const resolution = resolveClientTransport(app, clientEnv, {
|
|
9279
|
+
credentials: { ...credentials, apiKey: credential.apiKey }
|
|
9166
9280
|
});
|
|
9167
|
-
return
|
|
9281
|
+
return { baseUrl: resolution.baseUrl, apiKey: credential.apiKey };
|
|
9168
9282
|
}
|
|
9169
9283
|
function isNoAccountsCredentialConfigurationError(error) {
|
|
9170
9284
|
if (typeof error !== "object" || error === null)
|
|
@@ -9181,10 +9295,10 @@ function unconfiguredStore(env) {
|
|
|
9181
9295
|
return new NoRegistryStore;
|
|
9182
9296
|
}
|
|
9183
9297
|
function accountsLocalModeNotice() {
|
|
9184
|
-
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.";
|
|
9185
9299
|
}
|
|
9186
9300
|
function accountsRefusalNotice() {
|
|
9187
|
-
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.";
|
|
9188
9302
|
}
|
|
9189
9303
|
var accountsLocalNoticePrinted = false;
|
|
9190
9304
|
function announceAccountsLocalMode() {
|
|
@@ -9272,7 +9386,10 @@ async function profileForEnvDir(store, tool, env) {
|
|
|
9272
9386
|
if (!configuredDir)
|
|
9273
9387
|
return null;
|
|
9274
9388
|
const normalized = normalizeDir(configuredDir);
|
|
9275
|
-
|
|
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;
|
|
9276
9393
|
}
|
|
9277
9394
|
async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
9278
9395
|
const override = envOverride(agent, env);
|
|
@@ -9286,8 +9403,15 @@ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
|
9286
9403
|
continue;
|
|
9287
9404
|
const envProfile = await profileForEnvDir(store, tool, env);
|
|
9288
9405
|
if (envProfile)
|
|
9289
|
-
return fromProfile(envProfile, "env");
|
|
9290
|
-
|
|
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);
|
|
9291
9415
|
if (appliedName) {
|
|
9292
9416
|
const applied = await store.findProfile(appliedName, toolId);
|
|
9293
9417
|
if (applied)
|
|
@@ -9312,12 +9436,41 @@ function withAccount(record, account) {
|
|
|
9312
9436
|
};
|
|
9313
9437
|
}
|
|
9314
9438
|
|
|
9439
|
+
// src/lib/paths.ts
|
|
9440
|
+
import { homedir as homedir3 } from "os";
|
|
9441
|
+
import { join as join5 } from "path";
|
|
9442
|
+
function getHomeDir2() {
|
|
9443
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir3();
|
|
9444
|
+
}
|
|
9445
|
+
function agentPaths() {
|
|
9446
|
+
const home = getHomeDir2();
|
|
9447
|
+
const claudeHome = process.env["CLAUDE_CONFIG_DIR"] || join5(home, ".claude");
|
|
9448
|
+
const codexHome = process.env["CODEX_HOME"] || join5(home, ".codex");
|
|
9449
|
+
return {
|
|
9450
|
+
claudeProjects: join5(claudeHome, "projects"),
|
|
9451
|
+
claudeCredentials: join5(claudeHome, ".credentials.json"),
|
|
9452
|
+
takumiProjects: join5(home, ".takumi", "projects"),
|
|
9453
|
+
codexDir: codexHome,
|
|
9454
|
+
codexDb: join5(codexHome, "state_5.sqlite"),
|
|
9455
|
+
codexAuth: join5(codexHome, "auth.json"),
|
|
9456
|
+
codexConfig: join5(codexHome, "config.toml"),
|
|
9457
|
+
geminiTmp: join5(home, ".gemini", "tmp"),
|
|
9458
|
+
geminiHistory: join5(home, ".gemini", "history"),
|
|
9459
|
+
opencodeMessages: join5(home, ".local", "share", "opencode", "storage", "message"),
|
|
9460
|
+
piSessions: join5(home, ".pi", "agent", "sessions"),
|
|
9461
|
+
hermesDir: join5(home, ".hermes"),
|
|
9462
|
+
hermesDb: join5(home, ".hermes", "state.db")
|
|
9463
|
+
};
|
|
9464
|
+
}
|
|
9465
|
+
|
|
9315
9466
|
// src/ingest/claude.ts
|
|
9316
9467
|
function autoDetectProject(cwd, projects) {
|
|
9317
9468
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
9318
9469
|
}
|
|
9319
|
-
var
|
|
9320
|
-
|
|
9470
|
+
var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join6(homedir4(), ".takumi", "projects");
|
|
9471
|
+
function claudeProjectsDir() {
|
|
9472
|
+
return process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? agentPaths().claudeProjects;
|
|
9473
|
+
}
|
|
9321
9474
|
function dirNameToPath(dirName) {
|
|
9322
9475
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
9323
9476
|
}
|
|
@@ -9327,9 +9480,9 @@ function collectJsonlFiles(projectDir) {
|
|
|
9327
9480
|
try {
|
|
9328
9481
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
9329
9482
|
if (entry.isDirectory())
|
|
9330
|
-
walk(
|
|
9483
|
+
walk(join6(dir, entry.name));
|
|
9331
9484
|
else if (entry.name.endsWith(".jsonl"))
|
|
9332
|
-
files.push(
|
|
9485
|
+
files.push(join6(dir, entry.name));
|
|
9333
9486
|
}
|
|
9334
9487
|
} catch {
|
|
9335
9488
|
collectionProblem("failed", "source_unreadable");
|
|
@@ -9338,7 +9491,7 @@ function collectJsonlFiles(projectDir) {
|
|
|
9338
9491
|
walk(projectDir);
|
|
9339
9492
|
return files;
|
|
9340
9493
|
}
|
|
9341
|
-
async function ingestClaude(db, verbose = false, projectsDir =
|
|
9494
|
+
async function ingestClaude(db, verbose = false, projectsDir = claudeProjectsDir()) {
|
|
9342
9495
|
return ingestJsonlProjects(db, projectsDir, "claude", verbose);
|
|
9343
9496
|
}
|
|
9344
9497
|
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
@@ -9360,7 +9513,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
9360
9513
|
const ingestState = loadIngestState(db, agentName);
|
|
9361
9514
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
9362
9515
|
for (const projectDirEntry of projectDirs) {
|
|
9363
|
-
const projectDirPath =
|
|
9516
|
+
const projectDirPath = join6(projectsDir, projectDirEntry.name);
|
|
9364
9517
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
9365
9518
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
9366
9519
|
for (const filePath of jsonlFiles) {
|
|
@@ -9511,24 +9664,19 @@ init_database();
|
|
|
9511
9664
|
init_pricing();
|
|
9512
9665
|
import { existsSync as existsSync5, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
9513
9666
|
import { createHash as createHash4 } from "crypto";
|
|
9514
|
-
import {
|
|
9515
|
-
import { join as join6, basename as basename2 } from "path";
|
|
9516
|
-
var DEFAULT_CODEX_DB_PATH = join6(homedir4(), ".codex", "state_5.sqlite");
|
|
9517
|
-
var DEFAULT_CODEX_CONFIG_PATH = join6(homedir4(), ".codex", "config.toml");
|
|
9518
|
-
var DEFAULT_CODEWITH_DB_PATH = join6(homedir4(), ".codewith", "state_5.sqlite");
|
|
9519
|
-
var DEFAULT_CODEWITH_CONFIG_PATH = join6(homedir4(), ".codewith", "config.toml");
|
|
9667
|
+
import { join as join7, basename as basename2 } from "path";
|
|
9520
9668
|
var CODEX_INGEST_VERSION = "rollout-events-v4";
|
|
9521
9669
|
function codexDbPath() {
|
|
9522
|
-
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ??
|
|
9670
|
+
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? agentPaths().codexDb;
|
|
9523
9671
|
}
|
|
9524
9672
|
function codexConfigPath() {
|
|
9525
|
-
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ??
|
|
9673
|
+
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? agentPaths().codexConfig;
|
|
9526
9674
|
}
|
|
9527
9675
|
function codewithDbPath() {
|
|
9528
|
-
return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ??
|
|
9676
|
+
return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? join7(getHomeDir2(), ".codewith", "state_5.sqlite");
|
|
9529
9677
|
}
|
|
9530
9678
|
function codewithConfigPath() {
|
|
9531
|
-
return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ??
|
|
9679
|
+
return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? join7(getHomeDir2(), ".codewith", "config.toml");
|
|
9532
9680
|
}
|
|
9533
9681
|
function codexSources() {
|
|
9534
9682
|
const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
|
|
@@ -9540,7 +9688,7 @@ function codexSources() {
|
|
|
9540
9688
|
sessionPrefix: "codex",
|
|
9541
9689
|
stateSource: "codex"
|
|
9542
9690
|
}];
|
|
9543
|
-
if (!explicitCodexPath || explicitCodewithPath) {
|
|
9691
|
+
if (!explicitCodexPath && !process.env["CODEX_HOME"] || explicitCodewithPath) {
|
|
9544
9692
|
sources.push({
|
|
9545
9693
|
label: "Codewith",
|
|
9546
9694
|
dbPath: codewithDbPath(),
|
|
@@ -9780,9 +9928,9 @@ init_database();
|
|
|
9780
9928
|
init_pricing();
|
|
9781
9929
|
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync6, statSync as statSync4 } from "fs";
|
|
9782
9930
|
import { homedir as homedir5 } from "os";
|
|
9783
|
-
import { join as
|
|
9784
|
-
var DEFAULT_GEMINI_TMP_DIR =
|
|
9785
|
-
var DEFAULT_GEMINI_HISTORY_DIR =
|
|
9931
|
+
import { join as join8, basename as basename3 } from "path";
|
|
9932
|
+
var DEFAULT_GEMINI_TMP_DIR = join8(homedir5(), ".gemini", "tmp");
|
|
9933
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join8(homedir5(), ".gemini", "history");
|
|
9786
9934
|
function geminiTmpDir() {
|
|
9787
9935
|
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
9788
9936
|
}
|
|
@@ -9804,7 +9952,7 @@ function listProjectDirs(...roots) {
|
|
|
9804
9952
|
try {
|
|
9805
9953
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
9806
9954
|
if (entry.isDirectory())
|
|
9807
|
-
dirs.add(
|
|
9955
|
+
dirs.add(join8(root, entry.name));
|
|
9808
9956
|
}
|
|
9809
9957
|
} catch {
|
|
9810
9958
|
collectionProblem("failed", "source_unreadable");
|
|
@@ -9817,7 +9965,7 @@ function projectRoot(projectDir, chatData) {
|
|
|
9817
9965
|
return chatData.projectPath;
|
|
9818
9966
|
if (chatData.project_path)
|
|
9819
9967
|
return chatData.project_path;
|
|
9820
|
-
const rootFile =
|
|
9968
|
+
const rootFile = join8(projectDir, ".project_root");
|
|
9821
9969
|
try {
|
|
9822
9970
|
if (existsSync6(rootFile))
|
|
9823
9971
|
return readFileSync4(rootFile, "utf-8").trim();
|
|
@@ -9842,12 +9990,12 @@ async function ingestGemini(db, verbose) {
|
|
|
9842
9990
|
const account = await resolveAccountForAgent("gemini");
|
|
9843
9991
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
9844
9992
|
for (const projectDir of projectDirs) {
|
|
9845
|
-
const chatsDir =
|
|
9993
|
+
const chatsDir = join8(projectDir, "chats");
|
|
9846
9994
|
if (!existsSync6(chatsDir))
|
|
9847
9995
|
continue;
|
|
9848
9996
|
let chatFiles = [];
|
|
9849
9997
|
try {
|
|
9850
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) =>
|
|
9998
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join8(chatsDir, f));
|
|
9851
9999
|
} catch {
|
|
9852
10000
|
collectionProblem("failed", "source_unreadable");
|
|
9853
10001
|
continue;
|
|
@@ -9959,13 +10107,13 @@ init_database();
|
|
|
9959
10107
|
init_pricing();
|
|
9960
10108
|
import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
9961
10109
|
import { homedir as homedir6 } from "os";
|
|
9962
|
-
import { join as
|
|
9963
|
-
var OPENCODE_STORAGE =
|
|
10110
|
+
import { join as join9 } from "path";
|
|
10111
|
+
var OPENCODE_STORAGE = join9(homedir6(), ".local", "share", "opencode", "storage");
|
|
9964
10112
|
function walkJsonFiles(dir, acc = []) {
|
|
9965
10113
|
if (!existsSync7(dir))
|
|
9966
10114
|
return acc;
|
|
9967
10115
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
9968
|
-
const p =
|
|
10116
|
+
const p = join9(dir, entry.name);
|
|
9969
10117
|
if (entry.isDirectory())
|
|
9970
10118
|
walkJsonFiles(p, acc);
|
|
9971
10119
|
else if (entry.name.endsWith(".json"))
|
|
@@ -9985,7 +10133,7 @@ function parseSessionIdFromPath(filePath) {
|
|
|
9985
10133
|
return null;
|
|
9986
10134
|
}
|
|
9987
10135
|
async function ingestOpenCode(db, verbose = false, sourceDir = OPENCODE_STORAGE) {
|
|
9988
|
-
const messageDir =
|
|
10136
|
+
const messageDir = join9(sourceDir, "message");
|
|
9989
10137
|
if (!existsSync7(messageDir))
|
|
9990
10138
|
collectionProblem("missing", "source_missing");
|
|
9991
10139
|
const files = walkJsonFiles(messageDir);
|
|
@@ -10180,13 +10328,13 @@ async function ingestCursor(db, verbose = false) {
|
|
|
10180
10328
|
init_database();
|
|
10181
10329
|
import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
10182
10330
|
import { homedir as homedir7 } from "os";
|
|
10183
|
-
import { join as
|
|
10184
|
-
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ??
|
|
10331
|
+
import { join as join10 } from "path";
|
|
10332
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join10(homedir7(), ".pi", "agent", "sessions");
|
|
10185
10333
|
function walkSessions(dir, acc = []) {
|
|
10186
10334
|
if (!existsSync8(dir))
|
|
10187
10335
|
return acc;
|
|
10188
10336
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
10189
|
-
const p =
|
|
10337
|
+
const p = join10(dir, entry.name);
|
|
10190
10338
|
if (entry.isDirectory())
|
|
10191
10339
|
walkSessions(p, acc);
|
|
10192
10340
|
else if (entry.name.endsWith(".json"))
|
|
@@ -10287,8 +10435,8 @@ async function ingestPi(db, verbose = false, sourceDir = PI_SESSION_DIR) {
|
|
|
10287
10435
|
init_database();
|
|
10288
10436
|
import { existsSync as existsSync9, statSync as statSync7 } from "fs";
|
|
10289
10437
|
import { homedir as homedir8 } from "os";
|
|
10290
|
-
import { join as
|
|
10291
|
-
var HERMES_DB =
|
|
10438
|
+
import { join as join11 } from "path";
|
|
10439
|
+
var HERMES_DB = join11(homedir8(), ".hermes", "state.db");
|
|
10292
10440
|
async function ingestHermes(db, verbose = false, sourcePath = HERMES_DB) {
|
|
10293
10441
|
if (!existsSync9(sourcePath)) {
|
|
10294
10442
|
collectionProblem("missing", "source_missing");
|
|
@@ -10358,7 +10506,7 @@ init_database();
|
|
|
10358
10506
|
init_pricing();
|
|
10359
10507
|
import { existsSync as existsSync10 } from "fs";
|
|
10360
10508
|
import { homedir as homedir9 } from "os";
|
|
10361
|
-
import { join as
|
|
10509
|
+
import { join as join12 } from "path";
|
|
10362
10510
|
import { homedir as pathsResolverHomedir2 } from "os";
|
|
10363
10511
|
import { join as pathsResolverJoin2 } from "path";
|
|
10364
10512
|
var PATHS_RESOLVER_KIND_ENV2 = {
|
|
@@ -10425,8 +10573,8 @@ function defaultLoopsDbPath() {
|
|
|
10425
10573
|
app: "loops",
|
|
10426
10574
|
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
10427
10575
|
});
|
|
10428
|
-
const legacy =
|
|
10429
|
-
return existsSync10(
|
|
10576
|
+
const legacy = join12(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
|
|
10577
|
+
return existsSync10(join12(resolved, "loops.db")) ? join12(resolved, "loops.db") : legacy;
|
|
10430
10578
|
}
|
|
10431
10579
|
function loopsDbPath() {
|
|
10432
10580
|
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
|
|
@@ -10610,33 +10758,6 @@ async function ingestLoops(db, verbose = false) {
|
|
|
10610
10758
|
|
|
10611
10759
|
// src/ingest/claude-quota.ts
|
|
10612
10760
|
import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
|
|
10613
|
-
|
|
10614
|
-
// src/lib/paths.ts
|
|
10615
|
-
import { homedir as homedir10 } from "os";
|
|
10616
|
-
import { join as join12 } from "path";
|
|
10617
|
-
function getHomeDir2() {
|
|
10618
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir10();
|
|
10619
|
-
}
|
|
10620
|
-
function agentPaths() {
|
|
10621
|
-
const home = getHomeDir2();
|
|
10622
|
-
return {
|
|
10623
|
-
claudeProjects: join12(home, ".claude", "projects"),
|
|
10624
|
-
claudeCredentials: join12(home, ".claude", ".credentials.json"),
|
|
10625
|
-
takumiProjects: join12(home, ".takumi", "projects"),
|
|
10626
|
-
codexDir: join12(home, ".codex"),
|
|
10627
|
-
codexDb: join12(home, ".codex", "state_5.sqlite"),
|
|
10628
|
-
codexAuth: join12(home, ".codex", "auth.json"),
|
|
10629
|
-
codexConfig: join12(home, ".codex", "config.toml"),
|
|
10630
|
-
geminiTmp: join12(home, ".gemini", "tmp"),
|
|
10631
|
-
geminiHistory: join12(home, ".gemini", "history"),
|
|
10632
|
-
opencodeMessages: join12(home, ".local", "share", "opencode", "storage", "message"),
|
|
10633
|
-
piSessions: join12(home, ".pi", "agent", "sessions"),
|
|
10634
|
-
hermesDir: join12(home, ".hermes"),
|
|
10635
|
-
hermesDb: join12(home, ".hermes", "state.db")
|
|
10636
|
-
};
|
|
10637
|
-
}
|
|
10638
|
-
|
|
10639
|
-
// src/ingest/claude-quota.ts
|
|
10640
10761
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
10641
10762
|
var OAUTH_BETA = "oauth-2025-04-20";
|
|
10642
10763
|
function readClaudeToken() {
|