@cmmd-center/forge 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -65224,7 +65224,7 @@ function normalizeNumberish(value) {
65224
65224
  }
65225
65225
  //#endregion
65226
65226
  //#region package.json
65227
- var version$1 = "0.10.0";
65227
+ var version$1 = "0.10.1";
65228
65228
  //#endregion
65229
65229
  //#region src/sentry.ts
65230
65230
  const SERVER_APP_NAME = "forge-server";
@@ -68388,6 +68388,13 @@ const resolveBaseDir = require_Schema$1.fn(function* (raw) {
68388
68388
  if (!raw || raw.trim().length === 0) return join(node_os.homedir(), ".forge");
68389
68389
  return resolve(yield* expandHomePath$1(raw.trim()));
68390
68390
  });
68391
+ function bunHttpServerOptions(config) {
68392
+ return {
68393
+ port: config.port,
68394
+ ...config.host ? { hostname: config.host } : {},
68395
+ idleTimeout: 120
68396
+ };
68397
+ }
68391
68398
  //#endregion
68392
68399
  //#region ../../packages/shared/src/loopback.ts
68393
68400
  const LOOPBACK_HOSTNAMES$1 = new Set([
@@ -97911,6 +97918,31 @@ function mapClaudeRateLimitInfo(info) {
97911
97918
  }
97912
97919
  };
97913
97920
  }
97921
+ const SEVEN_DAY_MODEL_PREFIX = "seven_day_";
97922
+ const SEVEN_DAY_OVERAGE_SUFFIX = "overage_included";
97923
+ function capitalizeModelSuffix(suffix) {
97924
+ return suffix.length === 0 ? suffix : suffix.charAt(0).toUpperCase() + suffix.slice(1);
97925
+ }
97926
+ /**
97927
+ * Maps a per-model weekly SDKRateLimitInfo (`seven_day_<model>`, e.g.
97928
+ * `seven_day_opus`) into a `ProviderUsageModelWindow`. `seven_day_overage_included`
97929
+ * is not a per-model window (it reports overage-inclusive quota, not a
97930
+ * specific model) and stays ignored here, same as plain `overage`.
97931
+ */
97932
+ function mapClaudeModelRateLimitInfo(info) {
97933
+ if (typeof info.utilization !== "number" || typeof info.resetsAt !== "number" || !info.status) return null;
97934
+ if (!info.rateLimitType?.startsWith(SEVEN_DAY_MODEL_PREFIX)) return null;
97935
+ const suffix = info.rateLimitType.slice(10);
97936
+ if (suffix.length === 0 || suffix === SEVEN_DAY_OVERAGE_SUFFIX) return null;
97937
+ return {
97938
+ model: capitalizeModelSuffix(suffix),
97939
+ window: {
97940
+ usedPercent: Math.round(info.utilization * 100),
97941
+ resetsAt: (/* @__PURE__ */ new Date(info.resetsAt * 1e3)).toISOString(),
97942
+ status: info.status
97943
+ }
97944
+ };
97945
+ }
97914
97946
  const cachedSnapshots = /* @__PURE__ */ new Map();
97915
97947
  let localClaudeGenerationHash = null;
97916
97948
  function setLocalClaudeUsageGeneration(generationId) {
@@ -97920,22 +97952,71 @@ function currentClaudeUsageGenerationHash() {
97920
97952
  const value = process.env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
97921
97953
  return /^[a-f0-9]{64}$/.test(value) ? value : localClaudeGenerationHash;
97922
97954
  }
97923
- /** Merges a single-window update into the cached snapshot without clobbering the other window. */
97924
- function recordClaudeRateLimitTelemetry(info, generationHash = currentClaudeUsageGenerationHash(), providerAccountId) {
97925
- const mapped = mapClaudeRateLimitInfo(info);
97926
- if (!mapped || generationHash === null) return;
97927
- const observedAt = (/* @__PURE__ */ new Date()).toISOString();
97928
- const cacheKey = providerAccountId ? `${providerAccountId}:${generationHash}` : generationHash;
97929
- const previous = cachedSnapshots.get(cacheKey);
97930
- const fiveHourObservedAt = mapped.kind === "fiveHour" ? observedAt : previous?.fiveHourObservedAt ?? null;
97931
- const sevenDayObservedAt = mapped.kind === "sevenDay" ? observedAt : previous?.sevenDayObservedAt ?? null;
97932
- cachedSnapshots.set(cacheKey, {
97933
- fiveHour: mapped.kind === "fiveHour" ? mapped.window : previous?.fiveHour ?? null,
97934
- sevenDay: mapped.kind === "sevenDay" ? mapped.window : previous?.sevenDay ?? null,
97955
+ function cacheKeyFor(generationHash, providerAccountId) {
97956
+ return providerAccountId ? `${providerAccountId}:${generationHash}` : generationHash;
97957
+ }
97958
+ /**
97959
+ * `observedAt` here is a placeholder, not an observation — it becomes
97960
+ * meaningful only once a standard (five_hour/seven_day) window actually
97961
+ * lands via `withStandardWindow`, which recomputes it from the retained
97962
+ * windows' own observation times. A per-model-only cache (no standard window
97963
+ * yet) still carries this placeholder, so callers should treat `observedAt`
97964
+ * as trustworthy only once `fiveHour` or `sevenDay` is non-null.
97965
+ */
97966
+ function emptyCachedSnapshot(observedAt) {
97967
+ return {
97968
+ fiveHour: null,
97969
+ sevenDay: null,
97970
+ modelWindowsByModel: {},
97971
+ observedAt,
97972
+ fiveHourObservedAt: null,
97973
+ sevenDayObservedAt: null
97974
+ };
97975
+ }
97976
+ /** Oldest observation among the retained standard windows, so one fresh event cannot mask stale data. */
97977
+ function oldestObservedAt(fiveHourObservedAt, sevenDayObservedAt) {
97978
+ return [fiveHourObservedAt, sevenDayObservedAt].filter((value) => value !== null).toSorted()[0];
97979
+ }
97980
+ function withStandardWindow(previous, mapped, observedAt) {
97981
+ const isFiveHour = mapped.kind === "fiveHour";
97982
+ const fiveHourObservedAt = isFiveHour ? observedAt : previous.fiveHourObservedAt;
97983
+ const sevenDayObservedAt = isFiveHour ? previous.sevenDayObservedAt : observedAt;
97984
+ return {
97985
+ ...previous,
97986
+ fiveHour: isFiveHour ? mapped.window : previous.fiveHour,
97987
+ sevenDay: isFiveHour ? previous.sevenDay : mapped.window,
97935
97988
  fiveHourObservedAt,
97936
97989
  sevenDayObservedAt,
97937
- observedAt: [fiveHourObservedAt, sevenDayObservedAt].filter((value) => value !== null).toSorted()[0]
97938
- });
97990
+ observedAt: oldestObservedAt(fiveHourObservedAt, sevenDayObservedAt)
97991
+ };
97992
+ }
97993
+ function withModelWindow(previous, mapped) {
97994
+ return {
97995
+ ...previous,
97996
+ modelWindowsByModel: {
97997
+ ...previous.modelWindowsByModel,
97998
+ [mapped.model]: mapped.window
97999
+ }
98000
+ };
98001
+ }
98002
+ /**
98003
+ * Merges a single-window update into the cached snapshot without clobbering
98004
+ * the other windows. A per-model weekly report (`seven_day_<model>`) merges
98005
+ * into `modelWindowsByModel` keyed by model, leaving every other model and
98006
+ * the plain five_hour/seven_day windows untouched.
98007
+ */
98008
+ function recordClaudeRateLimitTelemetry(info, generationHash = currentClaudeUsageGenerationHash(), providerAccountId) {
98009
+ if (generationHash === null) return;
98010
+ const cacheKey = cacheKeyFor(generationHash, providerAccountId);
98011
+ const observedAt = (/* @__PURE__ */ new Date()).toISOString();
98012
+ const previous = cachedSnapshots.get(cacheKey) ?? emptyCachedSnapshot(observedAt);
98013
+ const standard = mapClaudeRateLimitInfo(info);
98014
+ if (standard) {
98015
+ cachedSnapshots.set(cacheKey, withStandardWindow(previous, standard, observedAt));
98016
+ return;
98017
+ }
98018
+ const model = mapClaudeModelRateLimitInfo(info);
98019
+ if (model) cachedSnapshots.set(cacheKey, withModelWindow(previous, model));
97939
98020
  }
97940
98021
  //#endregion
97941
98022
  //#region src/providerAuth/ClaudeDeviceAuth.ts
@@ -98139,6 +98220,44 @@ function parseUsageLine(output, label, now) {
98139
98220
  status: usageStatus(usedPercent)
98140
98221
  };
98141
98222
  }
98223
+ const MODEL_WEEK_LINE = /^Current week \(([^)]+)\): (\d+(?:\.\d+)?)% used(?: · resets (.+))?$/gm;
98224
+ const ALL_MODELS_LABEL = "all models";
98225
+ /** Matches CMMD's own bound in `personal-claude-usage.ts` — a model name past
98226
+ * this is treated as noise, not a real per-model window. */
98227
+ const MAX_MODEL_NAME_LENGTH = 64;
98228
+ /** Matches CMMD's own cap in `personal-claude-usage.ts`. */
98229
+ const MAX_MODEL_WINDOWS = 32;
98230
+ /**
98231
+ * Parses every "Current week (<Model>)" line except the "(all models)" row
98232
+ * already handled by `parseUsageLine`. The exclusion is case-insensitive to
98233
+ * match CMMD's own parser. A per-model line usually carries no reset suffix
98234
+ * of its own — Claude Code shows the same weekly reset as the all-models row
98235
+ * — so a missing suffix falls back to `sevenDayResetsAt`. A malformed line
98236
+ * (bad percent, unparsable reset, an implausibly long model name) is
98237
+ * skipped, not fatal: the two required lines are what fail closed, not these
98238
+ * optional extras. The result is capped at CMMD's own `modelWindows` bound so
98239
+ * an unexpectedly large or noisy `/usage` output can't grow the publish body
98240
+ * without limit.
98241
+ */
98242
+ function parseModelUsageLines(output, sevenDayResetsAt, now) {
98243
+ const windows = [];
98244
+ for (const match of output.matchAll(MODEL_WEEK_LINE)) {
98245
+ const model = match[1];
98246
+ if (model.toLowerCase() === ALL_MODELS_LABEL) continue;
98247
+ if (model.length > MAX_MODEL_NAME_LENGTH) continue;
98248
+ const usedPercent = Number(match[2]);
98249
+ if (!Number.isFinite(usedPercent) || usedPercent < 0 || usedPercent > 100) continue;
98250
+ const resetsAt = match[3] ? parseResetAt(match[3], now) : sevenDayResetsAt;
98251
+ if (resetsAt === null) continue;
98252
+ windows.push({
98253
+ model,
98254
+ usedPercent,
98255
+ resetsAt,
98256
+ status: usageStatus(usedPercent)
98257
+ });
98258
+ }
98259
+ return windows.slice(0, MAX_MODEL_WINDOWS);
98260
+ }
98142
98261
  async function fetchClaudeUsageViaOfficialClient(input) {
98143
98262
  const raw = await (input.run ?? runClaudeUsageCommand)(input.binaryPath, USAGE_ARGS, input.claudeConfigDir);
98144
98263
  const parsed = JSON.parse(raw);
@@ -98150,7 +98269,8 @@ async function fetchClaudeUsageViaOfficialClient(input) {
98150
98269
  if (fiveHour === null || sevenDay === null) throw new Error("Claude client usage output shape changed.");
98151
98270
  return {
98152
98271
  fiveHour,
98153
- sevenDay
98272
+ sevenDay,
98273
+ modelWindows: parseModelUsageLines(parsed.result, sevenDay.resetsAt, now)
98154
98274
  };
98155
98275
  }
98156
98276
  //#endregion
@@ -98348,27 +98468,61 @@ const logUsagePublishFailure = (failure, path) => failure === null ? require_Sch
98348
98468
  stack: failure.cause instanceof Error ? failure.cause.stack : void 0,
98349
98469
  path
98350
98470
  });
98471
+ /**
98472
+ * True only when CMMD's own telemetry schema is the reason the publish
98473
+ * failed, and only for a body that actually carried the field it can't
98474
+ * parse — never for a 400 caused by something else in the same body.
98475
+ */
98476
+ function isModelWindowsRejection(cause, body) {
98477
+ return cause instanceof CmmdProxyRequestError && cause.status === 400 && "modelWindows" in body;
98478
+ }
98479
+ /**
98480
+ * Sends the usage-publish body, tolerant of a CMMD deployment that has not
98481
+ * yet rolled out per-model windows. CMMD's telemetry schema is `.strict()`,
98482
+ * so an unrecognized `modelWindows` key fails the WHOLE body with a 400 —
98483
+ * losing fiveHour/sevenDay too, not just the field it can't parse yet. Retry
98484
+ * exactly once with that key omitted rather than losing the publish
98485
+ * outright; deploy CMMD first so this retry becomes unnecessary. A second
98486
+ * failure (still 400, or anything else) propagates like any other publish
98487
+ * failure — this is a rollout accommodation, not a general retry policy.
98488
+ */
98489
+ async function publishUsageBody(jwt, path, body) {
98490
+ try {
98491
+ await cmmdGateway.request({
98492
+ method: "POST",
98493
+ jwt,
98494
+ path,
98495
+ body
98496
+ });
98497
+ } catch (cause) {
98498
+ if (!isModelWindowsRejection(cause, body)) throw cause;
98499
+ const { modelWindows: _modelWindows, ...bodyWithoutModelWindows } = body;
98500
+ console.info("[cmmd-claude-cred-sync] usage publish retried without modelWindows — CMMD predates per-model rate-limit windows", { path });
98501
+ await cmmdGateway.request({
98502
+ method: "POST",
98503
+ jwt,
98504
+ path,
98505
+ body: bodyWithoutModelWindows
98506
+ });
98507
+ }
98508
+ }
98351
98509
  async function publishClaudeUsageAfterSync(input) {
98352
98510
  try {
98353
98511
  const usage = await input.loadUsage({
98354
98512
  binaryPath: resolveClaudeBinaryPath("claude"),
98355
98513
  ...input.homePath ? { claudeConfigDir: input.homePath } : {}
98356
98514
  });
98357
- await cmmdGateway.request({
98358
- method: "POST",
98359
- jwt: input.auth.jwt,
98360
- path: input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH : USER_CLAUDE_USAGE_PATH,
98361
- body: {
98362
- providerAccountId: input.providerAccountId,
98363
- credentialGenerationId: input.credentialGenerationId,
98364
- accountEmail: input.accountEmail,
98365
- accountDisplayName: input.accountDisplayName,
98366
- planType: input.planType,
98367
- source: "active_poll",
98368
- fiveHour: usage.fiveHour,
98369
- sevenDay: usage.sevenDay,
98370
- observedAt: (/* @__PURE__ */ new Date()).toISOString()
98371
- }
98515
+ await publishUsageBody(input.auth.jwt, input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH : USER_CLAUDE_USAGE_PATH, {
98516
+ providerAccountId: input.providerAccountId,
98517
+ credentialGenerationId: input.credentialGenerationId,
98518
+ accountEmail: input.accountEmail,
98519
+ accountDisplayName: input.accountDisplayName,
98520
+ planType: input.planType,
98521
+ source: "active_poll",
98522
+ fiveHour: usage.fiveHour,
98523
+ sevenDay: usage.sevenDay,
98524
+ modelWindows: usage.modelWindows,
98525
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
98372
98526
  });
98373
98527
  return null;
98374
98528
  } catch (cause) {
@@ -288888,7 +289042,7 @@ function resolveBuildCommitFromEnv(env) {
288888
289042
  * environment descriptor down instead of reporting an honest "unknown".
288889
289043
  */
288890
289044
  function readBakedBuildCommit() {
288891
- return "4f80a79430e58ba6f2e487d692fdefeb66e8486f";
289045
+ return "abd184dd1d3a4d5156ad021326b71afb06430b1b";
288892
289046
  }
288893
289047
  async function resolveServerBuildCommit(input) {
288894
289048
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -310629,6 +310783,7 @@ const claudeUsageHandler = (dependencies) => require_Schema$1.gen(function* () {
310629
310783
  response = require_HttpServer.jsonUnsafe({
310630
310784
  fiveHour: usage.fiveHour,
310631
310785
  sevenDay: usage.sevenDay,
310786
+ modelWindows: usage.modelWindows,
310632
310787
  observedAt: observedAt.toISOString(),
310633
310788
  receipt: {
310634
310789
  orgId: claims.orgId,
@@ -310920,10 +311075,7 @@ const PtyAdapterLive = require_Schema$1.unwrap(require_Schema$1.gen(function* ()
310920
311075
  }));
310921
311076
  const HttpServerLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
310922
311077
  const config = yield* ServerConfig;
310923
- if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunHttpServer-Cp9k4ybO.cjs")))).layer({
310924
- port: config.port,
310925
- ...config.host ? { hostname: config.host } : {}
310926
- });
311078
+ if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunHttpServer-Cp9k4ybO.cjs")))).layer(bunHttpServerOptions(config));
310927
311079
  const [NodeHttpServer, NodeHttp] = yield* require_Schema$1.all([require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeHttpServer-ClnvzpdU.cjs"))), require_Schema$1.promise(() => import("node:http"))]);
310928
311080
  return NodeHttpServer.layer(NodeHttp.createServer, {
310929
311081
  host: config.host,