@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/otel/index.js
CHANGED
|
@@ -2384,7 +2384,7 @@ import { join as join3, basename } from "path";
|
|
|
2384
2384
|
// src/lib/accounts-store.ts
|
|
2385
2385
|
import { homedir as homedir2 } from "os";
|
|
2386
2386
|
import { join as join2 } from "path";
|
|
2387
|
-
import { z } from "zod";
|
|
2387
|
+
import { z as z2 } from "zod";
|
|
2388
2388
|
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
2389
2389
|
|
|
2390
2390
|
// src/lib/cloud-storage.ts
|
|
@@ -2515,32 +2515,61 @@ function economyCloudStorage(env = process.env) {
|
|
|
2515
2515
|
return cache;
|
|
2516
2516
|
}
|
|
2517
2517
|
|
|
2518
|
-
// src/lib/
|
|
2519
|
-
|
|
2518
|
+
// src/lib/subscriptions-registry.ts
|
|
2519
|
+
import { execFile } from "child_process";
|
|
2520
|
+
import { promisify } from "util";
|
|
2521
|
+
import { isAbsolute } from "path";
|
|
2522
|
+
import { z } from "zod";
|
|
2523
|
+
var execute = promisify(execFile);
|
|
2524
|
+
var nonempty = z.string().min(1);
|
|
2520
2525
|
var profileSchema = z.object({
|
|
2521
|
-
name:
|
|
2522
|
-
tool:
|
|
2526
|
+
name: nonempty,
|
|
2527
|
+
tool: nonempty,
|
|
2523
2528
|
email: z.string().optional(),
|
|
2524
2529
|
displayName: z.string().optional(),
|
|
2525
|
-
dir:
|
|
2530
|
+
dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
|
|
2531
|
+
createdAt: nonempty,
|
|
2532
|
+
lastUsedAt: z.string().optional(),
|
|
2526
2533
|
description: z.string().optional(),
|
|
2527
|
-
|
|
2528
|
-
|
|
2534
|
+
active: z.boolean(),
|
|
2535
|
+
applied: z.boolean(),
|
|
2536
|
+
switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
|
|
2537
|
+
});
|
|
2538
|
+
var toolsSchema = z.array(z.object({
|
|
2539
|
+
id: nonempty,
|
|
2540
|
+
label: nonempty,
|
|
2541
|
+
envVar: nonempty,
|
|
2542
|
+
defaultDir: nonempty,
|
|
2543
|
+
bin: nonempty
|
|
2544
|
+
}));
|
|
2545
|
+
|
|
2546
|
+
// src/lib/accounts-store.ts
|
|
2547
|
+
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");
|
|
2548
|
+
var profileSchema2 = z2.object({
|
|
2549
|
+
name: z2.string(),
|
|
2550
|
+
tool: z2.string(),
|
|
2551
|
+
email: z2.string().optional(),
|
|
2552
|
+
displayName: z2.string().optional(),
|
|
2553
|
+
dir: z2.string(),
|
|
2554
|
+
description: z2.string().optional(),
|
|
2555
|
+
createdAt: z2.string(),
|
|
2556
|
+
lastUsedAt: z2.string().optional(),
|
|
2557
|
+
switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
|
|
2529
2558
|
});
|
|
2530
|
-
var toolDefSchema =
|
|
2531
|
-
id:
|
|
2532
|
-
label:
|
|
2533
|
-
envVar:
|
|
2534
|
-
defaultDir:
|
|
2535
|
-
bin:
|
|
2559
|
+
var toolDefSchema = z2.object({
|
|
2560
|
+
id: z2.string(),
|
|
2561
|
+
label: z2.string(),
|
|
2562
|
+
envVar: z2.string(),
|
|
2563
|
+
defaultDir: z2.string(),
|
|
2564
|
+
bin: z2.string()
|
|
2536
2565
|
});
|
|
2537
|
-
var storeSchema =
|
|
2538
|
-
version:
|
|
2539
|
-
current:
|
|
2540
|
-
applied:
|
|
2541
|
-
toolLocks:
|
|
2542
|
-
profiles:
|
|
2543
|
-
tools:
|
|
2566
|
+
var storeSchema = z2.object({
|
|
2567
|
+
version: z2.literal(1),
|
|
2568
|
+
current: z2.record(z2.string(), z2.string()).default({}),
|
|
2569
|
+
applied: z2.record(z2.string(), z2.string()).default({}),
|
|
2570
|
+
toolLocks: z2.record(z2.string(), z2.string()).default({}),
|
|
2571
|
+
profiles: z2.array(profileSchema2).default([]),
|
|
2572
|
+
tools: z2.array(toolDefSchema).default([])
|
|
2544
2573
|
});
|
|
2545
2574
|
var BUILTIN_TOOLS = [
|
|
2546
2575
|
{ id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
|
|
@@ -2558,33 +2587,26 @@ var BUILTIN_TOOLS = [
|
|
|
2558
2587
|
];
|
|
2559
2588
|
|
|
2560
2589
|
// src/ingest/claude.ts
|
|
2561
|
-
var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
|
|
2562
2590
|
var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
|
|
2563
2591
|
|
|
2564
2592
|
// src/ingest/codex.ts
|
|
2565
2593
|
init_database();
|
|
2566
2594
|
init_pricing();
|
|
2567
|
-
import { homedir as homedir4 } from "os";
|
|
2568
|
-
import { join as join4, basename as basename2 } from "path";
|
|
2569
|
-
var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
|
|
2570
|
-
var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
|
|
2571
|
-
var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
|
|
2572
|
-
var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
|
|
2573
2595
|
|
|
2574
2596
|
// src/ingest/gemini.ts
|
|
2575
2597
|
init_database();
|
|
2576
2598
|
init_pricing();
|
|
2577
|
-
import { homedir as
|
|
2578
|
-
import { join as
|
|
2579
|
-
var DEFAULT_GEMINI_TMP_DIR =
|
|
2580
|
-
var DEFAULT_GEMINI_HISTORY_DIR =
|
|
2599
|
+
import { homedir as homedir4 } from "os";
|
|
2600
|
+
import { join as join4, basename as basename2 } from "path";
|
|
2601
|
+
var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
|
|
2602
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join4(homedir4(), ".gemini", "history");
|
|
2581
2603
|
|
|
2582
2604
|
// src/ingest/opencode.ts
|
|
2583
2605
|
init_database();
|
|
2584
2606
|
init_pricing();
|
|
2585
|
-
import { homedir as
|
|
2586
|
-
import { join as
|
|
2587
|
-
var OPENCODE_STORAGE =
|
|
2607
|
+
import { homedir as homedir5 } from "os";
|
|
2608
|
+
import { join as join5 } from "path";
|
|
2609
|
+
var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
|
|
2588
2610
|
|
|
2589
2611
|
// src/ingest/quota-observation.ts
|
|
2590
2612
|
init_database();
|
|
@@ -2592,15 +2614,15 @@ init_observation_provenance();
|
|
|
2592
2614
|
|
|
2593
2615
|
// src/ingest/pi.ts
|
|
2594
2616
|
init_database();
|
|
2595
|
-
import { homedir as
|
|
2596
|
-
import { join as
|
|
2597
|
-
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ??
|
|
2617
|
+
import { homedir as homedir6 } from "os";
|
|
2618
|
+
import { join as join6 } from "path";
|
|
2619
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
|
|
2598
2620
|
|
|
2599
2621
|
// src/ingest/hermes.ts
|
|
2600
2622
|
init_database();
|
|
2601
|
-
import { homedir as
|
|
2602
|
-
import { join as
|
|
2603
|
-
var HERMES_DB =
|
|
2623
|
+
import { homedir as homedir7 } from "os";
|
|
2624
|
+
import { join as join7 } from "path";
|
|
2625
|
+
var HERMES_DB = join7(homedir7(), ".hermes", "state.db");
|
|
2604
2626
|
|
|
2605
2627
|
// src/ingest/loops.ts
|
|
2606
2628
|
init_database();
|
|
@@ -2673,7 +2695,7 @@ init_ingest_validation();
|
|
|
2673
2695
|
init_database();
|
|
2674
2696
|
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync as mkdirSync3, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
2675
2697
|
import { createHash as createHash4 } from "crypto";
|
|
2676
|
-
import { join as
|
|
2698
|
+
import { join as join8, resolve as resolve2 } from "path";
|
|
2677
2699
|
var MAX_COMMANDS = 1000;
|
|
2678
2700
|
var MAX_BYTES = 64 * 1024 * 1024;
|
|
2679
2701
|
var RECEIPT_RESERVE = 4096;
|
|
@@ -2701,7 +2723,7 @@ function definitiveIngestRefusal(status, body) {
|
|
|
2701
2723
|
};
|
|
2702
2724
|
}
|
|
2703
2725
|
function getIngestJournalPath() {
|
|
2704
|
-
return process.env.HASNA_ECONOMY_INGEST_JOURNAL ??
|
|
2726
|
+
return process.env.HASNA_ECONOMY_INGEST_JOURNAL ?? join8(cacheDir({ app: "economy", home: process.env.HOME || process.env.USERPROFILE }), "pending-ingest");
|
|
2705
2727
|
}
|
|
2706
2728
|
function targetId(cloud) {
|
|
2707
2729
|
const url = new URL(cloud.client.transport.baseUrl);
|
|
@@ -2804,7 +2826,7 @@ class IngestJournal {
|
|
|
2804
2826
|
constructor(cloud, path = getIngestJournalPath()) {
|
|
2805
2827
|
this.target = targetId(cloud);
|
|
2806
2828
|
secureDirectory(path);
|
|
2807
|
-
this.directory =
|
|
2829
|
+
this.directory = join8(realpathSync(path), this.target);
|
|
2808
2830
|
secureDirectory(this.directory);
|
|
2809
2831
|
}
|
|
2810
2832
|
names() {
|
|
@@ -2830,7 +2852,7 @@ class IngestJournal {
|
|
|
2830
2852
|
throw new Error("Orphaned ingest outcome requires maintenance recovery");
|
|
2831
2853
|
if (name.endsWith(".refusal.json") && set.has(name.replace(".refusal.", ".receipt.")))
|
|
2832
2854
|
throw new Error("Conflicting ingest receipt and refusal");
|
|
2833
|
-
const stat = lstatSync(
|
|
2855
|
+
const stat = lstatSync(join8(this.directory, name));
|
|
2834
2856
|
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
|
|
2835
2857
|
throw new Error("Unsafe ingest journal record");
|
|
2836
2858
|
if (stat.size > MAX_INGEST_BYTES + 4096)
|
|
@@ -2842,10 +2864,10 @@ class IngestJournal {
|
|
|
2842
2864
|
return names;
|
|
2843
2865
|
}
|
|
2844
2866
|
file(key, kind) {
|
|
2845
|
-
return
|
|
2867
|
+
return join8(this.directory, `${sha(key)}.${kind}.json`);
|
|
2846
2868
|
}
|
|
2847
2869
|
intent(name) {
|
|
2848
|
-
const record = readJson(
|
|
2870
|
+
const record = readJson(join8(this.directory, name), MAX_INGEST_BYTES + 4096);
|
|
2849
2871
|
if (record.version !== 1 || record.target !== this.target || !record.command || typeof record.command.idempotency_key !== "string" || record.body_hash !== canonicalIngestBodyHash(record.command) || name !== `${sha(record.command.idempotency_key)}.intent.json`)
|
|
2850
2872
|
throw new Error("Invalid ingest journal intent");
|
|
2851
2873
|
validateIngest(record.command);
|
|
@@ -3004,7 +3026,7 @@ class IngestJournal {
|
|
|
3004
3026
|
// src/lib/ingest-journal-maintenance.ts
|
|
3005
3027
|
import { constants as constants2, closeSync as closeSync2, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
3006
3028
|
import { createHash as createHash5 } from "crypto";
|
|
3007
|
-
import { join as
|
|
3029
|
+
import { join as join9 } from "path";
|
|
3008
3030
|
var uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
3009
3031
|
var digest2 = /^[a-f0-9]{64}$/;
|
|
3010
3032
|
var sha2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
|
|
@@ -3064,7 +3086,7 @@ async function confirm(cloud, anchor, deadline) {
|
|
|
3064
3086
|
}
|
|
3065
3087
|
async function verifyJournalAuthority(journal, cloud) {
|
|
3066
3088
|
journal.assertCloud(cloud);
|
|
3067
|
-
const path =
|
|
3089
|
+
const path = join9(journal.directory, ".authority.json");
|
|
3068
3090
|
if (!exists(path)) {
|
|
3069
3091
|
const names = journal.names();
|
|
3070
3092
|
const name = names.find((n) => n.endsWith(".intent.json") && names.includes(n.replace(".intent.", ".receipt.")));
|
|
@@ -3108,8 +3130,8 @@ class IngestJournalMaintenance {
|
|
|
3108
3130
|
this.cloud = cloud;
|
|
3109
3131
|
this.factory = factory;
|
|
3110
3132
|
journal.assertCloud(cloud);
|
|
3111
|
-
this.control =
|
|
3112
|
-
this.operationRoot =
|
|
3133
|
+
this.control = join9(journal.directory, ".maintenance");
|
|
3134
|
+
this.operationRoot = join9(journal.directory, ".recovery");
|
|
3113
3135
|
}
|
|
3114
3136
|
manifest() {
|
|
3115
3137
|
secureDirectory(this.control);
|
|
@@ -3118,10 +3140,10 @@ class IngestJournalMaintenance {
|
|
|
3118
3140
|
throw new Error("Unexpected maintenance entry");
|
|
3119
3141
|
let size = 0;
|
|
3120
3142
|
for (const name of names)
|
|
3121
|
-
size += lstatSync2(
|
|
3143
|
+
size += lstatSync2(join9(this.control, name)).size;
|
|
3122
3144
|
if (size > MAINTENANCE_RESERVE)
|
|
3123
3145
|
throw new Error("Maintenance metadata exceeds reserve");
|
|
3124
|
-
const m = validAnchor(readJson(
|
|
3146
|
+
const m = validAnchor(readJson(join9(this.control, "manifest.json"), 8192), this.journal.target);
|
|
3125
3147
|
for (const part of [m.intent, m.receipt_file])
|
|
3126
3148
|
if (!part || !digest2.test(part.sha256) || !digest2.test(part.capture_sha256 ?? "") || !Number.isSafeInteger(part.sizeBytes) || part.sizeBytes < 1 || part.sizeBytes > 4198400 || part.mode !== 384 || !/^\d+$/.test(part.device) || !/^\d+$/.test(part.inode) || !Number.isFinite(part.ctime))
|
|
3127
3149
|
throw new Error("Invalid maintenance snapshot");
|
|
@@ -3146,7 +3168,7 @@ class IngestJournalMaintenance {
|
|
|
3146
3168
|
if (!operation.receipt || operation.receipt.sha256 !== expected.capture_sha256 || operation.receipt.sizeBytes !== expected.sizeBytes || operation.receipt.mode !== expected.mode)
|
|
3147
3169
|
throw new Error("Trash snapshot mismatch");
|
|
3148
3170
|
sameSource(operation.path, expected);
|
|
3149
|
-
const path =
|
|
3171
|
+
const path = join9(this.control, `${kind}.operation.json`);
|
|
3150
3172
|
createExact(path, { id: operation.id });
|
|
3151
3173
|
if (readJson(path, 1024).id !== operation.id)
|
|
3152
3174
|
throw new Error("Conflicting Trash operation");
|
|
@@ -3171,7 +3193,7 @@ class IngestJournalMaintenance {
|
|
|
3171
3193
|
async pairFile(backend, m, kind, deadline) {
|
|
3172
3194
|
await confirm(this.cloud, m, deadline);
|
|
3173
3195
|
const path = this.journal.file(m.key, kind), expected = kind === "receipt" ? m.receipt_file : m.intent;
|
|
3174
|
-
const saved =
|
|
3196
|
+
const saved = join9(this.control, `${kind}.operation.json`);
|
|
3175
3197
|
let id;
|
|
3176
3198
|
if (exists(saved))
|
|
3177
3199
|
id = readJson(saved, 1024).id;
|
|
@@ -3277,14 +3299,14 @@ class IngestJournalMaintenance {
|
|
|
3277
3299
|
key: intent.command.idempotency_key,
|
|
3278
3300
|
body_hash: intent.body_hash,
|
|
3279
3301
|
receipt,
|
|
3280
|
-
intent: snapshot(
|
|
3302
|
+
intent: snapshot(join9(this.journal.directory, name)),
|
|
3281
3303
|
receipt_file: snapshot(this.journal.file(intent.command.idempotency_key, "receipt"))
|
|
3282
3304
|
};
|
|
3283
3305
|
await confirm(this.cloud, m, deadline);
|
|
3284
3306
|
m.intent.capture_sha256 = backend.snapshot(this.journal.file(m.key, "intent")).sha256;
|
|
3285
3307
|
m.receipt_file.capture_sha256 = backend.snapshot(this.journal.file(m.key, "receipt")).sha256;
|
|
3286
3308
|
secureDirectory(this.control);
|
|
3287
|
-
if (!createExact(
|
|
3309
|
+
if (!createExact(join9(this.control, "manifest.json"), m))
|
|
3288
3310
|
throw new Error("Existing maintenance transaction");
|
|
3289
3311
|
await this.finish(backend, deadline);
|
|
3290
3312
|
retired++;
|
|
@@ -3316,7 +3338,7 @@ class IngestJournalMaintenance {
|
|
|
3316
3338
|
|
|
3317
3339
|
// src/lib/ingest-journal-lock.ts
|
|
3318
3340
|
import { constants as constants3, closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3 } from "fs";
|
|
3319
|
-
import { join as
|
|
3341
|
+
import { join as join10 } from "path";
|
|
3320
3342
|
async function withIngestJournalLock(directory, action) {
|
|
3321
3343
|
if (process.platform !== "linux" && process.platform !== "darwin")
|
|
3322
3344
|
throw new Error("Ingest journal locking requires Linux or macOS");
|
|
@@ -3324,7 +3346,7 @@ async function withIngestJournalLock(directory, action) {
|
|
|
3324
3346
|
const library = dlopen(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
|
|
3325
3347
|
flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 }
|
|
3326
3348
|
});
|
|
3327
|
-
const path =
|
|
3349
|
+
const path = join10(directory, ".lease");
|
|
3328
3350
|
let fd;
|
|
3329
3351
|
try {
|
|
3330
3352
|
fd = openSync3(path, constants3.O_RDWR | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
|
|
@@ -3345,11 +3367,11 @@ async function withIngestJournalLock(directory, action) {
|
|
|
3345
3367
|
}
|
|
3346
3368
|
|
|
3347
3369
|
// src/lib/ingest-journal-operation.ts
|
|
3348
|
-
import { z as
|
|
3349
|
-
var ingestJournalOperationSchema =
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3370
|
+
import { z as z3 } from "zod";
|
|
3371
|
+
var ingestJournalOperationSchema = z3.discriminatedUnion("action", [
|
|
3372
|
+
z3.object({ action: z3.literal("status") }).strict(),
|
|
3373
|
+
z3.object({ action: z3.literal("reconcile") }).strict(),
|
|
3374
|
+
z3.object({ action: z3.literal("maintain"), confirm: z3.literal(true), limit: z3.number().int().min(1).max(32).default(32) }).strict()
|
|
3353
3375
|
]);
|
|
3354
3376
|
|
|
3355
3377
|
// src/lib/cloud-ingest.ts
|
|
@@ -3458,7 +3480,7 @@ async function pushIngestRows(cloud, scratch) {
|
|
|
3458
3480
|
// package.json
|
|
3459
3481
|
var package_default = {
|
|
3460
3482
|
name: "@hasna/economy",
|
|
3461
|
-
version: "0.5.
|
|
3483
|
+
version: "0.5.5",
|
|
3462
3484
|
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",
|
|
3463
3485
|
repository: {
|
|
3464
3486
|
type: "git",
|