omnigateway 0.2.2 → 0.4.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.
Files changed (71) hide show
  1. package/README.md +506 -9
  2. package/bin/omni.js +3022 -163
  3. package/gateway.js +4094 -662
  4. package/package.json +1 -1
  5. package/public/assets/{Chip-DJwp7gBu.js → Chip-BltJSw8A.js} +5 -5
  6. package/public/assets/Confirm-gd8Y6snu.js +4 -0
  7. package/public/assets/CopyValue-Dds-Ow9a.js +27 -0
  8. package/public/assets/{Field-BDtp692L.js → Field-D-14HF0x.js} +11 -11
  9. package/public/assets/Lamp-BDf7-dMa.js +25 -0
  10. package/public/assets/Match-CgNxphvS.js +1 -0
  11. package/public/assets/Meter-stpt3vQu.js +14 -0
  12. package/public/assets/Modal-Dj3LvMS9.js +82 -0
  13. package/public/assets/Rack-B4c0dbIb.js +180 -0
  14. package/public/assets/{Readout-CEAoN5hu.js → Readout-Du15WYx0.js} +5 -5
  15. package/public/assets/{States-BEKLros0.js → States-CxLDlCaw.js} +5 -5
  16. package/public/assets/{Table-BBypbVWj.js → Table-CVDZ69Zx.js} +1 -1
  17. package/public/assets/Toggle-DMk46RX2.js +39 -0
  18. package/public/assets/TokenBreakdown-BySwFK6Q.js +31 -0
  19. package/public/assets/_app-Dx_xnlud.js +1 -0
  20. package/public/assets/_app.accounts-DvBYnbfS.js +64 -0
  21. package/public/assets/_app.console-CoaRdkB1.js +44 -0
  22. package/public/assets/_app.database-CES5ec5g.js +39 -0
  23. package/public/assets/_app.index-DwOZe1A-.js +62 -0
  24. package/public/assets/_app.keys-B5HrAu2U.js +76 -0
  25. package/public/assets/_app.logs-NbfIexz0.js +68 -0
  26. package/public/assets/_app.models-B1MdFRtQ.js +148 -0
  27. package/public/assets/_app.plugins._pluginId-ByMziW8p.js +20 -0
  28. package/public/assets/_app.settings-BkDkGkKF.js +46 -0
  29. package/public/assets/_app.usage-DNpM8Nub.js +83 -0
  30. package/public/assets/dist-9ps0Qd15.js +1 -0
  31. package/public/assets/index-gMv8Qs7m.js +178 -0
  32. package/public/assets/login-cCXnl34r.js +36 -0
  33. package/public/assets/matchContext-BOenO3J2.js +1 -0
  34. package/public/assets/plus-fXfUTt6_.js +1 -0
  35. package/public/assets/queries-CIOPbRcE.js +142 -0
  36. package/public/assets/reasons-Cb8yF4Oy.js +1 -0
  37. package/public/assets/shared-B4o-2quR.js +84 -0
  38. package/public/assets/{trash-2-CLYNAAJA.js → trash-2-ZPh3619D.js} +1 -1
  39. package/public/index.html +7 -2
  40. package/public/shared/chunks/jsx-runtime-CznXPbDH.js +1 -0
  41. package/public/shared/chunks/react-GitiQ0dt.js +1 -0
  42. package/public/shared/chunks/react-dom-BTlfKTOo.js +1 -0
  43. package/public/shared/chunks/rolldown-runtime-CbXtAM7H.js +1 -0
  44. package/public/shared/jsx-runtime.js +1 -0
  45. package/public/shared/react-dom-client.js +9 -0
  46. package/public/shared/react-dom.js +1 -0
  47. package/public/shared/react-query.js +1 -0
  48. package/public/shared/react.js +1 -0
  49. package/public/shared/styled-components.js +7 -0
  50. package/public/assets/Confirm-CVLz9F7a.js +0 -4
  51. package/public/assets/CopyValue-CNZbWS59.js +0 -27
  52. package/public/assets/Lamp-B65uWKbW.js +0 -25
  53. package/public/assets/Meter-BREBUFGX.js +0 -14
  54. package/public/assets/Modal-DbydSAlp.js +0 -82
  55. package/public/assets/Rack-Bbo4JfEX.js +0 -151
  56. package/public/assets/Toggle-BUr71O4J.js +0 -39
  57. package/public/assets/TokenBreakdown-C1ziK9d1.js +0 -31
  58. package/public/assets/_app-ByQ1bPbe.js +0 -1
  59. package/public/assets/_app.accounts-BTug_Xfo.js +0 -64
  60. package/public/assets/_app.console-DKyGL9xx.js +0 -44
  61. package/public/assets/_app.index-CSw9gmMf.js +0 -62
  62. package/public/assets/_app.keys-07Kbw4RS.js +0 -39
  63. package/public/assets/_app.logs-C9QiV39p.js +0 -32
  64. package/public/assets/_app.models-CDqVrAeF.js +0 -148
  65. package/public/assets/_app.settings-DmUEeTaP.js +0 -38
  66. package/public/assets/_app.usage-B1WuMGRr.js +0 -83
  67. package/public/assets/dist-Bka7ErcH.js +0 -1
  68. package/public/assets/index-DJfb1i4P.js +0 -186
  69. package/public/assets/login-DKZYqRh0.js +0 -33
  70. package/public/assets/queries-D8LJNQpL.js +0 -144
  71. package/public/assets/shared-CIhZ6FCO.js +0 -84
package/bin/omni.js CHANGED
@@ -102,9 +102,19 @@ function formatSpan(ms) {
102
102
  function formatUsd(value) {
103
103
  return `$${value.toFixed(value < 1 ? 4 : 2)}`;
104
104
  }
105
+ function formatBytes(bytes) {
106
+ if (!Number.isFinite(bytes) || bytes < 0)
107
+ return "\u2014";
108
+ if (bytes < 1024)
109
+ return `${Math.round(bytes)} B`;
110
+ const kb = bytes / 1024;
111
+ if (kb < 1024)
112
+ return `${kb.toFixed(1)} KB`;
113
+ return `${(kb / 1024).toFixed(1)} MB`;
114
+ }
105
115
 
106
116
  // apps/cli/src/run.ts
107
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
117
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
108
118
  import { homedir as homedir3 } from "os";
109
119
 
110
120
  // packages/control/src/adminAuth.ts
@@ -161,9 +171,34 @@ function createAdminAuth(store, opts) {
161
171
  },
162
172
  logout(token) {
163
173
  sessions.delete(token);
174
+ },
175
+ invalidateSessions() {
176
+ sessions.clear();
164
177
  }
165
178
  };
166
179
  }
180
+ // packages/control/src/bodies.ts
181
+ async function readRequestBody(store, requestId) {
182
+ const read = await store.bodies.get(requestId);
183
+ if (read === null) {
184
+ return {
185
+ requestId,
186
+ detailState: "none",
187
+ truncated: false,
188
+ sizeBytes: 0,
189
+ at: null,
190
+ artifact: null
191
+ };
192
+ }
193
+ return {
194
+ requestId: read.row.requestId,
195
+ detailState: read.row.detailState,
196
+ truncated: read.row.truncated,
197
+ sizeBytes: read.row.sizeBytes,
198
+ at: read.row.at,
199
+ artifact: read.artifact
200
+ };
201
+ }
167
202
  // packages/ir/src/betas.ts
168
203
  var CONTEXT_1M_BETA = "context-1m-2025-08-07";
169
204
  var CONTEXT_1M_TOKENS = 1e6;
@@ -362,6 +397,7 @@ function loadConfig(env) {
362
397
  const staticDir = env.OMNI_STATIC_DIR?.trim();
363
398
  const logFile = env.OMNI_LOG_FILE?.trim();
364
399
  const exposeClaudeCodeAliases = TRUTHY.has((env.OMNI_EXPOSE_CLAUDE_CODE_ALIASES ?? "").trim().toLowerCase());
400
+ const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
365
401
  const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
366
402
  const logLevel = parseLogLevel(rawLogLevel);
367
403
  return {
@@ -374,6 +410,7 @@ function loadConfig(env) {
374
410
  baseUrl,
375
411
  staticDir: staticDir === undefined || staticDir.length === 0 ? null : staticDir,
376
412
  exposeClaudeCodeAliases,
413
+ bodyLoggingAllowed,
377
414
  logFile: logFile === undefined || logFile.length === 0 ? null : logFile
378
415
  };
379
416
  }
@@ -513,14 +550,16 @@ function billingBlock() {
513
550
  return `${BILLING_PREFIX} cc_version=${CLI_VERSION}.${BUILD_REVISION}; ` + `cc_entrypoint=cli; cch=${CCH_PLACEHOLDER};`;
514
551
  }
515
552
  var BANNED_SUBSTRINGS = [
516
- "github.com/anomalyco/o\u200Dpencode",
517
- "o\u200Dpencode.ai/docs",
518
- "github.com/c\u200Dline/c\u200Dline",
519
- "github.com/getc\u200Dursor/c\u200Dursor",
520
- "c\u200Dontinue.dev"
553
+ "github.com/anomalyco/opencode",
554
+ "opencode.ai/docs",
555
+ "github.com/cline/cline",
556
+ "github.com/getcursor/cursor",
557
+ "continue.dev",
558
+ "hermes-agent.nousresearch.com"
521
559
  ];
560
+ var IDENTITY_PREFIXES = ["You are OpenCode", "You are Hermes Agent"];
522
561
  var REWRITES = [
523
- ["if O\u200DpenCode honestly", "if the assistant honestly"],
562
+ ["if OpenCode honestly", "if the assistant honestly"],
524
563
  [
525
564
  "Here is some useful information about the environment you are running in:",
526
565
  "Environment context you are running in:"
@@ -529,7 +568,7 @@ var REWRITES = [
529
568
  function applyAnthropicSystem(system) {
530
569
  const kept = [];
531
570
  for (const block of system) {
532
- const text = block.text.split(/\n{2,}/).filter((p) => !BANNED_SUBSTRINGS.some((b) => p.includes(b))).filter((p) => !p.trimStart().startsWith("You are O\u200DpenCode")).filter((p) => !p.includes(BILLING_PREFIX)).filter((p) => p.trim() !== AGENT_PREAMBLE).join(`
571
+ const text = block.text.split(/\n{2,}/).filter((p) => !BANNED_SUBSTRINGS.some((b) => p.includes(b))).filter((p) => !IDENTITY_PREFIXES.some((i) => p.trimStart().startsWith(i))).filter((p) => !p.includes(BILLING_PREFIX)).filter((p) => p.trim() !== AGENT_PREAMBLE).join(`
533
572
 
534
573
  `);
535
574
  let rewritten = text;
@@ -17811,6 +17850,45 @@ function date4(params) {
17811
17850
 
17812
17851
  // node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
17813
17852
  config(en_default());
17853
+ // packages/ratelimit/src/catalog.ts
17854
+ var WINDOWS = ["1m", "5h", "1w"];
17855
+ var WINDOW_MS = {
17856
+ "1m": 60000,
17857
+ "5h": 5 * 60 * 60 * 1000,
17858
+ "1w": 7 * 24 * 60 * 60 * 1000
17859
+ };
17860
+ var countLimit = exports_external.number().int().positive().nullable();
17861
+ var spendLimit = exports_external.number().positive().nullable();
17862
+ var countWindows = exports_external.object({
17863
+ "1m": countLimit.optional(),
17864
+ "5h": countLimit.optional(),
17865
+ "1w": countLimit.optional()
17866
+ }).strict();
17867
+ var spendWindows = exports_external.object({
17868
+ "5h": spendLimit.optional(),
17869
+ "1w": spendLimit.optional()
17870
+ }).strict();
17871
+ var limitConfigSchema = exports_external.object({
17872
+ requests: countWindows.optional(),
17873
+ tokens: countWindows.optional(),
17874
+ spend: spendWindows.optional(),
17875
+ concurrency: countLimit.optional()
17876
+ }).strict();
17877
+ function assertNoProto(value, path) {
17878
+ if (typeof value !== "object" || value === null)
17879
+ return;
17880
+ for (const key of Object.getOwnPropertyNames(value)) {
17881
+ if (key === "__proto__") {
17882
+ throw new Error(`limits cannot name "__proto__"${path === "" ? "" : ` under ${path}`}`);
17883
+ }
17884
+ assertNoProto(value[key], path === "" ? key : `${path}.${key}`);
17885
+ }
17886
+ }
17887
+ function parseLimitConfig(value) {
17888
+ assertNoProto(value, "");
17889
+ return limitConfigSchema.parse(value);
17890
+ }
17891
+
17814
17892
  // packages/control/src/schemas.ts
17815
17893
  function parseOrThrow(schema, body2) {
17816
17894
  const result = schema.safeParse(body2);
@@ -17888,7 +17966,13 @@ var modelSchema = exports_external.object({
17888
17966
  var keyCreateSchema = exports_external.object({
17889
17967
  label: exports_external.string().min(1).default("api key"),
17890
17968
  modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable().default(null),
17891
- rateLimitPerMin: exports_external.number().int().positive().nullable().default(null)
17969
+ limits: limitConfigSchema.default({}),
17970
+ bodyLoggingOptOut: exports_external.boolean().default(false)
17971
+ }).strict();
17972
+ var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
17973
+ var retentionSchema = exports_external.object({
17974
+ keepLatest: exports_external.number().int().min(1).max(100),
17975
+ maxAgeDays: exports_external.number().int().min(1).max(3650)
17892
17976
  }).strict();
17893
17977
  var settingsSchema = exports_external.object({
17894
17978
  weights: exports_external.object({
@@ -17905,7 +17989,11 @@ var settingsSchema = exports_external.object({
17905
17989
  breakerCooldownMs: exports_external.number().int().positive(),
17906
17990
  logRetentionDays: exports_external.number().int().min(1),
17907
17991
  quotaPollIntervalMs: exports_external.number().int().min(0),
17908
- rtkEnabled: exports_external.boolean()
17992
+ rtkEnabled: exports_external.boolean(),
17993
+ bodyLoggingEnabled: exports_external.boolean(),
17994
+ bodyLoggingCaptureStreamChunks: exports_external.boolean(),
17995
+ snapshotKeepLatest: retentionSchema.shape.keepLatest.optional(),
17996
+ snapshotMaxAgeDays: retentionSchema.shape.maxAgeDays.optional()
17909
17997
  });
17910
17998
  var credentialPatchSchema = exports_external.object({
17911
17999
  label: exports_external.string().min(1).optional(),
@@ -18128,7 +18216,11 @@ var DEFAULT_SETTINGS = {
18128
18216
  breakerCooldownMs: 30000,
18129
18217
  logRetentionDays: 30,
18130
18218
  quotaPollIntervalMs: 300000,
18131
- rtkEnabled: false
18219
+ rtkEnabled: false,
18220
+ bodyLoggingEnabled: false,
18221
+ bodyLoggingCaptureStreamChunks: false,
18222
+ snapshotKeepLatest: 5,
18223
+ snapshotMaxAgeDays: 30
18132
18224
  };
18133
18225
 
18134
18226
  // packages/router/src/quota.ts
@@ -18267,6 +18359,10 @@ function rank(input) {
18267
18359
  return { candidates: scored, excluded };
18268
18360
  }
18269
18361
 
18362
+ // packages/store/src/bodies/artifact.ts
18363
+ import { mkdir, readdir, readFile, rmdir, unlink, writeFile } from "fs/promises";
18364
+ import { dirname, join, sep } from "path";
18365
+
18270
18366
  // packages/store/src/encryption.ts
18271
18367
  var PREFIX = "enc:v1";
18272
18368
  var IV_BYTES = 12;
@@ -18306,6 +18402,362 @@ function unhex(s) {
18306
18402
  out[i] = Number.parseInt(s.slice(i * 2, i * 2 + 2), 16);
18307
18403
  return out;
18308
18404
  }
18405
+
18406
+ // packages/store/src/bodies/bound.ts
18407
+ var MAX_STRING_BYTES = 64 * 1024;
18408
+ var MAX_ARRAY_ITEMS = 24;
18409
+ var MAX_DEPTH = 6;
18410
+ var MAX_OBJECT_KEYS = 80;
18411
+ var STRING_TRUNCATION_MARKER = "\u2026[truncated]";
18412
+ var DEPTH_MARKER = `[omitted: nesting past ${MAX_DEPTH} levels]`;
18413
+ var encoder = new TextEncoder;
18414
+ var decoder = new TextDecoder;
18415
+ var CUT_BYTES = MAX_STRING_BYTES - encoder.encode(STRING_TRUNCATION_MARKER).length - 3;
18416
+ function truncateToBytes(value) {
18417
+ const bytes = encoder.encode(value);
18418
+ if (bytes.length <= MAX_STRING_BYTES)
18419
+ return value;
18420
+ return decoder.decode(bytes.subarray(0, CUT_BYTES)) + STRING_TRUNCATION_MARKER;
18421
+ }
18422
+ function boundValue(value) {
18423
+ let truncated = false;
18424
+ const walk = (input, depth) => {
18425
+ if (typeof input === "string") {
18426
+ const cut = truncateToBytes(input);
18427
+ if (cut !== input)
18428
+ truncated = true;
18429
+ return cut;
18430
+ }
18431
+ if (Array.isArray(input)) {
18432
+ if (depth > MAX_DEPTH) {
18433
+ truncated = true;
18434
+ return DEPTH_MARKER;
18435
+ }
18436
+ const kept = input.length > MAX_ARRAY_ITEMS ? input.slice(-MAX_ARRAY_ITEMS) : input;
18437
+ if (kept.length !== input.length)
18438
+ truncated = true;
18439
+ return kept.map((item) => walk(item, depth + 1));
18440
+ }
18441
+ if (input !== null && typeof input === "object") {
18442
+ if (depth > MAX_DEPTH) {
18443
+ truncated = true;
18444
+ return DEPTH_MARKER;
18445
+ }
18446
+ const entries = Object.entries(input);
18447
+ const kept = entries.length > MAX_OBJECT_KEYS ? entries.slice(0, MAX_OBJECT_KEYS) : entries;
18448
+ if (kept.length !== entries.length)
18449
+ truncated = true;
18450
+ return Object.fromEntries(kept.map(([k, v]) => [k, walk(v, depth + 1)]));
18451
+ }
18452
+ return input;
18453
+ };
18454
+ const bounded = walk(value, 1);
18455
+ return { value: bounded, truncated };
18456
+ }
18457
+
18458
+ // packages/store/src/bodies/mask.ts
18459
+ var ELIDED = "[redacted]";
18460
+ var BEARER = /\b(bearer)\s+[A-Za-z0-9._~+/=-]+/gi;
18461
+ var PREFIXED_KEY = /(?<![A-Za-z0-9_-])(?:sk|ak|pk)-[A-Za-z0-9_-]{8,}/g;
18462
+ var VENDOR_KEY = /(?<![A-Za-z0-9_-])(?:gh[posu]_|github_pat_|AIza|GOCSPX-)[A-Za-z0-9_-]{8,}/g;
18463
+ var VENDOR_PREFIX = /^(?:gh[posu]_|github_pat_|AIza|GOCSPX-)/;
18464
+ var XAI_KEY = /(?<![A-Za-z0-9_-])xai-[A-Za-z0-9_]{8,}/g;
18465
+ var OPAQUE = /[A-Za-z0-9_-]{41,}/g;
18466
+ var MASK_RULES = [
18467
+ {
18468
+ id: "bearer",
18469
+ pattern: BEARER,
18470
+ keep: (match) => match.length - match.replace(/^bearer\s+/i, "").length
18471
+ },
18472
+ { id: "prefixedKey", pattern: PREFIXED_KEY, keep: () => 3 },
18473
+ {
18474
+ id: "vendorKey",
18475
+ pattern: VENDOR_KEY,
18476
+ keep: (match) => VENDOR_PREFIX.exec(match)?.[0].length ?? 0
18477
+ },
18478
+ { id: "xaiKey", pattern: XAI_KEY, keep: () => "xai-".length },
18479
+ { id: "opaque", pattern: OPAQUE, keep: () => 0 }
18480
+ ];
18481
+ function maskString(value) {
18482
+ return MASK_RULES.reduce((masked, rule) => masked.replace(rule.pattern, (match) => `${match.slice(0, rule.keep(match))}${ELIDED}`), value);
18483
+ }
18484
+ function maskSecrets(value) {
18485
+ if (typeof value === "string")
18486
+ return maskString(value);
18487
+ if (Array.isArray(value))
18488
+ return value.map(maskSecrets);
18489
+ if (value !== null && typeof value === "object") {
18490
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, maskSecrets(v)]));
18491
+ }
18492
+ return value;
18493
+ }
18494
+
18495
+ // packages/store/src/bodies/artifact.ts
18496
+ var ARTIFACT_SCHEMA_VERSION = 1;
18497
+ var MAX_ARTIFACT_BYTES = 512 * 1024;
18498
+ var BODY_ROW_CAP = 1e5;
18499
+ var BODIES_DIRNAME = "request_bodies";
18500
+ var encoder2 = new TextEncoder;
18501
+ function bodiesDirFor(databasePath) {
18502
+ return join(dirname(databasePath), BODIES_DIRNAME);
18503
+ }
18504
+ var SAFE_REQUEST_ID = /^[A-Za-z0-9_-]{1,128}$/;
18505
+ function isSafeRequestId(id) {
18506
+ return SAFE_REQUEST_ID.test(id);
18507
+ }
18508
+ function relPathFor(requestId, at) {
18509
+ const date5 = new Date(at);
18510
+ const yyyy = String(date5.getUTCFullYear()).padStart(4, "0");
18511
+ const mm = String(date5.getUTCMonth() + 1).padStart(2, "0");
18512
+ const dd = String(date5.getUTCDate()).padStart(2, "0");
18513
+ return `${yyyy}/${mm}/${dd}/${requestId}.json.enc`;
18514
+ }
18515
+ async function sha256Hex(bytes) {
18516
+ const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
18517
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
18518
+ }
18519
+ function bodyPair(pair) {
18520
+ const request2 = boundValue(maskSecrets(pair.request));
18521
+ const response = boundValue(maskSecrets(pair.response));
18522
+ return {
18523
+ request: request2.value,
18524
+ response: response.value,
18525
+ truncated: pair.truncated === true || request2.truncated || response.truncated
18526
+ };
18527
+ }
18528
+ function omission(serializedBytes) {
18529
+ return {
18530
+ omitted: true,
18531
+ reason: `artifact exceeded ${MAX_ARTIFACT_BYTES} bytes after structural bounding`,
18532
+ serializedBytes
18533
+ };
18534
+ }
18535
+ function omitBodies(artifact, marker) {
18536
+ return {
18537
+ ...artifact,
18538
+ client: { request: marker, response: marker, truncated: true },
18539
+ attempts: artifact.attempts.map((attempt) => ({
18540
+ attempt: attempt.attempt,
18541
+ provider: attempt.provider,
18542
+ request: marker,
18543
+ response: marker,
18544
+ streamChunks: null,
18545
+ truncated: true
18546
+ }))
18547
+ };
18548
+ }
18549
+ function prepareArtifact(input) {
18550
+ const client = bodyPair(input.client);
18551
+ const attempts = input.attempts.map((attempt) => {
18552
+ const pair = bodyPair(attempt);
18553
+ const chunks = attempt.streamChunks === null ? null : boundValue(attempt.streamChunks.map(maskString));
18554
+ return {
18555
+ attempt: attempt.attempt,
18556
+ provider: attempt.provider,
18557
+ request: pair.request,
18558
+ response: pair.response,
18559
+ streamChunks: chunks === null ? null : chunks.value,
18560
+ truncated: pair.truncated || (chunks?.truncated ?? false)
18561
+ };
18562
+ });
18563
+ const error51 = boundValue(maskSecrets(input.error));
18564
+ const bounded = {
18565
+ schemaVersion: ARTIFACT_SCHEMA_VERSION,
18566
+ requestId: input.requestId,
18567
+ at: input.at,
18568
+ client,
18569
+ attempts,
18570
+ error: error51.value
18571
+ };
18572
+ const json7 = JSON.stringify(bounded);
18573
+ const size = encoder2.encode(json7).length;
18574
+ if (size <= MAX_ARTIFACT_BYTES)
18575
+ return { artifact: bounded, json: json7 };
18576
+ const marker = omission(size);
18577
+ const omitted = omitBodies(bounded, marker);
18578
+ const omittedJson = JSON.stringify(omitted);
18579
+ if (encoder2.encode(omittedJson).length <= MAX_ARTIFACT_BYTES) {
18580
+ return { artifact: omitted, json: omittedJson };
18581
+ }
18582
+ const stripped = { ...omitted, error: marker };
18583
+ return { artifact: stripped, json: JSON.stringify(stripped) };
18584
+ }
18585
+ async function sealArtifact(key, json7) {
18586
+ const bytes = encoder2.encode(await encrypt(key, json7));
18587
+ return { bytes, sha256: await sha256Hex(bytes) };
18588
+ }
18589
+ async function readArtifact(key, dir, relPath, expectedSha256) {
18590
+ let bytes;
18591
+ try {
18592
+ bytes = new Uint8Array(await readFile(join(dir, relPath)));
18593
+ } catch {
18594
+ return { ok: false, failure: "missing" };
18595
+ }
18596
+ try {
18597
+ if (expectedSha256 !== null && await sha256Hex(bytes) !== expectedSha256) {
18598
+ return { ok: false, failure: "corrupt" };
18599
+ }
18600
+ const parsed = JSON.parse(await decrypt(key, new TextDecoder().decode(bytes)));
18601
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
18602
+ return { ok: false, failure: "corrupt" };
18603
+ }
18604
+ return { ok: true, artifact: parsed };
18605
+ } catch {
18606
+ return { ok: false, failure: "corrupt" };
18607
+ }
18608
+ }
18609
+ async function writeArtifact(dir, relPath, bytes) {
18610
+ const full = join(dir, relPath);
18611
+ await mkdir(dirname(full), { recursive: true });
18612
+ await writeFile(full, bytes);
18613
+ }
18614
+ async function deleteArtifact(dir, relPath) {
18615
+ const full = join(dir, relPath);
18616
+ try {
18617
+ await unlink(full);
18618
+ } catch {
18619
+ return;
18620
+ }
18621
+ let parent = dirname(full);
18622
+ while (parent.length > dir.length && parent.startsWith(dir + sep)) {
18623
+ try {
18624
+ await rmdir(parent);
18625
+ } catch {
18626
+ return;
18627
+ }
18628
+ parent = dirname(parent);
18629
+ }
18630
+ }
18631
+ async function listArtifacts(dir) {
18632
+ const out = [];
18633
+ const walk = async (current, prefix) => {
18634
+ let entries;
18635
+ try {
18636
+ entries = await readdir(current, { withFileTypes: true });
18637
+ } catch {
18638
+ return;
18639
+ }
18640
+ for (const entry of entries) {
18641
+ const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
18642
+ if (entry.isDirectory())
18643
+ await walk(join(current, entry.name), rel);
18644
+ else if (entry.isFile())
18645
+ out.push(rel);
18646
+ }
18647
+ };
18648
+ await walk(dir, "");
18649
+ return out;
18650
+ }
18651
+ // packages/store/src/sqlite/bodies.ts
18652
+ function toDetailState(value) {
18653
+ return value === "ready" || value === "missing" || value === "corrupt" ? value : "none";
18654
+ }
18655
+ var toRow = (r) => ({
18656
+ requestId: r.request_id,
18657
+ at: r.at,
18658
+ relPath: r.rel_path,
18659
+ sizeBytes: r.size_bytes,
18660
+ sha256: r.sha256,
18661
+ detailState: toDetailState(r.detail_state),
18662
+ truncated: r.truncated === 1
18663
+ });
18664
+ function createBodyRepo(db, key, dir) {
18665
+ const removeRows = async (rows) => {
18666
+ for (const row of rows) {
18667
+ if (row.relPath !== null)
18668
+ await deleteArtifact(dir, row.relPath);
18669
+ }
18670
+ const remove = db.prepare("DELETE FROM request_bodies WHERE request_id = ?");
18671
+ db.transaction(() => {
18672
+ for (const row of rows)
18673
+ remove.run(row.requestId);
18674
+ })();
18675
+ return rows.length;
18676
+ };
18677
+ const recordState = (requestId, state) => {
18678
+ db.run("UPDATE request_bodies SET detail_state = ? WHERE request_id = ?", [state, requestId]);
18679
+ };
18680
+ return {
18681
+ async put(artifact) {
18682
+ if (!isSafeRequestId(artifact.requestId)) {
18683
+ throw new Error("request id is not safe to use as an artifact path segment");
18684
+ }
18685
+ const prepared = prepareArtifact(artifact);
18686
+ const sealed = await sealArtifact(key, prepared.json);
18687
+ const relPath = relPathFor(artifact.requestId, artifact.at);
18688
+ await writeArtifact(dir, relPath, sealed.bytes);
18689
+ const truncated = prepared.artifact.client.truncated || prepared.artifact.attempts.some((attempt) => attempt.truncated);
18690
+ const row = {
18691
+ requestId: artifact.requestId,
18692
+ at: artifact.at,
18693
+ relPath,
18694
+ sizeBytes: sealed.bytes.length,
18695
+ sha256: sealed.sha256,
18696
+ detailState: "ready",
18697
+ truncated
18698
+ };
18699
+ db.run(`INSERT INTO request_bodies (request_id, at, rel_path, size_bytes, sha256, detail_state, truncated)
18700
+ VALUES (?,?,?,?,?,?,?)
18701
+ ON CONFLICT (request_id) DO UPDATE SET
18702
+ at = excluded.at,
18703
+ rel_path = excluded.rel_path,
18704
+ size_bytes = excluded.size_bytes,
18705
+ sha256 = excluded.sha256,
18706
+ detail_state = excluded.detail_state,
18707
+ truncated = excluded.truncated`, [
18708
+ row.requestId,
18709
+ row.at,
18710
+ row.relPath,
18711
+ row.sizeBytes,
18712
+ row.sha256,
18713
+ row.detailState,
18714
+ row.truncated ? 1 : 0
18715
+ ]);
18716
+ return row;
18717
+ },
18718
+ async get(requestId) {
18719
+ const found = db.query("SELECT * FROM request_bodies WHERE request_id = ?").get(requestId);
18720
+ if (found === null)
18721
+ return null;
18722
+ const row = toRow(found);
18723
+ if (row.relPath === null || row.detailState === "none")
18724
+ return { row, artifact: null };
18725
+ const read = await readArtifact(key, dir, row.relPath, row.sha256);
18726
+ if (read.ok) {
18727
+ if (row.detailState !== "ready")
18728
+ recordState(requestId, "ready");
18729
+ return { row: { ...row, detailState: "ready" }, artifact: read.artifact };
18730
+ }
18731
+ recordState(requestId, read.failure);
18732
+ return { row: { ...row, detailState: read.failure }, artifact: null };
18733
+ },
18734
+ async prune(olderThan) {
18735
+ const rows = db.query("SELECT * FROM request_bodies WHERE at < ?").all(olderThan).map(toRow);
18736
+ return removeRows(rows);
18737
+ },
18738
+ async pruneToCap(cap = BODY_ROW_CAP) {
18739
+ const total = db.query("SELECT COUNT(*) AS n FROM request_bodies").get()?.n ?? 0;
18740
+ if (total <= cap)
18741
+ return 0;
18742
+ const rows = db.query("SELECT * FROM request_bodies ORDER BY at ASC, request_id ASC LIMIT ?").all(total - cap).map(toRow);
18743
+ return removeRows(rows);
18744
+ },
18745
+ async sweepOrphans() {
18746
+ const known = new Set(db.query("SELECT rel_path FROM request_bodies WHERE rel_path IS NOT NULL").all().map((r) => r.rel_path));
18747
+ const claimed = db.query("SELECT COUNT(*) AS n FROM request_bodies WHERE rel_path = ?");
18748
+ let removed = 0;
18749
+ for (const rel of await listArtifacts(dir)) {
18750
+ if (known.has(rel))
18751
+ continue;
18752
+ if ((claimed.get(rel)?.n ?? 0) > 0)
18753
+ continue;
18754
+ await deleteArtifact(dir, rel);
18755
+ removed += 1;
18756
+ }
18757
+ return removed;
18758
+ }
18759
+ };
18760
+ }
18309
18761
  // packages/store/src/sqlite/config.ts
18310
18762
  var SETTINGS_KEY = "settings";
18311
18763
  var ADMIN_HASH_KEY = "adminPasswordHash";
@@ -18361,6 +18813,8 @@ function createConfigRepo(db, emit2 = () => {}) {
18361
18813
  ...DEFAULT_SETTINGS,
18362
18814
  ...stored,
18363
18815
  rtkEnabled: stored.rtkEnabled === true,
18816
+ bodyLoggingEnabled: stored.bodyLoggingEnabled === true,
18817
+ bodyLoggingCaptureStreamChunks: stored.bodyLoggingCaptureStreamChunks === true,
18364
18818
  weights: knownWeights(stored.weights)
18365
18819
  };
18366
18820
  } catch {
@@ -18391,6 +18845,29 @@ function createConfigRepo(db, emit2 = () => {}) {
18391
18845
  };
18392
18846
  }
18393
18847
  // packages/store/src/sqlite/credentials.ts
18848
+ function toHealth(r) {
18849
+ return {
18850
+ credentialId: r.credential_id,
18851
+ model: r.model,
18852
+ breakerState: r.breaker_state,
18853
+ consecutiveFailures: r.consecutive_failures,
18854
+ openedAt: r.opened_at,
18855
+ rateLimitedUntil: r.rate_limited_until,
18856
+ ewmaTtftMs: r.ewma_ttft_ms,
18857
+ lastUsedAt: r.last_used_at
18858
+ };
18859
+ }
18860
+ var UPSERT_HEALTH = `INSERT INTO credential_health
18861
+ (credential_id, model, breaker_state, consecutive_failures, opened_at,
18862
+ rate_limited_until, ewma_ttft_ms, last_used_at)
18863
+ VALUES (?,?,?,?,?,?,?,?)
18864
+ ON CONFLICT (credential_id, model) DO UPDATE SET
18865
+ breaker_state = excluded.breaker_state,
18866
+ consecutive_failures = excluded.consecutive_failures,
18867
+ opened_at = excluded.opened_at,
18868
+ rate_limited_until = excluded.rate_limited_until,
18869
+ ewma_ttft_ms = excluded.ewma_ttft_ms,
18870
+ last_used_at = excluded.last_used_at`;
18394
18871
  function createCredentialRepo(db, key, emit2 = () => {}) {
18395
18872
  const open = async (v) => v === null ? null : decrypt(key, v);
18396
18873
  const secretsFrom = async (row) => ({
@@ -18565,29 +19042,10 @@ function createCredentialRepo(db, key, emit2 = () => {}) {
18565
19042
  emit2({ type: "credentialsChanged" });
18566
19043
  },
18567
19044
  async listHealth() {
18568
- return db.query("SELECT * FROM credential_health").all().map((r) => ({
18569
- credentialId: r.credential_id,
18570
- model: r.model,
18571
- breakerState: r.breaker_state,
18572
- consecutiveFailures: r.consecutive_failures,
18573
- openedAt: r.opened_at,
18574
- rateLimitedUntil: r.rate_limited_until,
18575
- ewmaTtftMs: r.ewma_ttft_ms,
18576
- lastUsedAt: r.last_used_at
18577
- }));
19045
+ return db.query("SELECT * FROM credential_health").all().map(toHealth);
18578
19046
  },
18579
19047
  async saveHealth(rows) {
18580
- const stmt = db.prepare(`INSERT INTO credential_health
18581
- (credential_id, model, breaker_state, consecutive_failures, opened_at,
18582
- rate_limited_until, ewma_ttft_ms, last_used_at)
18583
- VALUES (?,?,?,?,?,?,?,?)
18584
- ON CONFLICT (credential_id, model) DO UPDATE SET
18585
- breaker_state = excluded.breaker_state,
18586
- consecutive_failures = excluded.consecutive_failures,
18587
- opened_at = excluded.opened_at,
18588
- rate_limited_until = excluded.rate_limited_until,
18589
- ewma_ttft_ms = excluded.ewma_ttft_ms,
18590
- last_used_at = excluded.last_used_at`);
19048
+ const stmt = db.prepare(UPSERT_HEALTH);
18591
19049
  db.transaction(() => {
18592
19050
  for (const r of rows) {
18593
19051
  stmt.run(r.credentialId, r.model, r.breakerState, r.consecutiveFailures, r.openedAt, r.rateLimitedUntil, r.ewmaTtftMs, r.lastUsedAt);
@@ -18595,6 +19053,18 @@ function createCredentialRepo(db, key, emit2 = () => {}) {
18595
19053
  })();
18596
19054
  emit2({ type: "healthSaved", rows });
18597
19055
  },
19056
+ async updateHealth(credentialId, model, apply) {
19057
+ const current = db.prepare("SELECT * FROM credential_health WHERE credential_id = ? AND model = ?");
19058
+ const stmt = db.prepare(UPSERT_HEALTH);
19059
+ const written = db.transaction(() => {
19060
+ const row = current.get(credentialId, model);
19061
+ const next = apply(row === null ? null : toHealth(row));
19062
+ stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil, next.ewmaTtftMs, next.lastUsedAt);
19063
+ return next;
19064
+ })();
19065
+ emit2({ type: "healthSaved", rows: [written] });
19066
+ return written;
19067
+ },
18598
19068
  async listQuota() {
18599
19069
  return db.query("SELECT * FROM quota_windows").all().map((r) => ({
18600
19070
  credentialId: r.credential_id,
@@ -18906,7 +19376,219 @@ CREATE INDEX quota_samples_observed ON quota_samples (observed_at);
18906
19376
  ALTER TABLE quota_windows ADD COLUMN window_ms INTEGER;
18907
19377
  `;
18908
19378
 
19379
+ // packages/store/src/sqlite/migrations/008_body_logging.sql
19380
+ var _008_body_logging_default = `-- Captured request and response bodies, and the api key opt-out that suppresses
19381
+ -- them.
19382
+ --
19383
+ -- The row holds a pointer, never a body. Bodies are the largest thing this
19384
+ -- gateway could ever store \u2014 a single conversation with a pasted file in it
19385
+ -- dwarfs the whole of \`request_logs\` \u2014 and SQLite would carry every one of them
19386
+ -- through the same page cache, the same WAL, and the same backup that the
19387
+ -- routing tables live in. A prompt corpus inlined here would make the operating
19388
+ -- database grow without bound, make every \`VACUUM\` copy it, and put plaintext
19389
+ -- rows one \`sqlite3\` invocation away. The artifact instead lives at
19390
+ -- \`<dirname(databasePath)>/request_bodies/YYYY/MM/DD/<request_id>.json.enc\`,
19391
+ -- encrypted under the same key as provider credentials, and this row records
19392
+ -- only where it is and whether it can still be trusted. OmniRoute reached the
19393
+ -- same layout the expensive way: its \`call_logs_v1_legacy\` table exists purely
19394
+ -- to drain inline blobs it had already shipped into file artifacts.
19395
+ --
19396
+ -- \`rel_path\` is relative so moving an installation does not invalidate every
19397
+ -- row, and nullable because a row can exist with no artifact behind it: capture
19398
+ -- may have been suppressed, or the file may have been swept.
19399
+ --
19400
+ -- \`sha256\` is taken over the bytes as stored \u2014 the ciphertext \u2014 not over the
19401
+ -- plaintext. Truncation or bit-rot on disk is then detectable by a reader that
19402
+ -- does not hold \`OMNI_ENCRYPTION_KEY\` at all, which is what makes \`corrupt\` a
19403
+ -- state the reader can report rather than an exception it has to raise.
19404
+ --
19405
+ -- \`detail_state\` is one of \`none\`, \`ready\`, \`missing\`, or \`corrupt\`. A file tree
19406
+ -- and a table it is not written to transactionally will drift \u2014 a crash between
19407
+ -- the two writes is enough \u2014 so the reader records what it observed and hands
19408
+ -- back the metadata. \`missing\` and \`corrupt\` are answers, not failures.
19409
+ --
19410
+ -- \`truncated\` is set when structural bounding altered anything, so a reader can
19411
+ -- say "this is not the whole payload" without diffing it against nothing.
19412
+ --
19413
+ -- No foreign key to \`request_logs\`. Expiry is performed explicitly, deleting the
19414
+ -- file and the row together, because \`ON DELETE CASCADE\` only fires while the
19415
+ -- \`foreign_keys\` pragma is on: a pragma silently off would turn expiry of a
19416
+ -- prompt corpus into indefinite retention of one, and that failure is invisible
19417
+ -- until someone goes looking.
19418
+ CREATE TABLE request_bodies (
19419
+ request_id TEXT PRIMARY KEY,
19420
+ at INTEGER NOT NULL,
19421
+ rel_path TEXT,
19422
+ size_bytes INTEGER NOT NULL DEFAULT 0,
19423
+ sha256 TEXT,
19424
+ detail_state TEXT NOT NULL DEFAULT 'none',
19425
+ truncated INTEGER NOT NULL DEFAULT 0
19426
+ );
19427
+
19428
+ -- Descending, because every read of this table is time-ordered from the newest
19429
+ -- end: the retention sweep walks the oldest rows and the row cap walks them in
19430
+ -- the same direction, and the console only ever asks about recent requests.
19431
+ CREATE INDEX idx_request_bodies_at ON request_bodies (at DESC);
19432
+
19433
+ -- Per-key suppression, checked before any capture work begins.
19434
+ --
19435
+ -- A shared installation can be serving one client whose payloads must not be
19436
+ -- retained while capturing everything else, and that client's operator cannot be
19437
+ -- asked to trust a global switch they do not control. Default 0 keeps every
19438
+ -- existing key on the installation-wide setting, which is itself off by default.
19439
+ ALTER TABLE api_keys ADD COLUMN body_logging_opt_out INTEGER NOT NULL DEFAULT 0;
19440
+ `;
19441
+
19442
+ // packages/store/src/sqlite/migrations/009_key_limits.sql
19443
+ var _009_key_limits_default = `-- Per-key limits as a sparse \`(dimension, window)\` matrix, replacing the single
19444
+ -- requests-per-minute integer.
19445
+ --
19446
+ -- One JSON column rather than a column per pair, because the matrix is sparse
19447
+ -- and the pairs are not fixed: \`requests\` and \`tokens\` are meaningful at all
19448
+ -- three windows, \`spend\` only at the two long ones, and \`concurrency\` is a gauge
19449
+ -- with no window at all. Twelve mostly-null integer columns would encode the
19450
+ -- same thing while making every future pair a migration.
19451
+ --
19452
+ -- The JSON keys are a storage contract in the same class as \`RTK_FILTER_IDS\`:
19453
+ -- the dimension and window names are persisted in every row, so adding a name is
19454
+ -- free and renaming or removing one loses data. Unlike RTK ids, which are
19455
+ -- dropped silently on read, an unknown limit key is a parse failure \u2014 a limit
19456
+ -- the gateway cannot understand must never be read as "no limit", because that
19457
+ -- fails open on a control the operator explicitly set.
19458
+ --
19459
+ -- \`NOT NULL DEFAULT '{}'\` rather than nullable, so "unlimited" has exactly one
19460
+ -- spelling at the column level and every reader parses the same shape.
19461
+ ALTER TABLE api_keys ADD COLUMN limits TEXT NOT NULL DEFAULT '{}';
19462
+
19463
+ -- The old ceiling was requests per minute and nothing else, so it lands whole in
19464
+ -- \`requests["1m"]\`. A NULL meant unlimited and stays unlimited as the default
19465
+ -- \`{}\`: an absent key and an explicit null both mean the same thing, and the
19466
+ -- empty object is the shape a newly minted key starts at.
19467
+ --
19468
+ -- Only a value the reader accepts is carried over. \`rate_limit_per_min\` was
19469
+ -- \`INTEGER\` with no \`CHECK\`, so a hand-edited install can hold \`0\`, \`-5\` or
19470
+ -- \`1.5\` in it, while the new schema is \`z.number().int().positive()\`. Backfilled
19471
+ -- unconditionally, each of those writes a matrix \`parseLimitConfig\` refuses \u2014
19472
+ -- which is \`limits: null\`, which is every request for that key answered
19473
+ -- \`INTERNAL\` at the auth chokepoint. An upgrade must not manufacture the
19474
+ -- unreadable row the design describes as arising only from meddling.
19475
+ --
19476
+ -- Anything else stays at the \`{}\` default, which is unlimited: a ceiling that
19477
+ -- was already nonsense bounded nothing before this migration either, so
19478
+ -- dropping it changes no behaviour and leaves the key serving.
19479
+ --
19480
+ -- \`typeof()\` rather than a range test alone. SQLite's INTEGER affinity is a
19481
+ -- preference, not a constraint: \`1.5\` stays REAL and \`'sixty'\` stays TEXT, and
19482
+ -- TEXT sorts above every number so a bare \`> 0\` would admit the string.
19483
+ UPDATE api_keys
19484
+ SET limits = json_object('requests', json_object('1m', rate_limit_per_min))
19485
+ WHERE typeof(rate_limit_per_min) = 'integer'
19486
+ AND rate_limit_per_min > 0;
19487
+
19488
+ ALTER TABLE api_keys DROP COLUMN rate_limit_per_min;
19489
+
19490
+ -- Correctness-adjacent, not an optimisation, and it must not be dropped later as
19491
+ -- redundant with \`idx_request_logs_at\`.
19492
+ --
19493
+ -- \`request_logs\` leads its existing indexes with \`at\` and with \`credential_id\`;
19494
+ -- nothing leads with \`api_key_id\`. A sliding weekly sum for one key against
19495
+ -- \`idx_request_logs_at\` scans every row in the week for every key on the
19496
+ -- install, and that scan sits on the request hot path.
19497
+ --
19498
+ -- Composite order matters: \`(api_key_id, at DESC)\` lets the range scan start at
19499
+ -- the key. \`(at DESC, api_key_id)\` does not.
19500
+ CREATE INDEX idx_request_logs_key_at ON request_logs (api_key_id, at DESC);
19501
+ `;
19502
+
19503
+ // packages/store/src/sqlite/migrations/010_usage_rollup.sql
19504
+ var _010_usage_rollup_default = `-- Per-key hourly counters, so a long sliding window is read rather than scanned.
19505
+ --
19506
+ -- \`sumSince\` is on the admission path of every request, and \`bun:sqlite\` is
19507
+ -- synchronous: a \`SELECT SUM\` over a week of one key's rows blocks the entire
19508
+ -- event loop for its duration \u2014 \`/health\`, \`/api/*\`, the quiesce latch, and
19509
+ -- every other key's traffic with it. Measured on one machine, one key, WAL:
19510
+ -- 149ms at 0.2M rows in the window, 2.1s at 2M, 10.8s at 8M. The cost is
19511
+ -- O(accumulated history) and has no ceiling, and the eager-refresh rule makes it
19512
+ -- worst exactly when a key is busiest \u2014 a key inside the last tenth of a long
19513
+ -- ceiling reads through on every admission. The same reads against this table
19514
+ -- are flat at ~0.017ms, because a week is 168 buckets however much traffic each
19515
+ -- one summarises.
19516
+ --
19517
+ -- This reverses the design's own rejection of a counter table, and the reason it
19518
+ -- can be reversed is that this table is *derived*. \`request_logs\` stays the
19519
+ -- source of truth; every figure here is reproducible from it by one grouped
19520
+ -- select. The original objection was that a counter table can disagree with the
19521
+ -- log and nobody can tell which is right \u2014 that objection is answered by there
19522
+ -- being a rebuild, a restore that runs it, and a \`doctor\` check that compares
19523
+ -- the two. It would not be answered by care.
19524
+ --
19525
+ -- \`hour\` is \`at / 3600000\`, floored, and every SQL side spells that
19526
+ -- \`CAST(at / 3600000 AS INTEGER)\`. The cast is not decoration: nothing validates
19527
+ -- \`at\`, and SQLite's \`/\` is integer division only when both operands are
19528
+ -- integers, so a fractional \`at\` would give this INTEGER column a REAL key that
19529
+ -- no later integer-hour write ever merges with. Truncation toward zero and
19530
+ -- JavaScript's \`Math.floor\` then agree on every epoch this side of 1970, which
19531
+ -- is every epoch a request log holds.
19532
+ --
19533
+ -- \`api_key_id\` is NOT NULL and anonymous rows are left out: a WITHOUT ROWID
19534
+ -- primary key cannot hold a NULL, and nothing reads this table except a per-key
19535
+ -- lookup that could never match one.
19536
+ CREATE TABLE usage_rollup (
19537
+ api_key_id TEXT NOT NULL,
19538
+ hour INTEGER NOT NULL,
19539
+ requests INTEGER NOT NULL DEFAULT 0,
19540
+ input_tokens INTEGER NOT NULL DEFAULT 0,
19541
+ output_tokens INTEGER NOT NULL DEFAULT 0,
19542
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
19543
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
19544
+ cost_usd REAL NOT NULL DEFAULT 0,
19545
+ PRIMARY KEY (api_key_id, hour)
19546
+ ) WITHOUT ROWID;
19547
+
19548
+ -- The backfill is this migration's \`after\` hook rather than an INSERT here, and
19549
+ -- it is the same \`rebuildRollup\` a restore runs. Seeding an existing install and
19550
+ -- repairing a suspect one are the same statement, so they are the same code and
19551
+ -- cannot drift into disagreeing about which rows count.
19552
+ `;
19553
+
19554
+ // packages/store/src/sqlite/migrations/011_plugin_migrations.sql
19555
+ var _011_plugin_migrations_default = `-- The plugin migration ledger, kept deliberately apart from core's \`migrations\`.
19556
+ --
19557
+ -- Core's track is a single ascending integer owned by this repository. A plugin
19558
+ -- ships its own numbered migrations and knows nothing about ours, so the two
19559
+ -- cannot share a counter: if they did, installing a plugin would consume numbers
19560
+ -- that core's next migration then collides with, and the collision would surface
19561
+ -- as "migration already applied" on an upgrade rather than as anything legible.
19562
+ -- Core's next migration is 012 regardless of how many migrations any plugin
19563
+ -- ships, and a plugin's version 1 is its own version 1 forever.
19564
+ --
19565
+ -- The primary key is (plugin_id, version) rather than a surrogate id because
19566
+ -- that pair *is* the identity: applying the same version twice is the thing this
19567
+ -- table exists to prevent, and a uniqueness constraint states it once instead of
19568
+ -- leaving it to whichever code path happens to check first.
19569
+ --
19570
+ -- \`applied_at\` is milliseconds, matching every other timestamp in this schema.
19571
+ -- It is recorded for an operator reading the table during an incident, not read
19572
+ -- by the host: the host asks only whether a row exists.
19573
+ --
19574
+ -- Note what is *not* here: no row for the plugin's tables, and no cascade. A
19575
+ -- plugin's tables outlive its rows here on purpose \u2014 see \`PluginRepo.dropAll\`
19576
+ -- and \`orphanTables\`. Dropping a table because a plugin is currently absent
19577
+ -- destroys data irreversibly, and a restore is precisely the moment a plugin is
19578
+ -- most likely to be absent.
19579
+ CREATE TABLE plugin_migrations (
19580
+ plugin_id TEXT NOT NULL,
19581
+ version INTEGER NOT NULL,
19582
+ applied_at INTEGER NOT NULL,
19583
+ PRIMARY KEY (plugin_id, version)
19584
+ );
19585
+ `;
19586
+
18909
19587
  // packages/store/src/sqlite/rollup.ts
19588
+ var HOUR_MS = 3600000;
19589
+ function hourOf(at) {
19590
+ return Math.floor(at / HOUR_MS);
19591
+ }
18910
19592
  function startOfLocalDay(at) {
18911
19593
  const day = new Date(at);
18912
19594
  day.setHours(0, 0, 0, 0);
@@ -19015,6 +19697,88 @@ function backfillDaily(db) {
19015
19697
  upsert(db, group.key, group.counters);
19016
19698
  return groups.size;
19017
19699
  }
19700
+ var HOURLY_UPSERT = `
19701
+ INSERT INTO usage_rollup
19702
+ (api_key_id, hour, requests, input_tokens, output_tokens,
19703
+ cache_read_tokens, cache_write_tokens, cost_usd)
19704
+ VALUES (?,?,?,?,?,?,?,?)
19705
+ ON CONFLICT (api_key_id, hour)
19706
+ DO UPDATE SET
19707
+ requests = requests + excluded.requests,
19708
+ input_tokens = input_tokens + excluded.input_tokens,
19709
+ output_tokens = output_tokens + excluded.output_tokens,
19710
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
19711
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
19712
+ cost_usd = cost_usd + excluded.cost_usd`;
19713
+ function rollupHour(db, log) {
19714
+ const apiKeyId = log.apiKeyId;
19715
+ if (apiKeyId === null)
19716
+ return;
19717
+ db.run(HOURLY_UPSERT, [
19718
+ apiKeyId,
19719
+ hourOf(log.at),
19720
+ 1,
19721
+ log.inputTokens,
19722
+ log.outputTokens,
19723
+ log.cacheReadTokens,
19724
+ log.cacheWriteTokens,
19725
+ log.costUsd
19726
+ ]);
19727
+ }
19728
+ var REBUILD = `
19729
+ INSERT INTO usage_rollup
19730
+ (api_key_id, hour, requests, input_tokens, output_tokens,
19731
+ cache_read_tokens, cache_write_tokens, cost_usd)
19732
+ SELECT api_key_id,
19733
+ CAST(at / 3600000 AS INTEGER),
19734
+ COUNT(*),
19735
+ COALESCE(SUM(input_tokens), 0),
19736
+ COALESCE(SUM(output_tokens), 0),
19737
+ COALESCE(SUM(cache_read_tokens), 0),
19738
+ COALESCE(SUM(cache_write_tokens), 0),
19739
+ COALESCE(SUM(cost_usd), 0)
19740
+ FROM request_logs
19741
+ WHERE state = 'done' AND api_key_id IS NOT NULL
19742
+ GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
19743
+ function rebuildRollup(db) {
19744
+ db.run("DELETE FROM usage_rollup");
19745
+ db.run(REBUILD);
19746
+ }
19747
+ function auditRollup(db) {
19748
+ const row = db.query(`WITH truth AS (
19749
+ SELECT api_key_id AS k,
19750
+ CAST(at / 3600000 AS INTEGER) AS h,
19751
+ COUNT(*) AS requests,
19752
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
19753
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
19754
+ COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
19755
+ COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
19756
+ COALESCE(SUM(cost_usd), 0) AS cost_usd
19757
+ FROM request_logs
19758
+ WHERE state = 'done' AND api_key_id IS NOT NULL
19759
+ GROUP BY k, h
19760
+ )
19761
+ SELECT (SELECT COUNT(*) FROM truth) AS buckets,
19762
+ (SELECT COUNT(*)
19763
+ FROM truth
19764
+ LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
19765
+ WHERE r.api_key_id IS NULL
19766
+ OR r.requests <> truth.requests
19767
+ OR r.input_tokens <> truth.input_tokens
19768
+ OR r.output_tokens <> truth.output_tokens
19769
+ OR r.cache_read_tokens <> truth.cache_read_tokens
19770
+ OR r.cache_write_tokens <> truth.cache_write_tokens
19771
+ OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
19772
+ ) AS mismatched,
19773
+ (SELECT COUNT(*)
19774
+ FROM usage_rollup r
19775
+ LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
19776
+ WHERE truth.k IS NULL
19777
+ ) AS orphans`).get();
19778
+ const buckets = row?.buckets ?? 0;
19779
+ const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
19780
+ return { buckets, mismatched, ok: mismatched === 0 };
19781
+ }
19018
19782
  function backfillRtkUsage(db) {
19019
19783
  const groups = new Map;
19020
19784
  for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
@@ -19051,7 +19815,11 @@ var MIGRATIONS = [
19051
19815
  { id: 4, sql: _004_request_state_default },
19052
19816
  { id: 5, sql: _005_rtk_metrics_default },
19053
19817
  { id: 6, sql: _006_rtk_usage_default, after: backfillRtkUsage },
19054
- { id: 7, sql: _007_quota_samples_default }
19818
+ { id: 7, sql: _007_quota_samples_default },
19819
+ { id: 8, sql: _008_body_logging_default },
19820
+ { id: 9, sql: _009_key_limits_default },
19821
+ { id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
19822
+ { id: 11, sql: _011_plugin_migrations_default }
19055
19823
  ];
19056
19824
  function openDb(path) {
19057
19825
  const db = new Database(path, { create: true });
@@ -19082,44 +19850,290 @@ async function hashApiKey(raw) {
19082
19850
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(raw));
19083
19851
  return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
19084
19852
  }
19085
- var toKey = (r) => ({
19853
+ function parseLimits(id, raw, logger2) {
19854
+ try {
19855
+ const parsed = JSON.parse(raw);
19856
+ return parseLimitConfig(parsed);
19857
+ } catch {
19858
+ logger2.error("api key limits unreadable", { apiKeyId: id });
19859
+ return null;
19860
+ }
19861
+ }
19862
+ var toKey = (r, logger2) => ({
19086
19863
  id: r.id,
19087
19864
  label: r.label,
19088
19865
  prefix: r.prefix,
19089
19866
  hash: r.hash,
19090
19867
  modelAllowlist: r.model_allowlist === null ? null : JSON.parse(r.model_allowlist),
19091
- rateLimitPerMin: r.rate_limit_per_min,
19868
+ limits: parseLimits(r.id, r.limits, logger2),
19869
+ bodyLoggingOptOut: r.body_logging_opt_out === 1,
19092
19870
  createdAt: r.created_at,
19093
19871
  revokedAt: r.revoked_at
19094
19872
  });
19095
- function createKeyRepo(db) {
19873
+ function createKeyRepo(db, logger2 = noopLogger) {
19096
19874
  return {
19097
19875
  async list() {
19098
- return db.query("SELECT * FROM api_keys ORDER BY created_at DESC").all().map(toKey);
19876
+ return db.query("SELECT * FROM api_keys ORDER BY created_at DESC").all().map((row) => toKey(row, logger2));
19099
19877
  },
19100
19878
  async findByHash(hash3) {
19101
19879
  const row = db.query("SELECT * FROM api_keys WHERE hash = ?").get(hash3);
19102
- return row ? toKey(row) : null;
19880
+ return row ? toKey(row, logger2) : null;
19103
19881
  },
19104
19882
  async create(input) {
19105
19883
  const now = Date.now();
19106
- db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, rate_limit_per_min, created_at, revoked_at)
19107
- VALUES (?,?,?,?,?,?,?,NULL)`, [
19884
+ db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
19885
+ body_logging_opt_out, created_at, revoked_at)
19886
+ VALUES (?,?,?,?,?,?,?,?,NULL)`, [
19108
19887
  input.id,
19109
19888
  input.label,
19110
19889
  input.prefix,
19111
19890
  input.hash,
19112
19891
  input.modelAllowlist === null ? null : JSON.stringify(input.modelAllowlist),
19113
- input.rateLimitPerMin,
19892
+ JSON.stringify(parseLimitConfig(input.limits)),
19893
+ input.bodyLoggingOptOut ? 1 : 0,
19114
19894
  now
19115
19895
  ]);
19116
19896
  return { ...input, createdAt: now, revokedAt: null };
19117
19897
  },
19898
+ async setLimits(id, limits) {
19899
+ db.run("UPDATE api_keys SET limits = ? WHERE id = ?", [
19900
+ JSON.stringify(parseLimitConfig(limits)),
19901
+ id
19902
+ ]);
19903
+ },
19118
19904
  async revoke(id) {
19119
19905
  db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
19120
19906
  }
19121
19907
  };
19122
19908
  }
19909
+ // packages/store/src/sqlite/maintenance.ts
19910
+ import { Database as Database2 } from "bun:sqlite";
19911
+ var REQUIRED_TABLES = [
19912
+ "api_keys",
19913
+ "credential_health",
19914
+ "credentials",
19915
+ "migrations",
19916
+ "quota_windows",
19917
+ "request_logs",
19918
+ "settings",
19919
+ "virtual_models"
19920
+ ];
19921
+ var UNREADABLE = "unreadable";
19922
+ function sqlLiteral(value) {
19923
+ return `'${value.replaceAll("'", "''")}'`;
19924
+ }
19925
+ function createMaintenanceRepo(db) {
19926
+ return {
19927
+ async stats() {
19928
+ const pageSize = db.query("PRAGMA page_size").get();
19929
+ const pageCount = db.query("PRAGMA page_count").get();
19930
+ const freelist = db.query("PRAGMA freelist_count").get();
19931
+ const version2 = db.query("SELECT MAX(id) AS version FROM migrations").get();
19932
+ return {
19933
+ pageSize: pageSize?.page_size ?? 0,
19934
+ pageCount: pageCount?.page_count ?? 0,
19935
+ freelistCount: freelist?.freelist_count ?? 0,
19936
+ schemaVersion: version2?.version ?? 0
19937
+ };
19938
+ },
19939
+ async vacuum() {
19940
+ db.run("VACUUM");
19941
+ db.run("PRAGMA wal_checkpoint(TRUNCATE)");
19942
+ },
19943
+ async snapshotTo(path) {
19944
+ db.run(`VACUUM INTO ${sqlLiteral(path)}`);
19945
+ },
19946
+ async inspect(path) {
19947
+ let file2;
19948
+ try {
19949
+ file2 = new Database2(path, { readonly: true });
19950
+ } catch {
19951
+ return { ok: false, quickCheck: UNREADABLE, tables: [], counts: {} };
19952
+ }
19953
+ try {
19954
+ const problems = file2.query("PRAGMA quick_check").all();
19955
+ const quickCheck = problems[0]?.quick_check ?? UNREADABLE;
19956
+ const tables = file2.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' " + "ORDER BY name").all().map((r) => r.name);
19957
+ const present = new Set(tables);
19958
+ const counts = {};
19959
+ for (const table2 of REQUIRED_TABLES) {
19960
+ if (!present.has(table2))
19961
+ continue;
19962
+ const n = file2.query(`SELECT COUNT(*) AS n FROM ${table2}`).get();
19963
+ counts[table2] = n?.n ?? 0;
19964
+ }
19965
+ const complete = REQUIRED_TABLES.every((t) => present.has(t));
19966
+ return { ok: quickCheck === "ok" && complete, quickCheck, tables, counts };
19967
+ } catch {
19968
+ return { ok: false, quickCheck: UNREADABLE, tables: [], counts: {} };
19969
+ } finally {
19970
+ file2.close();
19971
+ }
19972
+ }
19973
+ };
19974
+ }
19975
+ // packages/store/src/sqlite/plugins.ts
19976
+ var PLUGIN_ID = /^[a-z][a-z0-9-]{0,31}$/;
19977
+ var TABLE_NAME = /^[a-z][a-z0-9_]{0,31}$/;
19978
+ var PLACEHOLDER = /\{\{([^{}]*)\}\}/g;
19979
+ var CORE_TABLES = [
19980
+ "api_keys",
19981
+ "credential_health",
19982
+ "credentials",
19983
+ "migrations",
19984
+ "plugin_migrations",
19985
+ "quota_samples",
19986
+ "quota_windows",
19987
+ "request_bodies",
19988
+ "request_logs",
19989
+ "settings",
19990
+ "usage_daily",
19991
+ "usage_rollup",
19992
+ "virtual_models"
19993
+ ];
19994
+ var CORE_TABLE_REFERENCE = new RegExp(`\\b(?:${CORE_TABLES.join("|")})\\b`);
19995
+ var PREFIX3 = "plugin_";
19996
+ function stripNoise(sql) {
19997
+ let out = "";
19998
+ let i = 0;
19999
+ while (i < sql.length) {
20000
+ const c = sql[i];
20001
+ const next = sql[i + 1];
20002
+ if (c === "'") {
20003
+ i += 1;
20004
+ while (i < sql.length) {
20005
+ if (sql[i] === "'") {
20006
+ if (sql[i + 1] === "'")
20007
+ i += 2;
20008
+ else
20009
+ break;
20010
+ } else
20011
+ i += 1;
20012
+ }
20013
+ i += 1;
20014
+ out += " ";
20015
+ } else if (c === "-" && next === "-") {
20016
+ while (i < sql.length && sql[i] !== `
20017
+ `)
20018
+ i += 1;
20019
+ out += " ";
20020
+ } else if (c === "/" && next === "*") {
20021
+ i += 2;
20022
+ while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/"))
20023
+ i += 1;
20024
+ i += 2;
20025
+ out += " ";
20026
+ } else {
20027
+ out += c;
20028
+ i += 1;
20029
+ }
20030
+ }
20031
+ return out;
20032
+ }
20033
+ function tableFor(pluginId, name) {
20034
+ return `"${PREFIX3}${pluginId}_${name}"`;
20035
+ }
20036
+ function prefixFor(pluginId) {
20037
+ return `${PREFIX3}${pluginId}_`;
20038
+ }
20039
+ function assertPluginId(pluginId) {
20040
+ if (!PLUGIN_ID.test(pluginId)) {
20041
+ throw new Error(`plugin id ${JSON.stringify(pluginId)} is not a valid identifier`);
20042
+ }
20043
+ }
20044
+ function prepare(pluginId, sql) {
20045
+ assertPluginId(pluginId);
20046
+ const expanded = sql.replace(PLACEHOLDER, (_match, name) => {
20047
+ if (!TABLE_NAME.test(name)) {
20048
+ throw new Error(`plugin table placeholder ${JSON.stringify(name)} must match ${TABLE_NAME.source}`);
20049
+ }
20050
+ return tableFor(pluginId, name);
20051
+ });
20052
+ const found = CORE_TABLE_REFERENCE.exec(stripNoise(expanded));
20053
+ if (found !== null) {
20054
+ throw new Error(`plugin sql may not reference the core table ${found[0]}`);
20055
+ }
20056
+ return expanded;
20057
+ }
20058
+ function toBinding(value, index) {
20059
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" || value instanceof Uint8Array) {
20060
+ return value;
20061
+ }
20062
+ throw new Error(`plugin sql parameter ${index} is not a bindable value`);
20063
+ }
20064
+ var toBindings = (params) => (params ?? []).map(toBinding);
20065
+ function createPluginRepo(db) {
20066
+ const pluginTables = () => db.query("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all().map((r) => r.name).filter((name) => name.startsWith(PREFIX3) && name !== "plugin_migrations");
20067
+ const tablesOf = (pluginId) => {
20068
+ const prefix = prefixFor(pluginId);
20069
+ return pluginTables().filter((name) => name.startsWith(prefix));
20070
+ };
20071
+ const appliedVersions = (pluginId) => new Set(db.query("SELECT version FROM plugin_migrations WHERE plugin_id = ?").all(pluginId).map((r) => r.version));
20072
+ return {
20073
+ migrate(pluginId, migrations) {
20074
+ assertPluginId(pluginId);
20075
+ const done = appliedVersions(pluginId);
20076
+ const applied = [];
20077
+ const ordered = [...migrations].sort((a, b) => a.version - b.version);
20078
+ for (const migration of ordered) {
20079
+ if (done.has(migration.version))
20080
+ continue;
20081
+ try {
20082
+ if (!Number.isSafeInteger(migration.version) || migration.version < 1) {
20083
+ throw new Error("migration version must be a positive integer");
20084
+ }
20085
+ const sql = prepare(pluginId, migration.sql);
20086
+ db.transaction(() => {
20087
+ db.run(sql);
20088
+ db.run("INSERT INTO plugin_migrations (plugin_id, version, applied_at) VALUES (?,?,?)", [pluginId, migration.version, Date.now()]);
20089
+ })();
20090
+ applied.push(migration.version);
20091
+ } catch (error51) {
20092
+ return {
20093
+ applied,
20094
+ failed: {
20095
+ version: migration.version,
20096
+ reason: error51 instanceof Error ? error51.message : String(error51)
20097
+ }
20098
+ };
20099
+ }
20100
+ }
20101
+ return { applied };
20102
+ },
20103
+ run(pluginId, sql, params) {
20104
+ db.run(prepare(pluginId, sql), toBindings(params));
20105
+ },
20106
+ all(pluginId, sql, params) {
20107
+ return db.prepare(prepare(pluginId, sql)).all(...toBindings(params));
20108
+ },
20109
+ get(pluginId, sql, params) {
20110
+ const row = db.prepare(prepare(pluginId, sql)).get(...toBindings(params));
20111
+ return row === null || row === undefined ? null : row;
20112
+ },
20113
+ transaction(pluginId, fn) {
20114
+ assertPluginId(pluginId);
20115
+ return db.transaction(fn)();
20116
+ },
20117
+ listTables(pluginId) {
20118
+ assertPluginId(pluginId);
20119
+ return tablesOf(pluginId);
20120
+ },
20121
+ dropAll(pluginId) {
20122
+ assertPluginId(pluginId);
20123
+ const tables = tablesOf(pluginId);
20124
+ db.transaction(() => {
20125
+ for (const table2 of tables)
20126
+ db.run(`DROP TABLE IF EXISTS "${table2}"`);
20127
+ db.run("DELETE FROM plugin_migrations WHERE plugin_id = ?", [pluginId]);
20128
+ })();
20129
+ return tables.length;
20130
+ },
20131
+ orphanTables(installedIds) {
20132
+ const prefixes = installedIds.filter((id) => PLUGIN_ID.test(id)).map(prefixFor);
20133
+ return pluginTables().filter((name) => !prefixes.some((p) => name.startsWith(p)));
20134
+ }
20135
+ };
20136
+ }
19123
20137
  // packages/rtk/src/catalog.ts
19124
20138
  var RTK_FILTER_IDS = [
19125
20139
  "git-diff",
@@ -19186,7 +20200,7 @@ var GROUP_COLUMN = {
19186
20200
  requestedModel: "requested_model",
19187
20201
  apiKey: "api_key_id",
19188
20202
  provider: "resolved_provider",
19189
- hour: "at / 3600000",
20203
+ hour: "CAST(at / 3600000 AS INTEGER)",
19190
20204
  day: null
19191
20205
  },
19192
20206
  daily: {
@@ -19283,8 +20297,30 @@ function createUsageRepo(db) {
19283
20297
  const complete = db.transaction((log) => {
19284
20298
  db.run(COMPLETE, values(log, "done"));
19285
20299
  const stored = db.query("SELECT * FROM request_logs WHERE id = ?").get(log.id);
19286
- if (stored !== null)
19287
- rollupLog(db, toLog(stored));
20300
+ if (stored !== null) {
20301
+ const restored = toLog(stored);
20302
+ rollupLog(db, restored);
20303
+ rollupHour(db, restored);
20304
+ }
20305
+ });
20306
+ const pruneLogs = db.transaction((olderThan) => {
20307
+ db.run("DELETE FROM request_logs WHERE at < ?", [olderThan]);
20308
+ const removed = db.query("SELECT changes() AS n").get()?.n ?? 0;
20309
+ const boundary = hourOf(olderThan);
20310
+ db.run("DELETE FROM usage_rollup WHERE hour <= ?", [boundary]);
20311
+ db.run(`INSERT INTO usage_rollup
20312
+ (api_key_id, hour, requests, input_tokens, output_tokens,
20313
+ cache_read_tokens, cache_write_tokens, cost_usd)
20314
+ SELECT api_key_id, ?, COUNT(*),
20315
+ COALESCE(SUM(input_tokens), 0),
20316
+ COALESCE(SUM(output_tokens), 0),
20317
+ COALESCE(SUM(cache_read_tokens), 0),
20318
+ COALESCE(SUM(cache_write_tokens), 0),
20319
+ COALESCE(SUM(cost_usd), 0)
20320
+ FROM request_logs
20321
+ WHERE state = 'done' AND api_key_id IS NOT NULL AND at >= ? AND at < ?
20322
+ GROUP BY api_key_id`, [boundary, boundary * HOUR_MS, (boundary + 1) * HOUR_MS]);
20323
+ return removed;
19288
20324
  });
19289
20325
  return {
19290
20326
  async begin(log) {
@@ -19308,6 +20344,38 @@ function createUsageRepo(db) {
19308
20344
  async recent(limit) {
19309
20345
  return db.query("SELECT * FROM request_logs ORDER BY at DESC LIMIT ?").all(limit).map(toLog);
19310
20346
  },
20347
+ async sumSince(apiKeyId, sinceMs) {
20348
+ const boundary = hourOf(sinceMs);
20349
+ const whole = db.query(`SELECT COALESCE(SUM(requests), 0) AS requests,
20350
+ COALESCE(SUM(input_tokens + output_tokens
20351
+ + cache_read_tokens + cache_write_tokens), 0) AS tokens,
20352
+ COALESCE(SUM(cost_usd), 0) AS cost_usd
20353
+ FROM usage_rollup
20354
+ WHERE api_key_id = ? AND hour > ?`).get(apiKeyId, boundary);
20355
+ const edge = db.query(`SELECT COUNT(*) AS requests,
20356
+ COALESCE(SUM(input_tokens + output_tokens
20357
+ + cache_read_tokens + cache_write_tokens), 0) AS tokens,
20358
+ COALESCE(SUM(cost_usd), 0) AS cost_usd
20359
+ FROM request_logs
20360
+ WHERE api_key_id = ? AND state = 'done' AND at >= ? AND at < ?`).get(apiKeyId, sinceMs, (boundary + 1) * HOUR_MS);
20361
+ return {
20362
+ requests: (whole?.requests ?? 0) + (edge?.requests ?? 0),
20363
+ tokens: (whole?.tokens ?? 0) + (edge?.tokens ?? 0),
20364
+ costUsd: (whole?.cost_usd ?? 0) + (edge?.cost_usd ?? 0)
20365
+ };
20366
+ },
20367
+ async rebuildRollup() {
20368
+ rebuildRollup(db);
20369
+ },
20370
+ async auditRollup() {
20371
+ return auditRollup(db);
20372
+ },
20373
+ async oldestSince(apiKeyId, sinceMs) {
20374
+ const row = db.query(`SELECT MIN(at) AS at
20375
+ FROM request_logs
20376
+ WHERE api_key_id = ? AND state = 'done' AND at >= ?`).get(apiKeyId, sinceMs);
20377
+ return row?.at ?? null;
20378
+ },
19311
20379
  async aggregate(q) {
19312
20380
  const grain = q.grain ?? "raw";
19313
20381
  const daily = grain === "daily";
@@ -19348,8 +20416,7 @@ function createUsageRepo(db) {
19348
20416
  });
19349
20417
  },
19350
20418
  async prune(olderThan) {
19351
- db.run("DELETE FROM request_logs WHERE at < ?", [olderThan]);
19352
- return db.query("SELECT changes() AS n").get()?.n ?? 0;
20419
+ return pruneLogs(olderThan);
19353
20420
  },
19354
20421
  async pruneDaily(olderThan) {
19355
20422
  db.run("DELETE FROM usage_daily WHERE day < ?", [olderThan]);
@@ -19361,8 +20428,6 @@ function createUsageRepo(db) {
19361
20428
  // packages/store/src/sqlite/store.ts
19362
20429
  async function createStore(opts) {
19363
20430
  const logger2 = opts.logger ?? noopLogger;
19364
- const db = openDb(opts.path);
19365
- logger2.debug("store opened", { path: opts.path });
19366
20431
  const listeners = new Set;
19367
20432
  const emit2 = (change) => {
19368
20433
  for (const listener of listeners) {
@@ -19371,23 +20436,123 @@ async function createStore(opts) {
19371
20436
  } catch {}
19372
20437
  }
19373
20438
  };
20439
+ const open = () => {
20440
+ const db = openDb(opts.path);
20441
+ logger2.debug("store opened", { path: opts.path });
20442
+ return {
20443
+ db,
20444
+ credentials: createCredentialRepo(db, opts.encryptionKey, emit2),
20445
+ config: createConfigRepo(db, emit2),
20446
+ keys: createKeyRepo(db, logger2),
20447
+ usage: createUsageRepo(db),
20448
+ bodies: createBodyRepo(db, opts.encryptionKey, bodiesDirFor(opts.path)),
20449
+ maintenance: createMaintenanceRepo(db),
20450
+ plugins: createPluginRepo(db)
20451
+ };
20452
+ };
20453
+ let handle = open();
20454
+ let live = true;
20455
+ const closeHandle = () => {
20456
+ if (!live)
20457
+ return;
20458
+ handle.db.close();
20459
+ live = false;
20460
+ };
19374
20461
  return {
19375
- credentials: createCredentialRepo(db, opts.encryptionKey, emit2),
19376
- config: createConfigRepo(db, emit2),
19377
- keys: createKeyRepo(db),
19378
- usage: createUsageRepo(db),
19379
- routing: {
19380
- version: () => db.query("PRAGMA data_version").get()?.data_version ?? 0,
19381
- subscribe(listener) {
19382
- listeners.add(listener);
19383
- return () => listeners.delete(listener);
19384
- }
20462
+ databasePath: opts.path,
20463
+ credentials: {
20464
+ list: () => handle.credentials.list(),
20465
+ listRouting: () => handle.credentials.listRouting(),
20466
+ get: (id) => handle.credentials.get(id),
20467
+ create: (input) => handle.credentials.create(input),
20468
+ update: (id, patch) => handle.credentials.update(id, patch),
20469
+ updateSecrets: (id, secrets, expiresAt) => handle.credentials.updateSecrets(id, secrets, expiresAt),
20470
+ remove: (id) => handle.credentials.remove(id),
20471
+ listHealth: () => handle.credentials.listHealth(),
20472
+ saveHealth: (rows) => handle.credentials.saveHealth(rows),
20473
+ updateHealth: (id, model, apply) => handle.credentials.updateHealth(id, model, apply),
20474
+ listQuota: () => handle.credentials.listQuota(),
20475
+ saveQuota: (rows) => handle.credentials.saveQuota(rows),
20476
+ listQuotaSamples: (q) => handle.credentials.listQuotaSamples(q),
20477
+ pruneQuotaSamples: (olderThan) => handle.credentials.pruneQuotaSamples(olderThan)
19385
20478
  },
19386
- close: () => db.close()
19387
- };
19388
- }
20479
+ config: {
20480
+ listModels: () => handle.config.listModels(),
20481
+ putModel: (model) => handle.config.putModel(model),
20482
+ removeModel: (id) => handle.config.removeModel(id),
20483
+ getSettings: () => handle.config.getSettings(),
20484
+ putSettings: (patch) => handle.config.putSettings(patch),
20485
+ getAdminPasswordHash: () => handle.config.getAdminPasswordHash(),
20486
+ setAdminPasswordHashIfAbsent: (hash3) => handle.config.setAdminPasswordHashIfAbsent(hash3),
20487
+ setAdminPasswordHash: (hash3) => handle.config.setAdminPasswordHash(hash3)
20488
+ },
20489
+ keys: {
20490
+ list: () => handle.keys.list(),
20491
+ findByHash: (hash3) => handle.keys.findByHash(hash3),
20492
+ create: (input) => handle.keys.create(input),
20493
+ setLimits: (id, limits) => handle.keys.setLimits(id, limits),
20494
+ revoke: (id) => handle.keys.revoke(id)
20495
+ },
20496
+ usage: {
20497
+ begin: (log) => handle.usage.begin(log),
20498
+ route: (id, target) => handle.usage.route(id, target),
20499
+ append: (log) => handle.usage.append(log),
20500
+ sweepPending: () => handle.usage.sweepPending(),
20501
+ recent: (limit) => handle.usage.recent(limit),
20502
+ aggregate: (q) => handle.usage.aggregate(q),
20503
+ sumSince: (apiKeyId, sinceMs) => handle.usage.sumSince(apiKeyId, sinceMs),
20504
+ oldestSince: (apiKeyId, sinceMs) => handle.usage.oldestSince(apiKeyId, sinceMs),
20505
+ rebuildRollup: () => handle.usage.rebuildRollup(),
20506
+ auditRollup: () => handle.usage.auditRollup(),
20507
+ prune: (olderThan) => handle.usage.prune(olderThan),
20508
+ pruneDaily: (olderThan) => handle.usage.pruneDaily(olderThan)
20509
+ },
20510
+ bodies: {
20511
+ put: (artifact) => handle.bodies.put(artifact),
20512
+ get: (requestId) => handle.bodies.get(requestId),
20513
+ prune: (olderThan) => handle.bodies.prune(olderThan),
20514
+ pruneToCap: (cap) => handle.bodies.pruneToCap(cap),
20515
+ sweepOrphans: () => handle.bodies.sweepOrphans()
20516
+ },
20517
+ maintenance: {
20518
+ stats: () => handle.maintenance.stats(),
20519
+ vacuum: () => handle.maintenance.vacuum(),
20520
+ snapshotTo: (path) => handle.maintenance.snapshotTo(path),
20521
+ inspect: (path) => handle.maintenance.inspect(path)
20522
+ },
20523
+ plugins: {
20524
+ migrate: (pluginId, migrations) => handle.plugins.migrate(pluginId, migrations),
20525
+ run: (pluginId, sql, params) => handle.plugins.run(pluginId, sql, params),
20526
+ all(pluginId, sql, params) {
20527
+ return handle.plugins.all(pluginId, sql, params);
20528
+ },
20529
+ get(pluginId, sql, params) {
20530
+ return handle.plugins.get(pluginId, sql, params);
20531
+ },
20532
+ transaction(pluginId, fn) {
20533
+ return handle.plugins.transaction(pluginId, fn);
20534
+ },
20535
+ listTables: (pluginId) => handle.plugins.listTables(pluginId),
20536
+ dropAll: (pluginId) => handle.plugins.dropAll(pluginId),
20537
+ orphanTables: (installedIds) => handle.plugins.orphanTables(installedIds)
20538
+ },
20539
+ routing: {
20540
+ version: () => handle.db.query("PRAGMA data_version").get()?.data_version ?? 0,
20541
+ subscribe(listener) {
20542
+ listeners.add(listener);
20543
+ return () => listeners.delete(listener);
20544
+ }
20545
+ },
20546
+ async reopen() {
20547
+ closeHandle();
20548
+ handle = open();
20549
+ live = true;
20550
+ },
20551
+ close: closeHandle
20552
+ };
20553
+ }
19389
20554
  // packages/control/src/quota/burn.ts
19390
- var HOUR_MS = 3600000;
20555
+ var HOUR_MS2 = 3600000;
19391
20556
  function windowStartOf(window) {
19392
20557
  if (window.resetsAt === null)
19393
20558
  return null;
@@ -19410,9 +20575,9 @@ function burnFor(window, input) {
19410
20575
  }
19411
20576
  const windowStartsAt = windowStartOf(window);
19412
20577
  const elapsedMs = windowStartsAt === null ? null : window.observedAt - windowStartsAt;
19413
- const elapsedHours = elapsedMs === null ? null : elapsedMs / HOUR_MS;
20578
+ const elapsedHours = elapsedMs === null ? null : elapsedMs / HOUR_MS2;
19414
20579
  const ratePerHour = elapsedHours === null ? null : elapsedHours <= 0 || window.used === 0 ? 0 : window.used / elapsedHours;
19415
- const exhaustsAt = window.limit === null || ratePerHour === null || ratePerHour <= 0 ? null : window.observedAt + Math.max(0, window.limit - window.used) / ratePerHour * HOUR_MS;
20580
+ const exhaustsAt = window.limit === null || ratePerHour === null || ratePerHour <= 0 ? null : window.observedAt + Math.max(0, window.limit - window.used) / ratePerHour * HOUR_MS2;
19416
20581
  return {
19417
20582
  credentialId: window.credentialId,
19418
20583
  windowType: window.windowType,
@@ -19592,6 +20757,243 @@ async function patchCredential(deps, id, input) {
19592
20757
  async function removeCredential(store, id) {
19593
20758
  await store.credentials.remove(id);
19594
20759
  }
20760
+ // packages/control/src/database.ts
20761
+ import { basename, dirname as dirname2, join as join2, resolve } from "path";
20762
+ var exclusive = null;
20763
+ async function withExclusive(label2, operation) {
20764
+ if (exclusive !== null) {
20765
+ throw new GatewayError("CONFLICT", `${exclusive} is already running on this database`);
20766
+ }
20767
+ exclusive = label2;
20768
+ try {
20769
+ return await operation();
20770
+ } finally {
20771
+ exclusive = null;
20772
+ }
20773
+ }
20774
+ var SNAPSHOTS_DIRNAME = "snapshots";
20775
+ function snapshotsDir(deps) {
20776
+ return join2(dirname2(deps.store.databasePath), SNAPSHOTS_DIRNAME);
20777
+ }
20778
+ function snapshotName(at, reason) {
20779
+ const stamp = new Date(at).toISOString().replaceAll(":", "-").replaceAll(".", "-");
20780
+ return `db_${stamp}_${reason}.sqlite`;
20781
+ }
20782
+ var SNAPSHOT_ID = /^db_(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z_([A-Za-z]+)\.sqlite$/;
20783
+ function parseSnapshotName(name) {
20784
+ const match = SNAPSHOT_ID.exec(name);
20785
+ if (match === null)
20786
+ return null;
20787
+ const [, date5, hh, mm, ss, ms, reason] = match;
20788
+ const at = Date.parse(`${date5}T${hh}:${mm}:${ss}.${ms}Z`);
20789
+ if (Number.isNaN(at) || reason === undefined)
20790
+ return null;
20791
+ return { at, reason };
20792
+ }
20793
+ function snapshotPath(deps, id) {
20794
+ const dir = resolve(snapshotsDir(deps));
20795
+ const path = resolve(dir, id);
20796
+ if (parseSnapshotName(id) === null || dirname2(path) !== dir) {
20797
+ throw new GatewayError("BAD_REQUEST", "invalid snapshot id");
20798
+ }
20799
+ const real = deps.fs.realpath(path);
20800
+ if (real !== null && dirname2(real) !== (deps.fs.realpath(dir) ?? dir)) {
20801
+ throw new GatewayError("BAD_REQUEST", "invalid snapshot id");
20802
+ }
20803
+ return path;
20804
+ }
20805
+ function listSnapshots(deps) {
20806
+ const dir = snapshotsDir(deps);
20807
+ const snapshots = [];
20808
+ for (const name of deps.fs.readdir(dir)) {
20809
+ const parsed = parseSnapshotName(name);
20810
+ if (parsed === null)
20811
+ continue;
20812
+ snapshots.push({
20813
+ id: name,
20814
+ filename: name,
20815
+ createdAt: parsed.at,
20816
+ sizeBytes: deps.fs.stat(join2(dir, name))?.size ?? 0,
20817
+ reason: parsed.reason
20818
+ });
20819
+ }
20820
+ return snapshots.sort((a, b) => b.createdAt - a.createdAt);
20821
+ }
20822
+ var DAY_MS = 86400000;
20823
+ var SNAPSHOT_HEADROOM = 1.1;
20824
+ function retentionOf(settings) {
20825
+ return { keepLatest: settings.snapshotKeepLatest, maxAgeDays: settings.snapshotMaxAgeDays };
20826
+ }
20827
+ function applyRetention(deps, policy, now) {
20828
+ const dir = snapshotsDir(deps);
20829
+ const cutoff = now - policy.maxAgeDays * DAY_MS;
20830
+ const snapshots = listSnapshots(deps);
20831
+ const undo = snapshots.find((snapshot) => snapshot.reason === "preRestore")?.id;
20832
+ let removed = 0;
20833
+ snapshots.forEach((snapshot, index) => {
20834
+ if (index === 0)
20835
+ return;
20836
+ if (snapshot.id === undo)
20837
+ return;
20838
+ if (index < policy.keepLatest && snapshot.createdAt >= cutoff)
20839
+ return;
20840
+ deps.fs.unlink(join2(dir, snapshot.id));
20841
+ removed++;
20842
+ });
20843
+ return removed;
20844
+ }
20845
+ var STAGING_MAX_AGE_MS = 60 * 60 * 1000;
20846
+ async function writeSnapshot(deps, input) {
20847
+ const dir = snapshotsDir(deps);
20848
+ const at = deps.now();
20849
+ const filename = snapshotName(at, input.reason);
20850
+ const path = join2(dir, filename);
20851
+ deps.fs.mkdir(dir);
20852
+ if (deps.fs.stat(path) !== null) {
20853
+ throw new GatewayError("CONFLICT", "a snapshot for this instant already exists");
20854
+ }
20855
+ const free = deps.fs.freeBytes(dir);
20856
+ const needed = Math.ceil((deps.fs.stat(deps.store.databasePath)?.size ?? 0) * SNAPSHOT_HEADROOM);
20857
+ if (free !== null && free < needed) {
20858
+ throw new GatewayError("CONFLICT", "not enough free disk space to write a snapshot");
20859
+ }
20860
+ try {
20861
+ await deps.store.maintenance.snapshotTo(path);
20862
+ } catch (error51) {
20863
+ deps.fs.unlink(path);
20864
+ throw error51;
20865
+ }
20866
+ const info = {
20867
+ id: filename,
20868
+ filename,
20869
+ createdAt: at,
20870
+ sizeBytes: deps.fs.stat(path)?.size ?? 0,
20871
+ reason: input.reason
20872
+ };
20873
+ if (input.force !== true) {
20874
+ applyRetention(deps, retentionOf(await deps.store.config.getSettings()), at);
20875
+ }
20876
+ return info;
20877
+ }
20878
+ async function createSnapshot(deps, input) {
20879
+ return withExclusive("a snapshot", () => writeSnapshot(deps, input));
20880
+ }
20881
+
20882
+ class SwapFailedError extends Error {
20883
+ preRestoreSnapshotId;
20884
+ cause;
20885
+ reopened;
20886
+ constructor(preRestoreSnapshotId, cause, reopened) {
20887
+ super("the database swap failed and the database may be incomplete");
20888
+ this.preRestoreSnapshotId = preRestoreSnapshotId;
20889
+ this.cause = cause;
20890
+ this.reopened = reopened;
20891
+ this.name = "SwapFailedError";
20892
+ }
20893
+ }
20894
+ var NAMED_TABLES = 5;
20895
+ function describeTables(tables) {
20896
+ if (tables.length === 0)
20897
+ return "it has no tables at all";
20898
+ const named = tables.slice(0, NAMED_TABLES);
20899
+ const rest = tables.length - named.length;
20900
+ return `it has ${named.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`;
20901
+ }
20902
+ async function swapIn(deps, candidate) {
20903
+ const inspection = await deps.store.maintenance.inspect(candidate.path);
20904
+ if (!inspection.ok) {
20905
+ throw new GatewayError("BAD_REQUEST", inspection.quickCheck === "ok" ? `that file is a database, but not one of ours: ${describeTables(inspection.tables)}` : `that file failed its integrity check: ${inspection.quickCheck}`);
20906
+ }
20907
+ const preRestoreSnapshot = await writeSnapshot(deps, { reason: "preRestore", force: true });
20908
+ const adminHashBefore = await deps.store.config.getAdminPasswordHash();
20909
+ const live = deps.store.databasePath;
20910
+ const staged = `${live}.incoming`;
20911
+ try {
20912
+ if (candidate.consume)
20913
+ deps.fs.rename(candidate.path, staged);
20914
+ else
20915
+ deps.fs.copyFile(candidate.path, staged);
20916
+ } catch (error51) {
20917
+ deps.fs.unlink(staged);
20918
+ throw error51;
20919
+ }
20920
+ try {
20921
+ deps.store.close();
20922
+ deps.fs.unlink(`${live}-wal`);
20923
+ deps.fs.unlink(`${live}-shm`);
20924
+ deps.fs.rename(staged, live);
20925
+ await deps.store.reopen();
20926
+ } catch (error51) {
20927
+ let reopened = true;
20928
+ try {
20929
+ await deps.store.reopen();
20930
+ } catch {
20931
+ reopened = false;
20932
+ }
20933
+ throw new SwapFailedError(preRestoreSnapshot.id, error51, reopened);
20934
+ }
20935
+ const adminHashAfter = await deps.store.config.getAdminPasswordHash();
20936
+ try {
20937
+ await deps.store.usage.rebuildRollup();
20938
+ } catch (error51) {
20939
+ (deps.logger ?? noopLogger).warn("usage rollup not rebuilt after the swap; run omni doctor", {
20940
+ reason: error51 instanceof Error ? error51.message : "unknown"
20941
+ });
20942
+ }
20943
+ try {
20944
+ await deps.reapplyPluginSchema?.();
20945
+ } catch (error51) {
20946
+ (deps.logger ?? noopLogger).warn("plugin schema not reapplied after the swap; restart", {
20947
+ reason: error51 instanceof Error ? error51.message : "unknown"
20948
+ });
20949
+ }
20950
+ return {
20951
+ ok: true,
20952
+ counts: inspection.counts,
20953
+ preRestoreSnapshot,
20954
+ adminPasswordChanged: adminHashAfter !== adminHashBefore
20955
+ };
20956
+ }
20957
+ async function restoreSnapshot(deps, id) {
20958
+ const path = snapshotPath(deps, id);
20959
+ if (deps.fs.stat(path) === null)
20960
+ throw new GatewayError("BAD_REQUEST", "no such snapshot");
20961
+ return withExclusive("a restore", () => swapIn(deps, { path, consume: false }));
20962
+ }
20963
+ var MAX_IMPORT_BYTES = 2 * 1024 * 1024 * 1024;
20964
+ async function vacuum(deps) {
20965
+ return withExclusive("a vacuum", async () => {
20966
+ const before = deps.fs.stat(deps.store.databasePath)?.size ?? 0;
20967
+ const startedAt = deps.now();
20968
+ await deps.store.maintenance.vacuum();
20969
+ const after = deps.fs.stat(deps.store.databasePath)?.size ?? 0;
20970
+ return {
20971
+ reclaimedBytes: Math.max(0, before - after),
20972
+ durationMs: Math.max(0, deps.now() - startedAt)
20973
+ };
20974
+ });
20975
+ }
20976
+ async function getDatabaseOverview(deps) {
20977
+ const live = deps.store.databasePath;
20978
+ const stats = await deps.store.maintenance.stats();
20979
+ const settings = await deps.store.config.getSettings();
20980
+ const snapshots = listSnapshots(deps);
20981
+ return {
20982
+ stats,
20983
+ fileBytes: deps.fs.stat(live)?.size ?? 0,
20984
+ walBytes: deps.fs.stat(`${live}-wal`)?.size ?? 0,
20985
+ bodiesBytes: deps.fs.dirBytes(bodiesDirFor(live)),
20986
+ logicalBytes: stats.pageSize * stats.pageCount,
20987
+ freePageBytes: stats.pageSize * stats.freelistCount,
20988
+ freeDiskBytes: deps.fs.freeBytes(dirname2(live)),
20989
+ retention: retentionOf(settings),
20990
+ snapshots: {
20991
+ count: snapshots.length,
20992
+ totalBytes: snapshots.reduce((sum, snapshot) => sum + snapshot.sizeBytes, 0),
20993
+ latestAt: snapshots[0]?.createdAt ?? null
20994
+ }
20995
+ };
20996
+ }
19595
20997
  // packages/control/src/dryRun.ts
19596
20998
  async function dryRun(deps, modelId, input) {
19597
20999
  const need = parseOrThrow(dryRunSchema, input);
@@ -19636,17 +21038,67 @@ async function dryRun(deps, modelId, input) {
19636
21038
  };
19637
21039
  }
19638
21040
  // packages/control/src/keys.ts
19639
- async function listKeys(store) {
21041
+ function configuredWindows(limits) {
21042
+ const needed = new Set;
21043
+ for (const dimension of ["requests", "tokens", "spend"]) {
21044
+ const windows = limits[dimension];
21045
+ if (windows === undefined)
21046
+ continue;
21047
+ for (const window of WINDOWS) {
21048
+ const limit = windows[window];
21049
+ if (limit !== undefined && limit !== null)
21050
+ needed.add(window);
21051
+ }
21052
+ }
21053
+ return [...needed];
21054
+ }
21055
+ async function readLimitUsage(store, id, limits, now) {
21056
+ if (limits === null)
21057
+ return [];
21058
+ const sums = new Map;
21059
+ for (const window of configuredWindows(limits)) {
21060
+ sums.set(window, await store.usage.sumSince(id, now - WINDOW_MS[window]));
21061
+ }
21062
+ const readings = [];
21063
+ for (const dimension of ["requests", "tokens", "spend"]) {
21064
+ const windows = limits[dimension];
21065
+ if (windows === undefined)
21066
+ continue;
21067
+ for (const window of WINDOWS) {
21068
+ const limit = windows[window];
21069
+ if (limit === undefined || limit === null)
21070
+ continue;
21071
+ const sum = sums.get(window);
21072
+ const used = sum === undefined ? null : dimension === "requests" ? sum.requests : dimension === "tokens" ? sum.tokens : sum.costUsd;
21073
+ readings.push({ dimension, window, limit, used });
21074
+ }
21075
+ }
21076
+ if (limits.concurrency !== undefined && limits.concurrency !== null) {
21077
+ readings.push({
21078
+ dimension: "concurrency",
21079
+ window: null,
21080
+ limit: limits.concurrency,
21081
+ used: null
21082
+ });
21083
+ }
21084
+ return readings;
21085
+ }
21086
+ async function toSummary(store, key, now) {
21087
+ return {
21088
+ id: key.id,
21089
+ label: key.label,
21090
+ prefix: key.prefix,
21091
+ modelAllowlist: key.modelAllowlist,
21092
+ limits: key.limits,
21093
+ limitUsage: await readLimitUsage(store, key.id, key.limits, now),
21094
+ bodyLoggingOptOut: key.bodyLoggingOptOut,
21095
+ createdAt: key.createdAt,
21096
+ revokedAt: key.revokedAt
21097
+ };
21098
+ }
21099
+ async function listKeys(store, now = Date.now()) {
19640
21100
  const keys = await store.keys.list();
19641
- return keys.map((k) => ({
19642
- id: k.id,
19643
- label: k.label,
19644
- prefix: k.prefix,
19645
- modelAllowlist: k.modelAllowlist,
19646
- rateLimitPerMin: k.rateLimitPerMin,
19647
- createdAt: k.createdAt,
19648
- revokedAt: k.revokedAt
19649
- }));
21101
+ return Promise.all(keys.map((key) => toSummary(store, key, now)));
19650
21102
  }
19651
21103
  async function createKey(store, input) {
19652
21104
  const body2 = parseOrThrow(keyCreateSchema, input);
@@ -19657,10 +21109,19 @@ async function createKey(store, input) {
19657
21109
  prefix: raw.slice(0, 12),
19658
21110
  hash: await hashApiKey(raw),
19659
21111
  modelAllowlist: body2.modelAllowlist,
19660
- rateLimitPerMin: body2.rateLimitPerMin
21112
+ limits: body2.limits,
21113
+ bodyLoggingOptOut: body2.bodyLoggingOptOut
19661
21114
  });
19662
21115
  return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
19663
21116
  }
21117
+ async function setKeyLimits(store, id, input, now = Date.now()) {
21118
+ const body2 = parseOrThrow(keyLimitsSchema, input);
21119
+ const key = (await store.keys.list()).find((entry) => entry.id === id);
21120
+ if (key === undefined)
21121
+ throw new GatewayError("BAD_REQUEST", "no such api key");
21122
+ await store.keys.setLimits(id, body2.limits);
21123
+ return toSummary(store, { ...key, limits: body2.limits }, now);
21124
+ }
19664
21125
  async function revokeKey(store, id) {
19665
21126
  await store.keys.revoke(id);
19666
21127
  }
@@ -19779,8 +21240,754 @@ async function putModel(store, id, input) {
19779
21240
  throw stranded;
19780
21241
  await store.config.putModel(model);
19781
21242
  }
19782
- async function removeModel(store, id) {
19783
- await store.config.removeModel(id);
21243
+ async function removeModel(store, id) {
21244
+ await store.config.removeModel(id);
21245
+ }
21246
+ // packages/control/src/nodeFs.ts
21247
+ import {
21248
+ copyFileSync,
21249
+ mkdirSync,
21250
+ readdirSync,
21251
+ readFileSync,
21252
+ realpathSync,
21253
+ renameSync,
21254
+ rmSync,
21255
+ statfsSync,
21256
+ statSync,
21257
+ writeFileSync
21258
+ } from "fs";
21259
+ import { join as join4 } from "path";
21260
+
21261
+ // packages/control/src/plugins.ts
21262
+ import { Buffer as Buffer2 } from "buffer";
21263
+ import { basename as basename2, join as join3, resolve as resolve2, sep as sep2 } from "path";
21264
+
21265
+ // packages/plugin-api/src/version.ts
21266
+ var PLUGIN_API_VERSION = 1;
21267
+ var DASHBOARD_SDK_VERSION = "0.1.0";
21268
+ // packages/plugin-api/src/manifest.ts
21269
+ var CAPABILITIES = [
21270
+ "storage",
21271
+ "files",
21272
+ "net:outbound",
21273
+ "events:request",
21274
+ "events:limit"
21275
+ ];
21276
+ var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
21277
+ var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
21278
+ var entrySchema = exports_external.string().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), {
21279
+ message: "entry must be a relative path with no '..' segment"
21280
+ });
21281
+ var originSchema = exports_external.string().refine((value) => {
21282
+ let url2;
21283
+ try {
21284
+ url2 = new URL(value);
21285
+ } catch {
21286
+ return false;
21287
+ }
21288
+ if (url2.protocol !== "https:" && url2.protocol !== "http:")
21289
+ return false;
21290
+ if (url2.hostname === "")
21291
+ return false;
21292
+ if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "")
21293
+ return false;
21294
+ return value === url2.origin || value === `${url2.origin}/`;
21295
+ }, { message: "origin must be scheme://host with no path, query or fragment" });
21296
+ var navSchema = exports_external.object({
21297
+ label: exports_external.string().min(1).max(32)
21298
+ }).strict();
21299
+ var manifestSchema = exports_external.object({
21300
+ id: idSchema,
21301
+ name: exports_external.string().min(1).max(64),
21302
+ version: exports_external.string().min(1).max(32),
21303
+ api: exports_external.number().int().nonnegative(),
21304
+ sdk: exports_external.string().min(1).max(32).optional(),
21305
+ server: entrySchema.optional(),
21306
+ ui: entrySchema.optional(),
21307
+ nav: navSchema.optional(),
21308
+ capabilities: exports_external.array(exports_external.enum(CAPABILITIES)).default([]),
21309
+ origins: exports_external.array(originSchema).min(1).optional(),
21310
+ defaults: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21311
+ }).strict().refine((m) => m.server !== undefined || m.ui !== undefined, {
21312
+ message: "manifest must declare a server entry, a ui entry, or both"
21313
+ }).refine((m) => m.ui === undefined || m.sdk !== undefined, {
21314
+ message: "a ui entry requires an sdk range"
21315
+ }).refine((m) => !m.capabilities.includes("net:outbound") || m.origins !== undefined, {
21316
+ message: "net:outbound requires origins"
21317
+ }).refine((m) => m.capabilities.includes("net:outbound") || m.origins === undefined, {
21318
+ message: "origins requires the net:outbound capability"
21319
+ });
21320
+ function safeParseManifest(input) {
21321
+ const parsed = manifestSchema.safeParse(input);
21322
+ if (parsed.success)
21323
+ return { ok: true, manifest: parsed.data };
21324
+ const first = parsed.error.issues[0];
21325
+ const path = first?.path.join(".") ?? "";
21326
+ const message = first?.message ?? "invalid manifest";
21327
+ return { ok: false, reason: path === "" ? message : `${path}: ${message}` };
21328
+ }
21329
+ function isApiCompatible(manifest) {
21330
+ return manifest.api === PLUGIN_API_VERSION;
21331
+ }
21332
+
21333
+ // packages/control/src/plugins.ts
21334
+ var PLUGINS_DIRNAME = "plugins";
21335
+ function pluginsDir(root) {
21336
+ return join3(root, PLUGINS_DIRNAME);
21337
+ }
21338
+ var MANIFEST_FILENAME = "omni-plugin.json";
21339
+ var MAX_PLUGIN_BYTES = 32 * 1024 * 1024;
21340
+ var ID_PATTERN2 = /^[a-z][a-z0-9-]{0,31}$/;
21341
+ var NPM_TARBALL_ROOT = "package";
21342
+ function readManifest(fs, home) {
21343
+ const raw = fs.readText(join3(home, MANIFEST_FILENAME));
21344
+ if (raw === null) {
21345
+ return { check: "manifest", reason: `no ${MANIFEST_FILENAME}`, fatal: true };
21346
+ }
21347
+ let document;
21348
+ try {
21349
+ document = JSON.parse(raw);
21350
+ } catch (error51) {
21351
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21352
+ return {
21353
+ check: "manifest",
21354
+ reason: `${MANIFEST_FILENAME} is not valid JSON: ${detail}`,
21355
+ fatal: true
21356
+ };
21357
+ }
21358
+ const parsed = safeParseManifest(document);
21359
+ if (!parsed.ok)
21360
+ return { check: "manifest", reason: parsed.reason, fatal: true };
21361
+ return parsed.manifest;
21362
+ }
21363
+ function checkEntry(fs, home, entry, label2, fatal) {
21364
+ const target = resolve2(home, entry);
21365
+ if (target !== home && !target.startsWith(`${home}${sep2}`)) {
21366
+ return {
21367
+ check: "entry",
21368
+ reason: `${label2} entry resolves outside the plugin directory`,
21369
+ fatal
21370
+ };
21371
+ }
21372
+ if (!fs.isFile(target)) {
21373
+ return { check: "entry", reason: `${label2} entry ${entry} does not exist`, fatal };
21374
+ }
21375
+ return null;
21376
+ }
21377
+ function verifyPlugin(deps, root, id) {
21378
+ const home = join3(pluginsDir(root), id);
21379
+ if (!ID_PATTERN2.test(id) || !deps.fs.isDirectory(home)) {
21380
+ throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
21381
+ }
21382
+ return reportFor(deps, home, id);
21383
+ }
21384
+ function reportFor(deps, home, id) {
21385
+ const problems = [];
21386
+ const read = readManifest(deps.fs, home);
21387
+ if (!("id" in read)) {
21388
+ problems.push(read);
21389
+ return { id, path: home, manifest: null, problems, loadable: false };
21390
+ }
21391
+ const manifest = read;
21392
+ if (manifest.id !== id) {
21393
+ problems.push({
21394
+ check: "id",
21395
+ reason: `manifest id ${manifest.id} does not match its directory ${id}`,
21396
+ fatal: true
21397
+ });
21398
+ }
21399
+ if (!isApiCompatible(manifest)) {
21400
+ problems.push({
21401
+ check: "api",
21402
+ reason: `plugin api ${manifest.api} is not supported by this host (api ${PLUGIN_API_VERSION})`,
21403
+ fatal: true
21404
+ });
21405
+ }
21406
+ if (manifest.server !== undefined) {
21407
+ const problem = checkEntry(deps.fs, home, manifest.server, "server", true);
21408
+ if (problem !== null)
21409
+ problems.push(problem);
21410
+ }
21411
+ if (manifest.ui !== undefined) {
21412
+ const problem = checkEntry(deps.fs, home, manifest.ui, "ui", false);
21413
+ if (problem !== null)
21414
+ problems.push(problem);
21415
+ }
21416
+ const sdk = sdkProblem(deps, manifest);
21417
+ if (sdk !== null)
21418
+ problems.push(sdk);
21419
+ return {
21420
+ id,
21421
+ path: home,
21422
+ manifest,
21423
+ problems,
21424
+ loadable: !problems.some((problem) => problem.fatal)
21425
+ };
21426
+ }
21427
+ function sdkProblem(deps, manifest) {
21428
+ if (manifest.ui === undefined || manifest.sdk === undefined)
21429
+ return null;
21430
+ if (deps.sdkVersion === undefined)
21431
+ return null;
21432
+ if (Bun.semver.satisfies(deps.sdkVersion, manifest.sdk))
21433
+ return null;
21434
+ return {
21435
+ check: "sdk",
21436
+ reason: `requires dashboard sdk ${manifest.sdk}, host ships ${deps.sdkVersion}`,
21437
+ fatal: false
21438
+ };
21439
+ }
21440
+ function listPlugins(deps, root) {
21441
+ const dir = pluginsDir(root);
21442
+ const summaries = [];
21443
+ for (const entry of [...deps.fs.readdir(dir)].sort()) {
21444
+ const home = join3(dir, entry);
21445
+ if (!deps.fs.isDirectory(home))
21446
+ continue;
21447
+ if (!ID_PATTERN2.test(entry)) {
21448
+ summaries.push({
21449
+ id: entry,
21450
+ path: home,
21451
+ manifest: null,
21452
+ problems: [
21453
+ {
21454
+ check: "id",
21455
+ reason: `directory name ${entry} is not a valid plugin id`,
21456
+ fatal: true
21457
+ }
21458
+ ],
21459
+ loadable: false,
21460
+ name: null,
21461
+ version: null,
21462
+ api: null,
21463
+ sdk: null,
21464
+ capabilities: [],
21465
+ origins: []
21466
+ });
21467
+ continue;
21468
+ }
21469
+ const report = reportFor(deps, home, entry);
21470
+ const manifest = report.manifest;
21471
+ summaries.push({
21472
+ ...report,
21473
+ name: manifest?.name ?? null,
21474
+ version: manifest?.version ?? null,
21475
+ api: manifest?.api ?? null,
21476
+ sdk: manifest?.sdk ?? null,
21477
+ capabilities: manifest?.capabilities ?? [],
21478
+ origins: manifest?.origins ?? []
21479
+ });
21480
+ }
21481
+ return summaries;
21482
+ }
21483
+ var BLOCK = 512;
21484
+ var decoder2 = new TextDecoder;
21485
+ function fieldText(header, offset, length) {
21486
+ const slice = header.subarray(offset, offset + length);
21487
+ const end = slice.indexOf(0);
21488
+ return decoder2.decode(end === -1 ? slice : slice.subarray(0, end));
21489
+ }
21490
+ function fieldOctal(header, offset, length) {
21491
+ const text = fieldText(header, offset, length).trim();
21492
+ if (text.length === 0)
21493
+ return 0;
21494
+ const value = Number.parseInt(text, 8);
21495
+ return Number.isSafeInteger(value) && value >= 0 ? value : -1;
21496
+ }
21497
+ function isSafeArchivePath(path) {
21498
+ if (path.length === 0 || path.startsWith("/") || path.includes("\x00"))
21499
+ return false;
21500
+ if (path.includes("\\"))
21501
+ return false;
21502
+ return !path.split("/").includes("..");
21503
+ }
21504
+ function readTar(bytes) {
21505
+ const files = new Map;
21506
+ let total = 0;
21507
+ let longName = null;
21508
+ let offset = 0;
21509
+ while (offset + BLOCK <= bytes.length) {
21510
+ const header = bytes.subarray(offset, offset + BLOCK);
21511
+ if (header.every((byte) => byte === 0))
21512
+ break;
21513
+ const size = fieldOctal(header, 124, 12);
21514
+ if (size < 0)
21515
+ throw new GatewayError("BAD_REQUEST", "archive has an unreadable entry size");
21516
+ const type = String.fromCharCode(header[156] ?? 0);
21517
+ const name = fieldText(header, 0, 100);
21518
+ const prefix = fieldText(header, 345, 155);
21519
+ offset += BLOCK;
21520
+ total += size;
21521
+ if (total > MAX_PLUGIN_BYTES) {
21522
+ throw new GatewayError("BAD_REQUEST", `archive unpacks to more than ${MAX_PLUGIN_BYTES} bytes`);
21523
+ }
21524
+ if (offset + size > bytes.length) {
21525
+ throw new GatewayError("BAD_REQUEST", `archive is truncated: entry ${name || "(unnamed)"} declares ${size} bytes and the archive ends before them`);
21526
+ }
21527
+ const data = bytes.subarray(offset, offset + size);
21528
+ offset += Math.ceil(size / BLOCK) * BLOCK;
21529
+ if (type === "1" || type === "2") {
21530
+ throw new GatewayError("BAD_REQUEST", `archive contains a link (${name}); plugin packages must be plain files`);
21531
+ }
21532
+ if (type === "L") {
21533
+ longName = decoder2.decode(data).replace(/\0+$/, "");
21534
+ continue;
21535
+ }
21536
+ if (type === "K" || type === "x" || type === "g")
21537
+ continue;
21538
+ if (type !== "0" && type !== "\x00") {
21539
+ longName = null;
21540
+ continue;
21541
+ }
21542
+ const path = longName ?? (prefix.length > 0 ? `${prefix}/${name}` : name);
21543
+ longName = null;
21544
+ if (!isSafeArchivePath(path)) {
21545
+ throw new GatewayError("BAD_REQUEST", `archive contains an unsafe path: ${path}`);
21546
+ }
21547
+ files.set(path, new Uint8Array(data));
21548
+ }
21549
+ return files;
21550
+ }
21551
+ function gunzipIfNeeded(bytes) {
21552
+ if (bytes[0] !== 31 || bytes[1] !== 139)
21553
+ return bytes;
21554
+ try {
21555
+ return Bun.gunzipSync(new Uint8Array(bytes));
21556
+ } catch (error51) {
21557
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21558
+ throw new GatewayError("BAD_REQUEST", `could not decompress the archive: ${detail}`);
21559
+ }
21560
+ }
21561
+ function stripRoot(files) {
21562
+ const roots = new Set;
21563
+ for (const path of files.keys()) {
21564
+ const slash = path.indexOf("/");
21565
+ if (slash <= 0)
21566
+ return { files, claimedId: null };
21567
+ roots.add(path.slice(0, slash));
21568
+ }
21569
+ const [only] = [...roots];
21570
+ if (roots.size !== 1 || only === undefined)
21571
+ return { files, claimedId: null };
21572
+ const stripped = new Map;
21573
+ for (const [path, bytes] of files)
21574
+ stripped.set(path.slice(only.length + 1), bytes);
21575
+ return { files: stripped, claimedId: only === NPM_TARBALL_ROOT ? null : only };
21576
+ }
21577
+ function readTree(fs, dir, prefix = "") {
21578
+ const files = new Map;
21579
+ let total = 0;
21580
+ for (const entry of fs.readdir(dir)) {
21581
+ const path = join3(dir, entry);
21582
+ const relative = prefix.length === 0 ? entry : `${prefix}/${entry}`;
21583
+ if (fs.isDirectory(path)) {
21584
+ for (const [nested, bytes2] of readTree(fs, path, relative))
21585
+ files.set(nested, bytes2);
21586
+ continue;
21587
+ }
21588
+ const bytes = fs.readBytes(path);
21589
+ if (bytes === null)
21590
+ continue;
21591
+ total += bytes.byteLength;
21592
+ if (total > MAX_PLUGIN_BYTES) {
21593
+ throw new GatewayError("BAD_REQUEST", `${dir} holds more than ${MAX_PLUGIN_BYTES} bytes`);
21594
+ }
21595
+ files.set(relative, bytes);
21596
+ }
21597
+ return files;
21598
+ }
21599
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
21600
+ var NPM_PACKUMENT_ACCEPT = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
21601
+ var NPM_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
21602
+ var EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
21603
+ var SRI_ALGORITHMS = ["sha512", "sha384", "sha256"];
21604
+ function asRecord(value) {
21605
+ if (typeof value !== "object" || value === null || Array.isArray(value))
21606
+ return null;
21607
+ return value;
21608
+ }
21609
+ function asString(value) {
21610
+ return typeof value === "string" ? value : null;
21611
+ }
21612
+ function parseNpmSpec(spec) {
21613
+ const at = spec.lastIndexOf("@");
21614
+ const name = at <= 0 ? spec : spec.slice(0, at);
21615
+ const version2 = at <= 0 ? null : spec.slice(at + 1);
21616
+ if (name.length > 214 || !NPM_NAME_PATTERN.test(name))
21617
+ return null;
21618
+ return { name, version: version2 };
21619
+ }
21620
+ function registryUrl(deps) {
21621
+ const raw = deps.registry ?? DEFAULT_NPM_REGISTRY;
21622
+ let url2;
21623
+ try {
21624
+ url2 = new URL(raw);
21625
+ } catch {
21626
+ throw new GatewayError("BAD_REQUEST", `registry ${raw} is not a URL`);
21627
+ }
21628
+ if (url2.protocol !== "https:") {
21629
+ throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; ${raw} is not`);
21630
+ }
21631
+ return url2;
21632
+ }
21633
+ function packumentUrl(registry2, name) {
21634
+ return `${registry2.href.replace(/\/+$/, "")}/${name.replace("/", "%2f")}`;
21635
+ }
21636
+ function resolveDist(document, spec) {
21637
+ const root = asRecord(document);
21638
+ if (root === null) {
21639
+ throw new GatewayError("BAD_REQUEST", `the registry's answer for ${spec.name} is not a package`);
21640
+ }
21641
+ const versions2 = asRecord(root.versions);
21642
+ if (versions2 === null) {
21643
+ throw new GatewayError("BAD_REQUEST", `the registry lists no versions of ${spec.name}`);
21644
+ }
21645
+ let wanted = spec.version;
21646
+ if (wanted === null) {
21647
+ const tags = asRecord(root["dist-tags"]);
21648
+ wanted = tags === null ? null : asString(tags.latest);
21649
+ if (wanted === null) {
21650
+ throw new GatewayError("BAD_REQUEST", `the registry names no dist-tags.latest for ${spec.name}; install ${spec.name}@<exact version>`);
21651
+ }
21652
+ }
21653
+ const entry = asRecord(versions2[wanted]);
21654
+ if (entry === null) {
21655
+ throw new GatewayError("BAD_REQUEST", `the registry has no version ${wanted} of ${spec.name}`);
21656
+ }
21657
+ const dist = asRecord(entry.dist);
21658
+ const tarball = dist === null ? null : asString(dist.tarball);
21659
+ if (dist === null || tarball === null) {
21660
+ throw new GatewayError("BAD_REQUEST", `the registry's entry for ${spec.name}@${wanted} names no tarball`);
21661
+ }
21662
+ return {
21663
+ version: wanted,
21664
+ dist: { tarball, integrity: asString(dist.integrity), shasum: asString(dist.shasum) }
21665
+ };
21666
+ }
21667
+ function checkTarballUrl(tarball, registry2, what) {
21668
+ let url2;
21669
+ try {
21670
+ url2 = new URL(tarball);
21671
+ } catch {
21672
+ throw new GatewayError("BAD_REQUEST", `the registry gave ${what} a tarball that is not a URL`);
21673
+ }
21674
+ if (url2.protocol !== "https:") {
21675
+ throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; the registry points ${what} at ${tarball}`);
21676
+ }
21677
+ if (url2.host !== registry2.host) {
21678
+ throw new GatewayError("BAD_REQUEST", `registry ${registry2.host} points ${what} at ${url2.host}; ` + "install that URL directly if it is the one you meant");
21679
+ }
21680
+ }
21681
+ function digestsFor(dist, what) {
21682
+ const digests = [];
21683
+ if (dist.integrity !== null) {
21684
+ for (const entry of dist.integrity.trim().split(/\s+/)) {
21685
+ const dash = entry.indexOf("-");
21686
+ const algorithm = dash === -1 ? "" : entry.slice(0, dash);
21687
+ const value = dash === -1 ? "" : entry.slice(dash + 1);
21688
+ if (value.length === 0)
21689
+ continue;
21690
+ const known = SRI_ALGORITHMS.find((candidate) => candidate === algorithm);
21691
+ if (known === undefined)
21692
+ continue;
21693
+ digests.push({
21694
+ algorithm: known,
21695
+ expected: Buffer2.from(value, "base64").toString("hex"),
21696
+ source: `${known} integrity`
21697
+ });
21698
+ }
21699
+ }
21700
+ if (digests.length === 0 && dist.shasum !== null && /^[0-9a-f]{40}$/i.test(dist.shasum)) {
21701
+ digests.push({ algorithm: "sha1", expected: dist.shasum.toLowerCase(), source: "sha1 shasum" });
21702
+ }
21703
+ if (digests.length === 0) {
21704
+ throw new GatewayError("BAD_REQUEST", `the registry advertises no usable integrity or shasum for ${what}; ` + "refusing to install bytes nothing vouches for");
21705
+ }
21706
+ return digests;
21707
+ }
21708
+ function verifyDigests(bytes, digests, what) {
21709
+ for (const digest of digests) {
21710
+ const actual = new Bun.CryptoHasher(digest.algorithm).update(bytes).digest("hex");
21711
+ if (actual === digest.expected)
21712
+ return;
21713
+ }
21714
+ const first = digests[0];
21715
+ throw new GatewayError("BAD_REQUEST", `${what} does not match the ${first?.source ?? "digest"} the registry advertised; ` + "the download was corrupted or the registry served something else");
21716
+ }
21717
+ async function npmPayload(deps, spec) {
21718
+ if (spec.version !== null && !EXACT_VERSION_PATTERN.test(spec.version)) {
21719
+ throw new GatewayError("BAD_REQUEST", `${spec.name}@${spec.version} is not an exact version; this installer resolves no ranges ` + `or tags \u2014 install ${spec.name}@1.2.3, or ${spec.name} for the registry's latest`);
21720
+ }
21721
+ const fetchBytes = deps.fetchBytes;
21722
+ if (fetchBytes === undefined) {
21723
+ throw new GatewayError("BAD_REQUEST", "this caller cannot install from a registry");
21724
+ }
21725
+ const registry2 = registryUrl(deps);
21726
+ const document = parseJson(await fetchBytes(packumentUrl(registry2, spec.name), NPM_PACKUMENT_ACCEPT), `the registry's answer for ${spec.name}`);
21727
+ const { version: version2, dist } = resolveDist(document, spec);
21728
+ const what = `${spec.name}@${version2}`;
21729
+ checkTarballUrl(dist.tarball, registry2, what);
21730
+ const digests = digestsFor(dist, what);
21731
+ const bytes = await fetchBytes(dist.tarball);
21732
+ verifyDigests(bytes, digests, what);
21733
+ return stripRoot(readTar(gunzipIfNeeded(bytes)));
21734
+ }
21735
+ function parseJson(bytes, what) {
21736
+ try {
21737
+ return JSON.parse(decoder2.decode(bytes));
21738
+ } catch (error51) {
21739
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21740
+ throw new GatewayError("BAD_REQUEST", `${what} is not valid JSON: ${detail}`);
21741
+ }
21742
+ }
21743
+ async function loadPayload(deps, spec) {
21744
+ if (spec.startsWith("http://")) {
21745
+ throw new GatewayError("BAD_REQUEST", "plugins may only be installed over https");
21746
+ }
21747
+ if (spec.startsWith("https://")) {
21748
+ if (deps.fetchBytes === undefined) {
21749
+ throw new GatewayError("BAD_REQUEST", "this caller cannot install from a URL");
21750
+ }
21751
+ return stripRoot(readTar(gunzipIfNeeded(await deps.fetchBytes(spec))));
21752
+ }
21753
+ const source = resolve2(spec);
21754
+ if (deps.fs.isDirectory(source)) {
21755
+ const files = readTree(deps.fs, source);
21756
+ const claimed = basename2(source);
21757
+ return { files, claimedId: ID_PATTERN2.test(claimed) ? claimed : null };
21758
+ }
21759
+ const bytes = deps.fs.readBytes(source);
21760
+ if (bytes !== null)
21761
+ return stripRoot(readTar(gunzipIfNeeded(bytes)));
21762
+ const npm = parseNpmSpec(spec);
21763
+ if (npm !== null)
21764
+ return npmPayload(deps, npm);
21765
+ throw new GatewayError("BAD_REQUEST", `no directory or archive at ${source}, and "${spec}" is not a package name`);
21766
+ }
21767
+ async function installPlugin(deps, root, spec) {
21768
+ const payload = await loadPayload(deps, spec);
21769
+ const raw = payload.files.get(MANIFEST_FILENAME);
21770
+ if (raw === undefined) {
21771
+ throw new GatewayError("BAD_REQUEST", `${spec} has no ${MANIFEST_FILENAME} at its root`);
21772
+ }
21773
+ let document;
21774
+ try {
21775
+ document = JSON.parse(decoder2.decode(raw));
21776
+ } catch (error51) {
21777
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21778
+ throw new GatewayError("BAD_REQUEST", `${MANIFEST_FILENAME} is not valid JSON: ${detail}`);
21779
+ }
21780
+ const parsed = safeParseManifest(document);
21781
+ if (!parsed.ok)
21782
+ throw new GatewayError("BAD_REQUEST", `invalid ${MANIFEST_FILENAME}: ${parsed.reason}`);
21783
+ const manifest = parsed.manifest;
21784
+ const targetId = payload.claimedId ?? manifest.id;
21785
+ if (manifest.id !== targetId) {
21786
+ throw new GatewayError("BAD_REQUEST", `manifest id ${manifest.id} does not match its directory ${targetId}; ` + "rename one of them so the gateway loads what you installed");
21787
+ }
21788
+ const dir = pluginsDir(root);
21789
+ const target = join3(dir, targetId);
21790
+ const replaced = deps.fs.isDirectory(target);
21791
+ const staging = join3(dir, `.staging-${targetId}`);
21792
+ deps.fs.rm(staging);
21793
+ try {
21794
+ for (const [path, bytes] of payload.files) {
21795
+ const destination = join3(staging, path);
21796
+ deps.fs.mkdir(join3(destination, ".."));
21797
+ deps.fs.writeBytes(destination, bytes);
21798
+ }
21799
+ deps.fs.rm(target);
21800
+ deps.fs.rename(staging, target);
21801
+ } finally {
21802
+ deps.fs.rm(staging);
21803
+ }
21804
+ return {
21805
+ id: targetId,
21806
+ name: manifest.name,
21807
+ version: manifest.version,
21808
+ path: target,
21809
+ replaced,
21810
+ restartRequired: true
21811
+ };
21812
+ }
21813
+ function removePlugin(deps, root, id, options = {}) {
21814
+ if (!ID_PATTERN2.test(id)) {
21815
+ throw new GatewayError("BAD_REQUEST", `"${id}" is not a valid plugin id`);
21816
+ }
21817
+ const target = join3(pluginsDir(root), id);
21818
+ const present = deps.fs.isDirectory(target);
21819
+ const purge = options.purge === true;
21820
+ if (purge && deps.store === undefined) {
21821
+ throw new GatewayError("INTERNAL", "purging a plugin's data needs a database");
21822
+ }
21823
+ const tables = purge && deps.store !== undefined ? deps.store.plugins.listTables(id) : [];
21824
+ if (!present && tables.length === 0) {
21825
+ throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
21826
+ }
21827
+ if (present)
21828
+ deps.fs.rm(target);
21829
+ if (purge && deps.store !== undefined)
21830
+ deps.store.plugins.dropAll(id);
21831
+ return { id, removed: present, droppedTables: tables };
21832
+ }
21833
+ function orphanPluginTables(deps, root, store) {
21834
+ const installed = listPlugins(deps, root).map((plugin) => plugin.id);
21835
+ return store.plugins.orphanTables(installed);
21836
+ }
21837
+
21838
+ // packages/control/src/nodeFs.ts
21839
+ function nodeDatabaseFs() {
21840
+ const dirBytes = (dir) => {
21841
+ let entries;
21842
+ try {
21843
+ entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
21844
+ } catch {
21845
+ return 0;
21846
+ }
21847
+ let total = 0;
21848
+ for (const entry of entries) {
21849
+ const path = join4(dir, entry.name);
21850
+ if (entry.isDirectory()) {
21851
+ total += dirBytes(path);
21852
+ continue;
21853
+ }
21854
+ try {
21855
+ total += statSync(path).size;
21856
+ } catch {}
21857
+ }
21858
+ return total;
21859
+ };
21860
+ return {
21861
+ readdir: (dir) => {
21862
+ try {
21863
+ return readdirSync(dir);
21864
+ } catch {
21865
+ return [];
21866
+ }
21867
+ },
21868
+ stat: (path) => {
21869
+ try {
21870
+ const stat = statSync(path);
21871
+ return { size: stat.size, mtimeMs: stat.mtimeMs };
21872
+ } catch {
21873
+ return null;
21874
+ }
21875
+ },
21876
+ unlink: (path) => {
21877
+ rmSync(path, { force: true });
21878
+ },
21879
+ rename: (from, to) => renameSync(from, to),
21880
+ copyFile: (from, to) => copyFileSync(from, to),
21881
+ mkdir: (dir) => {
21882
+ mkdirSync(dir, { recursive: true });
21883
+ },
21884
+ realpath: (path) => {
21885
+ try {
21886
+ return realpathSync(path);
21887
+ } catch {
21888
+ return null;
21889
+ }
21890
+ },
21891
+ freeBytes: (dir) => {
21892
+ try {
21893
+ const stat = statfsSync(dir);
21894
+ return Number(stat.bavail) * Number(stat.bsize);
21895
+ } catch {
21896
+ return null;
21897
+ }
21898
+ },
21899
+ dirBytes
21900
+ };
21901
+ }
21902
+ function nodePluginFs() {
21903
+ return {
21904
+ readdir: (dir) => {
21905
+ try {
21906
+ return readdirSync(dir);
21907
+ } catch {
21908
+ return [];
21909
+ }
21910
+ },
21911
+ readText: (path) => {
21912
+ try {
21913
+ return readFileSync(path, "utf8");
21914
+ } catch {
21915
+ return null;
21916
+ }
21917
+ },
21918
+ readBytes: (path) => {
21919
+ try {
21920
+ return new Uint8Array(readFileSync(path));
21921
+ } catch {
21922
+ return null;
21923
+ }
21924
+ },
21925
+ writeBytes: (path, bytes) => {
21926
+ writeFileSync(path, bytes);
21927
+ },
21928
+ isDirectory: (path) => {
21929
+ try {
21930
+ return statSync(path).isDirectory();
21931
+ } catch {
21932
+ return false;
21933
+ }
21934
+ },
21935
+ isFile: (path) => {
21936
+ try {
21937
+ return statSync(path).isFile();
21938
+ } catch {
21939
+ return false;
21940
+ }
21941
+ },
21942
+ mkdir: (dir) => {
21943
+ mkdirSync(dir, { recursive: true });
21944
+ },
21945
+ rm: (path) => {
21946
+ rmSync(path, { force: true, recursive: true });
21947
+ },
21948
+ rename: (from, to) => renameSync(from, to)
21949
+ };
21950
+ }
21951
+ function nodeFetchBytes(options = {}) {
21952
+ const impl = options.fetchImpl ?? fetch;
21953
+ const maxBytes = options.maxBytes ?? MAX_PLUGIN_BYTES;
21954
+ return async (url2, accept) => {
21955
+ if (!url2.startsWith("https://")) {
21956
+ throw new GatewayError("BAD_REQUEST", `plugins may only be fetched over https; ${url2} is not`);
21957
+ }
21958
+ const response = await impl(url2, {
21959
+ headers: accept === undefined ? {} : { accept },
21960
+ redirect: "follow"
21961
+ });
21962
+ if (!response.ok) {
21963
+ throw new GatewayError("BAD_REQUEST", `${url2} answered ${response.status}`);
21964
+ }
21965
+ const reader = response.body?.getReader();
21966
+ if (reader === undefined)
21967
+ return new Uint8Array(0);
21968
+ const chunks = [];
21969
+ let total = 0;
21970
+ for (;; ) {
21971
+ const { done, value } = await reader.read();
21972
+ if (done)
21973
+ break;
21974
+ if (value === undefined)
21975
+ continue;
21976
+ total += value.byteLength;
21977
+ if (total > maxBytes) {
21978
+ await reader.cancel();
21979
+ throw new GatewayError("BAD_REQUEST", `${url2} sent more than ${maxBytes} bytes`);
21980
+ }
21981
+ chunks.push(value);
21982
+ }
21983
+ const bytes = new Uint8Array(total);
21984
+ let offset = 0;
21985
+ for (const chunk of chunks) {
21986
+ bytes.set(chunk, offset);
21987
+ offset += chunk.byteLength;
21988
+ }
21989
+ return bytes;
21990
+ };
19784
21991
  }
19785
21992
  // packages/control/src/oauth/pkce.ts
19786
21993
  function randomToken() {
@@ -20692,8 +22899,8 @@ function createRefresher(deps) {
20692
22899
  };
20693
22900
  }
20694
22901
  // packages/control/src/quota/history.ts
20695
- var DAY_MS = 24 * 60 * 60 * 1000;
20696
- var HOUR_MS2 = 3600000;
22902
+ var DAY_MS2 = 24 * 60 * 60 * 1000;
22903
+ var HOUR_MS3 = 3600000;
20697
22904
  async function gatewayRatesFor(store, windows) {
20698
22905
  const rates = [];
20699
22906
  for (const window of windows) {
@@ -20717,7 +22924,7 @@ async function gatewayRatesFor(store, windows) {
20717
22924
  rates.push({
20718
22925
  credentialId: window.credentialId,
20719
22926
  windowType: window.windowType,
20720
- gatewayRatePerHour: tokens2 / ((window.observedAt - since) / HOUR_MS2)
22927
+ gatewayRatePerHour: tokens2 / ((window.observedAt - since) / HOUR_MS3)
20721
22928
  });
20722
22929
  }
20723
22930
  return rates;
@@ -20725,7 +22932,7 @@ async function gatewayRatesFor(store, windows) {
20725
22932
  async function quotaHistory(deps, input) {
20726
22933
  const now = deps.now();
20727
22934
  const settings = await deps.store.config.getSettings();
20728
- const oldest = now - settings.logRetentionDays * DAY_MS;
22935
+ const oldest = now - settings.logRetentionDays * DAY_MS2;
20729
22936
  const since = Math.max(optionalNumber(input.since, oldest), oldest);
20730
22937
  const until = Math.min(optionalNumber(input.until, now), now);
20731
22938
  const raw = input.credentialId?.trim();
@@ -20748,9 +22955,13 @@ async function getSettings(store) {
20748
22955
  return store.config.getSettings();
20749
22956
  }
20750
22957
  async function putSettings(store, input) {
20751
- const settings = parseOrThrow(settingsSchema, input);
20752
- await store.config.putSettings(settings);
20753
- return settings;
22958
+ const { snapshotKeepLatest, snapshotMaxAgeDays, ...rest } = parseOrThrow(settingsSchema, input);
22959
+ const patch = {
22960
+ ...rest,
22961
+ ...snapshotKeepLatest === undefined ? {} : { snapshotKeepLatest },
22962
+ ...snapshotMaxAgeDays === undefined ? {} : { snapshotMaxAgeDays }
22963
+ };
22964
+ return store.config.putSettings(patch);
20754
22965
  }
20755
22966
  // packages/control/src/setup.ts
20756
22967
  var KEY_PLACEHOLDER = "<your OmniGateway key>";
@@ -20863,7 +23074,7 @@ function opencodeConfig(described, input, mapping) {
20863
23074
  return { path: "opencode.json", contents };
20864
23075
  }
20865
23076
  // packages/control/src/tail.ts
20866
- import { closeSync, fstatSync, openSync, readSync, statSync } from "fs";
23077
+ import { closeSync, fstatSync, openSync, readSync, statSync as statSync2 } from "fs";
20867
23078
  var CHUNK = 64 * 1024;
20868
23079
  var MAX_BYTES = 8 * 1024 * 1024;
20869
23080
  function tailFile(path, lines) {
@@ -20901,7 +23112,7 @@ function tailFile(path, lines) {
20901
23112
  }
20902
23113
  function fileExists(path) {
20903
23114
  try {
20904
- return statSync(path).isFile();
23115
+ return statSync2(path).isFile();
20905
23116
  } catch {
20906
23117
  return false;
20907
23118
  }
@@ -20985,9 +23196,9 @@ function requirePositional(parsed, index, name) {
20985
23196
  }
20986
23197
 
20987
23198
  // apps/cli/src/context.ts
20988
- import { existsSync, readFileSync } from "fs";
23199
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
20989
23200
  import { homedir } from "os";
20990
- import { isAbsolute, join, resolve } from "path";
23201
+ import { isAbsolute, join as join5, resolve as resolve3 } from "path";
20991
23202
  class CliError extends Error {
20992
23203
  exitCode;
20993
23204
  constructor(message, exitCode = 1) {
@@ -20996,21 +23207,21 @@ class CliError extends Error {
20996
23207
  this.exitCode = exitCode;
20997
23208
  }
20998
23209
  }
20999
- var DEFAULT_ROOT = join(homedir(), ".config", "omnigateway");
23210
+ var DEFAULT_ROOT = join5(homedir(), ".config", "omnigateway");
21000
23211
  function looksLikeRoot(dir) {
21001
- return existsSync(join(dir, ".env")) || existsSync(join(dir, "omnigateway.db")) || existsSync(join(dir, "apps", "gateway", "src", "index.ts"));
23212
+ return existsSync(join5(dir, ".env")) || existsSync(join5(dir, "omnigateway.db")) || existsSync(join5(dir, "apps", "gateway", "src", "index.ts"));
21002
23213
  }
21003
23214
  function resolveRoot(flags, env2, cwd) {
21004
23215
  const withEnvFile = (root, source) => {
21005
- const envFile = join(root, ".env");
23216
+ const envFile = join5(root, ".env");
21006
23217
  return { root, source, envFile: existsSync(envFile) ? envFile : null };
21007
23218
  };
21008
23219
  if (flags.root !== undefined && flags.root.length > 0) {
21009
- return withEnvFile(resolve(cwd, flags.root), "flag");
23220
+ return withEnvFile(resolve3(cwd, flags.root), "flag");
21010
23221
  }
21011
23222
  const fromEnv = env2.OMNI_ROOT;
21012
23223
  if (typeof fromEnv === "string" && fromEnv.length > 0) {
21013
- return withEnvFile(resolve(cwd, fromEnv), "env");
23224
+ return withEnvFile(resolve3(cwd, fromEnv), "env");
21014
23225
  }
21015
23226
  if (looksLikeRoot(cwd))
21016
23227
  return withEnvFile(cwd, "cwd");
@@ -21041,7 +23252,7 @@ function createContext(parsed, options = {}) {
21041
23252
  const processEnv = options.env ?? process.env;
21042
23253
  const cwd = options.cwd ?? process.cwd();
21043
23254
  const root = resolveRoot({ root: stringFlag(parsed.values, "root") }, processEnv, cwd);
21044
- const fileEnv = root.envFile === null ? {} : parseEnvFile(readFileSync(root.envFile, "utf8"));
23255
+ const fileEnv = root.envFile === null ? {} : parseEnvFile(readFileSync2(root.envFile, "utf8"));
21045
23256
  const env2 = { ...processEnv, ...fileEnv };
21046
23257
  const rawDbFlag = stringFlag(parsed.values, "db");
21047
23258
  const dbFlag = rawDbFlag === undefined || rawDbFlag.trim().length === 0 ? undefined : rawDbFlag;
@@ -21057,7 +23268,7 @@ function createContext(parsed, options = {}) {
21057
23268
  configError = error51 instanceof Error ? error51.message : "invalid configuration";
21058
23269
  }
21059
23270
  const configuredPath = dbFlag ?? config2?.databasePath ?? "omnigateway.db";
21060
- const databasePath = isAbsolute(configuredPath) ? configuredPath : resolve(root.root, configuredPath);
23271
+ const databasePath = isAbsolute(configuredPath) ? configuredPath : resolve3(root.root, configuredPath);
21061
23272
  const warnings = suppressedDbPath === null ? [] : [
21062
23273
  `ignoring OMNI_DB_PATH=${suppressedDbPath} from the environment because --root was given; using ${databasePath}`
21063
23274
  ];
@@ -21099,6 +23310,143 @@ function createContext(parsed, options = {}) {
21099
23310
  };
21100
23311
  }
21101
23312
 
23313
+ // apps/cli/src/command.ts
23314
+ var PROVIDER_TONE = {
23315
+ anthropic: "magenta",
23316
+ openai: "green",
23317
+ kimi: "blue",
23318
+ kilo: "orange",
23319
+ grok: "yellow",
23320
+ custom: "cyan"
23321
+ };
23322
+ function provider(ctx, id) {
23323
+ return paint(ctx, PROVIDER_TONE[id], id);
23324
+ }
23325
+ function state(ctx, ok, text) {
23326
+ return paint(ctx, ok ? "green" : "red", text);
23327
+ }
23328
+
23329
+ // apps/cli/src/commands/bodies.ts
23330
+ var ABSENCE = {
23331
+ none: [
23332
+ "not captured",
23333
+ "Body capture was not running for this request. It needs OMNI_BODY_LOGGING_ALLOWED in the environment and bodyLoggingEnabled turned on, and the calling key must not have opted out."
23334
+ ],
23335
+ missing: [
23336
+ "captured, then lost",
23337
+ "This request was captured, but its artifact is no longer on disk. Retention or the row cap has since pruned it, or something removed the file underneath the gateway."
23338
+ ],
23339
+ corrupt: [
23340
+ "captured, but unreadable",
23341
+ "This request was captured and the artifact is still on disk, but it failed its checksum or would not decrypt. Changing OMNI_ENCRYPTION_KEY invalidates every artifact written under the old one."
23342
+ ]
23343
+ };
23344
+ var encoder3 = new TextEncoder;
23345
+ function payloadBytes(value) {
23346
+ if (value === null || value === undefined)
23347
+ return 0;
23348
+ return encoder3.encode(JSON.stringify(value)).length;
23349
+ }
23350
+ function frame(read) {
23351
+ return fields([
23352
+ ["STATE", read.detailState],
23353
+ ["CAPTURED", formatTime(read.at)],
23354
+ ["SIZE", `${formatBytes(read.sizeBytes)} on disk`],
23355
+ ["TRUNCATED", read.truncated ? "yes" : "no"]
23356
+ ]);
23357
+ }
23358
+ function pairs(ctx, artifact) {
23359
+ const frames = artifact.attempts.some((attempt) => attempt.streamChunks !== null);
23360
+ const label2 = (text, truncated) => truncated ? `${text} (truncated)` : text;
23361
+ return table([
23362
+ { header: "PAIR" },
23363
+ { header: "PROVIDER" },
23364
+ { header: "REQUEST", align: "right" },
23365
+ { header: "RESPONSE", align: "right" },
23366
+ ...frames ? [{ header: "FRAMES", align: "right" }] : [],
23367
+ { header: "REQUEST IS" }
23368
+ ], [
23369
+ [
23370
+ label2("CLIENT", artifact.client.truncated),
23371
+ "",
23372
+ formatBytes(payloadBytes(artifact.client.request)),
23373
+ formatBytes(payloadBytes(artifact.client.response)),
23374
+ ...frames ? ["\u2014"] : [],
23375
+ "pre-RTK"
23376
+ ],
23377
+ ...artifact.attempts.map((attempt) => [
23378
+ label2(`ATTEMPT ${attempt.attempt}`, attempt.truncated),
23379
+ provider(ctx, attempt.provider),
23380
+ formatBytes(payloadBytes(attempt.request)),
23381
+ formatBytes(payloadBytes(attempt.response)),
23382
+ ...frames ? [attempt.streamChunks === null ? "\u2014" : String(attempt.streamChunks.length)] : [],
23383
+ "post-RTK"
23384
+ ])
23385
+ ]);
23386
+ }
23387
+ function block(ctx, heading, value) {
23388
+ const body2 = value === null || value === undefined ? "(nothing recorded for this half \u2014 it never happened, or never completed)" : JSON.stringify(value, null, 2);
23389
+ return `${paint(ctx, "dim", heading)}
23390
+ ${body2}`;
23391
+ }
23392
+ function full(ctx, artifact) {
23393
+ const sections = [
23394
+ block(ctx, "CLIENT REQUEST (pre-RTK)", artifact.client.request),
23395
+ block(ctx, "CLIENT RESPONSE", artifact.client.response)
23396
+ ];
23397
+ for (const attempt of artifact.attempts) {
23398
+ const name = `ATTEMPT ${attempt.attempt} ${provider(ctx, attempt.provider)}`;
23399
+ sections.push(block(ctx, `${name} REQUEST (post-RTK)`, attempt.request));
23400
+ sections.push(block(ctx, `${name} RESPONSE`, attempt.response));
23401
+ if (attempt.streamChunks !== null) {
23402
+ sections.push(block(ctx, `${name} STREAM FRAMES (${attempt.streamChunks.length})`, attempt.streamChunks));
23403
+ }
23404
+ }
23405
+ if (artifact.error !== null && artifact.error !== undefined) {
23406
+ sections.push(block(ctx, "ERROR", artifact.error));
23407
+ }
23408
+ return sections.join(`
23409
+
23410
+ `);
23411
+ }
23412
+ var bodies = {
23413
+ usage: "bodies <request-id> [--full]",
23414
+ summary: "Show captured bodies for one request; withheld unless --full",
23415
+ options: { full: { type: "boolean" } },
23416
+ async run(args, { ctx, writer }) {
23417
+ const requestId = requirePositional(args, 0, "request id");
23418
+ const read = await readRequestBody(await ctx.store(), requestId);
23419
+ emit(ctx, writer, read, () => {
23420
+ const artifact = read.artifact;
23421
+ if (artifact === null) {
23422
+ const [legend, message] = ABSENCE[read.detailState === "ready" ? "corrupt" : read.detailState];
23423
+ return [frame(read), "", legend, message].join(`
23424
+ `);
23425
+ }
23426
+ const lines = [
23427
+ frame(read),
23428
+ "",
23429
+ pairs(ctx, artifact),
23430
+ "",
23431
+ paint(ctx, "dim", "REQUEST and RESPONSE are the stored payload, after masking and bounding; neither is the size sent over the wire")
23432
+ ];
23433
+ if (artifact.attempts.length > 0) {
23434
+ lines.push("", paint(ctx, "dim", "the client request is pre-RTK and every attempt request is post-RTK; they are not the same payload"));
23435
+ }
23436
+ if (artifact.error !== null && artifact.error !== undefined) {
23437
+ lines.push("", "an error was recorded for this request");
23438
+ }
23439
+ if (boolFlag(args.values, "full")) {
23440
+ lines.push("", full(ctx, artifact));
23441
+ } else {
23442
+ lines.push("", "bodies withheld; pass --full to print them, --json for the artifact");
23443
+ }
23444
+ return lines.join(`
23445
+ `);
23446
+ });
23447
+ }
23448
+ };
23449
+
21102
23450
  // apps/cli/src/commands/connect.ts
21103
23451
  var DEVICE_TIMEOUT_MS = 600000;
21104
23452
  var connect = {
@@ -21146,21 +23494,21 @@ async function pollUntilAuthorized(flows, start, deps) {
21146
23494
  }
21147
23495
 
21148
23496
  // apps/cli/src/service.ts
21149
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
23497
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
21150
23498
  import { homedir as homedir2 } from "os";
21151
- import { dirname, join as join2 } from "path";
23499
+ import { dirname as dirname3, join as join6 } from "path";
21152
23500
  var UNIT_NAME2 = "omnigateway.service";
21153
23501
  function unitPath(scope) {
21154
- return scope === "system" ? join2("/etc/systemd/system", UNIT_NAME2) : join2(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
23502
+ return scope === "system" ? join6("/etc/systemd/system", UNIT_NAME2) : join6(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
21155
23503
  }
21156
23504
  function systemctlArgs(scope, args) {
21157
23505
  return scope === "system" ? ["systemctl", ...args] : ["systemctl", "--user", ...args];
21158
23506
  }
21159
23507
  function pidFile(stateDir) {
21160
- return join2(stateDir, "gateway.pid");
23508
+ return join6(stateDir, "gateway.pid");
21161
23509
  }
21162
23510
  function logFile(stateDir) {
21163
- return join2(stateDir, "gateway.log");
23511
+ return join6(stateDir, "gateway.log");
21164
23512
  }
21165
23513
  function supervisedLogFile(deps) {
21166
23514
  const configured = deps.logFile?.trim();
@@ -21168,8 +23516,8 @@ function supervisedLogFile(deps) {
21168
23516
  }
21169
23517
  function defaultStateDir(env2) {
21170
23518
  const xdg = env2.XDG_STATE_HOME;
21171
- const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join2(homedir2(), ".local", "state");
21172
- return join2(base, "omnigateway");
23519
+ const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join6(homedir2(), ".local", "state");
23520
+ return join6(base, "omnigateway");
21173
23521
  }
21174
23522
  function unitFile(input) {
21175
23523
  return `[Unit]
@@ -21180,7 +23528,7 @@ Wants=network-online.target
21180
23528
  [Service]
21181
23529
  Type=simple
21182
23530
  WorkingDirectory=${input.root}
21183
- EnvironmentFile=${join2(input.root, ".env")}
23531
+ EnvironmentFile=${join6(input.root, ".env")}
21184
23532
  ExecStart=${input.bun} ${input.entrypoint}
21185
23533
  Restart=on-failure
21186
23534
  RestartSec=2
@@ -21196,7 +23544,7 @@ function readPid(stateDir) {
21196
23544
  const file2 = pidFile(stateDir);
21197
23545
  if (!existsSync2(file2))
21198
23546
  return null;
21199
- const pid = Number(readFileSync2(file2, "utf8").trim());
23547
+ const pid = Number(readFileSync3(file2, "utf8").trim());
21200
23548
  return Number.isInteger(pid) && pid > 0 ? pid : null;
21201
23549
  }
21202
23550
  function livePid(deps) {
@@ -21205,20 +23553,20 @@ function livePid(deps) {
21205
23553
  return null;
21206
23554
  if (deps.alive(pid))
21207
23555
  return pid;
21208
- rmSync(pidFile(deps.stateDir), { force: true });
23556
+ rmSync2(pidFile(deps.stateDir), { force: true });
21209
23557
  return null;
21210
23558
  }
21211
23559
  async function status(deps) {
21212
23560
  if (unitInstalled(deps)) {
21213
23561
  const active = await deps.run(systemctlArgs(deps.scope, ["is-active", UNIT_NAME2]));
21214
- const state = active.stdout.trim() || active.stderr.trim() || "unknown";
23562
+ const state2 = active.stdout.trim() || active.stderr.trim() || "unknown";
21215
23563
  const shown = await deps.run(systemctlArgs(deps.scope, ["show", UNIT_NAME2, "--property=MainPID", "--value"]));
21216
23564
  const pid2 = Number(shown.stdout.trim());
21217
23565
  return {
21218
23566
  supervisor: "systemd",
21219
- running: state === "active",
23567
+ running: state2 === "active",
21220
23568
  pid: Number.isInteger(pid2) && pid2 > 0 ? pid2 : null,
21221
- state,
23569
+ state: state2,
21222
23570
  unitPath: deps.unitPath,
21223
23571
  logFile: null
21224
23572
  };
@@ -21253,16 +23601,16 @@ async function start(deps, input) {
21253
23601
  healthy: await waitForHealth(deps, input.baseUrl)
21254
23602
  };
21255
23603
  }
21256
- mkdirSync(deps.stateDir, { recursive: true });
23604
+ mkdirSync2(deps.stateDir, { recursive: true });
21257
23605
  const file2 = supervisedLogFile(deps);
21258
- mkdirSync(dirname(file2), { recursive: true });
23606
+ mkdirSync2(dirname3(file2), { recursive: true });
21259
23607
  const pid = deps.spawn({
21260
23608
  argv: input.argv,
21261
23609
  cwd: deps.root,
21262
23610
  env: { ...input.env, OMNI_LOG_FILE: file2 },
21263
23611
  logFile: file2
21264
23612
  });
21265
- writeFileSync(pidFile(deps.stateDir), `${pid}
23613
+ writeFileSync2(pidFile(deps.stateDir), `${pid}
21266
23614
  `);
21267
23615
  return { supervisor: "pidfile", pid, healthy: await waitForHealth(deps, input.baseUrl) };
21268
23616
  }
@@ -21299,7 +23647,7 @@ async function stop(deps) {
21299
23647
  }
21300
23648
  await deps.sleep(STOP_INTERVAL_MS);
21301
23649
  }
21302
- rmSync(pidFile(deps.stateDir), { force: true });
23650
+ rmSync2(pidFile(deps.stateDir), { force: true });
21303
23651
  return { supervisor: "pidfile", stopped: true };
21304
23652
  }
21305
23653
  async function install(deps, input) {
@@ -21307,8 +23655,8 @@ async function install(deps, input) {
21307
23655
  if (existsSync2(path) && !input.force) {
21308
23656
  throw new Error(`${path} already exists; pass --force to replace it`);
21309
23657
  }
21310
- mkdirSync(dirname(path), { recursive: true });
21311
- writeFileSync(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
23658
+ mkdirSync2(dirname3(path), { recursive: true });
23659
+ writeFileSync2(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
21312
23660
  const reload = await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
21313
23661
  let enabled = false;
21314
23662
  if (input.enable) {
@@ -21322,7 +23670,7 @@ async function uninstall(deps) {
21322
23670
  if (!existsSync2(path))
21323
23671
  return { path, removed: false };
21324
23672
  await deps.run(systemctlArgs(deps.scope, ["disable", "--now", UNIT_NAME2]));
21325
- rmSync(path, { force: true });
23673
+ rmSync2(path, { force: true });
21326
23674
  await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
21327
23675
  return { path, removed: true };
21328
23676
  }
@@ -21417,22 +23765,6 @@ var console_ = {
21417
23765
  }
21418
23766
  };
21419
23767
 
21420
- // apps/cli/src/command.ts
21421
- var PROVIDER_TONE = {
21422
- anthropic: "magenta",
21423
- openai: "green",
21424
- kimi: "blue",
21425
- kilo: "orange",
21426
- grok: "yellow",
21427
- custom: "cyan"
21428
- };
21429
- function provider(ctx, id) {
21430
- return paint(ctx, PROVIDER_TONE[id], id);
21431
- }
21432
- function state(ctx, ok, text) {
21433
- return paint(ctx, ok ? "green" : "red", text);
21434
- }
21435
-
21436
23768
  // apps/cli/src/commands/credentials.ts
21437
23769
  function condition(credential) {
21438
23770
  if (!credential.enabled)
@@ -21658,6 +23990,9 @@ var credentialsHealth = {
21658
23990
 
21659
23991
  // apps/cli/src/commands/db.ts
21660
23992
  import { existsSync as existsSync3 } from "fs";
23993
+ async function database(env2) {
23994
+ return { store: await env2.ctx.store(), fs: nodeDatabaseFs(), now: env2.ctx.now };
23995
+ }
21661
23996
  var dbMigrate = {
21662
23997
  usage: "db migrate",
21663
23998
  summary: "Create or upgrade the database schema",
@@ -21671,8 +24006,234 @@ var dbMigrate = {
21671
24006
  ]));
21672
24007
  }
21673
24008
  };
24009
+ var dbStats = {
24010
+ usage: "db stats",
24011
+ summary: "Show database size, free pages, and what snapshots are held",
24012
+ async run(_args, env2) {
24013
+ const { ctx, writer } = env2;
24014
+ const overview = await getDatabaseOverview(await database(env2));
24015
+ emit(ctx, writer, overview, () => fields([
24016
+ ["database", ctx.databasePath],
24017
+ ["size", formatBytes(overview.fileBytes)],
24018
+ ["write-ahead log", formatBytes(overview.walBytes)],
24019
+ ["captured bodies", `${formatBytes(overview.bodiesBytes)} (never snapshotted)`],
24020
+ ["free pages", `${formatBytes(overview.freePageBytes)} \u2014 reclaimed by omni db vacuum`],
24021
+ ["schema version", String(overview.stats.schemaVersion)],
24022
+ [
24023
+ "snapshots",
24024
+ `${overview.snapshots.count} (${formatBytes(overview.snapshots.totalBytes)}), latest ${formatTime(overview.snapshots.latestAt)}`
24025
+ ],
24026
+ [
24027
+ "retention",
24028
+ `keep ${overview.retention.keepLatest}, up to ${overview.retention.maxAgeDays} days`
24029
+ ],
24030
+ [
24031
+ "free disk",
24032
+ overview.freeDiskBytes === null ? "unknown" : formatBytes(overview.freeDiskBytes)
24033
+ ]
24034
+ ]));
24035
+ }
24036
+ };
24037
+ var dbSnapshots = {
24038
+ usage: "db snapshots",
24039
+ summary: "List the snapshots held for this installation",
24040
+ async run(_args, env2) {
24041
+ const { ctx, writer } = env2;
24042
+ const deps = await database(env2);
24043
+ const snapshots = listSnapshots(deps);
24044
+ emit(ctx, writer, { snapshots }, () => {
24045
+ if (snapshots.length === 0) {
24046
+ return `no snapshots in ${snapshotsDir(deps)}; take one with omni db backup`;
24047
+ }
24048
+ return table([
24049
+ { header: "ID" },
24050
+ { header: "TAKEN" },
24051
+ { header: "SIZE", align: "right" },
24052
+ { header: "REASON" }
24053
+ ], snapshots.map((snapshot) => [
24054
+ snapshot.id,
24055
+ formatTime(snapshot.createdAt),
24056
+ formatBytes(snapshot.sizeBytes),
24057
+ snapshot.reason
24058
+ ]));
24059
+ });
24060
+ }
24061
+ };
24062
+ var dbBackup = {
24063
+ usage: "db backup",
24064
+ summary: "Take a snapshot of the database, pruning by the retention policy",
24065
+ async run(_args, env2) {
24066
+ const { ctx, writer } = env2;
24067
+ const snapshot = await createSnapshot(await database(env2), { reason: "manual" });
24068
+ emit(ctx, writer, snapshot, () => {
24069
+ note(ctx, writer, "a snapshot carries encrypted credentials and key hashes; captured bodies are not in it");
24070
+ return fields([
24071
+ ["snapshot", snapshot.id],
24072
+ ["size", formatBytes(snapshot.sizeBytes)],
24073
+ ["taken", formatTime(snapshot.createdAt)]
24074
+ ]);
24075
+ });
24076
+ }
24077
+ };
24078
+ var dbVacuum = {
24079
+ usage: "db vacuum",
24080
+ summary: "Rewrite the database, reclaiming the pages deletion left free",
24081
+ async run(_args, env2) {
24082
+ const { ctx, writer } = env2;
24083
+ note(ctx, writer, "compacting; the gateway's writes will block until this finishes\u2026");
24084
+ const result = await vacuum(await database(env2));
24085
+ emit(ctx, writer, result, () => fields([
24086
+ ["reclaimed", formatBytes(result.reclaimedBytes)],
24087
+ ["took", `${result.durationMs} ms`]
24088
+ ]));
24089
+ }
24090
+ };
24091
+ var dbRestore = {
24092
+ usage: "db restore <id>",
24093
+ summary: "Replace the database with a snapshot, keeping a copy of what was there",
24094
+ async run(args, env2) {
24095
+ const { ctx, writer, prompt } = env2;
24096
+ const id = requirePositional(args, 0, "snapshot id");
24097
+ const running = await status(env2.service());
24098
+ if (running.running) {
24099
+ const who = running.pid === null ? running.supervisor : `${running.supervisor}, pid ${running.pid}`;
24100
+ throw new CliError(`a gateway is running (${who}) against this installation; run omni stop first, ` + "or restore from the dashboard, which swaps the file behind its own quiesce latch");
24101
+ }
24102
+ if (!await prompt.confirm(`replace ${ctx.databasePath} with ${id}?`)) {
24103
+ throw new CliError("cancelled");
24104
+ }
24105
+ const result = await restoreSnapshot(await database(env2), id);
24106
+ emit(ctx, writer, result, () => fields([
24107
+ ["restored", id],
24108
+ ["undo", result.preRestoreSnapshot.id],
24109
+ ...Object.entries(result.counts).map(([table2, count]) => [table2, String(count)])
24110
+ ]));
24111
+ }
24112
+ };
21674
24113
 
21675
24114
  // apps/cli/src/commands/keys.ts
24115
+ var WINDOWED = new Set(["requests", "tokens", "spend"]);
24116
+ var PROTO = "__proto__";
24117
+ function rejectProto(flag, name) {
24118
+ if (name === PROTO)
24119
+ throw new UsageError(`${flag} cannot name "${PROTO}"`);
24120
+ }
24121
+ function parseLimitFlags(entries, flag, into = {}) {
24122
+ const limits = into;
24123
+ for (const entry of entries) {
24124
+ const equals = entry.indexOf("=");
24125
+ if (equals <= 0) {
24126
+ throw new UsageError(`${flag} must be dimension:window=value, got "${entry}"`);
24127
+ }
24128
+ const pair = entry.slice(0, equals);
24129
+ const raw = entry.slice(equals + 1);
24130
+ const value = Number(raw);
24131
+ if (raw.trim().length === 0 || !Number.isFinite(value)) {
24132
+ throw new UsageError(`${flag} ${pair} must be a number, got "${raw}"`);
24133
+ }
24134
+ const colon = pair.indexOf(":");
24135
+ if (colon < 0) {
24136
+ if (WINDOWED.has(pair)) {
24137
+ throw new UsageError(`${flag} ${pair} needs a window, e.g. ${flag} ${pair}:1m=60`);
24138
+ }
24139
+ rejectProto(flag, pair);
24140
+ limits[pair] = value;
24141
+ continue;
24142
+ }
24143
+ const dimension = pair.slice(0, colon);
24144
+ const window = pair.slice(colon + 1);
24145
+ if (dimension.length === 0 || window.length === 0) {
24146
+ throw new UsageError(`${flag} must be dimension:window=value, got "${entry}"`);
24147
+ }
24148
+ rejectProto(flag, dimension);
24149
+ rejectProto(flag, window);
24150
+ const existing = limits[dimension];
24151
+ const windows = typeof existing === "object" ? existing : {};
24152
+ windows[window] = value;
24153
+ limits[dimension] = windows;
24154
+ }
24155
+ return limits;
24156
+ }
24157
+ function toLoose(limits) {
24158
+ const loose = {};
24159
+ for (const dimension of ["requests", "tokens", "spend"]) {
24160
+ const windows = limits[dimension];
24161
+ if (windows === undefined)
24162
+ continue;
24163
+ const kept = {};
24164
+ for (const [window, value] of Object.entries(windows)) {
24165
+ if (value === null || value === undefined)
24166
+ continue;
24167
+ kept[window] = value;
24168
+ }
24169
+ if (Object.keys(kept).length > 0)
24170
+ loose[dimension] = kept;
24171
+ }
24172
+ if (limits.concurrency !== undefined && limits.concurrency !== null) {
24173
+ loose.concurrency = limits.concurrency;
24174
+ }
24175
+ return loose;
24176
+ }
24177
+ function applyUnsetFlags(limits, entries) {
24178
+ for (const entry of entries) {
24179
+ if (entry.includes("=")) {
24180
+ throw new UsageError(`--unset names a limit to remove, not a value, got "${entry}"`);
24181
+ }
24182
+ const colon = entry.indexOf(":");
24183
+ if (colon < 0) {
24184
+ if (WINDOWED.has(entry)) {
24185
+ throw new UsageError(`--unset ${entry} needs a window, e.g. --unset ${entry}:1m`);
24186
+ }
24187
+ rejectProto("--unset", entry);
24188
+ if (limits[entry] === undefined)
24189
+ throw new UsageError(`this key has no ${entry} limit`);
24190
+ delete limits[entry];
24191
+ continue;
24192
+ }
24193
+ const dimension = entry.slice(0, colon);
24194
+ const window = entry.slice(colon + 1);
24195
+ if (dimension.length === 0 || window.length === 0) {
24196
+ throw new UsageError(`--unset must be dimension:window, got "${entry}"`);
24197
+ }
24198
+ rejectProto("--unset", dimension);
24199
+ rejectProto("--unset", window);
24200
+ const windows = limits[dimension];
24201
+ if (typeof windows !== "object" || windows[window] === undefined) {
24202
+ throw new UsageError(`this key has no ${entry} limit`);
24203
+ }
24204
+ delete windows[window];
24205
+ if (Object.keys(windows).length === 0)
24206
+ delete limits[dimension];
24207
+ }
24208
+ }
24209
+ function limitRow(reading) {
24210
+ const money = reading.dimension === "spend";
24211
+ const amount = (value) => money ? formatUsd(value) : String(value);
24212
+ return [
24213
+ reading.dimension,
24214
+ reading.window ?? "\u2014",
24215
+ amount(reading.limit),
24216
+ reading.used === null ? "\u2014" : amount(reading.used),
24217
+ reading.used === null ? "\u2014" : `${Math.round(reading.used / reading.limit * 100)}%`
24218
+ ];
24219
+ }
24220
+ function summarizeLimits(limits) {
24221
+ const parts = [];
24222
+ for (const dimension of ["requests", "tokens", "spend"]) {
24223
+ const windows = limits[dimension];
24224
+ if (windows === undefined)
24225
+ continue;
24226
+ for (const [window, value] of Object.entries(windows)) {
24227
+ if (value === null || value === undefined)
24228
+ continue;
24229
+ parts.push(`${dimension}:${window}=${value}`);
24230
+ }
24231
+ }
24232
+ if (limits.concurrency !== undefined && limits.concurrency !== null) {
24233
+ parts.push(`concurrency=${limits.concurrency}`);
24234
+ }
24235
+ return parts.length === 0 ? "\u2014" : parts.join(" ");
24236
+ }
21676
24237
  var keysList = {
21677
24238
  usage: "keys list",
21678
24239
  summary: "List gateway API keys",
@@ -21686,7 +24247,8 @@ var keysList = {
21686
24247
  { header: "LABEL" },
21687
24248
  { header: "PREFIX" },
21688
24249
  { header: "MODELS" },
21689
- { header: "RATE/MIN", align: "right" },
24250
+ { header: "LIMITS" },
24251
+ { header: "BODY CAPTURE" },
21690
24252
  { header: "STATE" },
21691
24253
  { header: "CREATED" }
21692
24254
  ], keys.map((key) => [
@@ -21694,7 +24256,8 @@ var keysList = {
21694
24256
  key.label,
21695
24257
  key.prefix,
21696
24258
  key.modelAllowlist === null ? "all" : key.modelAllowlist.length === 0 ? "none" : key.modelAllowlist.join(","),
21697
- key.rateLimitPerMin === null ? "\u2014" : String(key.rateLimitPerMin),
24259
+ key.limits === null ? paint(ctx, "red", "unreadable") : summarizeLimits(key.limits),
24260
+ key.bodyLoggingOptOut ? "no bodies" : "\u2014",
21698
24261
  state(ctx, key.revokedAt === null, key.revokedAt === null ? "active" : "revoked"),
21699
24262
  formatTime(key.createdAt)
21700
24263
  ]));
@@ -21702,19 +24265,21 @@ var keysList = {
21702
24265
  }
21703
24266
  };
21704
24267
  var keysCreate = {
21705
- usage: "keys create [--label L] [--allow <model> ...] [--rate-limit N]",
24268
+ usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--no-bodies]",
21706
24269
  summary: "Mint a gateway API key, printed once",
21707
24270
  options: {
21708
24271
  label: { type: "string" },
21709
24272
  allow: { type: "string", multiple: true },
21710
- "rate-limit": { type: "string" }
24273
+ limit: { type: "string", multiple: true },
24274
+ "no-bodies": { type: "boolean" }
21711
24275
  },
21712
24276
  async run(args, { ctx, writer }) {
21713
24277
  const allow = listFlag(args.values, "allow");
21714
24278
  const created = await createKey(await ctx.store(), {
21715
24279
  ...stringFlag(args.values, "label") === undefined ? {} : { label: stringFlag(args.values, "label") },
21716
24280
  modelAllowlist: allow ?? null,
21717
- rateLimitPerMin: numberFlag(args.values, "rate-limit") ?? null
24281
+ limits: parseLimitFlags(listFlag(args.values, "limit") ?? [], "--limit"),
24282
+ bodyLoggingOptOut: boolFlag(args.values, "no-bodies")
21718
24283
  });
21719
24284
  emit(ctx, writer, created, () => {
21720
24285
  note(ctx, writer, paint(ctx, "yellow", "this key is shown once and stored only as a hash"));
@@ -21722,6 +24287,63 @@ var keysCreate = {
21722
24287
  });
21723
24288
  }
21724
24289
  };
24290
+ var keysLimits = {
24291
+ usage: "keys limits <id> [--set <d>:<w>=N ...] [--unset <d>:<w> ...]",
24292
+ summary: "Show or edit one key's limits, with what has been used against them",
24293
+ options: {
24294
+ set: { type: "string", multiple: true },
24295
+ unset: { type: "string", multiple: true }
24296
+ },
24297
+ async run(args, { ctx, writer }) {
24298
+ const id = requirePositional(args, 0, "key id");
24299
+ const sets = listFlag(args.values, "set") ?? [];
24300
+ const unsets = listFlag(args.values, "unset") ?? [];
24301
+ const store = await ctx.store();
24302
+ const existing = (await listKeys(store)).find((entry) => entry.id === id);
24303
+ if (existing === undefined)
24304
+ throw new CliError(`no api key "${id}"`);
24305
+ let key = existing;
24306
+ if (sets.length > 0 || unsets.length > 0) {
24307
+ if (existing.limits === null) {
24308
+ if (unsets.length > 0) {
24309
+ throw new CliError(`the stored limits for "${id}" cannot be read, so there is nothing to unset; ` + "replace them with --set instead");
24310
+ }
24311
+ note(ctx, writer, paint(ctx, "yellow", "the stored limits could not be read and are being replaced"));
24312
+ }
24313
+ const next = existing.limits === null ? {} : toLoose(existing.limits);
24314
+ applyUnsetFlags(next, unsets);
24315
+ parseLimitFlags(sets, "--set", next);
24316
+ key = await setKeyLimits(store, id, { limits: next });
24317
+ }
24318
+ emit(ctx, writer, key, () => {
24319
+ const head = fields([
24320
+ ["id", key.id],
24321
+ ["label", key.label],
24322
+ ["prefix", `${key.prefix}\u2026`]
24323
+ ]);
24324
+ if (key.limits === null) {
24325
+ return `${head}
24326
+
24327
+ ${paint(ctx, "red", "limits unreadable")}: this key is refused at /v1 until they are replaced with --set`;
24328
+ }
24329
+ if (key.limitUsage.length === 0) {
24330
+ return `${head}
24331
+
24332
+ no limits configured; this key is unlimited`;
24333
+ }
24334
+ return `${head}
24335
+
24336
+ ${table([
24337
+ { header: "DIMENSION" },
24338
+ { header: "WINDOW" },
24339
+ { header: "LIMIT", align: "right" },
24340
+ { header: "USED", align: "right" },
24341
+ { header: "USE%", align: "right" }
24342
+ ], key.limitUsage.map(limitRow))}
24343
+ ${paint(ctx, "dim", "usage is counted from completed requests still inside each window")}`;
24344
+ });
24345
+ }
24346
+ };
21725
24347
  var keysRevoke = {
21726
24348
  usage: "keys revoke <id>",
21727
24349
  summary: "Revoke a gateway API key, keeping its usage history",
@@ -21985,6 +24607,168 @@ ${candidates}${excluded}`;
21985
24607
  });
21986
24608
  }
21987
24609
  };
24610
+ // packages/plugin-api/src/events.ts
24611
+ var WINDOW_MS2 = {
24612
+ "1m": 60000,
24613
+ "5h": 5 * 60 * 60 * 1000,
24614
+ "1w": 7 * 24 * 60 * 60 * 1000
24615
+ };
24616
+ // apps/cli/src/commands/plugins.ts
24617
+ function pluginDeps() {
24618
+ return { fs: nodePluginFs(), sdkVersion: DASHBOARD_SDK_VERSION };
24619
+ }
24620
+ function installDeps(ctx, args) {
24621
+ const flag = stringFlag(args.values, "registry");
24622
+ const raw = flag ?? ctx.env.OMNI_PLUGIN_REGISTRY;
24623
+ const registry2 = raw === undefined || raw.trim().length === 0 ? undefined : raw.trim();
24624
+ return {
24625
+ ...pluginDeps(),
24626
+ fetchBytes: nodeFetchBytes(),
24627
+ ...registry2 === undefined ? {} : { registry: registry2 }
24628
+ };
24629
+ }
24630
+ function pluginState(ctx, plugin) {
24631
+ if (!plugin.loadable)
24632
+ return state(ctx, false, "will not load");
24633
+ if (plugin.problems.length > 0)
24634
+ return paint(ctx, "yellow", "loads, with warnings");
24635
+ return state(ctx, true, "ok");
24636
+ }
24637
+ var pluginList = {
24638
+ usage: "plugin list",
24639
+ summary: "List installed plugins and whether the gateway would load each one",
24640
+ async run(_args, { ctx, writer }) {
24641
+ const root = ctx.root.root;
24642
+ const plugins = listPlugins(pluginDeps(), root);
24643
+ emit(ctx, writer, { pluginsDir: pluginsDir(root), plugins }, () => {
24644
+ if (plugins.length === 0) {
24645
+ return `no plugins installed; unpack one into ${pluginsDir(root)} with: omni plugin install <path-or-package>`;
24646
+ }
24647
+ const rows = plugins.map((plugin) => [
24648
+ plugin.id,
24649
+ plugin.name ?? "\u2014",
24650
+ plugin.version ?? "\u2014",
24651
+ plugin.api === null ? "\u2014" : String(plugin.api),
24652
+ plugin.sdk ?? "\u2014",
24653
+ plugin.capabilities.length === 0 ? "\u2014" : plugin.capabilities.join(","),
24654
+ pluginState(ctx, plugin)
24655
+ ]);
24656
+ const body2 = table([
24657
+ { header: "ID" },
24658
+ { header: "NAME" },
24659
+ { header: "VERSION" },
24660
+ { header: "API" },
24661
+ { header: "SDK" },
24662
+ { header: "CAPABILITIES" },
24663
+ { header: "STATE" }
24664
+ ], rows);
24665
+ const notes = plugins.flatMap((plugin) => plugin.problems.map((problem) => `${plugin.id}: ${paint(ctx, problem.fatal ? "red" : "yellow", problem.reason)}`));
24666
+ return notes.length === 0 ? body2 : `${body2}
24667
+
24668
+ ${notes.join(`
24669
+ `)}`;
24670
+ });
24671
+ }
24672
+ };
24673
+ var pluginVerify = {
24674
+ usage: "plugin verify <id>",
24675
+ summary: "Run every load-time check on one plugin without loading it",
24676
+ async run(args, { ctx, writer }) {
24677
+ const id = requirePositional(args, 0, "plugin id");
24678
+ const report = verifyPlugin(pluginDeps(), ctx.root.root, id);
24679
+ emit(ctx, writer, report, () => {
24680
+ const manifest = report.manifest;
24681
+ const head = fields([
24682
+ ["id", report.id],
24683
+ ["path", report.path],
24684
+ ["name", manifest?.name ?? "\u2014"],
24685
+ ["version", manifest?.version ?? "\u2014"],
24686
+ ["api", manifest === null ? "\u2014" : String(manifest.api)],
24687
+ ["sdk", manifest?.sdk ?? "\u2014"],
24688
+ ["server", manifest?.server ?? "\u2014"],
24689
+ ["ui", manifest?.ui ?? "\u2014"],
24690
+ [
24691
+ "capabilities",
24692
+ manifest === null || manifest.capabilities.length === 0 ? "\u2014" : manifest.capabilities.join(",")
24693
+ ],
24694
+ ["origins", manifest?.origins?.join(",") ?? "\u2014"]
24695
+ ]);
24696
+ if (report.problems.length === 0) {
24697
+ return `${head}
24698
+
24699
+ ${state(ctx, true, "ok")}: this plugin would load`;
24700
+ }
24701
+ const lines = report.problems.map((problem) => ` ${paint(ctx, problem.fatal ? "red" : "yellow", problem.check)} ${problem.reason}`);
24702
+ const verdict = report.loadable ? paint(ctx, "yellow", "this plugin would load, with the warnings above") : state(ctx, false, "this plugin would be skipped at boot");
24703
+ return `${head}
24704
+
24705
+ ${lines.join(`
24706
+ `)}
24707
+
24708
+ ${verdict}`;
24709
+ });
24710
+ if (!report.loadable)
24711
+ throw new CliError(`plugin "${id}" would not load`);
24712
+ }
24713
+ };
24714
+ var pluginInstall = {
24715
+ usage: "plugin install <path|tarball|https url|package[@version]> [--registry <url>]",
24716
+ summary: "Unpack a plugin into this installation, running nothing from it",
24717
+ options: { registry: { type: "string" } },
24718
+ async run(args, { ctx, writer }) {
24719
+ const spec = requirePositional(args, 0, "plugin directory, tarball, url, or package name");
24720
+ const result = await installPlugin(installDeps(ctx, args), ctx.root.root, spec);
24721
+ emit(ctx, writer, result, () => {
24722
+ note(ctx, writer, paint(ctx, "dim", "no code from the package was executed; verify it before restarting"));
24723
+ const verb = result.replaced ? "replaced" : "installed";
24724
+ return [
24725
+ `${verb} ${result.name} ${result.version} at ${result.path}`,
24726
+ paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart")
24727
+ ].join(`
24728
+ `);
24729
+ });
24730
+ }
24731
+ };
24732
+ var pluginRemove = {
24733
+ usage: "plugin remove <id> [--purge]",
24734
+ summary: "Remove a plugin, optionally dropping its stored data too",
24735
+ options: { purge: { type: "boolean" } },
24736
+ async run(args, { ctx, writer, prompt }) {
24737
+ const id = requirePositional(args, 0, "plugin id");
24738
+ const purge = boolFlag(args.values, "purge");
24739
+ const question = purge ? `remove plugin "${id}" AND permanently drop its stored data?` : `remove plugin "${id}"? its stored data is kept`;
24740
+ if (!await prompt.confirm(question))
24741
+ throw new CliError("cancelled");
24742
+ const deps = purge ? { fs: nodePluginFs(), store: await ctx.store() } : { fs: nodePluginFs() };
24743
+ const result = removePlugin(deps, ctx.root.root, id, { purge });
24744
+ emit(ctx, writer, result, () => {
24745
+ const lines = [];
24746
+ lines.push(result.removed ? `removed ${id}` : `no directory for ${id} to remove`);
24747
+ if (purge) {
24748
+ lines.push(result.droppedTables.length === 0 ? "no stored tables to drop" : `dropped ${result.droppedTables.length} table(s): ${result.droppedTables.join(", ")}`);
24749
+ } else {
24750
+ lines.push(paint(ctx, "dim", "stored tables and migration history kept; --purge drops them"));
24751
+ }
24752
+ lines.push(paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart"));
24753
+ return lines.join(`
24754
+ `);
24755
+ });
24756
+ }
24757
+ };
24758
+ function doctorPluginDeps() {
24759
+ return pluginDeps();
24760
+ }
24761
+ function pluginDoctorLines(ctx, plugins) {
24762
+ return plugins.map((plugin) => {
24763
+ const version2 = plugin.version === null ? "" : ` ${plugin.version}`;
24764
+ const api2 = plugin.api === null ? "api ?" : `api ${plugin.api}`;
24765
+ const sdk = plugin.sdk === null ? "no ui" : `sdk ${plugin.sdk}`;
24766
+ const reasons = plugin.problems.filter((problem) => problem.fatal).map((p) => p.reason);
24767
+ const warnings = plugin.problems.length - reasons.length;
24768
+ const verdict = reasons.length > 0 ? state(ctx, false, `will not load: ${reasons.join("; ")}`) : warnings > 0 ? paint(ctx, "yellow", `loads, ${warnings} warning(s); see omni plugin verify ${plugin.id}`) : state(ctx, true, "ok");
24769
+ return `${plugin.id}${version2} (${api2}, ${sdk}) ${verdict}`;
24770
+ });
24771
+ }
21988
24772
 
21989
24773
  // apps/cli/src/commands/quota.ts
21990
24774
  var WINDOW_ORDER = {
@@ -22089,7 +24873,7 @@ import { existsSync as existsSync5 } from "fs";
22089
24873
 
22090
24874
  // apps/cli/src/runtime.ts
22091
24875
  import { closeSync as closeSync2, existsSync as existsSync4, openSync as openSync2 } from "fs";
22092
- import { join as join3 } from "path";
24876
+ import { join as join7 } from "path";
22093
24877
  var runCommand = async (argv) => {
22094
24878
  const proc = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
22095
24879
  const [stdout, stderr, code] = await Promise.all([
@@ -22146,9 +24930,9 @@ function processAlive(pid) {
22146
24930
  }
22147
24931
  function gatewayEntrypoint(root, cliDir = import.meta.dir) {
22148
24932
  const candidates = [
22149
- join3(root, "apps", "gateway", "src", "index.ts"),
22150
- join3(cliDir, "..", "gateway.js"),
22151
- join3(cliDir, "gateway.js")
24933
+ join7(root, "apps", "gateway", "src", "index.ts"),
24934
+ join7(cliDir, "..", "gateway.js"),
24935
+ join7(cliDir, "gateway.js")
22152
24936
  ];
22153
24937
  return candidates.find((path) => existsSync4(path)) ?? null;
22154
24938
  }
@@ -22275,6 +25059,25 @@ var serviceUninstall = {
22275
25059
  emit(ctx, writer, result, () => result.removed ? `removed ${result.path}` : `no unit at ${result.path}`);
22276
25060
  }
22277
25061
  };
25062
+ async function rollupState(ctx) {
25063
+ if (ctx.configError !== null || !existsSync5(ctx.databasePath))
25064
+ return null;
25065
+ try {
25066
+ const audit = await (await ctx.store()).usage.auditRollup();
25067
+ return audit.ok ? `ok (${audit.buckets} hourly buckets)` : `${audit.mismatched} of ${audit.buckets} hourly buckets disagree with request_logs`;
25068
+ } catch {
25069
+ return null;
25070
+ }
25071
+ }
25072
+ async function orphanTables(ctx) {
25073
+ if (ctx.configError !== null || !existsSync5(ctx.databasePath))
25074
+ return null;
25075
+ try {
25076
+ return orphanPluginTables(doctorPluginDeps(), ctx.root.root, await ctx.store());
25077
+ } catch {
25078
+ return null;
25079
+ }
25080
+ }
22278
25081
  var doctor = {
22279
25082
  usage: "doctor",
22280
25083
  summary: "Check what this CLI resolved, and whether it can do anything with it",
@@ -22284,6 +25087,9 @@ var doctor = {
22284
25087
  const key = ctx.env.OMNI_ENCRYPTION_KEY;
22285
25088
  const unit = unitInstalled(deps);
22286
25089
  const status2 = await status(deps);
25090
+ const usageRollup = await rollupState(ctx);
25091
+ const plugins = listPlugins(doctorPluginDeps(), deps.root);
25092
+ const orphans = await orphanTables(ctx);
22287
25093
  const checks3 = {
22288
25094
  root: deps.root,
22289
25095
  rootSource: ctx.root.source,
@@ -22293,10 +25099,14 @@ var doctor = {
22293
25099
  encryptionKey: typeof key === "string" ? `present (${key.length} chars)` : "missing",
22294
25100
  configError: ctx.configError,
22295
25101
  gatewayEntrypoint: gatewayEntrypoint(deps.root),
25102
+ usageRollup,
22296
25103
  unitInstalled: unit,
22297
25104
  supervisor: status2.supervisor,
22298
25105
  running: status2.running,
22299
25106
  consoleSource: consoleSource(deps).source,
25107
+ pluginsDir: pluginsDir(deps.root),
25108
+ plugins,
25109
+ orphanPluginTables: orphans,
22300
25110
  warnings: ctx.warnings
22301
25111
  };
22302
25112
  emit(ctx, writer, checks3, () => {
@@ -22311,9 +25121,18 @@ var doctor = {
22311
25121
  ["encryption key", checks3.encryptionKey],
22312
25122
  ["config", checks3.configError === null ? ok(true, "ok") : ok(false, checks3.configError)],
22313
25123
  ["entrypoint", checks3.gatewayEntrypoint ?? ok(false, "not found")],
25124
+ [
25125
+ "usage rollup",
25126
+ checks3.usageRollup === null ? paint(ctx, "dim", "not checked") : ok(checks3.usageRollup.startsWith("ok"), checks3.usageRollup)
25127
+ ],
22314
25128
  ["systemd unit", unit ? deps.scope : paint(ctx, "dim", "none")],
22315
25129
  ["gateway", ok(status2.running, status2.running ? "running" : "stopped")],
22316
25130
  ["console log", sourceHint(checks3.consoleSource)],
25131
+ ...plugins.length === 0 ? [["plugins", paint(ctx, "dim", "none")]] : pluginDoctorLines(ctx, plugins).map((line, index) => [index === 0 ? "plugins" : "", line]),
25132
+ [
25133
+ "orphan plugin tables",
25134
+ orphans === null ? paint(ctx, "dim", "not checked") : orphans.length === 0 ? ok(true, "none") : paint(ctx, "yellow", `${orphans.length}: ${orphans.join(", ")}`)
25135
+ ],
22317
25136
  ...checks3.warnings.map((warning) => ["ignored", paint(ctx, "yellow", warning)])
22318
25137
  ]);
22319
25138
  });
@@ -22342,31 +25161,59 @@ var settingsGet = {
22342
25161
  emit(ctx, writer, { settings }, () => fields(flatten(settings)));
22343
25162
  }
22344
25163
  };
25164
+ function has(value, key) {
25165
+ return Object.hasOwn(value, key);
25166
+ }
25167
+ function currentValue(settings, head, tail) {
25168
+ if (!has(settings, head))
25169
+ return;
25170
+ const top = settings[head];
25171
+ if (tail === undefined)
25172
+ return top;
25173
+ if (top === null || typeof top !== "object")
25174
+ return;
25175
+ if (!has(top, tail))
25176
+ return;
25177
+ return top[tail];
25178
+ }
25179
+ function asNumber(path, raw) {
25180
+ const value = raw.trim().length === 0 ? Number.NaN : Number(raw);
25181
+ if (!Number.isFinite(value))
25182
+ throw new UsageError(`${path} must be a number, got "${raw}"`);
25183
+ return value;
25184
+ }
25185
+ function asBoolean(path, raw) {
25186
+ const value = raw.trim().toLowerCase();
25187
+ if (value === "true")
25188
+ return true;
25189
+ if (value === "false")
25190
+ return false;
25191
+ throw new UsageError(`${path} must be true or false, got "${raw}"`);
25192
+ }
22345
25193
  var settingsSet = {
22346
25194
  usage: "settings set <path> <value>",
22347
- summary: "Change one setting, e.g. weights.cost 0.4",
25195
+ summary: "Change one setting, e.g. weights.cost 0.4 or rtkEnabled true",
22348
25196
  async run(args, { ctx, writer }) {
22349
25197
  const path = requirePositional(args, 0, "setting path");
22350
25198
  const raw = requirePositional(args, 1, "value");
22351
25199
  const store = await ctx.store();
22352
25200
  const current = await getSettings(store);
22353
- const value = raw.trim().length === 0 ? Number.NaN : Number(raw);
22354
- if (!Number.isFinite(value))
22355
- throw new UsageError(`${path} must be a number, got "${raw}"`);
22356
25201
  const [head, tail] = path.split(".");
22357
- if (head === undefined || !(head in current))
25202
+ if (head === undefined || !has(current, head))
22358
25203
  throw new UsageError(`no setting "${path}"`);
22359
- const next = tail === undefined ? { ...current, [head]: value } : { ...current, weights: { ...current.weights, [tail]: value } };
22360
25204
  if (tail !== undefined && head !== "weights") {
22361
25205
  throw new UsageError(`"${head}" has no sub-settings`);
22362
25206
  }
25207
+ const existing = currentValue(current, head, tail);
25208
+ const value = typeof existing === "boolean" ? asBoolean(path, raw) : asNumber(path, raw);
25209
+ const next = tail === undefined ? { ...current, [head]: value } : { ...current, weights: { ...current.weights, [tail]: value } };
22363
25210
  const saved = await putSettings(store, next);
22364
25211
  emit(ctx, writer, { settings: saved }, () => `${path} = ${value}`);
22365
25212
  }
22366
25213
  };
22367
25214
 
22368
25215
  // apps/cli/src/commands/setup.ts
22369
- import { join as join4 } from "path";
25216
+ import { join as join8 } from "path";
22370
25217
  var OPTIONS = {
22371
25218
  dir: { type: "string" },
22372
25219
  key: { type: "string" },
@@ -22398,7 +25245,7 @@ ${f.contents}`).join(`
22398
25245
  }
22399
25246
  }
22400
25247
  function at(dir, file2) {
22401
- return { path: join4(dir, file2.path), contents: file2.contents };
25248
+ return { path: join8(dir, file2.path), contents: file2.contents };
22402
25249
  }
22403
25250
  async function promptMapping(models, prompt) {
22404
25251
  const choices = models.map(({ model }) => model.id).join(", ");
@@ -22432,11 +25279,11 @@ var setupClaude = {
22432
25279
  options: OPTIONS,
22433
25280
  async run(args, { ctx, writer, prompt, setupFs }) {
22434
25281
  const models = await described(ctx);
22435
- const dir = stringFlag(args.values, "dir") ?? join4(setupFs.homeDir, ".claude");
25282
+ const dir = stringFlag(args.values, "dir") ?? join8(setupFs.homeDir, ".claude");
22436
25283
  const key = stringFlag(args.values, "key");
22437
25284
  const dryRun2 = boolFlag(args.values, "dry-run");
22438
25285
  const mapping = await promptMapping(models, prompt);
22439
- const path = join4(dir, "settings.json");
25286
+ const path = join8(dir, "settings.json");
22440
25287
  const file2 = claudeSettings(models, {
22441
25288
  baseUrl: baseUrl(ctx),
22442
25289
  discoveryMirrors: ctx.config().exposeClaudeCodeAliases,
@@ -22665,6 +25512,7 @@ var COMMANDS = {
22665
25512
  restart,
22666
25513
  doctor,
22667
25514
  logs,
25515
+ bodies,
22668
25516
  console: console_,
22669
25517
  usage,
22670
25518
  quota,
@@ -22688,13 +25536,23 @@ var COMMANDS = {
22688
25536
  "models catalog": modelsCatalog,
22689
25537
  "keys list": keysList,
22690
25538
  "keys create": keysCreate,
25539
+ "keys limits": keysLimits,
22691
25540
  "keys revoke": keysRevoke,
25541
+ "plugin list": pluginList,
25542
+ "plugin verify": pluginVerify,
25543
+ "plugin install": pluginInstall,
25544
+ "plugin remove": pluginRemove,
22692
25545
  "settings get": settingsGet,
22693
25546
  "settings set": settingsSet,
22694
25547
  "setup claude": setupClaude,
22695
25548
  "setup opencode": setupOpencode,
22696
25549
  "admin set-password": adminSetPassword,
22697
- "db migrate": dbMigrate
25550
+ "db migrate": dbMigrate,
25551
+ "db stats": dbStats,
25552
+ "db snapshots": dbSnapshots,
25553
+ "db backup": dbBackup,
25554
+ "db restore": dbRestore,
25555
+ "db vacuum": dbVacuum
22698
25556
  };
22699
25557
  var VALUED_GLOBALS = new Set(["--root", "--db"]);
22700
25558
  function resolveCommand(argv) {
@@ -22733,12 +25591,13 @@ function resolveCommand(argv) {
22733
25591
  var GROUPS = [
22734
25592
  {
22735
25593
  title: "Gateway",
22736
- prefixes: ["status", "start", "stop", "restart", "doctor", "logs", "console"]
25594
+ prefixes: ["status", "start", "stop", "restart", "doctor", "logs", "bodies", "console"]
22737
25595
  },
22738
25596
  { title: "Service", prefixes: ["service "] },
22739
25597
  { title: "Accounts", prefixes: ["connect", "credentials "] },
22740
25598
  { title: "Models", prefixes: ["models "] },
22741
25599
  { title: "Keys", prefixes: ["keys "] },
25600
+ { title: "Plugins", prefixes: ["plugin "] },
22742
25601
  { title: "Configuration", prefixes: ["settings ", "admin ", "db "] },
22743
25602
  { title: "Reports", prefixes: ["usage", "quota"] }
22744
25603
  ];
@@ -22795,10 +25654,10 @@ async function readHidden() {
22795
25654
  stdin.setRawMode?.(true);
22796
25655
  stdin.resume();
22797
25656
  try {
22798
- const decoder = new TextDecoder;
25657
+ const decoder3 = new TextDecoder;
22799
25658
  let line = "";
22800
25659
  for await (const chunk of stdin) {
22801
- const text = decoder.decode(chunk, { stream: true });
25660
+ const text = decoder3.decode(chunk, { stream: true });
22802
25661
  for (const char of text) {
22803
25662
  if (char === "\r" || char === `
22804
25663
  `)
@@ -22819,10 +25678,10 @@ async function readHidden() {
22819
25678
  }
22820
25679
  }
22821
25680
  async function readLine() {
22822
- const decoder = new TextDecoder;
25681
+ const decoder3 = new TextDecoder;
22823
25682
  let line = "";
22824
25683
  for await (const chunk of process.stdin) {
22825
- const text = decoder.decode(chunk, { stream: true });
25684
+ const text = decoder3.decode(chunk, { stream: true });
22826
25685
  const newline = text.indexOf(`
22827
25686
  `);
22828
25687
  if (newline >= 0)
@@ -22862,10 +25721,10 @@ function createPrompt(ctx, writer) {
22862
25721
  }
22863
25722
 
22864
25723
  // apps/cli/src/setupFs.ts
22865
- import { dirname as dirname2 } from "path";
25724
+ import { dirname as dirname4 } from "path";
22866
25725
  function atomicWriteFile(path, contents, ops) {
22867
25726
  const temporary = `${path}.${process.pid}.tmp`;
22868
- ops.mkdir(dirname2(path));
25727
+ ops.mkdir(dirname4(path));
22869
25728
  try {
22870
25729
  ops.write(temporary, contents);
22871
25730
  ops.rename(temporary, path);
@@ -22918,12 +25777,12 @@ async function run(argv, writer, options = {}) {
22918
25777
  setupFs: options.setupFs ?? {
22919
25778
  homeDir: homedir3(),
22920
25779
  cwd: process.cwd(),
22921
- read: (path) => existsSync6(path) ? readFileSync3(path, "utf8") : null,
25780
+ read: (path) => existsSync6(path) ? readFileSync4(path, "utf8") : null,
22922
25781
  write: (path, contents) => atomicWriteFile(path, contents, {
22923
- mkdir: (directory) => mkdirSync2(directory, { recursive: true }),
22924
- write: (temporary, data) => writeFileSync2(temporary, data),
22925
- rename: renameSync,
22926
- remove: (temporary) => rmSync2(temporary, { force: true })
25782
+ mkdir: (directory) => mkdirSync3(directory, { recursive: true }),
25783
+ write: (temporary, data) => writeFileSync3(temporary, data),
25784
+ rename: renameSync2,
25785
+ remove: (temporary) => rmSync3(temporary, { force: true })
22927
25786
  })
22928
25787
  },
22929
25788
  service: () => options.service?.({ root: ctx.root.root, env: ctx.env }) ?? createServiceDeps({ root: ctx.root.root, env: ctx.env, scope, now: ctx.now }),