@cmmd-center/forge 0.9.15 → 0.9.17

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.mjs CHANGED
@@ -22,7 +22,7 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicK
22
22
  import * as FS$1 from "node:fs";
23
23
  import fs, { accessSync, chmodSync, constants, createReadStream, existsSync, mkdirSync, mkdtempSync, promises, readFile, readFileSync, readdir, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
24
24
  import * as NodeOS from "node:os";
25
- import $U, { homedir, networkInterfaces, tmpdir } from "node:os";
25
+ import os, { homedir, networkInterfaces, tmpdir } from "node:os";
26
26
  import * as Path$1 from "node:path";
27
27
  import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
28
28
  import { pathToFileURL } from "node:url";
@@ -22164,6 +22164,40 @@ const ServerProviderAuth = Struct({
22164
22164
  accountEmail: optional$4(TrimmedNonEmptyString),
22165
22165
  provenance: optional$4(ProviderAuthProvenance)
22166
22166
  });
22167
+ /**
22168
+ * Whether this Environment will let a person select the provider.
22169
+ *
22170
+ * Published for every provider the build knows about. A provider is never
22171
+ * dropped from the list: absence is not a state, and four different reasons
22172
+ * once collapsed into one "Coming soon." string because the Shell had to infer
22173
+ * meaning from what was missing. See
22174
+ * `docs/adr/withheld-providers-carry-their-reason.md`.
22175
+ */
22176
+ const ProviderAvailability = Literals(["available", "unavailable"]);
22177
+ /**
22178
+ * Why a Withheld Provider is withheld. The four members are the glossary's,
22179
+ * in `CONTEXT.md` section "Provider availability": Unbuilt, Not Offered, Not
22180
+ * Usable, Not on this Environment. Only Unbuilt may read "Coming soon."
22181
+ *
22182
+ * An Environment can only ever report Unbuilt, Not Usable, or Not on this
22183
+ * Environment. Not Offered is a CMMD fact about the organization's Provider
22184
+ * Offer, which the Shell adds when it joins.
22185
+ */
22186
+ const ProviderUnavailableReason = Literals([
22187
+ "unbuilt",
22188
+ "not-offered",
22189
+ "not-usable",
22190
+ "not-on-this-environment"
22191
+ ]);
22192
+ /**
22193
+ * The Not Usable sub-case. It picks a second line of copy, not a fifth state,
22194
+ * which is why it is a separate field rather than three more reasons.
22195
+ */
22196
+ const ProviderUnavailableDetail = Literals([
22197
+ "no-connection",
22198
+ "delivery-pending",
22199
+ "reconnect-required"
22200
+ ]);
22167
22201
  const ServerProviderModel = Struct({
22168
22202
  slug: TrimmedNonEmptyString,
22169
22203
  name: TrimmedNonEmptyString,
@@ -22195,6 +22229,9 @@ const ServerProvider = Struct({
22195
22229
  checkedAt: IsoDateTime,
22196
22230
  message: optional$4(TrimmedNonEmptyString),
22197
22231
  models: ArraySchema(ServerProviderModel),
22232
+ availability: optional$4(ProviderAvailability),
22233
+ unavailableReason: optional$4(ProviderUnavailableReason),
22234
+ unavailableDetail: optional$4(ProviderUnavailableDetail),
22198
22235
  slashCommands: ArraySchema(ServerProviderSlashCommand).pipe(withDecodingDefault(succeed([]))),
22199
22236
  skills: ArraySchema(ServerProviderSkill).pipe(withDecodingDefault(succeed([])))
22200
22237
  });
@@ -49482,7 +49519,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
49482
49519
  const min = forceColor || 0;
49483
49520
  if (env.TERM === "dumb") return min;
49484
49521
  if (process$1.platform === "win32") {
49485
- const osRelease = $U.release().split(".");
49522
+ const osRelease = os.release().split(".");
49486
49523
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
49487
49524
  return 1;
49488
49525
  }
@@ -51925,7 +51962,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
51925
51962
  exports.processDetector = void 0;
51926
51963
  const api_1 = (init_esm$1(), __toCommonJS(esm_exports$1));
51927
51964
  const semconv_1 = require_semconv$1();
51928
- const os$3 = __require("os");
51965
+ const os$4 = __require("os");
51929
51966
  /**
51930
51967
  * ProcessDetector will be used to detect the resources related current process running
51931
51968
  * and being instrumented from the NodeJS Process module.
@@ -51947,7 +51984,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
51947
51984
  };
51948
51985
  if (process.argv.length > 1) attributes[semconv_1.ATTR_PROCESS_COMMAND] = process.argv[1];
51949
51986
  try {
51950
- const userInfo = os$3.userInfo();
51987
+ const userInfo = os$4.userInfo();
51951
51988
  attributes[semconv_1.ATTR_PROCESS_OWNER] = userInfo.username;
51952
51989
  } catch (e) {
51953
51990
  api_1.diag.debug(`error obtaining process owner: ${e}`);
@@ -64870,7 +64907,7 @@ function normalizeNumberish(value) {
64870
64907
  }
64871
64908
  //#endregion
64872
64909
  //#region package.json
64873
- var version$1 = "0.9.15";
64910
+ var version$1 = "0.9.17";
64874
64911
  //#endregion
64875
64912
  //#region src/sentry.ts
64876
64913
  const SERVER_APP_NAME = "forge-server";
@@ -73500,7 +73537,7 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
73500
73537
  var fs$6 = __require("fs");
73501
73538
  var path$5 = __require("path");
73502
73539
  var url$1 = __require("url");
73503
- var os$2 = __require("os");
73540
+ var os$3 = __require("os");
73504
73541
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
73505
73542
  var vars = process.config && process.config.variables || {};
73506
73543
  var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
@@ -73508,8 +73545,8 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
73508
73545
  var abi = versions.modules;
73509
73546
  if (versions.deno || process.isBun) abi = "unsupported";
73510
73547
  var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
73511
- var arch = process.env.npm_config_arch || os$2.arch();
73512
- var platform = process.env.npm_config_platform || os$2.platform();
73548
+ var arch = process.env.npm_config_arch || os$3.arch();
73549
+ var platform = process.env.npm_config_platform || os$3.platform();
73513
73550
  var libc = process.env.LIBC || (isMusl(platform) ? "musl" : "glibc");
73514
73551
  var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
73515
73552
  var uv = (versions.uv || "").split(".")[0];
@@ -91167,7 +91204,7 @@ const launchDetached = (launch) => gen(function* () {
91167
91204
  });
91168
91205
  const OpenLive = effect(Open, gen(function* () {
91169
91206
  const open = yield* tryPromise({
91170
- try: () => import("./open-D8mGyWmt.mjs"),
91207
+ try: () => import("./open-FIgTDosf.mjs"),
91171
91208
  catch: (cause) => new OpenError$1({
91172
91209
  message: "failed to load browser opener",
91173
91210
  cause
@@ -91964,8 +92001,8 @@ const ProjectionThread = Struct({
91964
92001
  createdAt: IsoDateTime,
91965
92002
  updatedAt: IsoDateTime,
91966
92003
  archivedAt: NullOr(IsoDateTime),
91967
- settledAt: optional$4(NullOr(IsoDateTime)),
91968
- settledOverride: optional$4(NullOr(Literals(["settled", "active"]))),
92004
+ settledAt: NullOr(IsoDateTime),
92005
+ settledOverride: NullOr(Literals(["settled", "active"])),
91969
92006
  latestUserMessageAt: NullOr(IsoDateTime),
91970
92007
  pendingApprovalCount: NonNegativeInt,
91971
92008
  pendingUserInputCount: NonNegativeInt,
@@ -92150,16 +92187,23 @@ const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
92150
92187
  /**
92151
92188
  * Resolve the commit this forge checkout actually booted from, so the
92152
92189
  * heartbeat can report it and CMMD can tell a stale Sprite from a current
92153
- * one. Sprites clone forge ONLY at bootstrap (see the CMMD-side bootstrap
92154
- * script) and nothing else refreshes them, so this value never changes for
92155
- * the life of the process — resolve it once and cache it.
92156
- *
92157
- * `git rev-parse HEAD` run with the process's own cwd (`apps/server`, a
92158
- * subdirectory of the cloned repo) walks up to find `.git` on its own; no
92159
- * `-C` path is needed. Returns `undefined` (never throws) when there is no
92160
- * git checkout to read local dev run from a tarball, or any environment
92161
- * this simply does not apply to so the heartbeat body omits the field
92162
- * exactly like an older Forge build that doesn't know about it yet.
92190
+ * one. The commit cannot change for the life of the process, so resolve it
92191
+ * once and cache it.
92192
+ *
92193
+ * This comment used to say Sprites clone forge at bootstrap and read their
92194
+ * commit from that checkout. They do not. A cloud runtime installs the
92195
+ * published `@cmmd-center/forge` tarball and has no `.git` anywhere near it,
92196
+ * which is why its descriptor reported no commit at all until the server
92197
+ * started baking one into its bundle. See `../environment/buildIdentity.ts`,
92198
+ * which calls this only as its last rung.
92199
+ *
92200
+ * `git rev-parse HEAD` run with the process's own cwd walks up to find `.git`
92201
+ * on its own, so no `-C` path is needed. That also means a cwd inside some
92202
+ * unrelated repository answers with that repository's HEAD, which is one more
92203
+ * reason this is the last rung rather than the first. Returns `undefined`
92204
+ * (never throws) when there is no checkout to read, so the heartbeat body
92205
+ * omits the field exactly like an older Forge build that does not know about
92206
+ * it yet.
92163
92207
  */
92164
92208
  let cached$1 = null;
92165
92209
  async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$6(command, [...args], { timeout: 5e3 })) {
@@ -184373,7 +184417,7 @@ var Vce = {
184373
184417
  function Ve() {
184374
184418
  return Vce;
184375
184419
  }
184376
- var Us = $U.homedir(), lk = $U.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
184420
+ var Us = os.homedir(), lk = os.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
184377
184421
  let t = path.join(Us, "Library");
184378
184422
  return {
184379
184423
  data: path.join(t, "Application Support", e),
@@ -194573,12 +194617,12 @@ function qZ(e, t) {
194573
194617
  }
194574
194618
  return n;
194575
194619
  }
194576
- var os$1 = "https://code.claude.com/docs/en", kKe = [
194620
+ var os$2 = "https://code.claude.com/docs/en", kKe = [
194577
194621
  {
194578
194622
  matches: (e) => e.path === "permissions.defaultMode" && e.code === "invalid_value",
194579
194623
  tip: {
194580
194624
  suggestion: "Valid modes: \"acceptEdits\" (ask before file changes), \"plan\" (analysis only), \"bypassPermissions\" (auto-accept all), or \"default\" (standard behavior)",
194581
- docLink: `${os$1}/iam#permission-modes`
194625
+ docLink: `${os$2}/iam#permission-modes`
194582
194626
  }
194583
194627
  },
194584
194628
  {
@@ -194593,7 +194637,7 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
194593
194637
  matches: (e) => e.path.startsWith("env.") && e.code === "invalid_type",
194594
194638
  tip: {
194595
194639
  suggestion: "Environment variables must be strings. Wrap numbers and booleans in quotes. Example: \"DEBUG\": \"true\", \"PORT\": \"3000\"",
194596
- docLink: `${os$1}/settings#environment-variables`
194640
+ docLink: `${os$2}/settings#environment-variables`
194597
194641
  }
194598
194642
  },
194599
194643
  {
@@ -194604,14 +194648,14 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
194604
194648
  matches: (e) => e.path.startsWith("hooks.") && e.code === "invalid_key",
194605
194649
  tip: {
194606
194650
  suggestion: "Not a recognized hook event. Common events: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop. Check spelling and capitalization.",
194607
- docLink: `${os$1}/hooks`
194651
+ docLink: `${os$2}/hooks`
194608
194652
  }
194609
194653
  },
194610
194654
  {
194611
194655
  matches: (e) => /\.hooks\.\d+\.command$/.test(e.path) && e.code === "invalid_type" && e.received === "undefined",
194612
194656
  tip: {
194613
194657
  suggestion: "Command hooks require `command`. For exec form (no shell), set `command` to the executable and `args` to its arguments: {\"type\": \"command\", \"command\": \"echo\", \"args\": [\"hi\"]}. For shell form, set `command` to the full shell string: {\"type\": \"command\", \"command\": \"echo hi\"}.",
194614
- docLink: `${os$1}/hooks#exec-form-and-shell-form`
194658
+ docLink: `${os$2}/hooks#exec-form-and-shell-form`
194615
194659
  }
194616
194660
  },
194617
194661
  {
@@ -194626,7 +194670,7 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
194626
194670
  matches: (e) => e.code === "unrecognized_keys",
194627
194671
  tip: {
194628
194672
  suggestion: "Check for typos or refer to the documentation for valid fields",
194629
- docLink: `${os$1}/settings`
194673
+ docLink: `${os$2}/settings`
194630
194674
  }
194631
194675
  },
194632
194676
  {
@@ -194641,13 +194685,13 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
194641
194685
  matches: (e) => e.path === "permissions.additionalDirectories" && e.code === "invalid_type",
194642
194686
  tip: {
194643
194687
  suggestion: "Must be an array of directory paths. Example: [\"~/projects\", \"/tmp/workspace\"]. You can also use --add-dir flag or /add-dir command",
194644
- docLink: `${os$1}/iam#working-directories`
194688
+ docLink: `${os$2}/iam#working-directories`
194645
194689
  }
194646
194690
  }
194647
194691
  ], AKe = {
194648
- permissions: `${os$1}/iam#configuring-permissions`,
194649
- env: `${os$1}/settings#environment-variables`,
194650
- hooks: `${os$1}/hooks`
194692
+ permissions: `${os$2}/iam#configuring-permissions`,
194693
+ env: `${os$2}/settings#environment-variables`,
194694
+ hooks: `${os$2}/hooks`
194651
194695
  };
194652
194696
  function GZ(e) {
194653
194697
  let t = kKe.find((r) => r.matches(e));
@@ -206252,7 +206296,7 @@ function createDevServerMcpServer(deps, threadId) {
206252
206296
  * per provider — this module only adapts descriptor + result shapes.
206253
206297
  *
206254
206298
  * Without it a Claude agent has no brokered push path at all and falls back to
206255
- * raw `git`, which the runtime deliberately blocks via `GIT_ASKPASS=/bin/false`.
206299
+ * raw `git`, which has no authenticated remote credentials in the agent runtime.
206256
206300
  *
206257
206301
  * The catalog is filtered through `applyPluginSkillPermissionsToDynamicTools`,
206258
206302
  * exactly as the Codex surface filters its dynamic tools. `settings` is required
@@ -258900,7 +258944,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
258900
258944
  var dgram = __require("dgram");
258901
258945
  var thunky = require_thunky();
258902
258946
  var events = __require("events");
258903
- var os = __require("os");
258947
+ var os$1 = __require("os");
258904
258948
  var noop = function() {};
258905
258949
  module.exports = function(opts) {
258906
258950
  if (!opts) opts = {};
@@ -259031,14 +259075,14 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
259031
259075
  return that;
259032
259076
  };
259033
259077
  function defaultInterface() {
259034
- var networks = os.networkInterfaces();
259078
+ var networks = os$1.networkInterfaces();
259035
259079
  var names = Object.keys(networks);
259036
259080
  for (var i = 0; i < names.length; i++) {
259037
259081
  var net = networks[names[i]];
259038
259082
  for (var j = 0; j < net.length; j++) {
259039
259083
  var iface = net[j];
259040
259084
  if (isIPv4(iface.family) && !iface.internal) {
259041
- if (os.platform() === "darwin" && names[i] === "en0") return iface.address;
259085
+ if (os$1.platform() === "darwin" && names[i] === "en0") return iface.address;
259042
259086
  return "0.0.0.0";
259043
259087
  }
259044
259088
  }
@@ -259046,7 +259090,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
259046
259090
  return "127.0.0.1";
259047
259091
  }
259048
259092
  function allInterfaces() {
259049
- var networks = os.networkInterfaces();
259093
+ var networks = os$1.networkInterfaces();
259050
259094
  var names = Object.keys(networks);
259051
259095
  var res = [];
259052
259096
  for (var i = 0; i < names.length; i++) {
@@ -284570,14 +284614,87 @@ const writeProviderStatusCache = (input) => {
284570
284614
  })));
284571
284615
  };
284572
284616
  //#endregion
284573
- //#region src/provider/Layers/ProviderRegistry.ts
284617
+ //#region ../../packages/shared/src/providerExecution.ts
284574
284618
  /**
284575
- * Providers surfaced in the UI but lacking a dedicated detection Layer.
284576
- * Every provider now has a real Layer; this array stays as a (currently
284577
- * empty) hook for any future provider that ships in the UI before its
284578
- * server-side detection lands.
284619
+ * Canonical provider-readiness gate for starting work.
284620
+ *
284621
+ * Cloud user VMs can execute through Forge's scoped provider proxy without a
284622
+ * provider-local login. In that case the provider reports unauthenticated but
284623
+ * its credential provenance is explicitly `proxied`.
284579
284624
  */
284580
- const STUB_PROVIDERS = [];
284625
+ function isServerProviderExecutable(provider) {
284626
+ const hasExecutionAuthority = provider.auth.status !== "unauthenticated" || provider.auth.provenance?.credentialScope === "proxied";
284627
+ return provider.enabled && provider.installed && provider.status === "ready" && hasExecutionAuthority && provider.models.length > 0;
284628
+ }
284629
+ //#endregion
284630
+ //#region src/provider/providerAvailability.ts
284631
+ /**
284632
+ * Derive what this Environment knows about a provider's availability.
284633
+ *
284634
+ * WHY THIS EXISTS
284635
+ *
284636
+ * Four different states (Unbuilt, Not Offered, Not Usable, Not on this
284637
+ * Environment) used to render as one "Coming soon." string, because consumers
284638
+ * were handed a filtered list and had to infer meaning from what was missing.
284639
+ * Every provider is now published with an explicit availability and, when
284640
+ * withheld, its reason. See `docs/adr/withheld-providers-carry-their-reason.md`
284641
+ * and the glossary in `CONTEXT.md` section "Provider availability".
284642
+ *
284643
+ * WHAT AN ENVIRONMENT MAY SAY
284644
+ *
284645
+ * Three of the four reasons only. Not Offered is a fact about the CMMD
284646
+ * organization's Provider Offer, and delivery-pending is a fact about a
284647
+ * credential in flight; neither is visible from inside an Environment, so the
284648
+ * Shell adds them when it joins. Nothing here may invent them.
284649
+ *
284650
+ * This reads the snapshot a provider Layer already produced. It runs no probe
284651
+ * and changes no probe: `isServerProviderExecutable` stays the single gate on
284652
+ * whether a turn can be dispatched, and this restates its answer in the
284653
+ * vocabulary a person sees.
284654
+ */
284655
+ /**
284656
+ * Providers Forge ships in the UI but cannot execute, published as Unbuilt.
284657
+ *
284658
+ * Empty in this build: codex, claudeAgent, ollama, cmmd, apex, cursor and
284659
+ * opencode all have a real detection Layer and a registered adapter. The list
284660
+ * stays as the one place a provider that ships ahead of its server-side
284661
+ * implementation is named, so "Coming soon." is a deliberate entry rather than
284662
+ * a fallback something fell into.
284663
+ */
284664
+ const UNBUILT_PROVIDERS = [];
284665
+ function deriveProviderAvailability(provider, unbuiltProviders = UNBUILT_PROVIDERS) {
284666
+ if (unbuiltProviders.includes(provider.provider)) return {
284667
+ availability: "unavailable",
284668
+ unavailableReason: "unbuilt"
284669
+ };
284670
+ if (isServerProviderExecutable(provider)) return { availability: "available" };
284671
+ const hasProxiedCredential = provider.auth.provenance?.credentialScope === "proxied";
284672
+ if (provider.installed && provider.auth.status === "unauthenticated" && !hasProxiedCredential) return {
284673
+ availability: "unavailable",
284674
+ unavailableReason: "not-usable",
284675
+ unavailableDetail: "no-connection"
284676
+ };
284677
+ return {
284678
+ availability: "unavailable",
284679
+ unavailableReason: "not-on-this-environment"
284680
+ };
284681
+ }
284682
+ /**
284683
+ * Stamp a snapshot with its derived availability.
284684
+ *
284685
+ * The previous fields are dropped before the new ones are applied. Merging
284686
+ * would leave a provider that just connected reporting `available` while still
284687
+ * carrying the reason it was withheld for.
284688
+ */
284689
+ function withProviderAvailability(provider, unbuiltProviders = UNBUILT_PROVIDERS) {
284690
+ const { availability: _availability, unavailableReason: _unavailableReason, unavailableDetail: _unavailableDetail, ...rest } = provider;
284691
+ return {
284692
+ ...rest,
284693
+ ...deriveProviderAvailability(provider, unbuiltProviders)
284694
+ };
284695
+ }
284696
+ //#endregion
284697
+ //#region src/provider/Layers/ProviderRegistry.ts
284581
284698
  const loadProviders = (codexProvider, claudeProvider, ollamaProvider, cmmdProvider, apexProvider, cursorProvider, opencodeProvider) => all([
284582
284699
  codexProvider.getSnapshot,
284583
284700
  claudeProvider.getSnapshot,
@@ -284614,9 +284731,9 @@ const ProviderRegistryLive = effect(ProviderRegistry, gen(function* () {
284614
284731
  cachedProvider,
284615
284732
  fallbackProvider
284616
284733
  });
284617
- return STUB_PROVIDERS.includes(provider) ? fallbackProvider : void 0;
284734
+ return UNBUILT_PROVIDERS.includes(provider) ? fallbackProvider : void 0;
284618
284735
  }));
284619
- }, { concurrency: "unbounded" }).pipe(map$3((providers) => orderProviderSnapshots(providers.filter((provider) => provider !== void 0)))));
284736
+ }, { concurrency: "unbounded" }).pipe(map$3((providers) => orderProviderSnapshots(providers.filter((provider) => provider !== void 0).map((provider) => withProviderAvailability(provider))))));
284620
284737
  const persistProvider = (provider) => writeProviderStatusCache({
284621
284738
  filePath: cachePathByProvider.get(provider.provider),
284622
284739
  provider
@@ -284624,7 +284741,8 @@ const ProviderRegistryLive = effect(ProviderRegistry, gen(function* () {
284624
284741
  provider: provider.provider,
284625
284742
  error
284626
284743
  })), ignore);
284627
- const upsertProviders = fn$1("upsertProviders")(function* (nextProviders, options) {
284744
+ const upsertProviders = fn$1("upsertProviders")(function* (incomingProviders, options) {
284745
+ const nextProviders = incomingProviders.map((provider) => withProviderAvailability(provider));
284628
284746
  const [previousProviders, providers] = yield* modify$3(providersRef, (previousProviders) => {
284629
284747
  const mergedProviders = new Map(previousProviders.map((provider) => [provider.provider, provider]));
284630
284748
  for (const provider of nextProviders) mergedProviders.set(provider.provider, provider);
@@ -287328,13 +287446,32 @@ const ObservabilityLive = unwrap(gen(function* () {
287328
287446
  //#region ../../packages/shared/src/buildCommit.ts
287329
287447
  const COMMIT_SHA_PATTERN$1 = /^[0-9a-f]{40}$/i;
287330
287448
  /**
287449
+ * Whether a value is a full commit sha.
287450
+ *
287451
+ * Exported so each surface can keep its own list of env names while sharing one
287452
+ * definition of "valid". Surfaces genuinely differ: Railway injects
287453
+ * RAILWAY_GIT_COMMIT_SHA into the cloud shell build, while the desktop release
287454
+ * workflows pass VITE_SENTRY_COMMIT_SHA. What must not differ is the check.
287455
+ */
287456
+ function isFullCommitSha(value) {
287457
+ const candidate = value?.trim();
287458
+ return candidate !== void 0 && candidate !== "" && COMMIT_SHA_PATTERN$1.test(candidate);
287459
+ }
287460
+ /**
287331
287461
  * Build identity from the environment.
287332
287462
  *
287333
287463
  * `.dockerignore` excludes `.git`, so `git rev-parse HEAD` cannot resolve inside
287334
287464
  * the Railway container — the surface where "which commit is live?" is asked
287335
287465
  * most. The commit therefore has to arrive as env there, with git left as the
287336
- * fallback for surfaces that genuinely are checkouts (a sprite clones forge at
287337
- * bootstrap; desktop and dev run from a repo).
287466
+ * fallback for surfaces that genuinely are checkouts (desktop and dev run from
287467
+ * a repo).
287468
+ *
287469
+ * A cloud runtime is neither: it installs the published `@cmmd-center/forge`
287470
+ * tarball, so it has no build variables and no `.git`, and both rungs below
287471
+ * return nothing there. That is why the server bakes the commit into its bundle
287472
+ * at build time and reads that first — see `apps/server/src/environment/
287473
+ * buildIdentity.ts`. This comment previously claimed a sprite clones forge at
287474
+ * bootstrap; it does not, and believing it cost a diagnosis.
287338
287475
  *
287339
287476
  * `RAILWAY_GIT_COMMIT_SHA` is injected by the platform, which is what lets this
287340
287477
  * work on the cloud shell with no Dockerfile plumbing. `FORGE_BUILD_COMMIT`
@@ -287352,6 +287489,30 @@ function resolveBuildCommitFromEnv(env) {
287352
287489
  }
287353
287490
  }
287354
287491
  //#endregion
287492
+ //#region src/environment/buildIdentity.ts
287493
+ /**
287494
+ * Which commit this server was built from.
287495
+ *
287496
+ * @module
287497
+ */
287498
+ /**
287499
+ * The commit baked into this bundle, if it is a bundle at all.
287500
+ *
287501
+ * `typeof` rather than a bare reference: running from source there is no such
287502
+ * binding, and a bare reference is a ReferenceError that would take the whole
287503
+ * environment descriptor down instead of reporting an honest "unknown".
287504
+ */
287505
+ function readBakedBuildCommit() {
287506
+ return "86c71b55bb7a5dfbbad063d6e4c082570759f006";
287507
+ }
287508
+ async function resolveServerBuildCommit(input) {
287509
+ if (isFullCommitSha(input.baked)) return input.baked;
287510
+ const fromEnv = resolveBuildCommitFromEnv(input.env);
287511
+ if (fromEnv !== void 0) return fromEnv;
287512
+ const fromGit = await input.readGitCommit();
287513
+ return isFullCommitSha(fromGit) ? fromGit : void 0;
287514
+ }
287515
+ //#endregion
287355
287516
  //#region src/environment/Layers/ServerEnvironmentLabel.ts
287356
287517
  function normalizeLabel(value) {
287357
287518
  const trimmed = value?.trim();
@@ -287499,7 +287660,11 @@ const ServerEnvironmentLive = effect(ServerEnvironment, fn$1("makeServerEnvironm
287499
287660
  const writeMetadata = (metadata) => fileSystem.writeFileString(serverConfig.environmentMetadataPath, `${JSON.stringify(metadata, null, 2)}\n`);
287500
287661
  const displayNameOverrideRef = yield* make$47(normalizeDisplayName((yield* readMetadata).displayName ?? null));
287501
287662
  const posthog = yield* PosthogPublicConfig.asEffect();
287502
- const buildCommit = resolveBuildCommitFromEnv(process.env) ?? (yield* promise(() => resolveForgeGitCommit()));
287663
+ const buildCommit = yield* promise(() => resolveServerBuildCommit({
287664
+ baked: readBakedBuildCommit(),
287665
+ env: process.env,
287666
+ readGitCommit: resolveForgeGitCommit
287667
+ }));
287503
287668
  const buildDescriptor = gen(function* () {
287504
287669
  const displayNameOverride = yield* get$5(displayNameOverrideRef);
287505
287670
  const os = platformOs();
@@ -296243,6 +296408,71 @@ function selectProviderUsageEnvironment(rawCatalog, ownerUserId) {
296243
296408
  };
296244
296409
  }
296245
296410
  //#endregion
296411
+ //#region src/command/claudeUsageRefreshFailure.ts
296412
+ /**
296413
+ * Why a Claude usage refresh failed, in words the Usage page can show.
296414
+ *
296415
+ * CMMD answers a failed refresh with a machine reason (`runtime_not_active`,
296416
+ * `credential_changed`, …). The Shell used to collapse every one of them into
296417
+ * "Claude usage refresh is unavailable.", so the card could only say the
296418
+ * refresh failed, never what to do about it.
296419
+ *
296420
+ * The mapping is a closed allowlist in both directions: a reason CMMD may add
296421
+ * later falls back to the generic code, and the text a client sees is written
296422
+ * here, never forwarded from upstream. That keeps a credential or runtime
296423
+ * detail in a CMMD error body from reaching the browser.
296424
+ */
296425
+ const FAILURE_TEXT = {
296426
+ CLAUDE_USAGE_ENVIRONMENT_UNAVAILABLE: "Refresh needs your Environment running.",
296427
+ CLAUDE_USAGE_CREDENTIAL_NOT_READY: "The Claude credential is not applied to your Environment yet.",
296428
+ CLAUDE_USAGE_REFRESH_IN_PROGRESS: "A refresh for this account is already running.",
296429
+ CLAUDE_USAGE_PROBE_FAILED: "Your Environment could not read Claude usage.",
296430
+ CLAUDE_USAGE_RECONNECT_REQUIRED: "Reconnect this Claude account to refresh usage.",
296431
+ CLAUDE_USAGE_ACCOUNT_NOT_FOUND: "This Claude account is no longer connected.",
296432
+ CLAUDE_USAGE_REFRESH_UNAVAILABLE: "Claude usage refresh is unavailable."
296433
+ };
296434
+ /** Verbatim from CMMD `personal-claude-usage.ts` and `provider-usage-service.ts`. */
296435
+ const CODE_BY_UPSTREAM_REASON = {
296436
+ runtime_not_active: "CLAUDE_USAGE_ENVIRONMENT_UNAVAILABLE",
296437
+ claude_native_delivery_disabled: "CLAUDE_USAGE_ENVIRONMENT_UNAVAILABLE",
296438
+ runtime_identity_mismatch: "CLAUDE_USAGE_ENVIRONMENT_UNAVAILABLE",
296439
+ credential_generation_not_applied: "CLAUDE_USAGE_CREDENTIAL_NOT_READY",
296440
+ credential_generation_unavailable: "CLAUDE_USAGE_CREDENTIAL_NOT_READY",
296441
+ credential_changed: "CLAUDE_USAGE_CREDENTIAL_NOT_READY",
296442
+ refresh_in_progress: "CLAUDE_USAGE_REFRESH_IN_PROGRESS",
296443
+ usage_probe_failed: "CLAUDE_USAGE_PROBE_FAILED",
296444
+ usage_receipt_invalid: "CLAUDE_USAGE_PROBE_FAILED",
296445
+ usage_output_invalid: "CLAUDE_USAGE_PROBE_FAILED"
296446
+ };
296447
+ function failure(code, status) {
296448
+ return {
296449
+ status,
296450
+ body: {
296451
+ error: FAILURE_TEXT[code],
296452
+ code
296453
+ }
296454
+ };
296455
+ }
296456
+ /**
296457
+ * CMMD sends the reason as `{ "error": "<reason>" }`. Anything else, including
296458
+ * a longer sentence that happens to start with an allowlisted word, is treated
296459
+ * as unknown.
296460
+ */
296461
+ function readAllowlistedReasonCode(body) {
296462
+ if (body === null || typeof body !== "object") return null;
296463
+ const reason = body.error;
296464
+ if (typeof reason !== "string") return null;
296465
+ return Object.hasOwn(CODE_BY_UPSTREAM_REASON, reason) ? CODE_BY_UPSTREAM_REASON[reason] ?? null : null;
296466
+ }
296467
+ function describeClaudeUsageRefreshFailure(error) {
296468
+ if (!(error instanceof CmmdProxyRequestError)) return failure("CLAUDE_USAGE_REFRESH_UNAVAILABLE", 502);
296469
+ if (error.status === 409) return failure("CLAUDE_USAGE_RECONNECT_REQUIRED", 409);
296470
+ if (error.status === 404) return failure("CLAUDE_USAGE_ACCOUNT_NOT_FOUND", 404);
296471
+ const reasonCode = readAllowlistedReasonCode(error.body);
296472
+ if (reasonCode !== null) return failure(reasonCode, 503);
296473
+ return failure("CLAUDE_USAGE_REFRESH_UNAVAILABLE", error.status === 503 ? 503 : 502);
296474
+ }
296475
+ //#endregion
296246
296476
  //#region src/command/wsToken.ts
296247
296477
  /**
296248
296478
  * Build the WS token response given an optional PAT and the configured CMMD
@@ -297979,10 +298209,8 @@ function resolveProviderUsageOwnerUserId(session) {
297979
298209
  return Number.isSafeInteger(userId) && userId > 0 ? userId : null;
297980
298210
  }
297981
298211
  function buildClaudeUsageRefreshErrorResponse(error) {
297982
- return jsonUnsafe({
297983
- error: "Claude usage refresh is unavailable.",
297984
- code: "CLAUDE_USAGE_REFRESH_UNAVAILABLE"
297985
- }, { status: error instanceof CmmdProxyRequestError && error.status === 503 ? 503 : 502 });
298212
+ const failure = describeClaudeUsageRefreshFailure(error);
298213
+ return jsonUnsafe(failure.body, { status: failure.status });
297986
298214
  }
297987
298215
  function makeRefreshProviderUsageHandler(dependencies = {}) {
297988
298216
  return gen(function* () {
@@ -299937,21 +300165,24 @@ function processRuntimeReplicationOutboxItem(item, dependencies) {
299937
300165
  //#endregion
299938
300166
  //#region src/runtimeReplication/Services/ActiveHourMeter.ts
299939
300167
  var ActiveHourMeter = class extends Service$1()("forge/runtimeReplication/Services/ActiveHourMeter") {};
299940
- //#endregion
299941
- //#region ../../packages/shared/src/providerExecution.ts
300168
+ /** `slug` and `name` are `.min(1).max(512)` on the receiving schema. */
300169
+ const FIELD_MAX_LENGTH = 512;
300170
+ /** `version` is `.max(128)` on the receiving schema, with no length floor. */
300171
+ const VERSION_MAX_LENGTH = 128;
300172
+ /** `binaryPath` is `.max(2048)` on the receiving schema, with no length floor. */
300173
+ const BINARY_PATH_MAX_LENGTH = 2048;
299942
300174
  /**
299943
- * Canonical provider-readiness gate for starting work.
300175
+ * Trim to the receiving schema's maximum, or null.
299944
300176
  *
299945
- * Cloud user VMs can execute through Forge's scoped provider proxy without a
299946
- * provider-local login. In that case the provider reports unauthenticated but
299947
- * its credential provenance is explicitly `proxied`.
300177
+ * Neither field has a length floor, so an empty string would be legal — but it
300178
+ * is not an answer. A version nobody resolved and a binary nobody found are
300179
+ * both null, which is what the control plane stores and what a reader can tell
300180
+ * apart from a real value.
299948
300181
  */
299949
- function isServerProviderExecutable(provider) {
299950
- const hasExecutionAuthority = provider.auth.status !== "unauthenticated" || provider.auth.provenance?.credentialScope === "proxied";
299951
- return provider.enabled && provider.installed && provider.status === "ready" && hasExecutionAuthority && provider.models.length > 0;
300182
+ function cappedText(value, max) {
300183
+ const normalized = value?.trim().slice(0, max);
300184
+ return normalized ? normalized : null;
299952
300185
  }
299953
- /** `slug` and `name` are `.min(1).max(512)` on the receiving schema. */
299954
- const FIELD_MAX_LENGTH = 512;
299955
300186
  function serializeModel(model) {
299956
300187
  const slug = model.slug.trim().slice(0, FIELD_MAX_LENGTH);
299957
300188
  const name = model.name.trim().slice(0, FIELD_MAX_LENGTH);
@@ -299965,20 +300196,48 @@ function serializeModel(model) {
299965
300196
  name
299966
300197
  };
299967
300198
  }
300199
+ /**
300200
+ * The availability to publish for one provider.
300201
+ *
300202
+ * The registry stamps every snapshot before it leaves, so this normally copies.
300203
+ * Deriving over a stamped snapshot would lose the one fact only the registry
300204
+ * holds — which providers this build cannot run at all — and republish every
300205
+ * Unbuilt provider as Not on this Environment. The fallback covers a snapshot
300206
+ * that reached the outbox from some other path.
300207
+ *
300208
+ * Built by spread so a key with no value is ABSENT rather than `undefined`: the
300209
+ * receiving object is `.strict()`, `undefined` is not a legal enum member, and
300210
+ * the outbox retries the resulting 400 forever.
300211
+ */
300212
+ function publishedAvailability(provider) {
300213
+ if (provider.availability === void 0) return deriveProviderAvailability(provider);
300214
+ const reason = provider.unavailableReason;
300215
+ const detail = provider.unavailableDetail;
300216
+ return {
300217
+ availability: provider.availability,
300218
+ ...reason === void 0 ? {} : { unavailableReason: reason },
300219
+ ...detail === void 0 ? {} : { unavailableDetail: detail }
300220
+ };
300221
+ }
300222
+ function serializeProvider(provider) {
300223
+ const models = [];
300224
+ for (const model of provider.models) {
300225
+ if (models.length >= 256) break;
300226
+ const serialized = serializeModel(model);
300227
+ if (serialized) models.push(serialized);
300228
+ }
300229
+ return {
300230
+ provider: provider.provider,
300231
+ models,
300232
+ ...publishedAvailability(provider),
300233
+ version: cappedText(provider.version, VERSION_MAX_LENGTH),
300234
+ binaryPath: cappedText(provider.auth.provenance?.binaryPath, BINARY_PATH_MAX_LENGTH)
300235
+ };
300236
+ }
299968
300237
  function serializeProviderCatalog(providers) {
299969
300238
  const catalog = [];
299970
300239
  for (const provider of providers) {
299971
- if (!isServerProviderExecutable(provider)) continue;
299972
- const models = [];
299973
- for (const model of provider.models) {
299974
- if (models.length >= 256) break;
299975
- const serialized = serializeModel(model);
299976
- if (serialized) models.push(serialized);
299977
- }
299978
- catalog.push({
299979
- provider: provider.provider,
299980
- models
299981
- });
300240
+ catalog.push(serializeProvider(provider));
299982
300241
  if (catalog.length >= 16) break;
299983
300242
  }
299984
300243
  return catalog;
@@ -310426,7 +310685,7 @@ const connectLoginCommand = make$32("login", {
310426
310685
  })).json();
310427
310686
  },
310428
310687
  openBrowser: async (url) => {
310429
- const open = (await import("./open-D8mGyWmt.mjs")).default;
310688
+ const open = (await import("./open-FIgTDosf.mjs")).default;
310430
310689
  await open(url);
310431
310690
  return true;
310432
310691
  },