agent-cli-proxy 0.1.0

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/dist/cli.js ADDED
@@ -0,0 +1,4287 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+
19
+ // data/plans.default.json
20
+ var plans_default_default;
21
+ var init_plans_default = __esm(() => {
22
+ plans_default_default = {
23
+ plans: [
24
+ {
25
+ code: "claude_pro",
26
+ provider: "anthropic",
27
+ display_name: "Anthropic Claude Pro",
28
+ monthly_price_usd: 20,
29
+ currency: "USD",
30
+ billing_period_days: 30,
31
+ vendor_url: "https://www.anthropic.com/claude/pricing",
32
+ notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
33
+ },
34
+ {
35
+ code: "claude_max5",
36
+ provider: "anthropic",
37
+ display_name: "Anthropic Claude Max 5x",
38
+ monthly_price_usd: 100,
39
+ currency: "USD",
40
+ billing_period_days: 30,
41
+ vendor_url: "https://www.anthropic.com/claude/pricing",
42
+ notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
43
+ },
44
+ {
45
+ code: "claude_max20",
46
+ provider: "anthropic",
47
+ display_name: "Anthropic Claude Max 20x",
48
+ monthly_price_usd: 200,
49
+ currency: "USD",
50
+ billing_period_days: 30,
51
+ vendor_url: "https://www.anthropic.com/claude/pricing",
52
+ notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
53
+ },
54
+ {
55
+ code: "chatgpt_plus",
56
+ provider: "openai",
57
+ display_name: "OpenAI ChatGPT Plus",
58
+ monthly_price_usd: 20,
59
+ currency: "USD",
60
+ billing_period_days: 30,
61
+ vendor_url: "https://openai.com/chatgpt/pricing/",
62
+ notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
63
+ },
64
+ {
65
+ code: "chatgpt_pro",
66
+ provider: "openai",
67
+ display_name: "OpenAI ChatGPT Pro",
68
+ monthly_price_usd: 200,
69
+ currency: "USD",
70
+ billing_period_days: 30,
71
+ vendor_url: "https://openai.com/chatgpt/pricing/",
72
+ notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
73
+ },
74
+ {
75
+ code: "chatgpt_business",
76
+ provider: "openai",
77
+ display_name: "OpenAI ChatGPT Business",
78
+ monthly_price_usd: 25,
79
+ currency: "USD",
80
+ billing_period_days: 30,
81
+ vendor_url: "https://openai.com/chatgpt/pricing/",
82
+ notes: "Conservative estimate (per-seat); verify with vendor \u2014 last updated 2026-05"
83
+ },
84
+ {
85
+ code: "kimi_pro",
86
+ provider: "moonshot",
87
+ display_name: "Moonshot Kimi",
88
+ monthly_price_usd: 15,
89
+ currency: "USD",
90
+ billing_period_days: 30,
91
+ vendor_url: "https://www.moonshot.cn/",
92
+ notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
93
+ },
94
+ {
95
+ code: "glm_pro",
96
+ provider: "bigmodel",
97
+ display_name: "BigModel GLM",
98
+ monthly_price_usd: 10,
99
+ currency: "USD",
100
+ billing_period_days: 30,
101
+ vendor_url: "https://open.bigmodel.cn/",
102
+ notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
103
+ },
104
+ {
105
+ code: "local_byok",
106
+ provider: "local",
107
+ display_name: "Bring Your Own Key (BYOK / Self-hosted)",
108
+ monthly_price_usd: 0,
109
+ currency: "USD",
110
+ billing_period_days: 30,
111
+ notes: "Self-hosted or BYOK provider. No vendor billing. verify with vendor \u2014 last updated 2026-05"
112
+ }
113
+ ]
114
+ };
115
+ });
116
+
117
+ // src/util/logger.ts
118
+ var Logger;
119
+ var init_logger = __esm(() => {
120
+ ((Logger) => {
121
+ const LEVELS = {
122
+ debug: 10,
123
+ info: 20,
124
+ warn: 30,
125
+ error: 40
126
+ };
127
+ const REDACTED = "[REDACTED]";
128
+ const defaultSink = {
129
+ stdout(line) {
130
+ process.stdout.write(`${line}
131
+ `);
132
+ },
133
+ stderr(line) {
134
+ process.stderr.write(`${line}
135
+ `);
136
+ }
137
+ };
138
+ function create(options = {}) {
139
+ return new StructuredLogger({
140
+ level: normalizeLevel(options.level),
141
+ format: normalizeFormat(options.format),
142
+ base: redact(options.base ?? {}),
143
+ sink: options.sink ?? defaultSink
144
+ });
145
+ }
146
+ Logger.create = create;
147
+ function fromConfig(options = {}) {
148
+ return create({
149
+ ...options,
150
+ level: normalizeLevel(process.env.LOG_LEVEL),
151
+ format: normalizeFormat(process.env.LOG_FORMAT)
152
+ });
153
+ }
154
+ Logger.fromConfig = fromConfig;
155
+ function redactValue(value) {
156
+ return redact(value);
157
+ }
158
+ Logger.redactValue = redactValue;
159
+
160
+ class StructuredLogger {
161
+ options;
162
+ constructor(options) {
163
+ this.options = options;
164
+ }
165
+ child(base) {
166
+ return new StructuredLogger({
167
+ ...this.options,
168
+ base: {
169
+ ...this.options.base,
170
+ ...redact(base)
171
+ }
172
+ });
173
+ }
174
+ debug(msg, fields) {
175
+ this.write("debug", msg, fields);
176
+ }
177
+ info(msg, fields) {
178
+ this.write("info", msg, fields);
179
+ }
180
+ warn(msg, fields) {
181
+ this.write("warn", msg, fields);
182
+ }
183
+ error(msg, fields) {
184
+ this.write("error", msg, fields);
185
+ }
186
+ write(level, msg, fields = {}) {
187
+ if (LEVELS[level] < LEVELS[this.options.level])
188
+ return;
189
+ const record = {
190
+ ts: new Date().toISOString(),
191
+ level,
192
+ msg,
193
+ ...this.options.base,
194
+ ...redact(fields)
195
+ };
196
+ const line = this.options.format === "pretty" ? formatPretty(record) : JSON.stringify(record);
197
+ if (level === "error") {
198
+ this.options.sink.stderr(line);
199
+ return;
200
+ }
201
+ this.options.sink.stdout(line);
202
+ }
203
+ }
204
+ function normalizeLevel(value) {
205
+ if (value === "debug" || value === "info" || value === "warn" || value === "error")
206
+ return value;
207
+ return "info";
208
+ }
209
+ function normalizeFormat(value) {
210
+ if (value === "pretty")
211
+ return "pretty";
212
+ return "json";
213
+ }
214
+ function isSensitiveKey(key) {
215
+ return /authorization|x[-_]?api[-_]?key|api[-_]?key|token|password|secret/i.test(key);
216
+ }
217
+ function redact(value, seen = new WeakSet) {
218
+ if (value === null || value === undefined)
219
+ return value;
220
+ if (typeof value !== "object")
221
+ return value;
222
+ if (value instanceof Error) {
223
+ return {
224
+ name: value.name,
225
+ message: value.message,
226
+ stack: value.stack
227
+ };
228
+ }
229
+ if (seen.has(value))
230
+ return "[Circular]";
231
+ seen.add(value);
232
+ if (Array.isArray(value)) {
233
+ return value.map((item) => redact(item, seen));
234
+ }
235
+ if (value instanceof Headers) {
236
+ const out2 = {};
237
+ for (const [key, headerValue] of value.entries()) {
238
+ out2[key] = isSensitiveKey(key) ? REDACTED : headerValue;
239
+ }
240
+ return out2;
241
+ }
242
+ const out = {};
243
+ for (const [key, entry] of Object.entries(value)) {
244
+ out[key] = isSensitiveKey(key) ? REDACTED : redact(entry, seen);
245
+ }
246
+ return out;
247
+ }
248
+ function formatPretty(record) {
249
+ const { ts, level, msg, ...fields } = record;
250
+ const suffix = Object.entries(fields).map(([key, value]) => `${key}=${formatPrettyValue(value)}`).join(" ");
251
+ return suffix ? `${ts} ${level.toUpperCase()} ${msg} ${suffix}` : `${ts} ${level.toUpperCase()} ${msg}`;
252
+ }
253
+ function formatPrettyValue(value) {
254
+ if (typeof value === "string")
255
+ return value.includes(" ") ? JSON.stringify(value) : value;
256
+ return JSON.stringify(value);
257
+ }
258
+ })(Logger ||= {});
259
+ });
260
+
261
+ // src/plans/index.ts
262
+ import { existsSync, readFileSync } from "fs";
263
+ import { join } from "path";
264
+ var Plans;
265
+ var init_plans = __esm(() => {
266
+ init_plans_default();
267
+ init_logger();
268
+ ((Plans) => {
269
+
270
+ class SchemaError extends Error {
271
+ issues;
272
+ name = "PlansSchemaError";
273
+ code = "PLANS_SCHEMA_INVALID";
274
+ constructor(issues) {
275
+ super(`Plans schema validation failed: ${issues.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`);
276
+ this.issues = issues;
277
+ }
278
+ }
279
+ Plans.SchemaError = SchemaError;
280
+ const logger = Logger.fromConfig().child({ component: "plans" });
281
+ let cache = null;
282
+ function load() {
283
+ return readCache().list;
284
+ }
285
+ Plans.load = load;
286
+ function list() {
287
+ return load();
288
+ }
289
+ Plans.list = list;
290
+ function byCode(code) {
291
+ return readCache().byCode.get(code) ?? null;
292
+ }
293
+ Plans.byCode = byCode;
294
+ function validateBindingInput(account, code) {
295
+ const normalizedAccount = account.trim();
296
+ if (!normalizedAccount)
297
+ throw new Error("Account must be a non-empty string");
298
+ const normalizedCode = code.trim();
299
+ if (!normalizedCode)
300
+ throw new Error("Plan code must be a non-empty string");
301
+ if (!byCode(normalizedCode))
302
+ throw new Error(`Unknown plan code: ${normalizedCode}`);
303
+ return { account: normalizedAccount, code: normalizedCode };
304
+ }
305
+ Plans.validateBindingInput = validateBindingInput;
306
+ function reload() {
307
+ cache = null;
308
+ return load();
309
+ }
310
+ Plans.reload = reload;
311
+ function readCache() {
312
+ if (cache)
313
+ return cache;
314
+ const source = resolveSource();
315
+ const plans = parseSourceOrFallback(source);
316
+ const frozenPlans = Object.freeze(plans.map((plan) => Object.freeze({ ...plan })));
317
+ cache = {
318
+ list: frozenPlans,
319
+ byCode: new Map(frozenPlans.map((plan) => [plan.code, plan]))
320
+ };
321
+ return cache;
322
+ }
323
+ function resolveSource() {
324
+ const inline = process.env.PLANS_JSON;
325
+ if (inline !== undefined && inline.trim() !== "")
326
+ return { kind: "PLANS_JSON", raw: inline };
327
+ const envPath = process.env.PLANS_PATH?.trim();
328
+ if (envPath)
329
+ return readPathSource("PLANS_PATH", envPath);
330
+ const xdgPath = xdgPlansPath();
331
+ if (xdgPath && existsSync(xdgPath))
332
+ return readPathSource("XDG_CONFIG_HOME", xdgPath);
333
+ return { kind: "default", value: plans_default_default };
334
+ }
335
+ function readPathSource(kind, configPath) {
336
+ try {
337
+ return { kind, raw: readFileSync(configPath, "utf-8"), configPath };
338
+ } catch (err) {
339
+ return { kind, raw: undefined, configPath, value: err };
340
+ }
341
+ }
342
+ function xdgPlansPath() {
343
+ const base = process.env.XDG_CONFIG_HOME?.trim() || (process.env.HOME?.trim() ? join(process.env.HOME.trim(), ".config") : "");
344
+ if (!base)
345
+ return null;
346
+ return join(base, "agent-cli-proxy", "plans.json");
347
+ }
348
+ function parseSourceOrFallback(source) {
349
+ if (source.kind === "default")
350
+ return validateDefault(plans_default_default);
351
+ const result = parseSource(source);
352
+ if (result.ok)
353
+ return result.plans;
354
+ logger.warn("plans config invalid; falling back to packaged defaults", {
355
+ event: "plans.config.invalid",
356
+ source: source.kind,
357
+ configPath: source.configPath,
358
+ path: result.issues[0]?.path ?? source.kind,
359
+ issues: result.issues
360
+ });
361
+ return validateDefault(plans_default_default);
362
+ }
363
+ function parseSource(source) {
364
+ if (source.raw === undefined) {
365
+ return {
366
+ ok: false,
367
+ issues: [{ path: source.kind, message: source.value instanceof Error ? source.value.message : "could not be read" }]
368
+ };
369
+ }
370
+ let parsed;
371
+ try {
372
+ parsed = JSON.parse(source.raw);
373
+ } catch (err) {
374
+ return {
375
+ ok: false,
376
+ issues: [{ path: "plans", message: err instanceof Error ? err.message : "must be valid JSON" }]
377
+ };
378
+ }
379
+ const validation = parsePlanDocument(parsed);
380
+ if (validation.issues.length > 0)
381
+ return { ok: false, issues: validation.issues };
382
+ return { ok: true, plans: validation.plans };
383
+ }
384
+ function validateDefault(value) {
385
+ const validation = parsePlanDocument(value);
386
+ if (validation.issues.length > 0)
387
+ throw new SchemaError(validation.issues);
388
+ return validation.plans;
389
+ }
390
+ function parsePlanDocument(value) {
391
+ const issues = [];
392
+ const plans = [];
393
+ if (!isRecord(value)) {
394
+ issues.push({ path: "plans", message: "must be contained in an object" });
395
+ return { plans, issues };
396
+ }
397
+ if (!Array.isArray(value.plans)) {
398
+ issues.push({ path: "plans", message: "must be an array" });
399
+ return { plans, issues };
400
+ }
401
+ value.plans.forEach((entry, index) => {
402
+ const plan = parsePlan(entry, `plans[${index}]`, issues);
403
+ if (plan)
404
+ plans.push(plan);
405
+ });
406
+ const seen = new Set;
407
+ plans.forEach((plan, index) => {
408
+ if (seen.has(plan.code))
409
+ issues.push({ path: `plans[${index}].code`, message: "must be unique" });
410
+ seen.add(plan.code);
411
+ });
412
+ return { plans: issues.length === 0 ? plans : [], issues };
413
+ }
414
+ function parsePlan(value, path, issues) {
415
+ if (!isRecord(value)) {
416
+ issues.push({ path, message: "must be an object" });
417
+ return null;
418
+ }
419
+ const code = readRequiredString(value, "code", path, issues);
420
+ const provider = readRequiredString(value, "provider", path, issues);
421
+ const displayName = readRequiredString(value, "display_name", path, issues);
422
+ const monthlyPriceUsd = readRequiredNumber(value, "monthly_price_usd", path, issues);
423
+ const currency = readRequiredString(value, "currency", path, issues);
424
+ const billingPeriodDays = readRequiredPositiveInteger(value, "billing_period_days", path, issues);
425
+ const vendorUrl = readOptionalHttpUrl(value, "vendor_url", path, issues);
426
+ const notes = readOptionalString(value, "notes", path, issues);
427
+ if (!code || !provider || !displayName || monthlyPriceUsd === undefined || !currency || billingPeriodDays === undefined)
428
+ return null;
429
+ const plan = {
430
+ code,
431
+ provider,
432
+ display_name: displayName,
433
+ monthly_price_usd: monthlyPriceUsd,
434
+ currency,
435
+ billing_period_days: billingPeriodDays
436
+ };
437
+ if (vendorUrl !== undefined)
438
+ plan.vendor_url = vendorUrl;
439
+ if (notes !== undefined)
440
+ plan.notes = notes;
441
+ return plan;
442
+ }
443
+ function readRequiredString(record, key, path, issues) {
444
+ const value = record[key];
445
+ if (typeof value !== "string" || value.trim() === "") {
446
+ issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
447
+ return;
448
+ }
449
+ return value.trim();
450
+ }
451
+ function readOptionalString(record, key, path, issues) {
452
+ const value = record[key];
453
+ if (value === undefined)
454
+ return;
455
+ if (typeof value !== "string" || value.trim() === "") {
456
+ issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
457
+ return;
458
+ }
459
+ return value.trim();
460
+ }
461
+ function readRequiredNumber(record, key, path, issues) {
462
+ const value = record[key];
463
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
464
+ issues.push({ path: `${path}.${key}`, message: "must be a non-negative finite number" });
465
+ return;
466
+ }
467
+ return value;
468
+ }
469
+ function readRequiredPositiveInteger(record, key, path, issues) {
470
+ const value = record[key];
471
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
472
+ issues.push({ path: `${path}.${key}`, message: "must be a positive integer" });
473
+ return;
474
+ }
475
+ return value;
476
+ }
477
+ function readOptionalHttpUrl(record, key, path, issues) {
478
+ const value = record[key];
479
+ if (value === undefined)
480
+ return;
481
+ if (typeof value !== "string" || value.trim() === "") {
482
+ issues.push({ path: `${path}.${key}`, message: "must be a non-empty http(s) URL string" });
483
+ return;
484
+ }
485
+ try {
486
+ const url = new URL(value);
487
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
488
+ issues.push({ path: `${path}.${key}`, message: "must be an http(s) URL" });
489
+ return;
490
+ }
491
+ return url.toString();
492
+ } catch {
493
+ issues.push({ path: `${path}.${key}`, message: "must be a parseable http(s) URL" });
494
+ return;
495
+ }
496
+ }
497
+ function isRecord(value) {
498
+ return typeof value === "object" && value !== null && !Array.isArray(value);
499
+ }
500
+ })(Plans ||= {});
501
+ });
502
+
503
+ // src/storage/account-subscriptions.ts
504
+ var AccountSubscriptionRepo;
505
+ var init_account_subscriptions = __esm(() => {
506
+ ((AccountSubscriptionRepo) => {
507
+ function bind(db, cliproxyAccount, subscriptionCode) {
508
+ db.prepare(`
509
+ INSERT INTO account_subscriptions (
510
+ cliproxy_account, subscription_code, bound_at
511
+ ) VALUES (?, ?, CURRENT_TIMESTAMP)
512
+ ON CONFLICT(cliproxy_account) DO UPDATE SET
513
+ subscription_code = excluded.subscription_code,
514
+ bound_at = CURRENT_TIMESTAMP
515
+ `).run(cliproxyAccount, subscriptionCode);
516
+ }
517
+ AccountSubscriptionRepo.bind = bind;
518
+ function unbind(db, cliproxyAccount) {
519
+ db.prepare("DELETE FROM account_subscriptions WHERE cliproxy_account = ?").run(cliproxyAccount);
520
+ }
521
+ AccountSubscriptionRepo.unbind = unbind;
522
+ function get(db, cliproxyAccount) {
523
+ return db.prepare(`
524
+ SELECT cliproxy_account, subscription_code, bound_at
525
+ FROM account_subscriptions
526
+ WHERE cliproxy_account = ?
527
+ `).get(cliproxyAccount);
528
+ }
529
+ AccountSubscriptionRepo.get = get;
530
+ function list(db) {
531
+ return db.prepare(`
532
+ SELECT cliproxy_account, subscription_code, bound_at
533
+ FROM account_subscriptions
534
+ ORDER BY cliproxy_account ASC
535
+ `).all();
536
+ }
537
+ AccountSubscriptionRepo.list = list;
538
+ })(AccountSubscriptionRepo ||= {});
539
+ });
540
+
541
+ // src/storage/db.ts
542
+ var exports_db = {};
543
+ __export(exports_db, {
544
+ Storage: () => Storage,
545
+ STALE_PENDING_MAX_AGE_MS: () => STALE_PENDING_MAX_AGE_MS
546
+ });
547
+ import { Database } from "bun:sqlite";
548
+ import { readFileSync as readFileSync2, readdirSync } from "fs";
549
+ import { join as join2 } from "path";
550
+ function parseStalePendingMaxAgeMs(raw) {
551
+ if (raw === undefined)
552
+ return 600000;
553
+ const parsed = Number(raw);
554
+ if (!Number.isFinite(parsed) || parsed <= 0)
555
+ return 600000;
556
+ return parsed;
557
+ }
558
+ var logger, STALE_PENDING_MAX_AGE_MS, Storage;
559
+ var init_db = __esm(() => {
560
+ init_logger();
561
+ logger = Logger.fromConfig().child({ component: "storage-db" });
562
+ STALE_PENDING_MAX_AGE_MS = parseStalePendingMaxAgeMs(process.env.STALE_PENDING_MAX_AGE_MS);
563
+ ((Storage) => {
564
+ function splitStatements(sql) {
565
+ const stripped = sql.replace(/^\s*--.*$/gm, "");
566
+ return stripped.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
567
+ }
568
+ function execSafe(db, statement) {
569
+ try {
570
+ db.exec(statement);
571
+ } catch (err) {
572
+ const msg = err instanceof Error ? err.message : String(err);
573
+ const ignorable = msg.includes("duplicate column name") || msg.includes("already exists") || msg.includes("no such column") || statement.toUpperCase().includes("ADD COLUMN") && msg.includes("syntax error");
574
+ if (!ignorable)
575
+ throw err;
576
+ }
577
+ }
578
+ function ensureColumn(db, table, column, typeDef) {
579
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
580
+ if (cols.some((c) => c.name === column))
581
+ return;
582
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${typeDef}`);
583
+ }
584
+ function initDb(dbPath) {
585
+ const db = new Database(dbPath);
586
+ db.exec("PRAGMA journal_mode = WAL");
587
+ db.exec("PRAGMA synchronous = NORMAL");
588
+ db.exec(`
589
+ CREATE TABLE IF NOT EXISTS schema_migrations (
590
+ name TEXT PRIMARY KEY,
591
+ applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
592
+ )
593
+ `);
594
+ const migrationsDir = join2(import.meta.dir, "migrations");
595
+ const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
596
+ for (const file of files) {
597
+ const applied = db.prepare("SELECT name FROM schema_migrations WHERE name = ?").get(file);
598
+ if (applied)
599
+ continue;
600
+ const sql = readFileSync2(join2(migrationsDir, file), "utf-8");
601
+ const txn = db.transaction(() => {
602
+ for (const stmt of splitStatements(sql)) {
603
+ execSafe(db, stmt);
604
+ }
605
+ db.prepare("INSERT INTO schema_migrations (name) VALUES (?)").run(file);
606
+ });
607
+ try {
608
+ txn();
609
+ } catch (err) {
610
+ logger.error("migration failed", { err, file });
611
+ throw err;
612
+ }
613
+ }
614
+ ensureColumn(db, "request_logs", "cliproxy_account", "TEXT");
615
+ ensureColumn(db, "request_logs", "cliproxy_auth_index", "TEXT");
616
+ ensureColumn(db, "request_logs", "cliproxy_source", "TEXT");
617
+ ensureColumn(db, "request_logs", "request_id", "TEXT");
618
+ ensureColumn(db, "request_logs", "reasoning_tokens", "INTEGER DEFAULT 0");
619
+ ensureColumn(db, "request_logs", "actual_model", "TEXT");
620
+ ensureColumn(db, "request_logs", "user_agent", "TEXT");
621
+ ensureColumn(db, "request_logs", "source_ip", "TEXT");
622
+ ensureColumn(db, "request_logs", "correlated_at", "TEXT");
623
+ ensureColumn(db, "request_logs", "agent", "TEXT");
624
+ ensureColumn(db, "request_logs", "source", "TEXT DEFAULT 'proxy'");
625
+ ensureColumn(db, "request_logs", "msg_id", "TEXT");
626
+ ensureColumn(db, "request_logs", "lifecycle_status", "TEXT NOT NULL DEFAULT 'pending' CHECK(lifecycle_status IN ('pending', 'completed', 'error', 'aborted'))");
627
+ ensureColumn(db, "request_logs", "cost_status", "TEXT NOT NULL DEFAULT 'unresolved' CHECK(cost_status IN ('unresolved', 'ok', 'pending', 'unsupported'))");
628
+ ensureColumn(db, "request_logs", "subscription_code", "TEXT");
629
+ ensureColumn(db, "request_logs", "finalized_at", "TEXT");
630
+ ensureColumn(db, "request_logs", "error_message", "TEXT");
631
+ db.exec(`
632
+ UPDATE request_logs
633
+ SET lifecycle_status = CASE
634
+ WHEN incomplete = 1
635
+ OR error_code IS NOT NULL
636
+ OR status >= 400 THEN 'error'
637
+ ELSE 'completed'
638
+ END,
639
+ finalized_at = COALESCE(finalized_at, finished_at, started_at),
640
+ cost_status = CASE
641
+ WHEN cost_usd > 0 THEN 'ok'
642
+ ELSE 'pending'
643
+ END
644
+ WHERE lifecycle_status = 'pending'
645
+ AND (finalized_at IS NULL OR cost_status = 'unresolved')
646
+ AND (finished_at IS NOT NULL OR incomplete = 1 OR error_code IS NOT NULL OR status IS NOT NULL)
647
+ `);
648
+ db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_cliproxy_account ON request_logs(cliproxy_account)");
649
+ db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_cliproxy_auth_index ON request_logs(cliproxy_auth_index)");
650
+ db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_request_id ON request_logs(request_id)");
651
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_request_logs_msg_id ON request_logs(msg_id) WHERE msg_id IS NOT NULL");
652
+ db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_lifecycle_status ON request_logs(lifecycle_status)");
653
+ db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_cost_status ON request_logs(cost_status)");
654
+ db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_subscription_code ON request_logs(subscription_code) WHERE subscription_code IS NOT NULL");
655
+ db.exec(`
656
+ CREATE TABLE IF NOT EXISTS cost_audit (
657
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
658
+ request_log_id INTEGER,
659
+ model TEXT,
660
+ provider TEXT,
661
+ source TEXT,
662
+ base_cost_usd REAL,
663
+ calc_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
664
+ FOREIGN KEY (request_log_id) REFERENCES request_logs(id)
665
+ )
666
+ `);
667
+ db.exec("CREATE INDEX IF NOT EXISTS idx_cost_audit_request_log_id ON cost_audit(request_log_id)");
668
+ db.exec(`
669
+ CREATE TABLE IF NOT EXISTS quota_snapshots (
670
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
671
+ timestamp TEXT NOT NULL,
672
+ provider TEXT NOT NULL,
673
+ account TEXT NOT NULL,
674
+ quota_type TEXT NOT NULL,
675
+ used_pct REAL,
676
+ remaining REAL,
677
+ remaining_raw TEXT,
678
+ resets_at TEXT,
679
+ raw_json TEXT
680
+ )
681
+ `);
682
+ db.exec("CREATE INDEX IF NOT EXISTS idx_quota_snapshots_provider ON quota_snapshots(provider, account, timestamp)");
683
+ db.exec(`
684
+ CREATE TABLE IF NOT EXISTS daily_account_usage (
685
+ day TEXT NOT NULL,
686
+ provider TEXT NOT NULL,
687
+ model TEXT NOT NULL,
688
+ cliproxy_account TEXT NOT NULL,
689
+ cliproxy_auth_index TEXT,
690
+ request_count INTEGER DEFAULT 0,
691
+ prompt_tokens INTEGER DEFAULT 0,
692
+ completion_tokens INTEGER DEFAULT 0,
693
+ cache_creation_tokens INTEGER DEFAULT 0,
694
+ cache_read_tokens INTEGER DEFAULT 0,
695
+ reasoning_tokens INTEGER DEFAULT 0,
696
+ total_tokens INTEGER DEFAULT 0,
697
+ cost_usd REAL DEFAULT 0,
698
+ PRIMARY KEY (day, provider, model, cliproxy_account)
699
+ )
700
+ `);
701
+ db.exec("CREATE INDEX IF NOT EXISTS idx_daily_account_usage_day ON daily_account_usage(day)");
702
+ db.exec("CREATE INDEX IF NOT EXISTS idx_daily_account_usage_account ON daily_account_usage(cliproxy_account)");
703
+ return db;
704
+ }
705
+ Storage.initDb = initDb;
706
+ function recoverStalePending(db, maxAgeMs = STALE_PENDING_MAX_AGE_MS) {
707
+ const now = new Date().toISOString();
708
+ const threshold = new Date(Date.now() - maxAgeMs).toISOString();
709
+ const stmt = db.prepare(`
710
+ UPDATE request_logs
711
+ SET lifecycle_status = 'aborted',
712
+ error_message = 'boot-recovery',
713
+ finalized_at = ?,
714
+ finished_at = COALESCE(finished_at, ?),
715
+ incomplete = 1,
716
+ cost_status = CASE
717
+ WHEN cost_status = 'unresolved' THEN 'pending'
718
+ ELSE cost_status
719
+ END
720
+ WHERE lifecycle_status = 'pending'
721
+ AND started_at < ?
722
+ `);
723
+ const result = stmt.run(now, now, threshold);
724
+ const recovered = result.changes;
725
+ if (recovered > 0) {
726
+ logger.warn("recovered stale pending request logs", {
727
+ event: "lifecycle.boot_recovery",
728
+ recovered,
729
+ max_age_ms: maxAgeMs,
730
+ threshold
731
+ });
732
+ }
733
+ return recovered;
734
+ }
735
+ Storage.recoverStalePending = recoverStalePending;
736
+ })(Storage ||= {});
737
+ });
738
+
739
+ // src/provider/registry-schema.ts
740
+ function parseProviderInput(value, path = "provider") {
741
+ const issues = [];
742
+ const provider = normalizeProvider(value, path, issues);
743
+ return {
744
+ ok: issues.length === 0 && provider !== undefined,
745
+ provider: issues.length === 0 ? provider : undefined,
746
+ issues
747
+ };
748
+ }
749
+ function validateProviderDocument(value) {
750
+ const issues = [];
751
+ const providers = [];
752
+ if (!isRecord(value)) {
753
+ issues.push({ path: "providers", message: "must be contained in an object" });
754
+ return { providers, issues };
755
+ }
756
+ if (!Array.isArray(value.providers)) {
757
+ issues.push({ path: "providers", message: "must be an array" });
758
+ return { providers, issues };
759
+ }
760
+ value.providers.forEach((entry, index) => {
761
+ const result = parseProviderInput(entry, `providers[${index}]`);
762
+ if (result.provider)
763
+ providers.push(result.provider);
764
+ issues.push(...result.issues);
765
+ });
766
+ return { providers, issues };
767
+ }
768
+ function normalizeProvider(value, path, issues) {
769
+ if (!isRecord(value)) {
770
+ issues.push({ path, message: "must be an object" });
771
+ return;
772
+ }
773
+ const id = readRequiredString(value, "id", path, issues);
774
+ const type = readProviderType(value.type, `${path}.type`, issues);
775
+ const paths = readRequiredStringArray(value, "paths", path, issues);
776
+ const upstreamBaseUrl = readRequiredHttpUrl(value.upstreamBaseUrl, `${path}.upstreamBaseUrl`, issues);
777
+ const upstreamPath = readOptionalString(value, "upstreamPath", path, issues);
778
+ const models = readOptionalStringArray(value, "models", path, issues);
779
+ const headers = readOptionalHeaders(value, path, issues);
780
+ const auth = readOptionalAuth(value.auth, `${path}.auth`, issues);
781
+ const stripProviderField = readOptionalBoolean(value, "stripProviderField", path, issues);
782
+ if (!id || !type || paths.length === 0 || !upstreamBaseUrl)
783
+ return;
784
+ const provider = {
785
+ id,
786
+ type,
787
+ paths,
788
+ upstreamBaseUrl
789
+ };
790
+ if (upstreamPath !== undefined)
791
+ provider.upstreamPath = upstreamPath;
792
+ if (models !== undefined)
793
+ provider.models = models;
794
+ if (headers !== undefined)
795
+ provider.headers = headers;
796
+ if (auth !== undefined)
797
+ provider.auth = auth;
798
+ if (stripProviderField !== undefined)
799
+ provider.stripProviderField = stripProviderField;
800
+ return provider;
801
+ }
802
+ function readRequiredString(record, key, path, issues) {
803
+ const value = record[key];
804
+ if (typeof value !== "string" || value.trim() === "") {
805
+ issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
806
+ return;
807
+ }
808
+ return value.trim();
809
+ }
810
+ function readProviderType(value, path, issues) {
811
+ if (typeof value !== "string" || !ALLOWED_PROVIDER_TYPES.has(value)) {
812
+ issues.push({ path, message: `must be one of ${Array.from(ALLOWED_PROVIDER_TYPES).join(", ")}` });
813
+ return;
814
+ }
815
+ return value;
816
+ }
817
+ function readRequiredStringArray(record, key, path, issues) {
818
+ const value = record[key];
819
+ if (!Array.isArray(value) || value.length === 0) {
820
+ issues.push({ path: `${path}.${key}`, message: "must be a non-empty string array" });
821
+ return [];
822
+ }
823
+ const out = [];
824
+ value.forEach((entry, index) => {
825
+ if (typeof entry !== "string" || entry.trim() === "") {
826
+ issues.push({ path: `${path}.${key}[${index}]`, message: "must be a non-empty string" });
827
+ return;
828
+ }
829
+ out.push(entry.trim());
830
+ });
831
+ return out;
832
+ }
833
+ function readRequiredHttpUrl(value, path, issues) {
834
+ if (typeof value !== "string" || value.trim() === "") {
835
+ issues.push({ path, message: "must be a non-empty http(s) URL string" });
836
+ return;
837
+ }
838
+ return normalizeHttpUrl(value, path, issues);
839
+ }
840
+ function normalizeHttpUrl(raw, path, issues) {
841
+ try {
842
+ const parsed = new URL(raw);
843
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
844
+ issues.push({ path, message: "must be an http(s) URL" });
845
+ return;
846
+ }
847
+ return parsed.toString().replace(/\/+$/, "");
848
+ } catch {
849
+ issues.push({ path, message: "must be a parseable http(s) URL" });
850
+ return;
851
+ }
852
+ }
853
+ function readOptionalString(record, key, path, issues) {
854
+ const value = record[key];
855
+ if (value === undefined)
856
+ return;
857
+ if (typeof value !== "string" || value.trim() === "") {
858
+ issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
859
+ return;
860
+ }
861
+ return value.trim();
862
+ }
863
+ function readOptionalStringArray(record, key, path, issues) {
864
+ const value = record[key];
865
+ if (value === undefined)
866
+ return;
867
+ if (!Array.isArray(value)) {
868
+ issues.push({ path: `${path}.${key}`, message: "must be a string array" });
869
+ return;
870
+ }
871
+ const out = [];
872
+ value.forEach((entry, index) => {
873
+ if (typeof entry !== "string" || entry.trim() === "") {
874
+ issues.push({ path: `${path}.${key}[${index}]`, message: "must be a non-empty string" });
875
+ return;
876
+ }
877
+ out.push(entry.trim());
878
+ });
879
+ return out;
880
+ }
881
+ function readOptionalBoolean(record, key, path, issues) {
882
+ const value = record[key];
883
+ if (value === undefined)
884
+ return;
885
+ if (typeof value !== "boolean") {
886
+ issues.push({ path: `${path}.${key}`, message: "must be a boolean" });
887
+ return;
888
+ }
889
+ return value;
890
+ }
891
+ function readOptionalHeaders(provider, path, issues) {
892
+ const value = provider.headers;
893
+ if (value === undefined)
894
+ return;
895
+ if (!isRecord(value)) {
896
+ issues.push({ path: `${path}.headers`, message: "must be an object" });
897
+ return;
898
+ }
899
+ const headers = {};
900
+ for (const [key, entry] of Object.entries(value)) {
901
+ if (key.trim() === "") {
902
+ issues.push({ path: `${path}.headers`, message: "must not contain empty header names" });
903
+ continue;
904
+ }
905
+ if (typeof entry !== "string") {
906
+ issues.push({ path: `${path}.headers.${key}`, message: "must be a string" });
907
+ continue;
908
+ }
909
+ headers[key] = entry;
910
+ }
911
+ return headers;
912
+ }
913
+ function readOptionalAuth(auth, path, issues) {
914
+ if (auth === undefined)
915
+ return;
916
+ if (typeof auth === "string") {
917
+ if (!ALLOWED_AUTH_TYPES.has(auth)) {
918
+ issues.push({ path, message: `must be one of ${Array.from(ALLOWED_AUTH_TYPES).join(", ")}` });
919
+ return;
920
+ }
921
+ return auth;
922
+ }
923
+ if (!isRecord(auth)) {
924
+ issues.push({ path, message: "must be a string or object" });
925
+ return;
926
+ }
927
+ for (const key of Object.keys(auth)) {
928
+ if (!ALLOWED_AUTH_OBJECT_KEYS.has(key))
929
+ issues.push({ path: `${path}.${key}`, message: "is not supported" });
930
+ }
931
+ const type = auth.type;
932
+ if (typeof type !== "string" || type.trim() === "") {
933
+ issues.push({ path: `${path}.type`, message: "must be a non-empty string" });
934
+ return;
935
+ }
936
+ if (!ALLOWED_AUTH_TYPES.has(type)) {
937
+ issues.push({ path: `${path}.type`, message: `must be one of ${Array.from(ALLOWED_AUTH_TYPES).join(", ")}` });
938
+ return;
939
+ }
940
+ const result = { type };
941
+ readAuthString(auth, "env", path, issues, result);
942
+ readAuthString(auth, "value", path, issues, result);
943
+ readAuthString(auth, "header", path, issues, result);
944
+ return result;
945
+ }
946
+ function readAuthString(auth, key, path, issues, result) {
947
+ const value = auth[key];
948
+ if (value === undefined)
949
+ return;
950
+ if (typeof value !== "string" || value.trim() === "") {
951
+ issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
952
+ return;
953
+ }
954
+ result[key] = value.trim();
955
+ }
956
+ function isRecord(value) {
957
+ return typeof value === "object" && value !== null && !Array.isArray(value);
958
+ }
959
+ var ALLOWED_PROVIDER_TYPES, ALLOWED_AUTH_TYPES, ALLOWED_AUTH_OBJECT_KEYS;
960
+ var init_registry_schema = __esm(() => {
961
+ ALLOWED_PROVIDER_TYPES = new Set(["openai-compatible", "anthropic"]);
962
+ ALLOWED_AUTH_TYPES = new Set(["none", "preserve", "bearer", "x-api-key"]);
963
+ ALLOWED_AUTH_OBJECT_KEYS = new Set(["type", "env", "value", "header"]);
964
+ });
965
+
966
+ // src/config/validate.ts
967
+ import { readFileSync as readFileSync3 } from "fs";
968
+ function readString(env, key, fallback) {
969
+ const value = env[key];
970
+ return value === undefined ? fallback : value;
971
+ }
972
+ function readPort(env, issues) {
973
+ const raw = env.PROXY_PORT ?? "3100";
974
+ const parsed = Number(raw);
975
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
976
+ issues.push({ path: "PROXY_PORT", message: "must be an integer from 1 to 65535" });
977
+ return 3100;
978
+ }
979
+ return parsed;
980
+ }
981
+ function readPositiveNumber(env, key, fallback, issues) {
982
+ const raw = env[key];
983
+ if (raw === undefined)
984
+ return fallback;
985
+ const parsed = Number(raw);
986
+ if (!Number.isFinite(parsed) || parsed <= 0) {
987
+ issues.push({ path: key, message: "must be a positive finite number" });
988
+ return fallback;
989
+ }
990
+ return parsed;
991
+ }
992
+ function readRequiredUrl(env, key, issues, warnings) {
993
+ const raw = env[key];
994
+ if (raw === undefined || raw.trim() === "") {
995
+ if (env.PROXY_LOCAL_OK === "1") {
996
+ warnings.push({
997
+ path: key,
998
+ message: `defaulted to ${DEFAULT_CLI_PROXY_API_URL} because PROXY_LOCAL_OK=1`
999
+ });
1000
+ return DEFAULT_CLI_PROXY_API_URL;
1001
+ }
1002
+ issues.push({ path: key, message: "is required unless PROXY_LOCAL_OK=1 permits the local default" });
1003
+ return DEFAULT_CLI_PROXY_API_URL;
1004
+ }
1005
+ return normalizeHttpUrl2(raw, key, issues) ?? raw;
1006
+ }
1007
+ function normalizeHttpUrl2(raw, path, issues) {
1008
+ try {
1009
+ const parsed = new URL(raw);
1010
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1011
+ issues.push({ path, message: "must be an http(s) URL" });
1012
+ return;
1013
+ }
1014
+ return parsed.toString().replace(/\/+$/, "");
1015
+ } catch {
1016
+ issues.push({ path, message: "must be a parseable http(s) URL" });
1017
+ return;
1018
+ }
1019
+ }
1020
+ function readCchPositions(env, issues) {
1021
+ const raw = env.CCH_POSITIONS ?? "[4,7,20]";
1022
+ let parsed;
1023
+ try {
1024
+ parsed = JSON.parse(raw);
1025
+ } catch {
1026
+ issues.push({ path: "CCH_POSITIONS", message: "must be JSON array of finite non-negative integers" });
1027
+ return [4, 7, 20];
1028
+ }
1029
+ if (!Array.isArray(parsed)) {
1030
+ issues.push({ path: "CCH_POSITIONS", message: "must be an array" });
1031
+ return [4, 7, 20];
1032
+ }
1033
+ parsed.forEach((value, index) => {
1034
+ if (!Number.isInteger(value) || value < 0) {
1035
+ issues.push({ path: `CCH_POSITIONS[${index}]`, message: "must be a finite non-negative integer" });
1036
+ }
1037
+ });
1038
+ return parsed.filter((value) => Number.isInteger(value) && value >= 0);
1039
+ }
1040
+ function readClientNameMapping(env, issues) {
1041
+ const mapping = new Map;
1042
+ const raw = env.CLIENT_NAME_MAPPING;
1043
+ if (raw === undefined || raw.trim() === "")
1044
+ return mapping;
1045
+ raw.split(",").forEach((entry, index) => {
1046
+ const pair = entry.trim();
1047
+ const splitAt = pair.indexOf("=");
1048
+ const key = splitAt >= 0 ? pair.slice(0, splitAt).trim() : "";
1049
+ const value = splitAt >= 0 ? pair.slice(splitAt + 1).trim() : "";
1050
+ if (!key || !value) {
1051
+ issues.push({ path: `CLIENT_NAME_MAPPING[${index}]`, message: "must be a non-empty key=value entry" });
1052
+ return;
1053
+ }
1054
+ mapping.set(key, value);
1055
+ });
1056
+ return mapping;
1057
+ }
1058
+ function validateProviderConfig(env, issues) {
1059
+ const inline = env.PROVIDERS_JSON;
1060
+ const filePath = env.PROVIDERS_CONFIG_PATH;
1061
+ if (inline !== undefined && inline.trim() !== "") {
1062
+ validateProviderJson(inline, "PROVIDERS_JSON", issues);
1063
+ return;
1064
+ }
1065
+ if (filePath === undefined || filePath.trim() === "")
1066
+ return;
1067
+ try {
1068
+ validateProviderJson(readFileSync3(filePath, "utf-8"), "PROVIDERS_CONFIG_PATH", issues);
1069
+ } catch (err) {
1070
+ issues.push({
1071
+ path: "PROVIDERS_CONFIG_PATH",
1072
+ message: `could not be read: ${err instanceof Error ? err.message : String(err)}`
1073
+ });
1074
+ }
1075
+ }
1076
+ function validateProviderJson(raw, basePath, issues) {
1077
+ let parsed;
1078
+ try {
1079
+ parsed = JSON.parse(raw);
1080
+ } catch (err) {
1081
+ issues.push({
1082
+ path: basePath,
1083
+ message: `must be valid JSON: ${err instanceof Error ? err.message : String(err)}`
1084
+ });
1085
+ return;
1086
+ }
1087
+ const result = validateProviderDocument(parsed);
1088
+ issues.push(...result.issues);
1089
+ }
1090
+ function isLoopbackHost(host) {
1091
+ return LOOPBACK_HOSTS.has(host.trim().toLowerCase());
1092
+ }
1093
+ var ConfigError, Config, DEFAULT_CLI_PROXY_API_URL = "http://localhost:8317", LOOPBACK_HOSTS;
1094
+ var init_validate = __esm(() => {
1095
+ init_registry_schema();
1096
+ ConfigError = class ConfigError extends Error {
1097
+ issues;
1098
+ name = "ConfigError";
1099
+ code = "CONFIG_INVALID";
1100
+ constructor(issues) {
1101
+ super(`Configuration validation failed: ${issues.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`);
1102
+ this.issues = issues;
1103
+ }
1104
+ };
1105
+ ((Config) => {
1106
+ function validate(env = process.env, options = {}) {
1107
+ const issues = [];
1108
+ const warnings = [];
1109
+ const host = readString(env, "PROXY_HOST", "127.0.0.1");
1110
+ const cliProxyApiUrl = readRequiredUrl(env, "CLI_PROXY_API_URL", issues, warnings);
1111
+ const config = {
1112
+ port: readPort(env, issues),
1113
+ host,
1114
+ adminApiKey: readString(env, "ADMIN_API_KEY", ""),
1115
+ cliProxyApiUrl,
1116
+ claudeCodeVersion: readString(env, "CLAUDE_CODE_VERSION", "2.1.87"),
1117
+ cchSalt: readString(env, "CCH_SALT", "59cf53e54c78"),
1118
+ cchPositions: readCchPositions(env, issues),
1119
+ toolPrefix: readString(env, "TOOL_PREFIX", "mcp_"),
1120
+ cliProxyApiKey: readString(env, "CLI_PROXY_API_KEY", "proxy"),
1121
+ dbPath: readString(env, "DB_PATH", "data/proxy.db"),
1122
+ pricingCacheTtlMs: readPositiveNumber(env, "PRICING_CACHE_TTL_MS", 3600000, issues),
1123
+ pricingCachePath: readString(env, "PRICING_CACHE_PATH", "data/pricing-cache.json"),
1124
+ readyPricingMaxAgeMs: readPositiveNumber(env, "READY_PRICING_MAX_AGE_MS", 86400000, issues),
1125
+ pricingRefreshIntervalMs: readPositiveNumber(env, "PRICING_REFRESH_INTERVAL_MS", 21600000, issues),
1126
+ costBackfillIntervalMs: readPositiveNumber(env, "COST_BACKFILL_INTERVAL_MS", 1800000, issues),
1127
+ costBackfillLookbackMs: readPositiveNumber(env, "COST_BACKFILL_LOOKBACK_MS", 604800000, issues),
1128
+ logLevel: readString(env, "LOG_LEVEL", "info"),
1129
+ clientNameMapping: readClientNameMapping(env, issues),
1130
+ cliproxyMgmtKey: readString(env, "CLIPROXY_MGMT_KEY", ""),
1131
+ cliproxyCorrelationIntervalMs: readPositiveNumber(env, "CLIPROXY_CORRELATION_INTERVAL_MS", 15000, issues),
1132
+ cliproxyCorrelationLookbackMs: readPositiveNumber(env, "CLIPROXY_CORRELATION_LOOKBACK_MS", 300000, issues),
1133
+ cliproxyAuthDir: readString(env, "CLIPROXY_AUTH_DIR", ""),
1134
+ quotaRefreshIntervalMs: readPositiveNumber(env, "QUOTA_REFRESH_INTERVAL_MS", 300000, issues),
1135
+ quotaRefreshTimeoutMs: readPositiveNumber(env, "QUOTA_REFRESH_TIMEOUT_MS", 15000, issues),
1136
+ upstreamTimeoutMs: readPositiveNumber(env, "UPSTREAM_TIMEOUT_MS", 300000, issues),
1137
+ upstreamConnectTimeoutMs: readPositiveNumber(env, "UPSTREAM_CONNECT_TIMEOUT_MS", 1e4, issues)
1138
+ };
1139
+ if (!isLoopbackHost(config.host) && !config.adminApiKey) {
1140
+ issues.push({
1141
+ path: "ADMIN_API_KEY",
1142
+ message: "is required when PROXY_HOST is not loopback"
1143
+ });
1144
+ }
1145
+ validateProviderConfig(env, issues);
1146
+ if (issues.length > 0)
1147
+ throw new ConfigError(issues);
1148
+ for (const warning of warnings)
1149
+ options.onWarning?.(warning);
1150
+ return Object.freeze(config);
1151
+ }
1152
+ Config.validate = validate;
1153
+ })(Config ||= {});
1154
+ LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
1155
+ });
1156
+
1157
+ // src/storage/repo.ts
1158
+ function parseLifecycleStatus(value) {
1159
+ if (value === "completed" || value === "error" || value === "aborted")
1160
+ return value;
1161
+ return "pending";
1162
+ }
1163
+ var RequestRepo, UsageRepo, QuotaRepo;
1164
+ var init_repo = __esm(() => {
1165
+ ((RequestRepo) => {
1166
+ function insert(db, log) {
1167
+ const lifecycleStatus = log.lifecycle_status ?? (log.incomplete === 1 || log.error_code || (log.status ?? 0) >= 400 ? "error" : "completed");
1168
+ const costStatus = log.cost_status ?? (log.cost_usd > 0 ? "ok" : lifecycleStatus === "pending" ? "unresolved" : "pending");
1169
+ const finalizedAt = log.finalized_at ?? (lifecycleStatus === "pending" ? null : log.finished_at ?? log.started_at);
1170
+ const stmt = db.prepare(`
1171
+ INSERT INTO request_logs (
1172
+ request_id, provider, model, actual_model, tool, client_id, path,
1173
+ streamed, status, prompt_tokens, completion_tokens,
1174
+ cache_creation_tokens, cache_read_tokens, reasoning_tokens,
1175
+ total_tokens, cost_usd, incomplete, error_code, latency_ms,
1176
+ started_at, finished_at, meta_json, user_agent, source_ip,
1177
+ agent, source, msg_id, lifecycle_status, cost_status,
1178
+ subscription_code, finalized_at, error_message
1179
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1180
+ `);
1181
+ const result = stmt.run(log.request_id ?? null, log.provider, log.model, log.actual_model ?? null, log.tool, log.client_id, log.path, log.streamed, log.status ?? null, log.prompt_tokens, log.completion_tokens, log.cache_creation_tokens, log.cache_read_tokens, log.reasoning_tokens ?? 0, log.total_tokens, log.cost_usd, log.incomplete, log.error_code ?? null, log.latency_ms ?? null, log.started_at, log.finished_at ?? null, log.meta_json ?? null, log.user_agent ?? null, log.source_ip ?? null, log.agent ?? null, log.source ?? "proxy", log.msg_id ?? null, lifecycleStatus, costStatus, log.subscription_code ?? null, finalizedAt, log.error_message ?? null);
1182
+ return result.lastInsertRowid;
1183
+ }
1184
+ RequestRepo.insert = insert;
1185
+ function getRecent(db, limit, offset, tool, clientId) {
1186
+ let sql = `SELECT * FROM request_logs WHERE 1=1`;
1187
+ const params = [];
1188
+ if (tool) {
1189
+ sql += ` AND tool = ?`;
1190
+ params.push(tool);
1191
+ }
1192
+ if (clientId) {
1193
+ sql += ` AND client_id = ?`;
1194
+ params.push(clientId);
1195
+ }
1196
+ sql += ` ORDER BY started_at DESC LIMIT ? OFFSET ?`;
1197
+ params.push(limit, offset);
1198
+ const stmt = db.prepare(sql);
1199
+ return stmt.all(...params);
1200
+ }
1201
+ RequestRepo.getRecent = getRecent;
1202
+ function getById(db, id) {
1203
+ const stmt = db.prepare("SELECT * FROM request_logs WHERE id = ?");
1204
+ return stmt.get(id) || null;
1205
+ }
1206
+ RequestRepo.getById = getById;
1207
+ function aggregateByAccountForMonth(db, monthStart, monthEnd) {
1208
+ const stmt = db.prepare(`
1209
+ SELECT
1210
+ rl.cliproxy_account AS cliproxy_account,
1211
+ sub.subscription_code AS subscription_code,
1212
+ COUNT(*) AS total_requests,
1213
+ COALESCE(SUM(rl.cost_usd), 0) AS total_cost_usd
1214
+ FROM request_logs rl
1215
+ LEFT JOIN account_subscriptions sub
1216
+ ON sub.cliproxy_account = rl.cliproxy_account
1217
+ WHERE rl.lifecycle_status = 'completed'
1218
+ AND rl.started_at >= ?
1219
+ AND rl.started_at < ?
1220
+ AND rl.cliproxy_account IS NOT NULL
1221
+ AND rl.cliproxy_account <> ''
1222
+ GROUP BY rl.cliproxy_account, sub.subscription_code
1223
+ ORDER BY total_cost_usd DESC, rl.cliproxy_account ASC
1224
+ `);
1225
+ return stmt.all(monthStart, monthEnd).map((row) => {
1226
+ const record = row;
1227
+ return {
1228
+ cliproxy_account: String(record.cliproxy_account),
1229
+ subscription_code: typeof record.subscription_code === "string" ? record.subscription_code : null,
1230
+ total_requests: Number(record.total_requests ?? 0),
1231
+ total_cost_usd: Number(record.total_cost_usd ?? 0)
1232
+ };
1233
+ });
1234
+ }
1235
+ RequestRepo.aggregateByAccountForMonth = aggregateByAccountForMonth;
1236
+ function getRecentByAccount(db, cliproxyAccount, limit) {
1237
+ const stmt = db.prepare(`
1238
+ SELECT started_at, model, total_tokens, cost_usd, lifecycle_status
1239
+ FROM request_logs
1240
+ WHERE cliproxy_account = ?
1241
+ ORDER BY started_at DESC
1242
+ LIMIT ?
1243
+ `);
1244
+ return stmt.all(cliproxyAccount, limit).map((row) => {
1245
+ const record = row;
1246
+ return {
1247
+ started_at: String(record.started_at),
1248
+ model: String(record.model),
1249
+ total_tokens: Number(record.total_tokens ?? 0),
1250
+ cost_usd: Number(record.cost_usd ?? 0),
1251
+ lifecycle_status: parseLifecycleStatus(record.lifecycle_status)
1252
+ };
1253
+ });
1254
+ }
1255
+ RequestRepo.getRecentByAccount = getRecentByAccount;
1256
+ function getUncorrelated(db, sinceMs, limit) {
1257
+ const sinceIso = new Date(Date.now() - sinceMs).toISOString();
1258
+ const stmt = db.prepare(`
1259
+ SELECT * FROM request_logs
1260
+ WHERE cliproxy_account IS NULL
1261
+ AND status = 200
1262
+ AND started_at >= ?
1263
+ ORDER BY started_at DESC
1264
+ LIMIT ?
1265
+ `);
1266
+ return stmt.all(sinceIso, limit);
1267
+ }
1268
+ RequestRepo.getUncorrelated = getUncorrelated;
1269
+ function applyCorrelation(db, id, fields) {
1270
+ const stmt = db.prepare(`
1271
+ UPDATE request_logs
1272
+ SET cliproxy_account = COALESCE(?, cliproxy_account),
1273
+ cliproxy_auth_index = COALESCE(?, cliproxy_auth_index),
1274
+ cliproxy_source = COALESCE(?, cliproxy_source),
1275
+ reasoning_tokens = COALESCE(?, reasoning_tokens),
1276
+ actual_model = COALESCE(?, actual_model),
1277
+ correlated_at = ?
1278
+ WHERE id = ?
1279
+ `);
1280
+ stmt.run(fields.cliproxy_account ?? null, fields.cliproxy_auth_index ?? null, fields.cliproxy_source ?? null, fields.reasoning_tokens ?? null, fields.actual_model ?? null, new Date().toISOString(), id);
1281
+ }
1282
+ RequestRepo.applyCorrelation = applyCorrelation;
1283
+ function updateLifecycle(db, id, fields) {
1284
+ const stmt = db.prepare(`
1285
+ UPDATE request_logs
1286
+ SET lifecycle_status = COALESCE(?, lifecycle_status),
1287
+ finalized_at = COALESCE(?, finalized_at),
1288
+ error_message = COALESCE(?, error_message),
1289
+ cost_status = COALESCE(?, cost_status),
1290
+ subscription_code = COALESCE(?, subscription_code)
1291
+ WHERE id = ?
1292
+ `);
1293
+ stmt.run(fields.lifecycle_status ?? null, fields.finalized_at ?? null, fields.error_message ?? null, fields.cost_status ?? null, fields.subscription_code ?? null, id);
1294
+ }
1295
+ RequestRepo.updateLifecycle = updateLifecycle;
1296
+ function applySubscription(db, id, subscriptionCode) {
1297
+ db.prepare("UPDATE request_logs SET subscription_code = ? WHERE id = ?").run(subscriptionCode, id);
1298
+ }
1299
+ RequestRepo.applySubscription = applySubscription;
1300
+ function updateFinalize(db, id, fields) {
1301
+ const stmt = db.prepare(`
1302
+ UPDATE request_logs
1303
+ SET provider = COALESCE(?, provider),
1304
+ model = COALESCE(?, model),
1305
+ actual_model = COALESCE(?, actual_model),
1306
+ streamed = COALESCE(?, streamed),
1307
+ status = COALESCE(?, status),
1308
+ prompt_tokens = COALESCE(?, prompt_tokens),
1309
+ completion_tokens = COALESCE(?, completion_tokens),
1310
+ cache_creation_tokens = COALESCE(?, cache_creation_tokens),
1311
+ cache_read_tokens = COALESCE(?, cache_read_tokens),
1312
+ reasoning_tokens = COALESCE(?, reasoning_tokens),
1313
+ total_tokens = COALESCE(?, total_tokens),
1314
+ cost_usd = COALESCE(?, cost_usd),
1315
+ incomplete = COALESCE(?, incomplete),
1316
+ error_code = COALESCE(?, error_code),
1317
+ latency_ms = COALESCE(?, latency_ms),
1318
+ finished_at = COALESCE(?, finished_at),
1319
+ lifecycle_status = ?,
1320
+ finalized_at = ?,
1321
+ error_message = COALESCE(?, error_message),
1322
+ cost_status = ?,
1323
+ subscription_code = COALESCE(?, subscription_code)
1324
+ WHERE id = ? AND lifecycle_status = 'pending'
1325
+ `);
1326
+ const result = stmt.run(fields.provider ?? null, fields.model ?? null, fields.actual_model ?? null, fields.streamed ?? null, fields.status ?? null, fields.prompt_tokens ?? null, fields.completion_tokens ?? null, fields.cache_creation_tokens ?? null, fields.cache_read_tokens ?? null, fields.reasoning_tokens ?? null, fields.total_tokens ?? null, fields.cost_usd ?? null, fields.incomplete ?? null, fields.error_code ?? null, fields.latency_ms ?? null, fields.finished_at ?? null, fields.lifecycle_status, fields.finalized_at, fields.error_message ?? null, fields.cost_status, fields.subscription_code ?? null, id);
1327
+ return result.changes;
1328
+ }
1329
+ RequestRepo.updateFinalize = updateFinalize;
1330
+ function insertCostAudit(db, audit) {
1331
+ const stmt = db.prepare(`
1332
+ INSERT INTO cost_audit (
1333
+ request_log_id, model, provider, source, base_cost_usd, calc_at
1334
+ ) VALUES (?, ?, ?, ?, ?, COALESCE(?, CURRENT_TIMESTAMP))
1335
+ `);
1336
+ const result = stmt.run(audit.request_log_id ?? null, audit.model ?? null, audit.provider ?? null, audit.source ?? null, audit.base_cost_usd ?? null, audit.calc_at ?? null);
1337
+ return result.lastInsertRowid;
1338
+ }
1339
+ RequestRepo.insertCostAudit = insertCostAudit;
1340
+ })(RequestRepo ||= {});
1341
+ ((UsageRepo) => {
1342
+ function upsertDaily(db, usage) {
1343
+ const stmt = db.prepare(`
1344
+ INSERT INTO daily_usage (
1345
+ day, provider, model, request_count, prompt_tokens,
1346
+ completion_tokens, cache_creation_tokens, cache_read_tokens,
1347
+ total_tokens, cost_usd
1348
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1349
+ ON CONFLICT(day, provider, model) DO UPDATE SET
1350
+ request_count = request_count + excluded.request_count,
1351
+ prompt_tokens = prompt_tokens + excluded.prompt_tokens,
1352
+ completion_tokens = completion_tokens + excluded.completion_tokens,
1353
+ cache_creation_tokens = cache_creation_tokens + excluded.cache_creation_tokens,
1354
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
1355
+ total_tokens = total_tokens + excluded.total_tokens,
1356
+ cost_usd = cost_usd + excluded.cost_usd
1357
+ `);
1358
+ stmt.run(usage.day, usage.provider, usage.model, usage.request_count, usage.prompt_tokens, usage.completion_tokens, usage.cache_creation_tokens, usage.cache_read_tokens, usage.total_tokens, usage.cost_usd);
1359
+ }
1360
+ UsageRepo.upsertDaily = upsertDaily;
1361
+ function upsertDailyAccount(db, usage) {
1362
+ const stmt = db.prepare(`
1363
+ INSERT INTO daily_account_usage (
1364
+ day, provider, model, cliproxy_account, cliproxy_auth_index,
1365
+ request_count, prompt_tokens, completion_tokens,
1366
+ cache_creation_tokens, cache_read_tokens, reasoning_tokens,
1367
+ total_tokens, cost_usd
1368
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1369
+ ON CONFLICT(day, provider, model, cliproxy_account) DO UPDATE SET
1370
+ cliproxy_auth_index = COALESCE(excluded.cliproxy_auth_index, cliproxy_auth_index),
1371
+ request_count = request_count + excluded.request_count,
1372
+ prompt_tokens = prompt_tokens + excluded.prompt_tokens,
1373
+ completion_tokens = completion_tokens + excluded.completion_tokens,
1374
+ cache_creation_tokens = cache_creation_tokens + excluded.cache_creation_tokens,
1375
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
1376
+ reasoning_tokens = reasoning_tokens + excluded.reasoning_tokens,
1377
+ total_tokens = total_tokens + excluded.total_tokens,
1378
+ cost_usd = cost_usd + excluded.cost_usd
1379
+ `);
1380
+ stmt.run(usage.day, usage.provider, usage.model, usage.cliproxy_account, usage.cliproxy_auth_index ?? null, usage.request_count, usage.prompt_tokens, usage.completion_tokens, usage.cache_creation_tokens, usage.cache_read_tokens, usage.reasoning_tokens, usage.total_tokens, usage.cost_usd);
1381
+ }
1382
+ UsageRepo.upsertDailyAccount = upsertDailyAccount;
1383
+ function getDaily(db, day) {
1384
+ const stmt = db.prepare(`
1385
+ SELECT * FROM daily_usage
1386
+ WHERE day = ?
1387
+ ORDER BY provider, model
1388
+ `);
1389
+ return stmt.all(day);
1390
+ }
1391
+ UsageRepo.getDaily = getDaily;
1392
+ function getDailyByAccount(db, day) {
1393
+ const stmt = db.prepare(`
1394
+ SELECT * FROM daily_account_usage
1395
+ WHERE day = ?
1396
+ ORDER BY cliproxy_account, provider, model
1397
+ `);
1398
+ return stmt.all(day);
1399
+ }
1400
+ UsageRepo.getDailyByAccount = getDailyByAccount;
1401
+ function getRange(db, from, to) {
1402
+ const stmt = db.prepare(`
1403
+ SELECT * FROM daily_usage
1404
+ WHERE day >= ? AND day <= ?
1405
+ ORDER BY day DESC, provider, model
1406
+ `);
1407
+ return stmt.all(from, to);
1408
+ }
1409
+ UsageRepo.getRange = getRange;
1410
+ function getAccountRange(db, from, to) {
1411
+ const stmt = db.prepare(`
1412
+ SELECT * FROM daily_account_usage
1413
+ WHERE day >= ? AND day <= ?
1414
+ ORDER BY day DESC, cliproxy_account, provider, model
1415
+ `);
1416
+ return stmt.all(from, to);
1417
+ }
1418
+ UsageRepo.getAccountRange = getAccountRange;
1419
+ function getAccountSummary(db, from, to) {
1420
+ const stmt = db.prepare(`
1421
+ SELECT
1422
+ cliproxy_account,
1423
+ cliproxy_auth_index,
1424
+ provider,
1425
+ SUM(request_count) AS request_count,
1426
+ SUM(total_tokens) AS total_tokens,
1427
+ SUM(cost_usd) AS cost_usd
1428
+ FROM daily_account_usage
1429
+ WHERE day >= ? AND day <= ?
1430
+ GROUP BY cliproxy_account, cliproxy_auth_index, provider
1431
+ ORDER BY cost_usd DESC
1432
+ `);
1433
+ return stmt.all(from, to);
1434
+ }
1435
+ UsageRepo.getAccountSummary = getAccountSummary;
1436
+ })(UsageRepo ||= {});
1437
+ ((QuotaRepo) => {
1438
+ function insertSnapshot(db, snapshot) {
1439
+ const stmt = db.prepare(`
1440
+ INSERT INTO quota_snapshots (
1441
+ timestamp, provider, account, quota_type, used_pct,
1442
+ remaining, remaining_raw, resets_at, raw_json
1443
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1444
+ `);
1445
+ const result = stmt.run(snapshot.timestamp, snapshot.provider, snapshot.account, snapshot.quota_type, snapshot.used_pct ?? null, snapshot.remaining ?? null, snapshot.remaining_raw ?? null, snapshot.resets_at ?? null, snapshot.raw_json ?? null);
1446
+ return result.lastInsertRowid;
1447
+ }
1448
+ QuotaRepo.insertSnapshot = insertSnapshot;
1449
+ function getLatest(db) {
1450
+ const stmt = db.prepare(`
1451
+ SELECT q.*
1452
+ FROM quota_snapshots q
1453
+ JOIN (
1454
+ SELECT provider, account, quota_type, MAX(timestamp) AS max_timestamp
1455
+ FROM quota_snapshots
1456
+ GROUP BY provider, account, quota_type
1457
+ ) latest
1458
+ ON latest.provider = q.provider
1459
+ AND latest.account = q.account
1460
+ AND latest.quota_type = q.quota_type
1461
+ AND latest.max_timestamp = q.timestamp
1462
+ ORDER BY q.provider, q.account, q.quota_type
1463
+ `);
1464
+ return stmt.all();
1465
+ }
1466
+ QuotaRepo.getLatest = getLatest;
1467
+ function getLocalWindowUsage(db, provider, account, sinceIso) {
1468
+ const row = db.prepare(`
1469
+ SELECT
1470
+ COUNT(*) AS requests,
1471
+ COALESCE(SUM(total_tokens), 0) AS total_tokens,
1472
+ COALESCE(SUM(cost_usd), 0) AS cost_usd
1473
+ FROM request_logs
1474
+ WHERE provider = ?
1475
+ AND cliproxy_account = ?
1476
+ AND started_at >= ?
1477
+ `).get(provider, account, sinceIso);
1478
+ return {
1479
+ since: sinceIso,
1480
+ requests: Number(row.requests ?? 0),
1481
+ total_tokens: Number(row.total_tokens ?? 0),
1482
+ cost_usd: Number(row.cost_usd ?? 0)
1483
+ };
1484
+ }
1485
+ QuotaRepo.getLocalWindowUsage = getLocalWindowUsage;
1486
+ })(QuotaRepo ||= {});
1487
+ });
1488
+
1489
+ // src/config/index.ts
1490
+ var exports_config = {};
1491
+ __export(exports_config, {
1492
+ ConfigError: () => ConfigError,
1493
+ Config: () => Config2
1494
+ });
1495
+ var configLogger, validated, Config2;
1496
+ var init_config = __esm(() => {
1497
+ init_logger();
1498
+ init_validate();
1499
+ init_validate();
1500
+ configLogger = Logger.fromConfig().child({ component: "config" });
1501
+ validated = Config.validate(process.env, {
1502
+ onWarning(issue) {
1503
+ configLogger.warn("configuration warning", { event: "config.warning", ...issue });
1504
+ }
1505
+ });
1506
+ Config2 = Object.freeze({
1507
+ ...validated,
1508
+ validate: Config.validate
1509
+ });
1510
+ });
1511
+
1512
+ // src/runtime/supervisor.ts
1513
+ var exports_supervisor = {};
1514
+ __export(exports_supervisor, {
1515
+ Supervisor: () => Supervisor
1516
+ });
1517
+ var Supervisor;
1518
+ var init_supervisor = __esm(() => {
1519
+ init_logger();
1520
+ ((Supervisor) => {
1521
+ const DEFAULT_JITTER_RATIO = 0.1;
1522
+ const DEFAULT_MAX_BACKOFF_MS = 60000;
1523
+ const DEFAULT_STOP_TIMEOUT_MS = 2000;
1524
+ const registry = new Set;
1525
+ let logger2 = Logger.fromConfig().child({ component: "supervisor" });
1526
+ function run(name, fn, options) {
1527
+ validateOptions(name, options);
1528
+ const controller = new AbortController;
1529
+ const signal = controller.signal;
1530
+ const intervalMs = options.intervalMs;
1531
+ const jitterRatio = options.jitterRatio ?? DEFAULT_JITTER_RATIO;
1532
+ const maxBackoffMs = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
1533
+ const runOnStart = options.runOnStart ?? true;
1534
+ const initialDelayMs = options.initialDelayMs ?? 0;
1535
+ let removeExternalAbort = null;
1536
+ if (options.signal) {
1537
+ if (options.signal.aborted)
1538
+ controller.abort();
1539
+ else {
1540
+ const abort = () => controller.abort();
1541
+ options.signal.addEventListener("abort", abort, { once: true });
1542
+ removeExternalAbort = () => options.signal?.removeEventListener("abort", abort);
1543
+ }
1544
+ }
1545
+ const state = {
1546
+ name,
1547
+ controller,
1548
+ done: Promise.resolve(),
1549
+ stopRequested: false,
1550
+ stopLogged: false,
1551
+ async stop(timeoutMs) {
1552
+ this.stopRequested = true;
1553
+ this.controller.abort();
1554
+ const stopped = await resolveWithin(this.done, timeoutMs);
1555
+ if (stopped) {
1556
+ logStopped(this);
1557
+ return;
1558
+ }
1559
+ registry.delete(this);
1560
+ logStopTimeout(this, timeoutMs);
1561
+ }
1562
+ };
1563
+ state.done = loop({
1564
+ name,
1565
+ fn,
1566
+ signal,
1567
+ intervalMs,
1568
+ initialDelayMs,
1569
+ jitterRatio,
1570
+ maxBackoffMs,
1571
+ runOnStart
1572
+ }).finally(() => {
1573
+ removeExternalAbort?.();
1574
+ registry.delete(state);
1575
+ if (state.stopRequested || signal.aborted)
1576
+ logStopped(state);
1577
+ });
1578
+ registry.add(state);
1579
+ logger2.info("loop started", { name, event: "loop.started", interval_ms: intervalMs });
1580
+ return {
1581
+ stop() {
1582
+ return state.stop(DEFAULT_STOP_TIMEOUT_MS);
1583
+ }
1584
+ };
1585
+ }
1586
+ Supervisor.run = run;
1587
+ async function stopAll(timeoutMs = DEFAULT_STOP_TIMEOUT_MS) {
1588
+ const loops = Array.from(registry);
1589
+ await Promise.all(loops.map((loopState) => loopState.stop(timeoutMs)));
1590
+ }
1591
+ Supervisor.stopAll = stopAll;
1592
+ function list() {
1593
+ return Array.from(registry, (loopState) => loopState.name).sort();
1594
+ }
1595
+ Supervisor.list = list;
1596
+ function __setLoggerForTests(testLogger) {
1597
+ logger2 = testLogger ?? Logger.fromConfig().child({ component: "supervisor" });
1598
+ }
1599
+ Supervisor.__setLoggerForTests = __setLoggerForTests;
1600
+ async function loop(context) {
1601
+ let consecutiveFailures = 0;
1602
+ let nextDelayMs = context.runOnStart ? context.initialDelayMs : context.initialDelayMs > 0 ? context.initialDelayMs : context.intervalMs;
1603
+ while (!context.signal.aborted) {
1604
+ if (nextDelayMs > 0) {
1605
+ const slept = await sleep(nextDelayMs, context.signal);
1606
+ if (!slept)
1607
+ return;
1608
+ }
1609
+ if (context.signal.aborted)
1610
+ return;
1611
+ const startedAt = Date.now();
1612
+ try {
1613
+ await context.fn();
1614
+ const durationMs = Date.now() - startedAt;
1615
+ consecutiveFailures = 0;
1616
+ logger2.debug("loop tick", {
1617
+ name: context.name,
1618
+ event: "loop.tick",
1619
+ duration_ms: durationMs
1620
+ });
1621
+ nextDelayMs = applyJitter(context.intervalMs, context.jitterRatio);
1622
+ } catch (err) {
1623
+ consecutiveFailures += 1;
1624
+ const backoffMs = Math.min(context.intervalMs * 2 ** consecutiveFailures, context.maxBackoffMs);
1625
+ nextDelayMs = applyJitter(backoffMs, context.jitterRatio);
1626
+ logger2.error("loop error", {
1627
+ name: context.name,
1628
+ event: "loop.error",
1629
+ err,
1630
+ attempt: consecutiveFailures,
1631
+ next_delay_ms: nextDelayMs
1632
+ });
1633
+ }
1634
+ }
1635
+ }
1636
+ function validateOptions(name, options) {
1637
+ if (!name.trim())
1638
+ throw new Error("Supervisor loop name is required");
1639
+ if (!Number.isFinite(options.intervalMs) || options.intervalMs <= 0) {
1640
+ throw new Error("Supervisor intervalMs must be a positive finite number");
1641
+ }
1642
+ if (options.initialDelayMs !== undefined && (!Number.isFinite(options.initialDelayMs) || options.initialDelayMs < 0)) {
1643
+ throw new Error("Supervisor initialDelayMs must be a non-negative finite number");
1644
+ }
1645
+ if (options.jitterRatio !== undefined && (!Number.isFinite(options.jitterRatio) || options.jitterRatio < 0)) {
1646
+ throw new Error("Supervisor jitterRatio must be a non-negative finite number");
1647
+ }
1648
+ if (options.maxBackoffMs !== undefined && (!Number.isFinite(options.maxBackoffMs) || options.maxBackoffMs <= 0)) {
1649
+ throw new Error("Supervisor maxBackoffMs must be a positive finite number");
1650
+ }
1651
+ }
1652
+ function applyJitter(delayMs, jitterRatio) {
1653
+ if (delayMs <= 0 || jitterRatio <= 0)
1654
+ return Math.max(0, Math.round(delayMs));
1655
+ const spread = delayMs * jitterRatio;
1656
+ const offset = (Math.random() * 2 - 1) * spread;
1657
+ return Math.max(0, Math.round(delayMs + offset));
1658
+ }
1659
+ function sleep(delayMs, signal) {
1660
+ if (signal.aborted)
1661
+ return Promise.resolve(false);
1662
+ return new Promise((resolve) => {
1663
+ let timeout = null;
1664
+ const onAbort = () => {
1665
+ if (timeout)
1666
+ clearTimeout(timeout);
1667
+ resolve(false);
1668
+ };
1669
+ timeout = setTimeout(() => {
1670
+ signal.removeEventListener("abort", onAbort);
1671
+ resolve(true);
1672
+ }, delayMs);
1673
+ signal.addEventListener("abort", onAbort, { once: true });
1674
+ });
1675
+ }
1676
+ async function resolveWithin(promise, timeoutMs) {
1677
+ let timeout = null;
1678
+ try {
1679
+ return await Promise.race([
1680
+ promise.then(() => true),
1681
+ new Promise((resolve) => {
1682
+ timeout = setTimeout(() => resolve(false), timeoutMs);
1683
+ })
1684
+ ]);
1685
+ } finally {
1686
+ if (timeout)
1687
+ clearTimeout(timeout);
1688
+ }
1689
+ }
1690
+ function logStopped(state) {
1691
+ if (state.stopLogged)
1692
+ return;
1693
+ state.stopLogged = true;
1694
+ logger2.info("loop stopped", { name: state.name, event: "loop.stopped" });
1695
+ }
1696
+ function logStopTimeout(state, timeoutMs) {
1697
+ if (state.stopLogged)
1698
+ return;
1699
+ state.stopLogged = true;
1700
+ logger2.warn("loop stop timeout", {
1701
+ name: state.name,
1702
+ event: "loop.stop_timeout",
1703
+ timeout_ms: timeoutMs
1704
+ });
1705
+ }
1706
+ })(Supervisor ||= {});
1707
+ });
1708
+
1709
+ // src/storage/pricing.ts
1710
+ var exports_pricing = {};
1711
+ __export(exports_pricing, {
1712
+ Pricing: () => Pricing
1713
+ });
1714
+ import { dirname } from "path";
1715
+ import { mkdir } from "fs/promises";
1716
+ var logger2, Pricing;
1717
+ var init_pricing = __esm(() => {
1718
+ init_config();
1719
+ init_logger();
1720
+ init_supervisor();
1721
+ logger2 = Logger.fromConfig().child({ component: "pricing" });
1722
+ ((Pricing) => {
1723
+ const MODELS_DEV_URL = "https://models.dev/api.json";
1724
+ let cache = null;
1725
+ let inFlightFetch = null;
1726
+ let bypassDiskCacheForTests = false;
1727
+ async function fetchPricing(options = {}) {
1728
+ const now = Date.now();
1729
+ if (!options.force && cache && now - cache.fetchedAt < Config2.pricingCacheTtlMs) {
1730
+ return cache.data;
1731
+ }
1732
+ if (!options.force && inFlightFetch) {
1733
+ return inFlightFetch;
1734
+ }
1735
+ inFlightFetch = refreshPricing(options.force ?? false).finally(() => {
1736
+ inFlightFetch = null;
1737
+ });
1738
+ return inFlightFetch;
1739
+ }
1740
+ Pricing.fetchPricing = fetchPricing;
1741
+ function getPricing(model, provider) {
1742
+ return findPricing(model, provider)?.pricing ?? null;
1743
+ }
1744
+ Pricing.getPricing = getPricing;
1745
+ async function getPricingFreshness() {
1746
+ const entry = cache ?? await readDiskCache();
1747
+ if (!entry)
1748
+ return null;
1749
+ return { fetchedAt: entry.fetchedAt, ageMs: Date.now() - entry.fetchedAt };
1750
+ }
1751
+ Pricing.getPricingFreshness = getPricingFreshness;
1752
+ function startBackgroundRefresh(options = {}) {
1753
+ const intervalMs = options.intervalMs ?? Config2.pricingRefreshIntervalMs;
1754
+ return Supervisor.run("pricing-refresh", async () => {
1755
+ await fetchPricing();
1756
+ }, {
1757
+ intervalMs,
1758
+ runOnStart: false,
1759
+ signal: options.signal
1760
+ });
1761
+ }
1762
+ Pricing.startBackgroundRefresh = startBackgroundRefresh;
1763
+ function __setPricingForTests(entries, fetchedAt = Date.now()) {
1764
+ bypassDiskCacheForTests = false;
1765
+ cache = { data: new Map(entries), fetchedAt };
1766
+ }
1767
+ Pricing.__setPricingForTests = __setPricingForTests;
1768
+ function __clearPricingForTests() {
1769
+ cache = null;
1770
+ inFlightFetch = null;
1771
+ bypassDiskCacheForTests = true;
1772
+ }
1773
+ Pricing.__clearPricingForTests = __clearPricingForTests;
1774
+ function findPricing(model, provider) {
1775
+ if (!cache)
1776
+ return null;
1777
+ const normalizedModel = normalizeKey(model);
1778
+ const normalizedProvider = provider ? normalizeKey(provider) : null;
1779
+ const candidates = buildLookupCandidates(model, provider);
1780
+ for (const key of candidates) {
1781
+ const pricing = cache.data.get(key);
1782
+ if (pricing)
1783
+ return { pricing, key, source: "exact" };
1784
+ }
1785
+ for (const [key, pricing] of cache.data) {
1786
+ if (normalizeKey(key) === normalizedModel) {
1787
+ return { pricing, key, source: "normalized" };
1788
+ }
1789
+ if (normalizedProvider && normalizeKey(key) === `${normalizedProvider}/${normalizedModel}`) {
1790
+ return { pricing, key, source: "normalized" };
1791
+ }
1792
+ }
1793
+ const alias = aliasModel(normalizedModel);
1794
+ if (alias) {
1795
+ for (const key of buildLookupCandidates(alias, provider)) {
1796
+ const pricing = cache.data.get(key);
1797
+ if (pricing)
1798
+ return { pricing, key, source: "alias" };
1799
+ }
1800
+ }
1801
+ const fuzzy = findFuzzyMatch(normalizedModel, normalizedProvider, cache.data);
1802
+ if (fuzzy)
1803
+ return fuzzy;
1804
+ return null;
1805
+ }
1806
+ Pricing.findPricing = findPricing;
1807
+ function calculateCost(usage, pricing, provider) {
1808
+ if (provider && normalizeKey(provider) === "openai") {
1809
+ const billableInputTokens = Math.max(usage.prompt_tokens - usage.cache_read_tokens, 0);
1810
+ return (billableInputTokens * pricing.input + usage.completion_tokens * pricing.output + usage.cache_read_tokens * (pricing.cache_read ?? pricing.input)) / 1e6;
1811
+ }
1812
+ return (usage.prompt_tokens * pricing.input + usage.completion_tokens * pricing.output + usage.cache_read_tokens * (pricing.cache_read ?? pricing.input) + usage.cache_creation_tokens * (pricing.cache_write ?? pricing.input) + (usage.reasoning_tokens ?? 0) * (pricing.reasoning ?? pricing.output)) / 1e6;
1813
+ }
1814
+ Pricing.calculateCost = calculateCost;
1815
+ async function refreshPricing(force) {
1816
+ const now = Date.now();
1817
+ if (!force && !bypassDiskCacheForTests) {
1818
+ const diskCache = await readDiskCache();
1819
+ if (diskCache && now - diskCache.fetchedAt < Config2.pricingCacheTtlMs) {
1820
+ cache = diskCache;
1821
+ return diskCache.data;
1822
+ }
1823
+ }
1824
+ try {
1825
+ const res = await fetch(MODELS_DEV_URL, { signal: AbortSignal.timeout(30000) });
1826
+ if (!res.ok)
1827
+ throw new Error(`models.dev returned HTTP ${res.status}`);
1828
+ const raw = await res.json();
1829
+ const map = buildPricingMap(raw);
1830
+ addLocalOverrides(map);
1831
+ cache = { data: map, fetchedAt: now };
1832
+ await writeDiskCache(cache);
1833
+ logger2.info("loaded pricing aliases", { aliases: map.size, source: "models.dev" });
1834
+ return map;
1835
+ } catch (err) {
1836
+ logger2.warn("pricing fetch failed, using cached data", { err, source: "models.dev" });
1837
+ if (cache)
1838
+ return cache.data;
1839
+ const diskCache = bypassDiskCacheForTests ? null : await readDiskCache();
1840
+ if (diskCache) {
1841
+ cache = diskCache;
1842
+ return diskCache.data;
1843
+ }
1844
+ const fallback = new Map;
1845
+ addLocalOverrides(fallback);
1846
+ cache = { data: fallback, fetchedAt: 0 };
1847
+ return fallback;
1848
+ }
1849
+ }
1850
+ function buildPricingMap(raw) {
1851
+ const map = new Map;
1852
+ for (const [provider, providerData] of Object.entries(raw)) {
1853
+ if (!providerData.models)
1854
+ continue;
1855
+ for (const [modelId, modelData] of Object.entries(providerData.models)) {
1856
+ if (!modelData.cost)
1857
+ continue;
1858
+ const pricing = toPricing(modelData.cost);
1859
+ if (!pricing)
1860
+ continue;
1861
+ setPricingAlias(map, modelId, pricing);
1862
+ setPricingAlias(map, `${provider}/${modelId}`, pricing);
1863
+ if (modelData.id)
1864
+ setPricingAlias(map, modelData.id, pricing);
1865
+ if (modelData.name)
1866
+ setPricingAlias(map, modelData.name, pricing);
1867
+ }
1868
+ }
1869
+ return map;
1870
+ }
1871
+ function toPricing(cost) {
1872
+ if (typeof cost.input !== "number" || typeof cost.output !== "number")
1873
+ return null;
1874
+ return {
1875
+ input: cost.input,
1876
+ output: cost.output,
1877
+ cache_read: typeof cost.cache_read === "number" ? cost.cache_read : undefined,
1878
+ cache_write: typeof cost.cache_write === "number" ? cost.cache_write : undefined,
1879
+ reasoning: typeof cost.reasoning === "number" ? cost.reasoning : undefined
1880
+ };
1881
+ }
1882
+ function addLocalOverrides(map) {
1883
+ const overrides = {
1884
+ "gpt-5.4": { input: 2.5, output: 15, cache_read: 0.25 },
1885
+ "gpt-5.4-mini": { input: 0.75, output: 4.5, cache_read: 0.075 },
1886
+ "gpt-5.4-mini-2026-03-17": { input: 0.75, output: 4.5, cache_read: 0.075 },
1887
+ "kimi-for-coding": { input: 0.4, output: 2.5, cache_read: 0.4 },
1888
+ "kimi-k2": { input: 0.4, output: 2.5, cache_read: 0.4 },
1889
+ "kimi-k2.6": { input: 0.95, output: 4, cache_read: 0.16 }
1890
+ };
1891
+ for (const [model, pricing] of Object.entries(overrides)) {
1892
+ setPricingAlias(map, model, pricing);
1893
+ setPricingAlias(map, `openai/${model}`, pricing);
1894
+ }
1895
+ }
1896
+ function setPricingAlias(map, key, pricing) {
1897
+ map.set(key, pricing);
1898
+ map.set(normalizeKey(key), pricing);
1899
+ }
1900
+ function buildLookupCandidates(model, provider) {
1901
+ const candidates = new Set;
1902
+ candidates.add(model);
1903
+ candidates.add(normalizeKey(model));
1904
+ if (provider) {
1905
+ candidates.add(`${provider}/${model}`);
1906
+ candidates.add(`${normalizeKey(provider)}/${normalizeKey(model)}`);
1907
+ }
1908
+ return Array.from(candidates);
1909
+ }
1910
+ function aliasModel(normalizedModel) {
1911
+ if (normalizedModel === "kimi-for-coding")
1912
+ return "kimi-k2";
1913
+ if (normalizedModel.startsWith("gpt-5.4-mini"))
1914
+ return "gpt-5.4-mini";
1915
+ if (normalizedModel.startsWith("gpt-5.4"))
1916
+ return "gpt-5.4";
1917
+ return null;
1918
+ }
1919
+ function findFuzzyMatch(normalizedModel, normalizedProvider, map) {
1920
+ const eligible = Array.from(map.entries()).filter(([key, pricing]) => {
1921
+ if (pricing.input === 0 && pricing.output === 0)
1922
+ return false;
1923
+ const normalizedKey = normalizeKey(key);
1924
+ if (normalizedProvider && !normalizedKey.startsWith(`${normalizedProvider}/`) && normalizedKey.includes("/")) {
1925
+ return false;
1926
+ }
1927
+ return normalizedKey.endsWith(`/${normalizedModel}`) || normalizedKey === normalizedModel;
1928
+ });
1929
+ if (eligible.length > 0) {
1930
+ const [key, pricing] = eligible[0];
1931
+ return { key, pricing, source: "fuzzy" };
1932
+ }
1933
+ const broad = Array.from(map.entries()).find(([key, pricing]) => {
1934
+ if (pricing.input === 0 && pricing.output === 0)
1935
+ return false;
1936
+ const normalizedKey = normalizeKey(key);
1937
+ return normalizedKey.length >= 6 && normalizedModel.includes(normalizedKey);
1938
+ });
1939
+ if (!broad)
1940
+ return null;
1941
+ return { key: broad[0], pricing: broad[1], source: "fuzzy" };
1942
+ }
1943
+ function normalizeKey(key) {
1944
+ return key.trim().toLowerCase().replace(/[_\s]+/g, "-");
1945
+ }
1946
+ async function readDiskCache() {
1947
+ try {
1948
+ const file = Bun.file(Config2.pricingCachePath);
1949
+ if (!await file.exists())
1950
+ return null;
1951
+ const parsed = await file.json();
1952
+ if (typeof parsed.fetchedAt !== "number" || !Array.isArray(parsed.data))
1953
+ return null;
1954
+ return { fetchedAt: parsed.fetchedAt, data: new Map(parsed.data) };
1955
+ } catch (err) {
1956
+ logger2.warn("disk cache read failed", { err, path: Config2.pricingCachePath });
1957
+ return null;
1958
+ }
1959
+ }
1960
+ async function writeDiskCache(entry) {
1961
+ try {
1962
+ await mkdir(dirname(Config2.pricingCachePath), { recursive: true });
1963
+ await Bun.write(Config2.pricingCachePath, JSON.stringify({ fetchedAt: entry.fetchedAt, data: Array.from(entry.data.entries()) }));
1964
+ } catch (err) {
1965
+ logger2.warn("disk cache write failed", { err, path: Config2.pricingCachePath });
1966
+ }
1967
+ }
1968
+ })(Pricing ||= {});
1969
+ });
1970
+
1971
+ // src/storage/cost.ts
1972
+ var logger3, Cost;
1973
+ var init_cost = __esm(() => {
1974
+ init_pricing();
1975
+ init_logger();
1976
+ logger3 = Logger.fromConfig().child({ component: "cost" });
1977
+ ((Cost) => {
1978
+ const SENTINEL_MODELS = new Set(["", "unknown", "undefined"]);
1979
+ let activeLogger = logger3;
1980
+ function __setLoggerForTests(nextLogger) {
1981
+ activeLogger = nextLogger;
1982
+ }
1983
+ Cost.__setLoggerForTests = __setLoggerForTests;
1984
+ function __resetLoggerForTests() {
1985
+ activeLogger = logger3;
1986
+ }
1987
+ Cost.__resetLoggerForTests = __resetLoggerForTests;
1988
+ function compute(inputs) {
1989
+ const model = inputs.model?.trim() ?? "";
1990
+ if (SENTINEL_MODELS.has(model.toLowerCase())) {
1991
+ return { cost_usd: 0, cost_status: "unsupported", source: "unsupported_model" };
1992
+ }
1993
+ const pricing = Pricing.getPricing(model, inputs.provider);
1994
+ if (!pricing) {
1995
+ return { cost_usd: 0, cost_status: "pending", source: "pricing" };
1996
+ }
1997
+ const rawCost = Pricing.calculateCost({
1998
+ prompt_tokens: inputs.usage.prompt_tokens ?? 0,
1999
+ completion_tokens: inputs.usage.completion_tokens ?? 0,
2000
+ cache_creation_tokens: inputs.usage.cache_creation_tokens ?? 0,
2001
+ cache_read_tokens: inputs.usage.cache_read_tokens ?? 0,
2002
+ reasoning_tokens: inputs.usage.reasoning_tokens ?? 0
2003
+ }, pricing, inputs.provider);
2004
+ if (!Number.isFinite(rawCost) || Number.isNaN(rawCost) || rawCost < 0) {
2005
+ activeLogger.warn("cost guard rejected computed cost", {
2006
+ event: "cost.guard",
2007
+ provider: inputs.provider,
2008
+ model,
2009
+ raw_cost: rawCost
2010
+ });
2011
+ return { cost_usd: 0, cost_status: "pending", source: "guard" };
2012
+ }
2013
+ if (rawCost === 0) {
2014
+ return { cost_usd: 0, cost_status: "pending", source: "guard" };
2015
+ }
2016
+ return { cost_usd: rawCost, cost_status: "ok", source: "pricing" };
2017
+ }
2018
+ Cost.compute = compute;
2019
+ function inputsFromLog(log) {
2020
+ return {
2021
+ provider: log.provider,
2022
+ model: log.model,
2023
+ usage: {
2024
+ prompt_tokens: log.prompt_tokens,
2025
+ completion_tokens: log.completion_tokens,
2026
+ cache_creation_tokens: log.cache_creation_tokens,
2027
+ cache_read_tokens: log.cache_read_tokens,
2028
+ reasoning_tokens: log.reasoning_tokens ?? 0
2029
+ }
2030
+ };
2031
+ }
2032
+ Cost.inputsFromLog = inputsFromLog;
2033
+ })(Cost ||= {});
2034
+ });
2035
+
2036
+ // src/upstream/client.ts
2037
+ var exports_client = {};
2038
+ __export(exports_client, {
2039
+ UpstreamClient: () => UpstreamClient
2040
+ });
2041
+ var UpstreamClient;
2042
+ var init_client = __esm(() => {
2043
+ init_config();
2044
+ init_logger();
2045
+ ((UpstreamClient) => {
2046
+ UpstreamClient.DEFAULT_UPSTREAM_TIMEOUT_MS = 300000;
2047
+ UpstreamClient.DEFAULT_UPSTREAM_CONNECT_TIMEOUT_MS = 1e4;
2048
+ const MAX_RETRIES = 2;
2049
+ const OPEN_AFTER_FAILURES = 5;
2050
+ const HALF_OPEN_AFTER_MS = 30000;
2051
+ const breakers = new Map;
2052
+ let logger4 = Logger.fromConfig().child({ component: "upstream-client" });
2053
+ let sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2054
+ let now = () => Date.now();
2055
+ let random = () => Math.random();
2056
+ async function fetch2(options) {
2057
+ const providerId = options.providerId || "unknown";
2058
+ const breaker = breakerFor(providerId);
2059
+ const breakerState = currentBreakerState(breaker);
2060
+ if (breakerState === "open") {
2061
+ const normalized = normalizeShortCircuit(providerId);
2062
+ logger4.warn("upstream circuit breaker open", {
2063
+ event: "upstream.short_circuit",
2064
+ ...withoutCause(normalized)
2065
+ });
2066
+ logFailure(normalized, 0, false);
2067
+ return normalizedResponse(normalized);
2068
+ }
2069
+ const streaming = isStreamingRequest(options);
2070
+ const idempotent = options.idempotent === true;
2071
+ let attempt = 0;
2072
+ while (true) {
2073
+ const timeout = createTimeoutSignal(Config2.upstreamTimeoutMs, Config2.upstreamConnectTimeoutMs);
2074
+ const signal = composeSignals([timeout.signal, options.signal]);
2075
+ try {
2076
+ const response = await globalThis.fetch(options.url, {
2077
+ method: options.method,
2078
+ headers: options.headers,
2079
+ body: options.body,
2080
+ signal
2081
+ });
2082
+ timeout.clear();
2083
+ if (response.status >= 500) {
2084
+ const normalized = normalizeHttpFailure(response, providerId, canRetry(idempotent, streaming));
2085
+ const retrying = shouldRetry(normalized, attempt, streaming, idempotent);
2086
+ logFailure(normalized, attempt, retrying);
2087
+ if (retrying) {
2088
+ await discardResponse(response);
2089
+ await sleep(backoffMs(attempt));
2090
+ attempt += 1;
2091
+ continue;
2092
+ }
2093
+ recordFailure(breaker);
2094
+ return response;
2095
+ }
2096
+ recordSuccess(breaker);
2097
+ return response;
2098
+ } catch (err) {
2099
+ timeout.clear();
2100
+ const normalized = normalizeThrownFailure(err, providerId, canRetry(idempotent, streaming), timeout.kind);
2101
+ const retrying = shouldRetry(normalized, attempt, streaming, idempotent);
2102
+ logFailure(normalized, attempt, retrying);
2103
+ if (retrying) {
2104
+ await sleep(backoffMs(attempt));
2105
+ attempt += 1;
2106
+ continue;
2107
+ }
2108
+ recordFailure(breaker);
2109
+ return normalizedResponse(normalized);
2110
+ }
2111
+ }
2112
+ }
2113
+ UpstreamClient.fetch = fetch2;
2114
+ function __resetForTests() {
2115
+ breakers.clear();
2116
+ logger4 = Logger.fromConfig().child({ component: "upstream-client" });
2117
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2118
+ now = () => Date.now();
2119
+ random = () => Math.random();
2120
+ }
2121
+ UpstreamClient.__resetForTests = __resetForTests;
2122
+ function __setTestHooks(hooks) {
2123
+ if (hooks.logger)
2124
+ logger4 = hooks.logger;
2125
+ if (hooks.sleep)
2126
+ sleep = hooks.sleep;
2127
+ if (hooks.now)
2128
+ now = hooks.now;
2129
+ if (hooks.random)
2130
+ random = hooks.random;
2131
+ }
2132
+ UpstreamClient.__setTestHooks = __setTestHooks;
2133
+ function breakerFor(providerId) {
2134
+ const existing = breakers.get(providerId);
2135
+ if (existing)
2136
+ return existing;
2137
+ const created = { state: "closed", failures: 0, openedAt: 0 };
2138
+ breakers.set(providerId, created);
2139
+ return created;
2140
+ }
2141
+ function currentBreakerState(breaker) {
2142
+ if (breaker.state === "open" && now() - breaker.openedAt >= HALF_OPEN_AFTER_MS) {
2143
+ breaker.state = "half-open";
2144
+ }
2145
+ return breaker.state;
2146
+ }
2147
+ function recordFailure(breaker) {
2148
+ if (breaker.state === "half-open") {
2149
+ breaker.state = "open";
2150
+ breaker.openedAt = now();
2151
+ breaker.failures = OPEN_AFTER_FAILURES;
2152
+ return;
2153
+ }
2154
+ breaker.failures += 1;
2155
+ if (breaker.failures >= OPEN_AFTER_FAILURES) {
2156
+ breaker.state = "open";
2157
+ breaker.openedAt = now();
2158
+ }
2159
+ }
2160
+ function recordSuccess(breaker) {
2161
+ breaker.state = "closed";
2162
+ breaker.failures = 0;
2163
+ breaker.openedAt = 0;
2164
+ }
2165
+ function canRetry(idempotent, streaming) {
2166
+ return idempotent && !streaming;
2167
+ }
2168
+ function shouldRetry(failure, attempt, streaming, idempotent) {
2169
+ if (!canRetry(idempotent, streaming))
2170
+ return false;
2171
+ if (attempt >= MAX_RETRIES)
2172
+ return false;
2173
+ return failure.code === "network" || failure.code === "5xx" || failure.code === "aborted-due-to-timeout";
2174
+ }
2175
+ function backoffMs(attempt) {
2176
+ const jitter = Math.floor(random() * 100);
2177
+ return Math.min(2 ** attempt * 200 + jitter, 5000);
2178
+ }
2179
+ function createTimeoutSignal(totalMs, connectMs) {
2180
+ const controller = new AbortController;
2181
+ let kind = null;
2182
+ const abort = (nextKind) => {
2183
+ if (controller.signal.aborted)
2184
+ return;
2185
+ kind = nextKind;
2186
+ controller.abort(new Error(`upstream ${nextKind} timeout`));
2187
+ };
2188
+ const connectTimer = setTimeout(() => abort("connect"), connectMs);
2189
+ const totalTimer = setTimeout(() => abort("total"), totalMs);
2190
+ return {
2191
+ signal: controller.signal,
2192
+ clear() {
2193
+ clearTimeout(connectTimer);
2194
+ clearTimeout(totalTimer);
2195
+ },
2196
+ get kind() {
2197
+ return kind;
2198
+ }
2199
+ };
2200
+ }
2201
+ function composeSignals(signals) {
2202
+ const active = signals.filter((signal) => Boolean(signal));
2203
+ if (active.length === 1)
2204
+ return active[0];
2205
+ const abortSignal = AbortSignal;
2206
+ if (typeof abortSignal.any === "function")
2207
+ return abortSignal.any(active);
2208
+ const controller = new AbortController;
2209
+ const abort = (signal) => {
2210
+ if (!controller.signal.aborted)
2211
+ controller.abort(signal.reason);
2212
+ };
2213
+ for (const signal of active) {
2214
+ if (signal.aborted) {
2215
+ abort(signal);
2216
+ break;
2217
+ }
2218
+ signal.addEventListener("abort", () => abort(signal), { once: true });
2219
+ }
2220
+ return controller.signal;
2221
+ }
2222
+ function isStreamingRequest(options) {
2223
+ if (options.body instanceof ReadableStream)
2224
+ return true;
2225
+ const headers = new Headers(options.headers);
2226
+ const accept = headers.get("accept")?.toLowerCase() ?? "";
2227
+ const contentType = headers.get("content-type")?.toLowerCase() ?? "";
2228
+ return accept.includes("text/event-stream") || contentType.includes("text/event-stream");
2229
+ }
2230
+ function normalizeHttpFailure(response, providerId, retryable) {
2231
+ return {
2232
+ code: "5xx",
2233
+ status: response.status,
2234
+ providerId,
2235
+ retryable,
2236
+ cause: { statusText: response.statusText }
2237
+ };
2238
+ }
2239
+ function normalizeThrownFailure(err, providerId, retryable, timeoutKind) {
2240
+ if (timeoutKind) {
2241
+ return {
2242
+ code: "aborted-due-to-timeout",
2243
+ status: 504,
2244
+ providerId,
2245
+ retryable,
2246
+ cause: { timeoutKind, error: serializeCause(err) }
2247
+ };
2248
+ }
2249
+ if (isAbortError(err)) {
2250
+ return {
2251
+ code: "aborted",
2252
+ status: 499,
2253
+ providerId,
2254
+ retryable: false,
2255
+ cause: serializeCause(err)
2256
+ };
2257
+ }
2258
+ return {
2259
+ code: "network",
2260
+ status: 503,
2261
+ providerId,
2262
+ retryable,
2263
+ cause: serializeCause(err)
2264
+ };
2265
+ }
2266
+ function normalizeShortCircuit(providerId) {
2267
+ return {
2268
+ code: "short-circuit",
2269
+ status: 503,
2270
+ providerId,
2271
+ retryable: false,
2272
+ cause: "circuit breaker open"
2273
+ };
2274
+ }
2275
+ function isAbortError(err) {
2276
+ return err instanceof DOMException && err.name === "AbortError";
2277
+ }
2278
+ function logFailure(failure, attempt, retrying) {
2279
+ logger4.error("upstream failure", {
2280
+ event: "upstream.error",
2281
+ ...withoutCause(failure),
2282
+ cause: failure.cause,
2283
+ attempt,
2284
+ max_retries: MAX_RETRIES,
2285
+ retrying
2286
+ });
2287
+ }
2288
+ function withoutCause(failure) {
2289
+ const { cause: _cause, ...rest } = failure;
2290
+ return rest;
2291
+ }
2292
+ function normalizedResponse(failure) {
2293
+ return new Response(JSON.stringify({ error: { ...withoutCause(failure), cause: failure.cause } }), {
2294
+ status: failure.status,
2295
+ headers: { "content-type": "application/json" }
2296
+ });
2297
+ }
2298
+ function serializeCause(err) {
2299
+ if (err instanceof Error) {
2300
+ return { name: err.name, message: err.message };
2301
+ }
2302
+ return err;
2303
+ }
2304
+ async function discardResponse(response) {
2305
+ try {
2306
+ await response.body?.cancel();
2307
+ } catch {}
2308
+ }
2309
+ })(UpstreamClient ||= {});
2310
+ });
2311
+
2312
+ // src/cliproxy/quota.ts
2313
+ import { readdir, readFile } from "fs/promises";
2314
+ import { join as join3 } from "path";
2315
+ function normalizePercent(value) {
2316
+ if (typeof value !== "number" || !Number.isFinite(value))
2317
+ return;
2318
+ const pct = value <= 1 ? value * 100 : value;
2319
+ return Math.max(0, Math.min(100, pct));
2320
+ }
2321
+ function normalizeReset(value) {
2322
+ if (typeof value === "number" && Number.isFinite(value)) {
2323
+ const ms = value < 1000000000000 ? value * 1000 : value;
2324
+ return new Date(ms).toISOString();
2325
+ }
2326
+ if (typeof value === "string" && value.trim()) {
2327
+ const parsed = Date.parse(value);
2328
+ if (Number.isFinite(parsed))
2329
+ return new Date(parsed).toISOString();
2330
+ }
2331
+ return;
2332
+ }
2333
+ function quotaTypeFromSeconds(seconds, fallback) {
2334
+ if (typeof seconds !== "number" || !Number.isFinite(seconds))
2335
+ return fallback;
2336
+ const hours = Math.round(seconds / 3600);
2337
+ if (hours >= 24 * 6)
2338
+ return "week";
2339
+ if (hours >= 24)
2340
+ return `${Math.round(hours / 24)}d`;
2341
+ return `${hours}h`;
2342
+ }
2343
+ async function fetchJson(url, init) {
2344
+ const controller = new AbortController;
2345
+ const timer = setTimeout(() => controller.abort(), Config2.quotaRefreshTimeoutMs);
2346
+ try {
2347
+ const res = await UpstreamClient.fetch({
2348
+ method: init.method ?? "GET",
2349
+ url,
2350
+ headers: init.headers,
2351
+ body: init.body ?? null,
2352
+ providerId: `quota:${new URL(url).hostname}`,
2353
+ idempotent: (init.method ?? "GET") === "GET" || (init.method ?? "GET") === "HEAD",
2354
+ signal: controller.signal
2355
+ });
2356
+ const text = await res.text();
2357
+ let data = null;
2358
+ try {
2359
+ data = text ? JSON.parse(text) : null;
2360
+ } catch {
2361
+ data = null;
2362
+ }
2363
+ return { ok: res.ok, status: res.status, data, text };
2364
+ } finally {
2365
+ clearTimeout(timer);
2366
+ }
2367
+ }
2368
+ function errorMessage(data, fallback) {
2369
+ if (data && typeof data === "object" && "error" in data) {
2370
+ const err = data.error;
2371
+ if (err && typeof err === "object" && "message" in err) {
2372
+ const message = err.message;
2373
+ if (typeof message === "string" && message.trim())
2374
+ return message;
2375
+ }
2376
+ if (typeof err === "string" && err.trim())
2377
+ return err;
2378
+ }
2379
+ return fallback;
2380
+ }
2381
+ async function probeClaude(auth) {
2382
+ const account = auth.email ?? "claude";
2383
+ if (!auth.access_token) {
2384
+ return {
2385
+ provider: "claude",
2386
+ account,
2387
+ status: "error",
2388
+ unavailable: true,
2389
+ disabled: auth.disabled === true,
2390
+ error: "missing access_token",
2391
+ windows: []
2392
+ };
2393
+ }
2394
+ const res = await fetchJson("https://api.anthropic.com/api/oauth/usage", {
2395
+ method: "GET",
2396
+ headers: {
2397
+ Authorization: `Bearer ${auth.access_token}`,
2398
+ Accept: "application/json",
2399
+ "anthropic-version": "2023-06-01",
2400
+ "anthropic-beta": "oauth-2025-04-20",
2401
+ "User-Agent": "agent-cli-proxy"
2402
+ }
2403
+ });
2404
+ if (!res.ok) {
2405
+ return {
2406
+ provider: "claude",
2407
+ account,
2408
+ status: "error",
2409
+ unavailable: true,
2410
+ disabled: auth.disabled === true,
2411
+ error: errorMessage(res.data, `HTTP ${res.status}`),
2412
+ windows: []
2413
+ };
2414
+ }
2415
+ const data = res.data;
2416
+ const windows = [];
2417
+ for (const [quotaType, window] of [
2418
+ ["5h", data.five_hour],
2419
+ ["week", data.seven_day],
2420
+ ["week_sonnet", data.seven_day_sonnet],
2421
+ ["week_opus", data.seven_day_opus]
2422
+ ]) {
2423
+ if (!window)
2424
+ continue;
2425
+ const used = normalizePercent(window.utilization);
2426
+ if (used === undefined && !window.resets_at)
2427
+ continue;
2428
+ windows.push({
2429
+ quota_type: quotaType,
2430
+ used_pct: used,
2431
+ resets_at: normalizeReset(window.resets_at),
2432
+ raw: window
2433
+ });
2434
+ }
2435
+ return {
2436
+ provider: "claude",
2437
+ account,
2438
+ status: "active",
2439
+ unavailable: false,
2440
+ disabled: auth.disabled === true,
2441
+ windows
2442
+ };
2443
+ }
2444
+ async function probeCodex(auth) {
2445
+ const account = auth.email ?? "codex";
2446
+ if (!auth.access_token) {
2447
+ return {
2448
+ provider: "codex",
2449
+ account,
2450
+ status: "error",
2451
+ unavailable: true,
2452
+ disabled: auth.disabled === true,
2453
+ error: "missing access_token",
2454
+ windows: []
2455
+ };
2456
+ }
2457
+ const headers = {
2458
+ Authorization: `Bearer ${auth.access_token}`,
2459
+ Accept: "application/json",
2460
+ "User-Agent": "codex_cli_rs/0.101.0 (Linux; x86_64) agent-cli-proxy"
2461
+ };
2462
+ if (auth.account_id)
2463
+ headers["ChatGPT-Account-Id"] = auth.account_id;
2464
+ const res = await fetchJson("https://chatgpt.com/backend-api/wham/usage", {
2465
+ method: "GET",
2466
+ headers
2467
+ });
2468
+ if (!res.ok) {
2469
+ const data2 = res.data;
2470
+ const resetsAt = normalizeReset(data2?.error?.resets_at);
2471
+ const usedWindow = resetsAt ? [
2472
+ {
2473
+ quota_type: "exhausted",
2474
+ used_pct: 100,
2475
+ resets_at: resetsAt,
2476
+ raw: data2
2477
+ }
2478
+ ] : [];
2479
+ return {
2480
+ provider: "codex",
2481
+ account,
2482
+ status: data2?.error?.type ?? "error",
2483
+ unavailable: true,
2484
+ disabled: auth.disabled === true,
2485
+ plan: data2?.error?.plan_type,
2486
+ error: data2?.error?.message ?? `HTTP ${res.status}`,
2487
+ windows: usedWindow
2488
+ };
2489
+ }
2490
+ const data = res.data;
2491
+ const windows = [];
2492
+ for (const [fallback, window] of [
2493
+ ["5h", data.rate_limit?.primary_window],
2494
+ ["week", data.rate_limit?.secondary_window]
2495
+ ]) {
2496
+ if (!window)
2497
+ continue;
2498
+ const used = normalizePercent(window.used_percent);
2499
+ const reset = normalizeReset(window.reset_at);
2500
+ if (used === undefined && !reset)
2501
+ continue;
2502
+ windows.push({
2503
+ quota_type: quotaTypeFromSeconds(window.limit_window_seconds, fallback),
2504
+ used_pct: used,
2505
+ resets_at: reset,
2506
+ raw: window
2507
+ });
2508
+ }
2509
+ let plan = data.plan_type;
2510
+ if (data.credits?.balance !== undefined && data.credits.balance !== null) {
2511
+ plan = plan ? `${plan}` : undefined;
2512
+ }
2513
+ return {
2514
+ provider: "codex",
2515
+ account,
2516
+ status: data.rate_limit?.limit_reached ? "limit_reached" : "active",
2517
+ unavailable: data.rate_limit?.limit_reached === true,
2518
+ disabled: auth.disabled === true,
2519
+ plan,
2520
+ windows
2521
+ };
2522
+ }
2523
+ function readNumber(value) {
2524
+ if (typeof value === "number" && Number.isFinite(value))
2525
+ return value;
2526
+ if (typeof value === "string" && value.trim()) {
2527
+ const parsed = Number(value);
2528
+ if (Number.isFinite(parsed))
2529
+ return parsed;
2530
+ }
2531
+ return;
2532
+ }
2533
+ function kimiWindow(quotaType, detail, raw) {
2534
+ if (!detail)
2535
+ return null;
2536
+ const limit = readNumber(detail.limit);
2537
+ const remaining = readNumber(detail.remaining);
2538
+ const used = readNumber(detail.used) ?? (limit !== undefined && remaining !== undefined ? limit - remaining : undefined);
2539
+ const usedPct = limit && used !== undefined ? used / limit * 100 : undefined;
2540
+ const reset = normalizeReset(detail.resetTime);
2541
+ if (usedPct === undefined && remaining === undefined && !reset)
2542
+ return null;
2543
+ return {
2544
+ quota_type: quotaType,
2545
+ used_pct: normalizePercent(usedPct),
2546
+ resets_at: reset,
2547
+ raw
2548
+ };
2549
+ }
2550
+ async function probeKimi(auth) {
2551
+ const account = "kimi";
2552
+ if (!auth.access_token) {
2553
+ return {
2554
+ provider: "kimi",
2555
+ account,
2556
+ status: "error",
2557
+ unavailable: true,
2558
+ disabled: auth.disabled === true,
2559
+ error: "missing access_token",
2560
+ windows: []
2561
+ };
2562
+ }
2563
+ const headers = {
2564
+ Authorization: `Bearer ${auth.access_token}`,
2565
+ Accept: "application/json",
2566
+ "User-Agent": "KimiCLI/1.35 agent-cli-proxy"
2567
+ };
2568
+ let res = await fetchJson("https://api.kimi.com/coding/v1/usages", {
2569
+ method: "GET",
2570
+ headers
2571
+ });
2572
+ if (!res.ok) {
2573
+ res = await fetchJson("https://api.moonshot.ai/v1/usages", {
2574
+ method: "GET",
2575
+ headers
2576
+ });
2577
+ }
2578
+ if (!res.ok) {
2579
+ return {
2580
+ provider: "kimi",
2581
+ account,
2582
+ status: "error",
2583
+ unavailable: true,
2584
+ disabled: auth.disabled === true,
2585
+ error: errorMessage(res.data, `HTTP ${res.status}`),
2586
+ windows: []
2587
+ };
2588
+ }
2589
+ const data = res.data;
2590
+ const coding = data.usages?.find((u) => u.scope === "FEATURE_CODING") ?? data.usages?.[0];
2591
+ const windows = [];
2592
+ const weekly = kimiWindow("week", coding?.detail ?? data.usage, coding?.detail ?? data.usage);
2593
+ if (weekly)
2594
+ windows.push(weekly);
2595
+ for (const limit of coding?.limits ?? data.limits ?? []) {
2596
+ const duration = limit.window?.duration;
2597
+ const quotaType = duration === 300 ? "5h" : quotaTypeFromSeconds((duration ?? 0) * 60, "window");
2598
+ const window = kimiWindow(quotaType, limit.detail, limit);
2599
+ if (window)
2600
+ windows.push(window);
2601
+ }
2602
+ return {
2603
+ provider: "kimi",
2604
+ account,
2605
+ status: "active",
2606
+ unavailable: false,
2607
+ disabled: auth.disabled === true,
2608
+ windows
2609
+ };
2610
+ }
2611
+ function unsupported(auth) {
2612
+ const provider = auth.type ?? "unknown";
2613
+ return {
2614
+ provider,
2615
+ account: auth.email ?? provider,
2616
+ status: "unsupported",
2617
+ unavailable: false,
2618
+ disabled: auth.disabled === true,
2619
+ error: "quota endpoint is not known for this provider",
2620
+ windows: []
2621
+ };
2622
+ }
2623
+ async function readAuthFiles() {
2624
+ if (!Config2.cliproxyAuthDir)
2625
+ return [];
2626
+ const names = await readdir(Config2.cliproxyAuthDir);
2627
+ const out = [];
2628
+ for (const name of names) {
2629
+ if (!name.endsWith(".json"))
2630
+ continue;
2631
+ try {
2632
+ const raw = await readFile(join3(Config2.cliproxyAuthDir, name), "utf-8");
2633
+ const parsed = JSON.parse(raw);
2634
+ out.push(parsed);
2635
+ } catch (err) {
2636
+ logger4.warn("failed to read auth file", { err, name });
2637
+ }
2638
+ }
2639
+ return out;
2640
+ }
2641
+ var logger4, QuotaProbe;
2642
+ var init_quota = __esm(() => {
2643
+ init_config();
2644
+ init_client();
2645
+ init_logger();
2646
+ logger4 = Logger.fromConfig().child({ component: "quota" });
2647
+ ((QuotaProbe) => {
2648
+ async function refresh() {
2649
+ const timestamp = new Date().toISOString();
2650
+ const auths = await readAuthFiles();
2651
+ const accounts = [];
2652
+ for (const auth of auths) {
2653
+ let result;
2654
+ try {
2655
+ if (auth.type === "claude")
2656
+ result = await probeClaude(auth);
2657
+ else if (auth.type === "codex")
2658
+ result = await probeCodex(auth);
2659
+ else if (auth.type === "kimi")
2660
+ result = await probeKimi(auth);
2661
+ else
2662
+ result = unsupported(auth);
2663
+ } catch (err) {
2664
+ result = {
2665
+ provider: auth.type ?? "unknown",
2666
+ account: auth.email ?? auth.type ?? "unknown",
2667
+ status: "error",
2668
+ unavailable: true,
2669
+ disabled: auth.disabled === true,
2670
+ error: err instanceof Error ? err.message : String(err),
2671
+ windows: []
2672
+ };
2673
+ }
2674
+ const windows = result.windows.map((window) => ({
2675
+ timestamp,
2676
+ provider: result.provider,
2677
+ account: result.account,
2678
+ quota_type: window.quota_type,
2679
+ used_pct: window.used_pct ?? null,
2680
+ remaining: window.used_pct === undefined ? null : Math.max(0, 100 - window.used_pct),
2681
+ remaining_raw: window.used_pct === undefined ? null : `${Math.max(0, 100 - window.used_pct).toFixed(2)}%`,
2682
+ resets_at: window.resets_at ?? null,
2683
+ raw_json: JSON.stringify(window.raw)
2684
+ }));
2685
+ accounts.push({
2686
+ provider: result.provider,
2687
+ account: result.account,
2688
+ status: result.status,
2689
+ unavailable: result.unavailable,
2690
+ disabled: result.disabled,
2691
+ plan: result.plan,
2692
+ refreshed_at: timestamp,
2693
+ error: result.error,
2694
+ windows,
2695
+ local_usage: {
2696
+ five_hour: { since: "", requests: 0, total_tokens: 0, cost_usd: 0 },
2697
+ seven_day: { since: "", requests: 0, total_tokens: 0, cost_usd: 0 }
2698
+ }
2699
+ });
2700
+ }
2701
+ return { timestamp, accounts, inserted: 0 };
2702
+ }
2703
+ QuotaProbe.refresh = refresh;
2704
+ })(QuotaProbe ||= {});
2705
+ });
2706
+
2707
+ // src/storage/service.ts
2708
+ var exports_service = {};
2709
+ __export(exports_service, {
2710
+ UsageService: () => UsageService
2711
+ });
2712
+ import { readdir as readdir2 } from "fs/promises";
2713
+ var logger5, costBackfillLogger, unmappedSubscriptionWarnings, UsageService;
2714
+ var init_service = __esm(() => {
2715
+ init_account_subscriptions();
2716
+ init_repo();
2717
+ init_pricing();
2718
+ init_cost();
2719
+ init_quota();
2720
+ init_logger();
2721
+ init_config();
2722
+ init_supervisor();
2723
+ logger5 = Logger.fromConfig().child({ component: "usage-service" });
2724
+ costBackfillLogger = Logger.fromConfig().child({ component: "cost" });
2725
+ unmappedSubscriptionWarnings = new Map;
2726
+ ((UsageService) => {
2727
+ function create(db, options = {}) {
2728
+ const serviceLogger = options.logger ?? logger5;
2729
+ const now = options.now ?? (() => new Date);
2730
+ function preLog(log) {
2731
+ return RequestRepo.insert(db, log);
2732
+ }
2733
+ async function finalizeUsage(id, log) {
2734
+ const cost = computeCost(log);
2735
+ const logWithCost = { ...log, cost_usd: cost.cost_usd, cost_status: cost.cost_status };
2736
+ const txn = db.transaction(() => {
2737
+ const updated = RequestRepo.updateFinalize(db, id, {
2738
+ provider: logWithCost.provider,
2739
+ model: logWithCost.model,
2740
+ actual_model: logWithCost.actual_model,
2741
+ streamed: logWithCost.streamed,
2742
+ status: logWithCost.status,
2743
+ prompt_tokens: logWithCost.prompt_tokens,
2744
+ completion_tokens: logWithCost.completion_tokens,
2745
+ cache_creation_tokens: logWithCost.cache_creation_tokens,
2746
+ cache_read_tokens: logWithCost.cache_read_tokens,
2747
+ reasoning_tokens: logWithCost.reasoning_tokens ?? 0,
2748
+ total_tokens: logWithCost.total_tokens,
2749
+ cost_usd: cost.cost_usd,
2750
+ incomplete: logWithCost.incomplete,
2751
+ error_code: logWithCost.error_code,
2752
+ latency_ms: logWithCost.latency_ms,
2753
+ finished_at: logWithCost.finished_at,
2754
+ lifecycle_status: logWithCost.lifecycle_status ?? "completed",
2755
+ finalized_at: logWithCost.finalized_at ?? logWithCost.finished_at ?? new Date().toISOString(),
2756
+ error_message: logWithCost.error_message,
2757
+ cost_status: cost.cost_status,
2758
+ subscription_code: logWithCost.subscription_code
2759
+ });
2760
+ if (updated === 0)
2761
+ return false;
2762
+ insertCostAudit(id, logWithCost, cost);
2763
+ const day = logWithCost.started_at.slice(0, 10);
2764
+ UsageRepo.upsertDaily(db, {
2765
+ day,
2766
+ provider: logWithCost.provider,
2767
+ model: logWithCost.model,
2768
+ request_count: 1,
2769
+ prompt_tokens: logWithCost.prompt_tokens,
2770
+ completion_tokens: logWithCost.completion_tokens,
2771
+ cache_creation_tokens: logWithCost.cache_creation_tokens,
2772
+ cache_read_tokens: logWithCost.cache_read_tokens,
2773
+ total_tokens: logWithCost.total_tokens,
2774
+ cost_usd: cost.cost_usd
2775
+ });
2776
+ return true;
2777
+ });
2778
+ return txn();
2779
+ }
2780
+ async function recordUsage(log) {
2781
+ const cost = computeCost(log);
2782
+ const logWithCost = { ...log, cost_usd: cost.cost_usd, cost_status: cost.cost_status };
2783
+ const txn = db.transaction(() => {
2784
+ const id = RequestRepo.insert(db, logWithCost);
2785
+ insertCostAudit(id, logWithCost, cost);
2786
+ const day = log.started_at.slice(0, 10);
2787
+ UsageRepo.upsertDaily(db, {
2788
+ day,
2789
+ provider: log.provider,
2790
+ model: log.model,
2791
+ request_count: 1,
2792
+ prompt_tokens: log.prompt_tokens,
2793
+ completion_tokens: log.completion_tokens,
2794
+ cache_creation_tokens: log.cache_creation_tokens,
2795
+ cache_read_tokens: log.cache_read_tokens,
2796
+ total_tokens: log.total_tokens,
2797
+ cost_usd: cost.cost_usd
2798
+ });
2799
+ return id;
2800
+ });
2801
+ return txn();
2802
+ }
2803
+ async function backfillCosts(options2 = {}) {
2804
+ try {
2805
+ await Pricing.fetchPricing({ force: true });
2806
+ } catch (err) {
2807
+ logger5.warn("pricing refresh failed before cost backfill", { err, event: "cost.backfill_pricing_failed" });
2808
+ }
2809
+ const lookbackMs = options2.lookbackMs ?? Config2.costBackfillLookbackMs;
2810
+ const limitClause = options2.limit && options2.limit > 0 ? " LIMIT ?" : "";
2811
+ const sinceClause = options2.all ? "" : "AND started_at >= ?";
2812
+ const sinceIso = new Date(Date.now() - lookbackMs).toISOString();
2813
+ const params = [];
2814
+ if (!options2.all)
2815
+ params.push(sinceIso);
2816
+ if (options2.limit && options2.limit > 0)
2817
+ params.push(options2.limit);
2818
+ const rows = db.query(`
2819
+ SELECT id, provider, model, prompt_tokens, completion_tokens,
2820
+ cache_creation_tokens, cache_read_tokens, reasoning_tokens, cost_usd, cost_status
2821
+ FROM request_logs
2822
+ WHERE lifecycle_status IN ('completed', 'error')
2823
+ AND cost_status IN ('pending', 'unresolved')
2824
+ ${sinceClause}
2825
+ ORDER BY id ASC${limitClause}
2826
+ `).all(...params);
2827
+ let updated = 0;
2828
+ const statusCounts = { ok: 0, pending: 0, unsupported: 0 };
2829
+ const updateTxn = db.transaction(() => {
2830
+ const updateLog = db.prepare("UPDATE request_logs SET cost_usd = ?, cost_status = 'ok' WHERE id = ? AND cost_status IN ('pending', 'unresolved')");
2831
+ for (const row of rows) {
2832
+ const cost = computeCost(row);
2833
+ statusCounts[cost.cost_status] += 1;
2834
+ insertCostAudit(row.id, row, cost);
2835
+ if ((row.cost_status === "pending" || row.cost_status === "unresolved") && cost.cost_status === "ok") {
2836
+ const result = updateLog.run(cost.cost_usd, row.id);
2837
+ updated += result.changes;
2838
+ }
2839
+ }
2840
+ db.exec(`
2841
+ DELETE FROM daily_usage;
2842
+ INSERT INTO daily_usage (
2843
+ day, provider, model, request_count, prompt_tokens,
2844
+ completion_tokens, cache_creation_tokens, cache_read_tokens,
2845
+ total_tokens, cost_usd
2846
+ )
2847
+ SELECT
2848
+ substr(started_at, 1, 10), provider, model, COUNT(*),
2849
+ SUM(prompt_tokens), SUM(completion_tokens), SUM(cache_creation_tokens),
2850
+ SUM(cache_read_tokens), SUM(total_tokens), SUM(cost_usd)
2851
+ FROM request_logs
2852
+ GROUP BY substr(started_at, 1, 10), provider, model;
2853
+ `);
2854
+ });
2855
+ updateTxn();
2856
+ return { scanned: rows.length, updated, ...statusCounts };
2857
+ }
2858
+ function computeCost(log) {
2859
+ return Cost.compute(Cost.inputsFromLog(log));
2860
+ }
2861
+ function insertCostAudit(requestLogId, log, cost) {
2862
+ RequestRepo.insertCostAudit(db, {
2863
+ request_log_id: requestLogId,
2864
+ model: log.model,
2865
+ provider: log.provider,
2866
+ source: cost.source,
2867
+ base_cost_usd: cost.cost_usd
2868
+ });
2869
+ }
2870
+ function getToday() {
2871
+ const today = new Date().toISOString().slice(0, 10);
2872
+ const breakdown = UsageRepo.getDaily(db, today);
2873
+ const totals = breakdown.reduce((acc, row) => ({
2874
+ requests: acc.requests + row.request_count,
2875
+ total_tokens: acc.total_tokens + row.total_tokens,
2876
+ cost_usd: acc.cost_usd + row.cost_usd
2877
+ }), { requests: 0, total_tokens: 0, cost_usd: 0 });
2878
+ return {
2879
+ date: today,
2880
+ requests: totals.requests,
2881
+ total_tokens: totals.total_tokens,
2882
+ cost_usd: totals.cost_usd,
2883
+ breakdown
2884
+ };
2885
+ }
2886
+ function getDateRange(from, to) {
2887
+ const rows = UsageRepo.getRange(db, from, to);
2888
+ const byDay = new Map;
2889
+ for (const row of rows) {
2890
+ const existing = byDay.get(row.day) ?? [];
2891
+ existing.push(row);
2892
+ byDay.set(row.day, existing);
2893
+ }
2894
+ return Array.from(byDay.entries()).map(([day, breakdown]) => {
2895
+ const totals = breakdown.reduce((acc, row) => ({
2896
+ requests: acc.requests + row.request_count,
2897
+ total_tokens: acc.total_tokens + row.total_tokens,
2898
+ cost_usd: acc.cost_usd + row.cost_usd
2899
+ }), { requests: 0, total_tokens: 0, cost_usd: 0 });
2900
+ return { date: day, ...totals, breakdown };
2901
+ });
2902
+ }
2903
+ function getModelBreakdown(day) {
2904
+ return UsageRepo.getDaily(db, day);
2905
+ }
2906
+ function getProviderBreakdown(day) {
2907
+ const rows = UsageRepo.getDaily(db, day);
2908
+ const byProvider = new Map;
2909
+ for (const row of rows) {
2910
+ const existing = byProvider.get(row.provider) ?? {
2911
+ provider: row.provider,
2912
+ request_count: 0,
2913
+ total_tokens: 0,
2914
+ cost_usd: 0
2915
+ };
2916
+ existing.request_count += row.request_count;
2917
+ existing.total_tokens += row.total_tokens;
2918
+ existing.cost_usd += row.cost_usd;
2919
+ byProvider.set(row.provider, existing);
2920
+ }
2921
+ return Array.from(byProvider.values());
2922
+ }
2923
+ function getTotalStats() {
2924
+ const stmt = db.prepare(`
2925
+ SELECT
2926
+ COUNT(*) as total_requests,
2927
+ SUM(total_tokens) as total_tokens,
2928
+ SUM(cost_usd) as total_cost_usd,
2929
+ MIN(started_at) as first_request_at,
2930
+ MAX(started_at) as last_request_at
2931
+ FROM request_logs
2932
+ `);
2933
+ const row = stmt.get();
2934
+ return {
2935
+ total_requests: Number(row.total_requests ?? 0),
2936
+ total_tokens: Number(row.total_tokens ?? 0),
2937
+ total_cost_usd: Number(row.total_cost_usd ?? 0),
2938
+ first_request_at: row.first_request_at ?? null,
2939
+ last_request_at: row.last_request_at ?? null
2940
+ };
2941
+ }
2942
+ function getRecentLogs(limit, offset, tool, clientId) {
2943
+ return RequestRepo.getRecent(db, limit, offset, tool, clientId);
2944
+ }
2945
+ function getLogById(id) {
2946
+ return RequestRepo.getById(db, id);
2947
+ }
2948
+ function getUncorrelatedLogs(sinceMs, limit) {
2949
+ return RequestRepo.getUncorrelated(db, sinceMs, limit);
2950
+ }
2951
+ function applyCorrelation(id, log, fields) {
2952
+ const txn = db.transaction(() => {
2953
+ RequestRepo.applyCorrelation(db, id, fields);
2954
+ if (fields.cliproxy_account) {
2955
+ applySubscriptionAttribution(db, id, fields.cliproxy_account, serviceLogger, now);
2956
+ UsageRepo.upsertDailyAccount(db, {
2957
+ day: log.started_at.slice(0, 10),
2958
+ provider: log.provider,
2959
+ model: log.model,
2960
+ cliproxy_account: fields.cliproxy_account,
2961
+ cliproxy_auth_index: fields.cliproxy_auth_index,
2962
+ request_count: 1,
2963
+ prompt_tokens: log.prompt_tokens,
2964
+ completion_tokens: log.completion_tokens,
2965
+ cache_creation_tokens: log.cache_creation_tokens,
2966
+ cache_read_tokens: log.cache_read_tokens,
2967
+ reasoning_tokens: fields.reasoning_tokens ?? 0,
2968
+ total_tokens: log.total_tokens,
2969
+ cost_usd: log.cost_usd
2970
+ });
2971
+ }
2972
+ });
2973
+ txn();
2974
+ }
2975
+ function applySubscriptionAttribution(database, requestLogId, cliproxyAccount, targetLogger, currentDate) {
2976
+ const binding = AccountSubscriptionRepo.get(database, cliproxyAccount);
2977
+ if (binding) {
2978
+ RequestRepo.applySubscription(database, requestLogId, binding.subscription_code);
2979
+ return;
2980
+ }
2981
+ warnUnmappedSubscription(targetLogger, cliproxyAccount, currentDate());
2982
+ }
2983
+ function getAccountSummary(from, to) {
2984
+ return UsageRepo.getAccountSummary(db, from, to);
2985
+ }
2986
+ function getAccountDaily(day) {
2987
+ return UsageRepo.getDailyByAccount(db, day);
2988
+ }
2989
+ function getAccountRange(from, to) {
2990
+ return UsageRepo.getAccountRange(db, from, to);
2991
+ }
2992
+ function withLocalUsage(report) {
2993
+ const now2 = Date.now();
2994
+ const fiveHourSince = new Date(now2 - 5 * 60 * 60 * 1000).toISOString();
2995
+ const sevenDaySince = new Date(now2 - 7 * 24 * 60 * 60 * 1000).toISOString();
2996
+ const localProvider = report.provider === "claude" ? "anthropic" : "openai";
2997
+ return {
2998
+ ...report,
2999
+ local_usage: {
3000
+ five_hour: QuotaRepo.getLocalWindowUsage(db, localProvider, report.account, fiveHourSince),
3001
+ seven_day: QuotaRepo.getLocalWindowUsage(db, localProvider, report.account, sevenDaySince)
3002
+ }
3003
+ };
3004
+ }
3005
+ async function refreshQuotas() {
3006
+ const result = await QuotaProbe.refresh();
3007
+ let inserted = 0;
3008
+ const accounts = result.accounts.map(withLocalUsage);
3009
+ const txn = db.transaction(() => {
3010
+ for (const account of accounts) {
3011
+ for (const snapshot of account.windows) {
3012
+ QuotaRepo.insertSnapshot(db, snapshot);
3013
+ inserted += 1;
3014
+ }
3015
+ }
3016
+ });
3017
+ txn();
3018
+ return { ...result, inserted, accounts };
3019
+ }
3020
+ async function startQuotaRefresh(options2 = {}) {
3021
+ if (!Config2.cliproxyAuthDir) {
3022
+ logger5.info("quota background refresh skipped", {
3023
+ event: "quota.refresh_skipped",
3024
+ reason: "missing_auth_dir"
3025
+ });
3026
+ return null;
3027
+ }
3028
+ let authFileNames;
3029
+ try {
3030
+ authFileNames = await readdir2(Config2.cliproxyAuthDir);
3031
+ } catch (err) {
3032
+ logger5.warn("quota background refresh skipped", {
3033
+ event: "quota.refresh_skipped",
3034
+ reason: "auth_dir_unreadable",
3035
+ err,
3036
+ path: Config2.cliproxyAuthDir
3037
+ });
3038
+ return null;
3039
+ }
3040
+ if (!authFileNames.some((name) => name.endsWith(".json"))) {
3041
+ logger5.info("quota background refresh skipped", {
3042
+ event: "quota.refresh_skipped",
3043
+ reason: "no_auth_files",
3044
+ path: Config2.cliproxyAuthDir
3045
+ });
3046
+ return null;
3047
+ }
3048
+ return Supervisor.run("quota-refresh", async () => {
3049
+ await refreshQuotas();
3050
+ }, {
3051
+ intervalMs: options2.intervalMs ?? Config2.quotaRefreshIntervalMs,
3052
+ signal: options2.signal
3053
+ });
3054
+ }
3055
+ function getLatestQuotas() {
3056
+ return QuotaRepo.getLatest(db);
3057
+ }
3058
+ return {
3059
+ db,
3060
+ preLog,
3061
+ finalizeUsage,
3062
+ recordUsage,
3063
+ getToday,
3064
+ getDateRange,
3065
+ getModelBreakdown,
3066
+ getProviderBreakdown,
3067
+ getTotalStats,
3068
+ getRecentLogs,
3069
+ getLogById,
3070
+ backfillCosts,
3071
+ getUncorrelatedLogs,
3072
+ applyCorrelation,
3073
+ getAccountSummary,
3074
+ getAccountDaily,
3075
+ getAccountRange,
3076
+ refreshQuotas,
3077
+ startQuotaRefresh,
3078
+ getLatestQuotas
3079
+ };
3080
+ }
3081
+ UsageService.create = create;
3082
+ function warnUnmappedSubscription(targetLogger, cliproxyAccount, date = new Date) {
3083
+ const day = date.toISOString().slice(0, 10);
3084
+ const key = `${cliproxyAccount}:${day}`;
3085
+ if (unmappedSubscriptionWarnings.has(key))
3086
+ return;
3087
+ unmappedSubscriptionWarnings.set(key, true);
3088
+ targetLogger.warn("plans unmapped", {
3089
+ event: "plans.unmapped",
3090
+ cliproxy_account: cliproxyAccount
3091
+ });
3092
+ }
3093
+ UsageService.warnUnmappedSubscription = warnUnmappedSubscription;
3094
+ function startCostBackfillLoop(service, options = {}) {
3095
+ return Supervisor.run("cost-backfill", async () => {
3096
+ const result = await service.backfillCosts();
3097
+ costBackfillLogger.info("cost backfill completed", { event: "cost.backfill", ...result });
3098
+ }, {
3099
+ intervalMs: options.intervalMs ?? Config2.costBackfillIntervalMs,
3100
+ signal: options.signal
3101
+ });
3102
+ }
3103
+ UsageService.startCostBackfillLoop = startCostBackfillLoop;
3104
+ })(UsageService ||= {});
3105
+ });
3106
+
3107
+ // src/provider/registry.ts
3108
+ var exports_registry = {};
3109
+ __export(exports_registry, {
3110
+ ProviderRegistry: () => ProviderRegistry
3111
+ });
3112
+ import { readFileSync as readFileSync4 } from "fs";
3113
+ var ProviderRegistry;
3114
+ var init_registry = __esm(() => {
3115
+ init_config();
3116
+ init_logger();
3117
+ init_registry_schema();
3118
+ ((ProviderRegistry) => {
3119
+ const logger6 = Logger.fromConfig().child({ component: "provider-registry" });
3120
+ let cache = null;
3121
+ function loadProviders(options = {}) {
3122
+ if (cache && !options.force)
3123
+ return cache.providers;
3124
+ const customSource = readCustomConfig();
3125
+ const customProviders = customSource ? parseCustomProviders(customSource.raw) : [];
3126
+ const providers = mergeProviders([...builtInProviders(), ...customProviders]);
3127
+ const lastLoadedAt = new Date().toISOString();
3128
+ cache = {
3129
+ providers,
3130
+ source: customSource?.source ?? "built-in",
3131
+ configPath: customSource?.configPath,
3132
+ lastLoadedAt
3133
+ };
3134
+ return providers;
3135
+ }
3136
+ ProviderRegistry.loadProviders = loadProviders;
3137
+ function all() {
3138
+ return loadProviders();
3139
+ }
3140
+ ProviderRegistry.all = all;
3141
+ function handlesPath(path) {
3142
+ return loadProviders().some((provider) => provider.paths.includes(path));
3143
+ }
3144
+ ProviderRegistry.handlesPath = handlesPath;
3145
+ function resolve(input) {
3146
+ const providers = loadProviders();
3147
+ if (input.provider) {
3148
+ const explicit = providers.find((provider) => provider.id === input.provider);
3149
+ if (!explicit || !matchesPath(explicit, input.path) || !matchesModel(explicit, input.model))
3150
+ return null;
3151
+ return explicit;
3152
+ }
3153
+ const candidates = providers.filter((provider) => matchesPath(provider, input.path));
3154
+ if (input.model) {
3155
+ const modelMatch = candidates.find((provider) => matchesModel(provider, input.model));
3156
+ if (modelMatch)
3157
+ return modelMatch;
3158
+ }
3159
+ return candidates[0] ?? null;
3160
+ }
3161
+ ProviderRegistry.resolve = resolve;
3162
+ function forceReload() {
3163
+ return loadProviders({ force: true });
3164
+ }
3165
+ ProviderRegistry.forceReload = forceReload;
3166
+ function configPath() {
3167
+ return process.env.PROVIDERS_CONFIG_PATH?.trim() || undefined;
3168
+ }
3169
+ ProviderRegistry.configPath = configPath;
3170
+ function sourceInfo() {
3171
+ if (!cache) {
3172
+ return { source: "built-in", lastLoadedAt: null };
3173
+ }
3174
+ return {
3175
+ source: cache.source,
3176
+ configPath: cache.configPath,
3177
+ lastLoadedAt: cache.lastLoadedAt
3178
+ };
3179
+ }
3180
+ ProviderRegistry.sourceInfo = sourceInfo;
3181
+ function builtInProviders() {
3182
+ return [
3183
+ {
3184
+ id: "anthropic",
3185
+ type: "anthropic",
3186
+ paths: ["/v1/messages"],
3187
+ upstreamBaseUrl: Config2.cliProxyApiUrl,
3188
+ upstreamPath: "/v1/messages",
3189
+ auth: "preserve"
3190
+ },
3191
+ {
3192
+ id: "openai",
3193
+ type: "openai-compatible",
3194
+ paths: ["/v1/chat/completions"],
3195
+ upstreamBaseUrl: Config2.cliProxyApiUrl,
3196
+ upstreamPath: "/v1/chat/completions",
3197
+ auth: "preserve"
3198
+ }
3199
+ ];
3200
+ }
3201
+ function readCustomConfig() {
3202
+ const inline = process.env.PROVIDERS_JSON;
3203
+ if (inline !== undefined && inline.trim() !== "")
3204
+ return { source: "PROVIDERS_JSON", raw: inline };
3205
+ const path = configPath();
3206
+ if (!path)
3207
+ return null;
3208
+ try {
3209
+ return { source: "PROVIDERS_CONFIG_PATH", raw: readFileSync4(path, "utf-8"), configPath: path };
3210
+ } catch (err) {
3211
+ logger6.warn("provider config could not be read", {
3212
+ event: "provider.config.invalid",
3213
+ source: "PROVIDERS_CONFIG_PATH",
3214
+ path: "PROVIDERS_CONFIG_PATH",
3215
+ configPath: path,
3216
+ err
3217
+ });
3218
+ return null;
3219
+ }
3220
+ }
3221
+ function parseCustomProviders(raw) {
3222
+ let parsed;
3223
+ try {
3224
+ parsed = JSON.parse(raw);
3225
+ } catch (err) {
3226
+ logger6.warn("provider config JSON is invalid", {
3227
+ event: "provider.config.invalid",
3228
+ path: "providers",
3229
+ err
3230
+ });
3231
+ return [];
3232
+ }
3233
+ if (!isRecord2(parsed) || !Array.isArray(parsed.providers)) {
3234
+ const result = validateProviderDocument(parsed);
3235
+ logger6.warn("provider config document is invalid", {
3236
+ event: "provider.config.invalid",
3237
+ path: result.issues[0]?.path ?? "providers",
3238
+ issues: result.issues
3239
+ });
3240
+ return [];
3241
+ }
3242
+ const providers = [];
3243
+ parsed.providers.forEach((entry, index) => {
3244
+ const result = parseProviderInput(entry, `providers[${index}]`);
3245
+ if (result.provider) {
3246
+ providers.push(result.provider);
3247
+ return;
3248
+ }
3249
+ warnInvalidEntry(entry, `providers[${index}]`, result.issues);
3250
+ });
3251
+ return providers;
3252
+ }
3253
+ function warnInvalidEntry(entry, path, issues) {
3254
+ logger6.warn("provider config entry is invalid", {
3255
+ event: "provider.config.invalid",
3256
+ providerId: providerIdForLog(entry),
3257
+ path,
3258
+ issues
3259
+ });
3260
+ }
3261
+ function providerIdForLog(entry) {
3262
+ if (!isRecord2(entry) || typeof entry.id !== "string" || entry.id.trim() === "")
3263
+ return;
3264
+ return entry.id.trim();
3265
+ }
3266
+ function mergeProviders(providers) {
3267
+ const byId = new Map;
3268
+ for (const provider of providers)
3269
+ byId.set(provider.id, provider);
3270
+ return Array.from(byId.values());
3271
+ }
3272
+ function matchesPath(provider, path) {
3273
+ return provider.paths.includes(path);
3274
+ }
3275
+ function matchesModel(provider, model) {
3276
+ if (!model || !provider.models || provider.models.length === 0)
3277
+ return true;
3278
+ return provider.models.some((entry) => model === entry || model.startsWith(entry));
3279
+ }
3280
+ function isRecord2(value) {
3281
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3282
+ }
3283
+ })(ProviderRegistry ||= {});
3284
+ });
3285
+
3286
+ // src/cli.ts
3287
+ init_plans_default();
3288
+ init_plans();
3289
+ init_account_subscriptions();
3290
+ init_db();
3291
+ init_validate();
3292
+ init_logger();
3293
+ init_registry_schema();
3294
+ import { mkdir as mkdir2, chmod, copyFile, rm, cp, writeFile, open, rename, stat } from "fs/promises";
3295
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
3296
+ import { dirname as dirname2, join as join4, resolve } from "path";
3297
+ import { homedir, platform } from "os";
3298
+ import { createInterface } from "readline/promises";
3299
+ import { stdin as input, stdout as output } from "process";
3300
+ var APP_NAME = "agent-cli-proxy";
3301
+ var HOME = homedir();
3302
+ var XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME ?? join4(HOME, ".config");
3303
+ var XDG_DATA_HOME = process.env.XDG_DATA_HOME ?? join4(HOME, ".local", "share");
3304
+ var defaultConfigDir = join4(XDG_CONFIG_HOME, APP_NAME);
3305
+ var defaultDataDir = join4(XDG_DATA_HOME, APP_NAME);
3306
+ var defaultRuntimeDir = join4(XDG_DATA_HOME, APP_NAME, "runtime");
3307
+ var defaultEnvPath = join4(defaultConfigDir, ".env");
3308
+ var defaultDbPath = join4(defaultDataDir, "proxy.db");
3309
+ var defaultPricingCachePath = join4(defaultDataDir, "pricing-cache.json");
3310
+ var defaultPlansPath = join4(defaultConfigDir, "plans.json");
3311
+ var defaultProvidersPath = join4(defaultConfigDir, "providers.json");
3312
+ var packageRoot = resolve(dirname2(Bun.fileURLToPath(import.meta.url)), "..");
3313
+ var packagedDistDir = dirname2(Bun.fileURLToPath(import.meta.url));
3314
+ var logger6 = Logger.fromConfig().child({ component: "cli" });
3315
+ function writeOut(message = "") {
3316
+ process.stdout.write(`${message}
3317
+ `);
3318
+ }
3319
+ function writeErr(message = "") {
3320
+ process.stderr.write(`${message}
3321
+ `);
3322
+ }
3323
+ function parseArgs(argv) {
3324
+ const positional = [];
3325
+ const flags = new Map;
3326
+ for (let index = 0;index < argv.length; index += 1) {
3327
+ const arg = argv[index];
3328
+ if (!arg.startsWith("--")) {
3329
+ positional.push(arg);
3330
+ continue;
3331
+ }
3332
+ const eq = arg.indexOf("=");
3333
+ if (eq > 0) {
3334
+ const name = arg.slice(0, eq);
3335
+ const value = arg.slice(eq + 1);
3336
+ if (value.startsWith("--"))
3337
+ throw new Error(`${name} value must not start with --`);
3338
+ flags.set(name, value);
3339
+ continue;
3340
+ }
3341
+ const next = argv[index + 1];
3342
+ if (next !== undefined && !next.startsWith("--")) {
3343
+ flags.set(arg, next);
3344
+ index += 1;
3345
+ } else {
3346
+ flags.set(arg, true);
3347
+ }
3348
+ }
3349
+ return { positional, flags };
3350
+ }
3351
+ function getFlagValue(args, name) {
3352
+ const value = args.flags.get(name);
3353
+ if (value === undefined)
3354
+ return;
3355
+ if (value === true)
3356
+ throw new Error(`${name} requires a value`);
3357
+ if (value.startsWith("--"))
3358
+ throw new Error(`${name} value must not start with --`);
3359
+ return value;
3360
+ }
3361
+ function hasFlag(args, name) {
3362
+ return args.flags.has(name);
3363
+ }
3364
+ async function main() {
3365
+ const exitCode = await runCli(process.argv.slice(2));
3366
+ if (exitCode !== 0)
3367
+ process.exit(exitCode);
3368
+ }
3369
+ async function runCli(argv) {
3370
+ try {
3371
+ const args = parseArgs(argv);
3372
+ const [command = hasFlag(args, "--help") ? "help" : "help", subcommand = ""] = args.positional;
3373
+ const ctx = { args };
3374
+ switch (command) {
3375
+ case "init":
3376
+ await initCommand(ctx);
3377
+ return 0;
3378
+ case "doctor":
3379
+ return await doctorCommand(ctx);
3380
+ case "db":
3381
+ if (subcommand === "init") {
3382
+ await initDbCommand(ctx);
3383
+ return 0;
3384
+ }
3385
+ break;
3386
+ case "service":
3387
+ return await serviceCommand(ctx, subcommand);
3388
+ case "backfill-costs":
3389
+ await backfillCostsCommand(ctx);
3390
+ return 0;
3391
+ case "plans":
3392
+ await plansCommand(ctx, subcommand);
3393
+ return 0;
3394
+ case "providers":
3395
+ await providersCommand(ctx, subcommand);
3396
+ return 0;
3397
+ case "paths":
3398
+ printPaths();
3399
+ return 0;
3400
+ case "help":
3401
+ case "--help":
3402
+ case "-h":
3403
+ printHelp();
3404
+ return 0;
3405
+ }
3406
+ writeErr(`Unknown command: ${command}${subcommand ? ` ${subcommand}` : ""}`);
3407
+ printHelp();
3408
+ return 1;
3409
+ } catch (err) {
3410
+ if (err instanceof ConfigError || err instanceof Error && err.code === "CONFIG_INVALID") {
3411
+ logger6.error("configuration validation failed", { event: "config.error", err, issues: err.issues });
3412
+ writeErr(err instanceof Error ? err.message : String(err));
3413
+ return 1;
3414
+ }
3415
+ writeErr(err instanceof Error ? err.message : String(err));
3416
+ return 1;
3417
+ }
3418
+ }
3419
+ async function initCommand(ctx) {
3420
+ if (hasFlag(ctx.args, "--non-interactive")) {
3421
+ await initNonInteractive(ctx);
3422
+ return;
3423
+ }
3424
+ await initInteractive(ctx);
3425
+ }
3426
+ async function initInteractive(ctx) {
3427
+ const rl = createInterface({ input, output });
3428
+ try {
3429
+ writeOut(`agent-cli-proxy installer
3430
+ `);
3431
+ const envPath = await ask(rl, "Config .env path", getFlagValue(ctx.args, "--env") ?? defaultEnvPath);
3432
+ const dataDir = await ask(rl, "Data directory", getFlagValue(ctx.args, "--data-dir") ?? defaultDataDir);
3433
+ const runtimeDir = await ask(rl, "Runtime directory", getFlagValue(ctx.args, "--runtime-dir") ?? defaultRuntimeDir);
3434
+ const host = await ask(rl, "Proxy host", "127.0.0.1");
3435
+ const port = await ask(rl, "Proxy port", "3100");
3436
+ const cliProxyApiUrl = await ask(rl, "CLIProxyAPI URL", "http://localhost:8317");
3437
+ const cliProxyApiKey = await askSecret(rl, "CLIProxyAPI proxy key", "proxy");
3438
+ const exposeAdmin = host !== "127.0.0.1" && host !== "localhost" && host !== "::1";
3439
+ const adminApiKey = exposeAdmin || await confirm(rl, "Generate ADMIN_API_KEY for admin API?", false) ? crypto.randomUUID().replaceAll("-", "") : "";
3440
+ const enableCliproxyCorrelation = await confirm(rl, "Enable CLIProxyAPI account correlation?", false);
3441
+ const cliproxyMgmtKey = enableCliproxyCorrelation ? await askSecret(rl, "CLIProxyAPI management key", "") : "";
3442
+ const enableQuotaRefresh = await confirm(rl, "Enable subscription quota checks from local CLIProxyAPI auth files?", false);
3443
+ const cliproxyAuthDir = enableQuotaRefresh ? await ask(rl, "CLIProxyAPI auth directory", join4(HOME, ".cli-proxy-api")) : "";
3444
+ const env = buildEnv({
3445
+ host,
3446
+ port,
3447
+ adminApiKey,
3448
+ cliProxyApiUrl,
3449
+ cliProxyApiKey,
3450
+ dataDir,
3451
+ cliproxyMgmtKey,
3452
+ cliproxyAuthDir
3453
+ });
3454
+ await completeInit(envPath, dataDir, runtimeDir, env, writeOptions(ctx));
3455
+ if (await confirm(rl, "Install user daemon now?", platform() === "linux" || platform() === "darwin")) {
3456
+ await installRuntime(runtimeDir, envPath);
3457
+ await installService(ctx, runtimeDir, envPath);
3458
+ }
3459
+ } finally {
3460
+ rl.close();
3461
+ }
3462
+ }
3463
+ async function initNonInteractive(ctx) {
3464
+ logger6.info("non-interactive init started", { event: "cli.init.non_interactive" });
3465
+ const envPath = getFlagValue(ctx.args, "--env") ?? process.env.AGENT_CLI_PROXY_ENV ?? defaultEnvPath;
3466
+ const dataDir = getFlagValue(ctx.args, "--data-dir") ?? process.env.AGENT_CLI_PROXY_DATA_DIR ?? defaultDataDir;
3467
+ const runtimeDir = getFlagValue(ctx.args, "--runtime-dir") ?? process.env.AGENT_CLI_PROXY_RUNTIME_DIR ?? defaultRuntimeDir;
3468
+ const host = getFlagValue(ctx.args, "--host") ?? process.env.PROXY_HOST ?? "127.0.0.1";
3469
+ const port = getFlagValue(ctx.args, "--port") ?? process.env.PROXY_PORT ?? "3100";
3470
+ const cliProxyApiUrl = getFlagValue(ctx.args, "--cliproxy-api-url") ?? process.env.CLI_PROXY_API_URL;
3471
+ if (!cliProxyApiUrl && process.env.PROXY_LOCAL_OK !== "1") {
3472
+ throw new Error("CLI_PROXY_API_URL is required for init --non-interactive unless PROXY_LOCAL_OK=1");
3473
+ }
3474
+ const cliProxyApiKey = readSecretFromEnvFlag(ctx.args, "--cliproxy-api-key-env", "CLI_PROXY_API_KEY") ?? "proxy";
3475
+ const adminApiKey = getFlagValue(ctx.args, "--admin-token") ?? readNamedEnv(ctx.args, "--admin-token-env") ?? process.env.ADMIN_API_KEY ?? "";
3476
+ const cliproxyMgmtKey = readNamedEnv(ctx.args, "--cliproxy-mgmt-key-env") ?? process.env.CLIPROXY_MGMT_KEY ?? "";
3477
+ const cliproxyAuthDir = getFlagValue(ctx.args, "--cliproxy-auth-dir") ?? process.env.CLIPROXY_AUTH_DIR ?? "";
3478
+ const env = buildEnv({
3479
+ host,
3480
+ port,
3481
+ adminApiKey,
3482
+ cliProxyApiUrl: cliProxyApiUrl ?? "http://localhost:8317",
3483
+ cliProxyApiKey,
3484
+ dataDir,
3485
+ cliproxyMgmtKey,
3486
+ cliproxyAuthDir
3487
+ });
3488
+ await completeInit(envPath, dataDir, runtimeDir, env, writeOptions(ctx));
3489
+ }
3490
+ function readSecretFromEnvFlag(args, flagName, envName) {
3491
+ return readNamedEnv(args, flagName) ?? process.env[envName];
3492
+ }
3493
+ function readNamedEnv(args, flagName) {
3494
+ const name = getFlagValue(args, flagName);
3495
+ if (!name)
3496
+ return;
3497
+ const value = process.env[name];
3498
+ if (value === undefined || value === "")
3499
+ throw new Error(`${flagName} references missing environment variable ${name}`);
3500
+ return value;
3501
+ }
3502
+ function buildEnv(inputEnv) {
3503
+ return {
3504
+ PROXY_HOST: inputEnv.host,
3505
+ PROXY_PORT: inputEnv.port,
3506
+ ADMIN_API_KEY: inputEnv.adminApiKey,
3507
+ CLI_PROXY_API_URL: inputEnv.cliProxyApiUrl,
3508
+ CLI_PROXY_API_KEY: inputEnv.cliProxyApiKey,
3509
+ CLAUDE_CODE_VERSION: "2.1.87",
3510
+ DB_PATH: join4(inputEnv.dataDir, "proxy.db"),
3511
+ PRICING_CACHE_PATH: join4(inputEnv.dataDir, "pricing-cache.json"),
3512
+ PRICING_CACHE_TTL_MS: "3600000",
3513
+ CLIENT_NAME_MAPPING: "",
3514
+ CLIPROXY_MGMT_KEY: inputEnv.cliproxyMgmtKey,
3515
+ CLIPROXY_CORRELATION_INTERVAL_MS: "15000",
3516
+ CLIPROXY_CORRELATION_LOOKBACK_MS: "300000",
3517
+ CLIPROXY_AUTH_DIR: inputEnv.cliproxyAuthDir,
3518
+ QUOTA_REFRESH_TIMEOUT_MS: "15000"
3519
+ };
3520
+ }
3521
+ async function completeInit(envPath, dataDir, runtimeDir, env, options) {
3522
+ await mkdir2(dirname2(envPath), { recursive: true });
3523
+ await mkdir2(dataDir, { recursive: true });
3524
+ await mkdir2(runtimeDir, { recursive: true });
3525
+ await writeEnvAtomic(envPath, env, options);
3526
+ const persistedEnv = parseEnvFile(envPath);
3527
+ await initDbAt(persistedEnv.DB_PATH ?? env.DB_PATH);
3528
+ writeOut(`
3529
+ Created:`);
3530
+ writeOut(` env: ${envPath}`);
3531
+ writeOut(` db: ${persistedEnv.DB_PATH ?? env.DB_PATH}`);
3532
+ writeOut(` data: ${dataDir}`);
3533
+ writeOut(` runtime: ${runtimeDir}`);
3534
+ }
3535
+ function writeOptions(ctx) {
3536
+ return { force: hasFlag(ctx.args, "--force"), merge: hasFlag(ctx.args, "--merge") };
3537
+ }
3538
+ async function initDbCommand(ctx) {
3539
+ const envPath = getFlagValue(ctx.args, "--env") ?? defaultEnvPath;
3540
+ const env = parseEnvFile(envPath);
3541
+ await initDbAt(env.DB_PATH ?? defaultDbPath);
3542
+ writeOut(`Initialized DB at ${env.DB_PATH ?? defaultDbPath}`);
3543
+ }
3544
+ async function backfillCostsCommand(ctx) {
3545
+ const envPath = getFlagValue(ctx.args, "--env") ?? defaultEnvPath;
3546
+ const env = parseEnvFile(envPath);
3547
+ applyEnv(env);
3548
+ const { UsageService: UsageService2 } = await Promise.resolve().then(() => (init_service(), exports_service));
3549
+ Config.validate(process.env);
3550
+ const dbPath = env.DB_PATH ?? defaultDbPath;
3551
+ const db = Storage.initDb(dbPath);
3552
+ const usageService = UsageService2.create(db);
3553
+ const result = await usageService.backfillCosts({
3554
+ all: hasFlag(ctx.args, "--all"),
3555
+ limit: parsePositiveLimit(ctx.args)
3556
+ });
3557
+ writeOut(`[backfill-costs] scanned=${result.scanned} updated=${result.updated} ok=${result.ok} pending=${result.pending} unsupported=${result.unsupported}`);
3558
+ db.close();
3559
+ }
3560
+ async function doctorCommand(ctx) {
3561
+ logger6.info("doctor started", { event: "cli.doctor.started" });
3562
+ const report = await collectDoctorReport(ctx);
3563
+ if (hasFlag(ctx.args, "--json"))
3564
+ writeOut(JSON.stringify(report, null, 2));
3565
+ else
3566
+ printDoctorReport(report);
3567
+ return report.status === "PASS" ? 0 : 1;
3568
+ }
3569
+ async function collectDoctorReport(ctx) {
3570
+ const { envPath, env } = loadConfigEnv(ctx.args);
3571
+ applyEnv(env);
3572
+ const checks = {};
3573
+ let config = null;
3574
+ try {
3575
+ config = Config.validate(env);
3576
+ checks.config = { status: "PASS", issues: [], details: { envPath } };
3577
+ } catch (err) {
3578
+ const issues = err instanceof ConfigError ? err.issues.map(formatConfigIssue) : [errorMessage2(err)];
3579
+ checks.config = { status: "FAIL", issues, details: { envPath } };
3580
+ }
3581
+ const dbPath = config?.dbPath ?? env.DB_PATH ?? defaultDbPath;
3582
+ checks.database = databaseCheck(dbPath);
3583
+ checks.plans = plansCheck();
3584
+ checks.providers = await providersCheck(Boolean(config));
3585
+ checks.pricingCache = await pricingCacheCheck(config?.pricingCachePath ?? env.PRICING_CACHE_PATH ?? defaultPricingCachePath);
3586
+ checks.upstream = config ? await upstreamCheck(config) : { status: "FAIL", issues: ["skipped because config is invalid"] };
3587
+ checks.supervisor = await supervisorCheck();
3588
+ const status = Object.values(checks).every((check) => check.status === "PASS") ? "PASS" : "FAIL";
3589
+ return { status, checks };
3590
+ }
3591
+ function loadConfigEnv(args) {
3592
+ const envPath = getFlagValue(args, "--env") ?? defaultEnvPath;
3593
+ return { envPath, env: { ...envFromProcess(), ...parseEnvFile(envPath) } };
3594
+ }
3595
+ function envFromProcess() {
3596
+ const env = {};
3597
+ for (const [key, value] of Object.entries(process.env)) {
3598
+ if (value !== undefined)
3599
+ env[key] = value;
3600
+ }
3601
+ return env;
3602
+ }
3603
+ function databaseCheck(dbPath) {
3604
+ let db = null;
3605
+ try {
3606
+ db = Storage.initDb(dbPath);
3607
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all();
3608
+ const migrations = db.prepare("SELECT name FROM schema_migrations ORDER BY name DESC LIMIT 10").all();
3609
+ return {
3610
+ status: "PASS",
3611
+ issues: [],
3612
+ details: {
3613
+ path: dbPath,
3614
+ tablesCount: tables.length,
3615
+ tables: tables.map((table) => table.name),
3616
+ appliedMigrations: migrations.map((migration) => migration.name)
3617
+ }
3618
+ };
3619
+ } catch (err) {
3620
+ return { status: "FAIL", issues: [errorMessage2(err)], details: { path: dbPath } };
3621
+ } finally {
3622
+ db?.close();
3623
+ }
3624
+ }
3625
+ function plansCheck() {
3626
+ try {
3627
+ Plans.reload();
3628
+ const plans = Plans.list();
3629
+ const source = resolvePlansSource();
3630
+ return {
3631
+ status: "PASS",
3632
+ issues: [],
3633
+ details: {
3634
+ count: plans.length,
3635
+ source: source.kind,
3636
+ path: source.path ?? null
3637
+ }
3638
+ };
3639
+ } catch (err) {
3640
+ return { status: "FAIL", issues: [errorMessage2(err)] };
3641
+ }
3642
+ }
3643
+ async function providersCheck(canLoadRegistry) {
3644
+ const inspection = inspectProviderConfig(process.env);
3645
+ if (!canLoadRegistry) {
3646
+ return {
3647
+ status: "FAIL",
3648
+ issues: ["skipped because config is invalid", ...inspection.invalid.map((entry) => entry.reason)],
3649
+ details: { source: inspection.source, invalid: inspection.invalid }
3650
+ };
3651
+ }
3652
+ try {
3653
+ const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
3654
+ const providers = ProviderRegistry2.forceReload();
3655
+ return {
3656
+ status: inspection.invalid.length === 0 ? "PASS" : "FAIL",
3657
+ issues: inspection.invalid.map((entry) => entry.reason),
3658
+ details: {
3659
+ count: providers.length,
3660
+ source: inspection.source,
3661
+ invalid: inspection.invalid
3662
+ }
3663
+ };
3664
+ } catch (err) {
3665
+ return { status: "FAIL", issues: [errorMessage2(err)], details: { invalid: inspection.invalid } };
3666
+ }
3667
+ }
3668
+ async function pricingCacheCheck(path) {
3669
+ try {
3670
+ const info = await stat(path);
3671
+ return {
3672
+ status: "PASS",
3673
+ issues: [],
3674
+ details: {
3675
+ path,
3676
+ exists: true,
3677
+ size: info.size,
3678
+ ageMs: Date.now() - info.mtimeMs
3679
+ }
3680
+ };
3681
+ } catch (err) {
3682
+ return { status: "FAIL", issues: [errorMessage2(err)], details: { path, exists: false } };
3683
+ }
3684
+ }
3685
+ async function upstreamCheck(config) {
3686
+ const controller = new AbortController;
3687
+ const timeout = setTimeout(() => controller.abort(new Error("doctor upstream timeout")), 2000);
3688
+ try {
3689
+ const { UpstreamClient: UpstreamClient2 } = await Promise.resolve().then(() => (init_client(), exports_client));
3690
+ const response = await UpstreamClient2.fetch({
3691
+ method: "HEAD",
3692
+ url: `${config.cliProxyApiUrl}/health`,
3693
+ providerId: "doctor",
3694
+ idempotent: true,
3695
+ signal: controller.signal
3696
+ });
3697
+ if (response.status >= 200 && response.status < 500) {
3698
+ return { status: "PASS", issues: [], details: { statusCode: response.status } };
3699
+ }
3700
+ return { status: "FAIL", issues: [`upstream returned HTTP ${response.status}`], details: { statusCode: response.status } };
3701
+ } catch (err) {
3702
+ return { status: "FAIL", issues: [errorMessage2(err)] };
3703
+ } finally {
3704
+ clearTimeout(timeout);
3705
+ }
3706
+ }
3707
+ async function supervisorCheck() {
3708
+ try {
3709
+ const { Supervisor: Supervisor2 } = await Promise.resolve().then(() => (init_supervisor(), exports_supervisor));
3710
+ return { status: "PASS", issues: [], details: { loops: Supervisor2.list() } };
3711
+ } catch (err) {
3712
+ return { status: "FAIL", issues: [errorMessage2(err)] };
3713
+ }
3714
+ }
3715
+ function printDoctorReport(report) {
3716
+ writeOut(`doctor: ${report.status}`);
3717
+ for (const [name, check] of Object.entries(report.checks)) {
3718
+ writeOut(`${check.status === "PASS" ? "PASS" : "FAIL"} ${name}`);
3719
+ if (check.issues.length > 0) {
3720
+ for (const issue of check.issues)
3721
+ writeOut(` - ${issue}`);
3722
+ }
3723
+ if (check.details !== undefined)
3724
+ writeOut(` ${JSON.stringify(check.details)}`);
3725
+ }
3726
+ }
3727
+ async function plansCommand(ctx, subcommand) {
3728
+ const planArgs = ctx.args.positional.slice(2);
3729
+ const [accountArg = "", codeArg = ""] = planArgs;
3730
+ switch (subcommand) {
3731
+ case "show": {
3732
+ Plans.reload();
3733
+ const plans = Plans.list();
3734
+ if (hasFlag(ctx.args, "--json"))
3735
+ writeOut(JSON.stringify(plans, null, 2));
3736
+ else
3737
+ printPlansTable(plans);
3738
+ return;
3739
+ }
3740
+ case "path": {
3741
+ const source = resolvePlansSource();
3742
+ writeOut(source.path ?? source.label);
3743
+ return;
3744
+ }
3745
+ case "init": {
3746
+ await initPlansFile(hasFlag(ctx.args, "--force"));
3747
+ return;
3748
+ }
3749
+ case "edit": {
3750
+ const path = await ensureEditablePlansFile(hasFlag(ctx.args, "--force"));
3751
+ await openEditor(path);
3752
+ return;
3753
+ }
3754
+ case "bind": {
3755
+ const { account, code } = Plans.validateBindingInput(accountArg, codeArg);
3756
+ const db = await openConfiguredDb(ctx);
3757
+ try {
3758
+ AccountSubscriptionRepo.bind(db, account, code);
3759
+ } finally {
3760
+ db.close();
3761
+ }
3762
+ writeOut(`Bound ${account} \u2192 ${code}`);
3763
+ return;
3764
+ }
3765
+ case "unbind": {
3766
+ const account = accountArg.trim();
3767
+ if (!account)
3768
+ throw new Error("Account must be a non-empty string");
3769
+ const db = await openConfiguredDb(ctx);
3770
+ try {
3771
+ AccountSubscriptionRepo.unbind(db, account);
3772
+ } finally {
3773
+ db.close();
3774
+ }
3775
+ writeOut(`Unbound ${account}`);
3776
+ return;
3777
+ }
3778
+ case "list": {
3779
+ const db = await openConfiguredDb(ctx);
3780
+ try {
3781
+ writeOut("Plans:");
3782
+ for (const plan of Plans.list()) {
3783
+ writeOut(` ${plan.code} - ${plan.display_name}`);
3784
+ }
3785
+ writeOut("Bindings:");
3786
+ const bindings = AccountSubscriptionRepo.list(db);
3787
+ if (bindings.length === 0) {
3788
+ writeOut(" (none)");
3789
+ } else {
3790
+ for (const binding of bindings) {
3791
+ writeOut(` ${binding.cliproxy_account} \u2192 ${binding.subscription_code} (${binding.bound_at})`);
3792
+ }
3793
+ }
3794
+ } finally {
3795
+ db.close();
3796
+ }
3797
+ return;
3798
+ }
3799
+ }
3800
+ writeErr("Usage: agent-cli-proxy plans <show|edit|path|init|bind|unbind|list>");
3801
+ throw new Error("invalid plans subcommand");
3802
+ }
3803
+ async function providersCommand(ctx, subcommand) {
3804
+ switch (subcommand) {
3805
+ case "show": {
3806
+ const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
3807
+ const providers = ProviderRegistry2.all().map(maskProvider);
3808
+ if (hasFlag(ctx.args, "--json"))
3809
+ writeOut(JSON.stringify(providers, null, 2));
3810
+ else
3811
+ printProvidersTable(providers);
3812
+ return;
3813
+ }
3814
+ case "reload": {
3815
+ const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
3816
+ const providers = ProviderRegistry2.forceReload();
3817
+ writeOut(`Reloaded providers: ${providers.length}`);
3818
+ return;
3819
+ }
3820
+ case "path": {
3821
+ writeOut(providerPathLabel(process.env));
3822
+ return;
3823
+ }
3824
+ case "init": {
3825
+ const path = getFlagValue(ctx.args, "--path") ?? process.env.PROVIDERS_CONFIG_PATH ?? defaultProvidersPath;
3826
+ await writeJsonAtomic(path, starterProvidersDocument(), { force: hasFlag(ctx.args, "--force") });
3827
+ writeOut(`Created providers config: ${path}`);
3828
+ return;
3829
+ }
3830
+ }
3831
+ writeErr("Usage: agent-cli-proxy providers <show|reload|path|init>");
3832
+ throw new Error("invalid providers subcommand");
3833
+ }
3834
+ async function serviceCommand(ctx, subcommand) {
3835
+ const envPath = getFlagValue(ctx.args, "--env") ?? defaultEnvPath;
3836
+ const runtimeDir = getFlagValue(ctx.args, "--runtime-dir") ?? defaultRuntimeDir;
3837
+ switch (subcommand) {
3838
+ case "install":
3839
+ await installRuntime(runtimeDir, envPath);
3840
+ await installService(ctx, runtimeDir, envPath);
3841
+ return 0;
3842
+ case "start":
3843
+ case "stop":
3844
+ case "restart":
3845
+ case "status":
3846
+ return await controlService(subcommand);
3847
+ case "logs":
3848
+ return await serviceLogsCommand(hasFlag(ctx.args, "--follow"));
3849
+ }
3850
+ writeErr("Usage: agent-cli-proxy service <install|start|stop|restart|status|logs [--follow]>");
3851
+ return 1;
3852
+ }
3853
+ async function installRuntime(runtimeDir, envPath) {
3854
+ await mkdir2(runtimeDir, { recursive: true });
3855
+ const distDir = resolveDistDir();
3856
+ if (!existsSync2(join4(distDir, "index.js"))) {
3857
+ throw new Error("dist/index.js not found. Run `bun run build` first.");
3858
+ }
3859
+ await copyFile(join4(distDir, "index.js"), join4(runtimeDir, "index.js"));
3860
+ await copyGlobByPrefix(distDir, runtimeDir, "index-");
3861
+ await rm(join4(runtimeDir, "migrations"), { recursive: true, force: true });
3862
+ await cp(join4(distDir, "migrations"), join4(runtimeDir, "migrations"), { recursive: true });
3863
+ await writeFile(join4(runtimeDir, ".env.path"), `${envPath}
3864
+ `);
3865
+ }
3866
+ async function copyGlobByPrefix(fromDir, toDir, prefix) {
3867
+ const glob = new Bun.Glob(`${prefix}*`);
3868
+ for await (const file of glob.scan({ cwd: fromDir, onlyFiles: true })) {
3869
+ await copyFile(join4(fromDir, file), join4(toDir, file));
3870
+ }
3871
+ }
3872
+ async function installService(ctx, runtimeDir, envPath) {
3873
+ if (platform() === "linux") {
3874
+ const unitDir = join4(XDG_CONFIG_HOME, "systemd", "user");
3875
+ const unitPath = getFlagValue(ctx.args, "--service-path") ?? join4(unitDir, `${APP_NAME}.service`);
3876
+ await mkdir2(dirname2(unitPath), { recursive: true });
3877
+ await writeFile(unitPath, renderSystemdUserService(runtimeDir, envPath));
3878
+ writeOut(`Installed systemd user service: ${unitPath}`);
3879
+ writeOut(`Run: systemctl --user daemon-reload && systemctl --user enable --now ${APP_NAME}`);
3880
+ return;
3881
+ }
3882
+ if (platform() === "darwin") {
3883
+ const launchDir = join4(HOME, "Library", "LaunchAgents");
3884
+ const plistPath = getFlagValue(ctx.args, "--service-path") ?? join4(launchDir, `ai.agent-cli-proxy.plist`);
3885
+ await mkdir2(dirname2(plistPath), { recursive: true });
3886
+ await writeFile(plistPath, renderLaunchAgent(runtimeDir, envPath));
3887
+ writeOut(`Installed launchd agent: ${plistPath}`);
3888
+ writeOut(`Run: launchctl load ${plistPath}`);
3889
+ return;
3890
+ }
3891
+ throw new Error(`Unsupported service platform: ${platform()}`);
3892
+ }
3893
+ async function controlService(action) {
3894
+ if (platform() !== "linux") {
3895
+ writeOut("Service control is only automated for systemd user services. Use launchctl on macOS.");
3896
+ return 0;
3897
+ }
3898
+ const args = action === "status" ? ["--user", "--no-pager", "status", APP_NAME] : ["--user", action, APP_NAME];
3899
+ const proc = Bun.spawn(["systemctl", ...args], { stdout: "inherit", stderr: "inherit" });
3900
+ return await proc.exited;
3901
+ }
3902
+ async function serviceLogsCommand(follow) {
3903
+ const os = platform();
3904
+ if (os === "linux") {
3905
+ const args = ["--user", "-u", `${APP_NAME}.service`];
3906
+ if (follow)
3907
+ args.push("-f");
3908
+ const proc = Bun.spawn(["journalctl", ...args], { stdout: "inherit", stderr: "inherit" });
3909
+ return await proc.exited;
3910
+ }
3911
+ if (os === "darwin") {
3912
+ const command = follow ? "stream" : "show";
3913
+ const args = [command, "--predicate", `process == "${APP_NAME}"`, "--style", "compact"];
3914
+ const proc = Bun.spawn(["log", ...args], { stdout: "inherit", stderr: "inherit" });
3915
+ return await proc.exited;
3916
+ }
3917
+ writeErr("service logs not supported on this platform; check stderr/stdout");
3918
+ return 1;
3919
+ }
3920
+ function renderSystemdUserService(runtimeDir, envPath) {
3921
+ assertSafePath(runtimeDir, "runtimeDir");
3922
+ assertSafePath(envPath, "envPath");
3923
+ return `[Unit]
3924
+ Description=agent-cli-proxy - AI API Proxy with Usage Monitoring
3925
+ After=network-online.target
3926
+
3927
+ [Service]
3928
+ Type=simple
3929
+ WorkingDirectory=${systemdEscape(runtimeDir)}
3930
+ Environment=NODE_ENV=production
3931
+ Environment=PATH=${systemdEscape(`${HOME}/.bun/bin:/usr/local/bin:/usr/bin:/bin`)}
3932
+ EnvironmentFile=${systemdEscape(envPath)}
3933
+ ExecStart=${systemdEscape(`${HOME}/.bun/bin/bun`)} run index.js
3934
+ Restart=always
3935
+ RestartSec=5
3936
+
3937
+ [Install]
3938
+ WantedBy=default.target
3939
+ `;
3940
+ }
3941
+ function renderLaunchAgent(runtimeDir, envPath) {
3942
+ assertSafePath(runtimeDir, "runtimeDir");
3943
+ assertSafePath(envPath, "envPath");
3944
+ return `<?xml version="1.0" encoding="UTF-8"?>
3945
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3946
+ <plist version="1.0">
3947
+ <dict>
3948
+ <key>Label</key><string>ai.agent-cli-proxy</string>
3949
+ <key>WorkingDirectory</key><string>${xmlEscape(runtimeDir)}</string>
3950
+ <key>ProgramArguments</key>
3951
+ <array><string>${xmlEscape(`${HOME}/.bun/bin/bun`)}</string><string>--env-file</string><string>${xmlEscape(envPath)}</string><string>run</string><string>index.js</string></array>
3952
+ <key>EnvironmentVariables</key>
3953
+ <dict><key>NODE_ENV</key><string>production</string></dict>
3954
+ <key>RunAtLoad</key><true/>
3955
+ <key>KeepAlive</key><true/>
3956
+ </dict>
3957
+ </plist>
3958
+ `;
3959
+ }
3960
+ async function initDbAt(dbPath) {
3961
+ await mkdir2(dirname2(dbPath), { recursive: true });
3962
+ const db = Storage.initDb(dbPath);
3963
+ db.close();
3964
+ }
3965
+ async function openConfiguredDb(ctx) {
3966
+ const envPath = getFlagValue(ctx.args, "--env") ?? defaultEnvPath;
3967
+ const env = parseEnvFile(envPath);
3968
+ applyEnv(env);
3969
+ Config.validate(process.env);
3970
+ return Storage.initDb(env.DB_PATH ?? process.env.DB_PATH ?? defaultDbPath);
3971
+ }
3972
+ async function writeEnvAtomic(path, envMap, opts) {
3973
+ if (existsSync2(path) && !opts.force && !opts.merge) {
3974
+ throw new Error(`${path} already exists; use --force to overwrite, or --merge to preserve existing values`);
3975
+ }
3976
+ const existing = opts.merge ? parseEnvFile(path) : {};
3977
+ const finalEnv = opts.merge ? { ...envMap, ...existing } : envMap;
3978
+ await writeTextAtomic(path, renderEnv(finalEnv), 384);
3979
+ }
3980
+ async function writeJsonAtomic(path, value, opts) {
3981
+ if (existsSync2(path) && !opts.force)
3982
+ throw new Error(`${path} already exists; use --force to overwrite`);
3983
+ await writeTextAtomic(path, `${JSON.stringify(value, null, 2)}
3984
+ `, 384);
3985
+ }
3986
+ async function writeTextAtomic(path, text, mode) {
3987
+ await mkdir2(dirname2(path), { recursive: true });
3988
+ const temp = `${path}.tmp.${process.pid}.${crypto.randomUUID()}`;
3989
+ const handle = await open(temp, "w", mode);
3990
+ try {
3991
+ await handle.writeFile(text);
3992
+ } finally {
3993
+ await handle.close();
3994
+ }
3995
+ await chmod(temp, mode);
3996
+ await rename(temp, path);
3997
+ await chmod(path, mode);
3998
+ }
3999
+ function renderEnv(env) {
4000
+ const lines = Object.entries(env).map(([key, value]) => `${key}=${quoteEnv(value)}`);
4001
+ return `${lines.join(`
4002
+ `)}
4003
+ `;
4004
+ }
4005
+ function quoteEnv(value) {
4006
+ if (/^[A-Za-z0-9_./:@-]*$/.test(value))
4007
+ return value;
4008
+ return JSON.stringify(value);
4009
+ }
4010
+ function parseEnvFile(path) {
4011
+ if (!existsSync2(path))
4012
+ return {};
4013
+ const text = readFileSync5(path, "utf-8");
4014
+ const env = {};
4015
+ for (const line of text.split(`
4016
+ `)) {
4017
+ const trimmed = line.trim();
4018
+ if (!trimmed || trimmed.startsWith("#"))
4019
+ continue;
4020
+ const idx = trimmed.indexOf("=");
4021
+ if (idx === -1)
4022
+ continue;
4023
+ env[trimmed.slice(0, idx)] = parseEnvValue(trimmed.slice(idx + 1));
4024
+ }
4025
+ return env;
4026
+ }
4027
+ function parseEnvValue(raw) {
4028
+ if (!raw.startsWith('"'))
4029
+ return raw;
4030
+ try {
4031
+ const parsed = JSON.parse(raw);
4032
+ return typeof parsed === "string" ? parsed : raw.replace(/^"|"$/g, "");
4033
+ } catch {
4034
+ return raw.replace(/^"|"$/g, "");
4035
+ }
4036
+ }
4037
+ function parsePositiveLimit(args) {
4038
+ const value = getFlagValue(args, "--limit");
4039
+ if (!value)
4040
+ return;
4041
+ const parsed = Number(value);
4042
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
4043
+ }
4044
+ async function ask(rl, question, fallback) {
4045
+ const answer = await rl.question(`${question} (${fallback}): `);
4046
+ return answer.trim() || fallback;
4047
+ }
4048
+ async function askSecret(rl, question, fallback) {
4049
+ if (!process.stdin.isTTY) {
4050
+ throw new Error(`${question} requires a TTY; use init --non-interactive with environment-backed secret flags`);
4051
+ }
4052
+ const mutableOutput = output;
4053
+ const originalWrite = mutableOutput.write.bind(mutableOutput);
4054
+ let muted = false;
4055
+ mutableOutput.write = (chunk, encoding, cb) => {
4056
+ if (muted)
4057
+ return true;
4058
+ return originalWrite(chunk, encoding, cb);
4059
+ };
4060
+ try {
4061
+ muted = true;
4062
+ const answer = await rl.question(`${question}${fallback ? " (configured)" : ""}: `);
4063
+ muted = false;
4064
+ originalWrite(`
4065
+ `);
4066
+ return answer.trim() || fallback;
4067
+ } finally {
4068
+ mutableOutput.write = originalWrite;
4069
+ }
4070
+ }
4071
+ function resolveDistDir() {
4072
+ if (existsSync2(join4(packagedDistDir, "index.js")))
4073
+ return packagedDistDir;
4074
+ const sourceTreeDist = join4(packageRoot, "dist");
4075
+ if (existsSync2(join4(sourceTreeDist, "index.js")))
4076
+ return sourceTreeDist;
4077
+ const cwdDist = resolve("dist");
4078
+ if (existsSync2(join4(cwdDist, "index.js")))
4079
+ return cwdDist;
4080
+ return packagedDistDir;
4081
+ }
4082
+ function assertSafePath(path, label) {
4083
+ if (/[\u0000-\u001f\u007f\n\r]/.test(path)) {
4084
+ throw new Error(`${label} contains unsupported control characters`);
4085
+ }
4086
+ }
4087
+ function systemdEscape(value) {
4088
+ if (/^[A-Za-z0-9_/@%+=:,.-]+$/.test(value))
4089
+ return value;
4090
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
4091
+ }
4092
+ function xmlEscape(value) {
4093
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
4094
+ }
4095
+ async function confirm(rl, question, fallback) {
4096
+ const suffix = fallback ? "Y/n" : "y/N";
4097
+ const answer = (await rl.question(`${question} [${suffix}]: `)).trim().toLowerCase();
4098
+ if (!answer)
4099
+ return fallback;
4100
+ return answer === "y" || answer === "yes";
4101
+ }
4102
+ function printPaths() {
4103
+ writeOut(JSON.stringify({
4104
+ configDir: defaultConfigDir,
4105
+ dataDir: defaultDataDir,
4106
+ runtimeDir: defaultRuntimeDir,
4107
+ envPath: defaultEnvPath,
4108
+ dbPath: defaultDbPath,
4109
+ pricingCachePath: defaultPricingCachePath,
4110
+ plansPath: defaultPlansPath,
4111
+ providersPath: defaultProvidersPath
4112
+ }, null, 2));
4113
+ }
4114
+ function printHelp() {
4115
+ writeOut(`agent-cli-proxy
4116
+
4117
+ Usage:
4118
+ agent-cli-proxy init [--force|--merge]
4119
+ agent-cli-proxy init --non-interactive [--env PATH] [--data-dir PATH] [--runtime-dir PATH] [--admin-token VALUE|--admin-token-env NAME] [--cliproxy-mgmt-key-env NAME] [--force|--merge]
4120
+ agent-cli-proxy doctor [--env PATH] [--json]
4121
+ agent-cli-proxy db init [--env PATH]
4122
+ agent-cli-proxy service install [--env PATH] [--runtime-dir PATH] [--service-path PATH]
4123
+ agent-cli-proxy service start|stop|restart|status
4124
+ agent-cli-proxy service logs [--follow]
4125
+ agent-cli-proxy backfill-costs [--all] [--limit N]
4126
+ agent-cli-proxy plans show [--json]
4127
+ agent-cli-proxy plans edit|path|init [--force]
4128
+ agent-cli-proxy plans bind <account> <code> [--env PATH]
4129
+ agent-cli-proxy plans unbind <account> [--env PATH]
4130
+ agent-cli-proxy plans list [--env PATH]
4131
+ agent-cli-proxy providers show [--json]
4132
+ agent-cli-proxy providers reload|path|init [--force]
4133
+ agent-cli-proxy paths
4134
+ `);
4135
+ }
4136
+ function applyEnv(env) {
4137
+ for (const [key, value] of Object.entries(env))
4138
+ process.env[key] = value;
4139
+ }
4140
+ function formatConfigIssue(issue) {
4141
+ return `${issue.path} ${issue.message}`;
4142
+ }
4143
+ function errorMessage2(err) {
4144
+ return err instanceof Error ? err.message : String(err);
4145
+ }
4146
+ function resolvePlansSource() {
4147
+ if (process.env.PLANS_JSON?.trim())
4148
+ return { kind: "PLANS_JSON", label: "PLANS_JSON inline" };
4149
+ const envPath = process.env.PLANS_PATH?.trim();
4150
+ if (envPath)
4151
+ return { kind: "PLANS_PATH", path: envPath, label: envPath };
4152
+ if (existsSync2(defaultPlansPath))
4153
+ return { kind: "XDG_CONFIG_HOME", path: defaultPlansPath, label: defaultPlansPath };
4154
+ return { kind: "packaged", label: "packaged default" };
4155
+ }
4156
+ async function initPlansFile(force) {
4157
+ await writeJsonAtomic(defaultPlansPath, plans_default_default, { force });
4158
+ writeOut(`Created plans config: ${defaultPlansPath}`);
4159
+ }
4160
+ async function ensureEditablePlansFile(force) {
4161
+ const source = resolvePlansSource();
4162
+ if (source.path && source.kind !== "packaged")
4163
+ return source.path;
4164
+ if (existsSync2(defaultPlansPath) && !force)
4165
+ return defaultPlansPath;
4166
+ await writeJsonAtomic(defaultPlansPath, plans_default_default, { force: force || !existsSync2(defaultPlansPath) });
4167
+ return defaultPlansPath;
4168
+ }
4169
+ async function openEditor(path) {
4170
+ const editor = process.env.EDITOR?.trim() || "vi";
4171
+ const proc = Bun.spawn([editor, path], { stdout: "inherit", stderr: "inherit", stdin: "inherit" });
4172
+ const exitCode = await proc.exited;
4173
+ if (exitCode !== 0)
4174
+ throw new Error(`${editor} exited with ${exitCode}`);
4175
+ }
4176
+ function printPlansTable(plans) {
4177
+ writeOut("code\tprovider\tprice\tdisplay_name");
4178
+ for (const plan of plans) {
4179
+ writeOut(`${plan.code} ${plan.provider} ${plan.monthly_price_usd} ${plan.currency}/${plan.billing_period_days}d ${plan.display_name}`);
4180
+ }
4181
+ }
4182
+ function printProvidersTable(providers) {
4183
+ writeOut("id\ttype\tpaths\tmodels\tauth");
4184
+ for (const provider of providers) {
4185
+ writeOut(`${provider.id} ${provider.type} ${provider.paths.join(",")} ${provider.models?.join(",") ?? ""} ${formatAuth(provider.auth)}`);
4186
+ }
4187
+ }
4188
+ function maskProvider(provider) {
4189
+ if (typeof provider.auth !== "object" || provider.auth === null)
4190
+ return { ...provider };
4191
+ const auth = { ...provider.auth };
4192
+ if (auth.value !== undefined)
4193
+ auth.value = "[redacted]";
4194
+ return { ...provider, auth };
4195
+ }
4196
+ function formatAuth(auth) {
4197
+ if (auth === undefined)
4198
+ return "";
4199
+ if (typeof auth === "string")
4200
+ return auth;
4201
+ if (auth.env)
4202
+ return `${auth.type} env:${auth.env}`;
4203
+ if (auth.value)
4204
+ return `${auth.type} value:[redacted]`;
4205
+ return auth.type;
4206
+ }
4207
+ function providerPathLabel(env) {
4208
+ if (env.PROVIDERS_JSON?.trim())
4209
+ return "PROVIDERS_JSON inline";
4210
+ if (env.PROVIDERS_CONFIG_PATH?.trim())
4211
+ return env.PROVIDERS_CONFIG_PATH.trim();
4212
+ return "(no custom providers configured)";
4213
+ }
4214
+ function starterProvidersDocument() {
4215
+ return {
4216
+ providers: [
4217
+ {
4218
+ id: "local",
4219
+ type: "openai-compatible",
4220
+ paths: ["/v1/chat/completions"],
4221
+ upstreamBaseUrl: "http://localhost:11434",
4222
+ upstreamPath: "/v1/chat/completions",
4223
+ models: ["llama", "qwen"],
4224
+ auth: "none"
4225
+ },
4226
+ {
4227
+ id: "glm",
4228
+ type: "openai-compatible",
4229
+ paths: ["/v1/chat/completions"],
4230
+ upstreamBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
4231
+ models: ["glm"],
4232
+ auth: { type: "bearer", env: "GLM_API_KEY" }
4233
+ }
4234
+ ]
4235
+ };
4236
+ }
4237
+ function inspectProviderConfig(env) {
4238
+ const inline = env.PROVIDERS_JSON;
4239
+ if (inline !== undefined && inline.trim() !== "")
4240
+ return inspectProviderRaw("PROVIDERS_JSON", inline);
4241
+ const configPath = env.PROVIDERS_CONFIG_PATH?.trim();
4242
+ if (!configPath)
4243
+ return { source: "built-in", invalid: [] };
4244
+ try {
4245
+ return inspectProviderRaw(configPath, readFileSync5(configPath, "utf-8"));
4246
+ } catch (err) {
4247
+ return { source: configPath, invalid: [{ path: "PROVIDERS_CONFIG_PATH", reason: errorMessage2(err), issues: [{ path: "PROVIDERS_CONFIG_PATH", message: errorMessage2(err) }] }] };
4248
+ }
4249
+ }
4250
+ function inspectProviderRaw(source, raw) {
4251
+ let parsed;
4252
+ try {
4253
+ parsed = JSON.parse(raw);
4254
+ } catch (err) {
4255
+ const issue = { path: source, message: errorMessage2(err) };
4256
+ return { source, invalid: [{ path: source, reason: `${source} must be valid JSON: ${issue.message}`, issues: [issue] }] };
4257
+ }
4258
+ const result = validateProviderDocument(parsed);
4259
+ const grouped = new Map;
4260
+ for (const issue of result.issues) {
4261
+ const match = /^providers\[(\d+)]/.exec(issue.path);
4262
+ const path = match ? `providers[${match[1]}]` : issue.path;
4263
+ const issues = grouped.get(path) ?? [];
4264
+ issues.push(issue);
4265
+ grouped.set(path, issues);
4266
+ }
4267
+ return {
4268
+ source,
4269
+ invalid: Array.from(grouped, ([path, issues]) => ({
4270
+ path,
4271
+ reason: issues.map((issue) => `${issue.path} ${issue.message}`).join("; "),
4272
+ issues
4273
+ }))
4274
+ };
4275
+ }
4276
+ if (import.meta.main) {
4277
+ main().catch((err) => {
4278
+ writeErr(err instanceof Error ? err.message : String(err));
4279
+ process.exit(1);
4280
+ });
4281
+ }
4282
+ export {
4283
+ writeEnvAtomic,
4284
+ runCli,
4285
+ parseArgs,
4286
+ getFlagValue
4287
+ };