@cmmd-center/forge 0.9.14 → 0.9.16

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
@@ -65123,7 +65123,7 @@ function normalizeNumberish(value) {
65123
65123
  }
65124
65124
  //#endregion
65125
65125
  //#region package.json
65126
- var version$1 = "0.9.14";
65126
+ var version$1 = "0.9.16";
65127
65127
  //#endregion
65128
65128
  //#region src/sentry.ts
65129
65129
  const SERVER_APP_NAME = "forge-server";
@@ -66945,13 +66945,13 @@ function validateCommandBootstrapPayload(payload) {
66945
66945
  });
66946
66946
  const payloadIdentity = normalizeCmmdUserIdentity(payload.user);
66947
66947
  if (payloadIdentity) {
66948
- const cmmdIdentity = payloadIdentity.avatarUrl ? payloadIdentity : mergeCmmdUserIdentity(payloadIdentity, yield* fetchOptionalCmmdIdentityForAccessToken(payload.accessToken));
66948
+ const cmmdIdentity = payloadIdentity.avatarUrl ? payloadIdentity : mergeCmmdUserIdentity(payloadIdentity, yield* fetchOptionalCmmdIdentityForAccessToken(cmmdUserCredential(payload)));
66949
66949
  return {
66950
66950
  subjectUserId: cmmdIdentity.id,
66951
66951
  cmmdIdentity
66952
66952
  };
66953
66953
  }
66954
- const tokenIdentity = yield* fetchCmmdIdentityForAccessToken(payload.accessToken);
66954
+ const tokenIdentity = yield* fetchCmmdIdentityForAccessToken(cmmdUserCredential(payload));
66955
66955
  if (tokenIdentity) return {
66956
66956
  subjectUserId: tokenIdentity.id,
66957
66957
  cmmdIdentity: tokenIdentity
@@ -67044,6 +67044,23 @@ function isCmmdJwtToken(token) {
67044
67044
  return token?.startsWith("eyJ") ?? false;
67045
67045
  }
67046
67046
  /**
67047
+ * The credential CMMD's userinfo accepts.
67048
+ *
67049
+ * Since CMMD's OAuth update, `access_token` is a JWT and the personal access
67050
+ * token arrives separately as `mcp_token`. userinfo authenticates the PAT and
67051
+ * rejects the JWT with `INVALID_OR_NON_USER_TOKEN`, so presenting the JWT
67052
+ * resolves no identity: the bootstrap then finds no subject and answers 401,
67053
+ * even though the sign-in itself succeeded and the caller did nothing wrong.
67054
+ *
67055
+ * `resolveCmmdTokenBundleCredentials` already draws this distinction for the
67056
+ * owner lanes. This is the same rule for the bootstrap lane, which had only ever
67057
+ * seen the pre-update shape where `access_token` was itself the PAT.
67058
+ */
67059
+ function cmmdUserCredential(payload) {
67060
+ const pat = payload.mcpToken?.trim();
67061
+ return isCmmdJwtToken(payload.accessToken) && pat ? pat : payload.accessToken;
67062
+ }
67063
+ /**
67047
67064
  * Read one CMMD token-exchange response into the credential set Forge stores.
67048
67065
  *
67049
67066
  * Both owner lanes redeem the same `cmmd-forge-web` authorization code — the
@@ -92262,8 +92279,8 @@ const ProjectionThread = require_Schema$1.Struct({
92262
92279
  createdAt: IsoDateTime,
92263
92280
  updatedAt: IsoDateTime,
92264
92281
  archivedAt: require_Schema$1.NullOr(IsoDateTime),
92265
- settledAt: require_Schema$1.optional(require_Schema$1.NullOr(IsoDateTime)),
92266
- settledOverride: require_Schema$1.optional(require_Schema$1.NullOr(require_Schema$1.Literals(["settled", "active"]))),
92282
+ settledAt: require_Schema$1.NullOr(IsoDateTime),
92283
+ settledOverride: require_Schema$1.NullOr(require_Schema$1.Literals(["settled", "active"])),
92267
92284
  latestUserMessageAt: require_Schema$1.NullOr(IsoDateTime),
92268
92285
  pendingApprovalCount: NonNegativeInt,
92269
92286
  pendingUserInputCount: NonNegativeInt,
@@ -92449,16 +92466,23 @@ const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
92449
92466
  /**
92450
92467
  * Resolve the commit this forge checkout actually booted from, so the
92451
92468
  * heartbeat can report it and CMMD can tell a stale Sprite from a current
92452
- * one. Sprites clone forge ONLY at bootstrap (see the CMMD-side bootstrap
92453
- * script) and nothing else refreshes them, so this value never changes for
92454
- * the life of the process — resolve it once and cache it.
92455
- *
92456
- * `git rev-parse HEAD` run with the process's own cwd (`apps/server`, a
92457
- * subdirectory of the cloned repo) walks up to find `.git` on its own; no
92458
- * `-C` path is needed. Returns `undefined` (never throws) when there is no
92459
- * git checkout to read local dev run from a tarball, or any environment
92460
- * this simply does not apply to so the heartbeat body omits the field
92461
- * exactly like an older Forge build that doesn't know about it yet.
92469
+ * one. The commit cannot change for the life of the process, so resolve it
92470
+ * once and cache it.
92471
+ *
92472
+ * This comment used to say Sprites clone forge at bootstrap and read their
92473
+ * commit from that checkout. They do not. A cloud runtime installs the
92474
+ * published `@cmmd-center/forge` tarball and has no `.git` anywhere near it,
92475
+ * which is why its descriptor reported no commit at all until the server
92476
+ * started baking one into its bundle. See `../environment/buildIdentity.ts`,
92477
+ * which calls this only as its last rung.
92478
+ *
92479
+ * `git rev-parse HEAD` run with the process's own cwd walks up to find `.git`
92480
+ * on its own, so no `-C` path is needed. That also means a cwd inside some
92481
+ * unrelated repository answers with that repository's HEAD, which is one more
92482
+ * reason this is the last rung rather than the first. Returns `undefined`
92483
+ * (never throws) when there is no checkout to read, so the heartbeat body
92484
+ * omits the field exactly like an older Forge build that does not know about
92485
+ * it yet.
92462
92486
  */
92463
92487
  let cached$1 = null;
92464
92488
  async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$6(command, [...args], { timeout: 5e3 })) {
@@ -92830,7 +92854,7 @@ function hasValidRuntimeIdentity(value) {
92830
92854
  }
92831
92855
  function decodeRuntime$1(value) {
92832
92856
  if (!isRecord$16(value) || !hasValidRuntimeIdentity(value)) return null;
92833
- if (!nullableString(value.endpointUrl) || value.endpointUrl !== null && !URL.canParse(value.endpointUrl) || !nullableInteger(value.deviceId) || !nullableInteger(value.projectId) || !nullableString(value.projectName) || !nullableString(value.workspaceRoot) || !nullableString(value.githubRepoFullName) || !(value.lastSeenAt === null || isIsoDateTime(value.lastSeenAt)) || !(value.metadata === null || isRecord$16(value.metadata))) return null;
92857
+ if (!nullableString(value.endpointUrl) || value.endpointUrl !== null && !URL.canParse(value.endpointUrl) || !nullableInteger(value.deviceId) || !nullableString(value.workspaceRoot) || !nullableString(value.githubRepoFullName) || !(value.lastSeenAt === null || isIsoDateTime(value.lastSeenAt)) || !(value.metadata === null || isRecord$16(value.metadata))) return null;
92834
92858
  return value;
92835
92859
  }
92836
92860
  function decodeProject(value) {
@@ -93141,8 +93165,6 @@ function replicaRuntimeMatches(left, right) {
93141
93165
  [left.status, right.status],
93142
93166
  [left.endpointUrl, right.endpointUrl],
93143
93167
  [left.deviceId, right.deviceId],
93144
- [left.projectId, right.projectId],
93145
- [left.projectName, right.projectName],
93146
93168
  [left.workspaceRoot, right.workspaceRoot],
93147
93169
  [left.githubRepoFullName, right.githubRepoFullName],
93148
93170
  [left.lastSeenAt, right.lastSeenAt]
@@ -206595,7 +206617,7 @@ function createDevServerMcpServer(deps, threadId) {
206595
206617
  * per provider — this module only adapts descriptor + result shapes.
206596
206618
  *
206597
206619
  * Without it a Claude agent has no brokered push path at all and falls back to
206598
- * raw `git`, which the runtime deliberately blocks via `GIT_ASKPASS=/bin/false`.
206620
+ * raw `git`, which has no authenticated remote credentials in the agent runtime.
206599
206621
  *
206600
206622
  * The catalog is filtered through `applyPluginSkillPermissionsToDynamicTools`,
206601
206623
  * exactly as the Codex surface filters its dynamic tools. `settings` is required
@@ -287808,13 +287830,32 @@ const ObservabilityLive = require_Schema$1.unwrap(require_Schema$1.gen(function*
287808
287830
  //#region ../../packages/shared/src/buildCommit.ts
287809
287831
  const COMMIT_SHA_PATTERN$1 = /^[0-9a-f]{40}$/i;
287810
287832
  /**
287833
+ * Whether a value is a full commit sha.
287834
+ *
287835
+ * Exported so each surface can keep its own list of env names while sharing one
287836
+ * definition of "valid". Surfaces genuinely differ: Railway injects
287837
+ * RAILWAY_GIT_COMMIT_SHA into the cloud shell build, while the desktop release
287838
+ * workflows pass VITE_SENTRY_COMMIT_SHA. What must not differ is the check.
287839
+ */
287840
+ function isFullCommitSha(value) {
287841
+ const candidate = value?.trim();
287842
+ return candidate !== void 0 && candidate !== "" && COMMIT_SHA_PATTERN$1.test(candidate);
287843
+ }
287844
+ /**
287811
287845
  * Build identity from the environment.
287812
287846
  *
287813
287847
  * `.dockerignore` excludes `.git`, so `git rev-parse HEAD` cannot resolve inside
287814
287848
  * the Railway container — the surface where "which commit is live?" is asked
287815
287849
  * most. The commit therefore has to arrive as env there, with git left as the
287816
- * fallback for surfaces that genuinely are checkouts (a sprite clones forge at
287817
- * bootstrap; desktop and dev run from a repo).
287850
+ * fallback for surfaces that genuinely are checkouts (desktop and dev run from
287851
+ * a repo).
287852
+ *
287853
+ * A cloud runtime is neither: it installs the published `@cmmd-center/forge`
287854
+ * tarball, so it has no build variables and no `.git`, and both rungs below
287855
+ * return nothing there. That is why the server bakes the commit into its bundle
287856
+ * at build time and reads that first — see `apps/server/src/environment/
287857
+ * buildIdentity.ts`. This comment previously claimed a sprite clones forge at
287858
+ * bootstrap; it does not, and believing it cost a diagnosis.
287818
287859
  *
287819
287860
  * `RAILWAY_GIT_COMMIT_SHA` is injected by the platform, which is what lets this
287820
287861
  * work on the cloud shell with no Dockerfile plumbing. `FORGE_BUILD_COMMIT`
@@ -287832,6 +287873,30 @@ function resolveBuildCommitFromEnv(env) {
287832
287873
  }
287833
287874
  }
287834
287875
  //#endregion
287876
+ //#region src/environment/buildIdentity.ts
287877
+ /**
287878
+ * Which commit this server was built from.
287879
+ *
287880
+ * @module
287881
+ */
287882
+ /**
287883
+ * The commit baked into this bundle, if it is a bundle at all.
287884
+ *
287885
+ * `typeof` rather than a bare reference: running from source there is no such
287886
+ * binding, and a bare reference is a ReferenceError that would take the whole
287887
+ * environment descriptor down instead of reporting an honest "unknown".
287888
+ */
287889
+ function readBakedBuildCommit() {
287890
+ return "e65edd4d1b8a361863a52b171b5e73295d691e11";
287891
+ }
287892
+ async function resolveServerBuildCommit(input) {
287893
+ if (isFullCommitSha(input.baked)) return input.baked;
287894
+ const fromEnv = resolveBuildCommitFromEnv(input.env);
287895
+ if (fromEnv !== void 0) return fromEnv;
287896
+ const fromGit = await input.readGitCommit();
287897
+ return isFullCommitSha(fromGit) ? fromGit : void 0;
287898
+ }
287899
+ //#endregion
287835
287900
  //#region src/environment/Layers/ServerEnvironmentLabel.ts
287836
287901
  function normalizeLabel(value) {
287837
287902
  const trimmed = value?.trim();
@@ -287980,7 +288045,11 @@ const makeServerEnvironment = require_Schema$1.fn("makeServerEnvironment")(funct
287980
288045
  const initialMetadata = yield* readMetadata;
287981
288046
  const displayNameOverrideRef = yield* require_HttpServer.make$3(normalizeDisplayName(initialMetadata.displayName ?? null));
287982
288047
  const posthog = yield* PosthogPublicConfig.asEffect();
287983
- const buildCommit = resolveBuildCommitFromEnv(process.env) ?? (yield* require_Schema$1.promise(() => resolveForgeGitCommit()));
288048
+ const buildCommit = yield* require_Schema$1.promise(() => resolveServerBuildCommit({
288049
+ baked: readBakedBuildCommit(),
288050
+ env: process.env,
288051
+ readGitCommit: resolveForgeGitCommit
288052
+ }));
287984
288053
  const buildDescriptor = require_Schema$1.gen(function* () {
287985
288054
  const displayNameOverride = yield* require_HttpServer.get$1(displayNameOverrideRef);
287986
288055
  const os = platformOs();
@@ -296795,6 +296864,71 @@ function selectProviderUsageEnvironment(rawCatalog, ownerUserId) {
296795
296864
  };
296796
296865
  }
296797
296866
  //#endregion
296867
+ //#region src/command/claudeUsageRefreshFailure.ts
296868
+ /**
296869
+ * Why a Claude usage refresh failed, in words the Usage page can show.
296870
+ *
296871
+ * CMMD answers a failed refresh with a machine reason (`runtime_not_active`,
296872
+ * `credential_changed`, …). The Shell used to collapse every one of them into
296873
+ * "Claude usage refresh is unavailable.", so the card could only say the
296874
+ * refresh failed, never what to do about it.
296875
+ *
296876
+ * The mapping is a closed allowlist in both directions: a reason CMMD may add
296877
+ * later falls back to the generic code, and the text a client sees is written
296878
+ * here, never forwarded from upstream. That keeps a credential or runtime
296879
+ * detail in a CMMD error body from reaching the browser.
296880
+ */
296881
+ const FAILURE_TEXT = {
296882
+ CLAUDE_USAGE_ENVIRONMENT_UNAVAILABLE: "Refresh needs your Environment running.",
296883
+ CLAUDE_USAGE_CREDENTIAL_NOT_READY: "The Claude credential is not applied to your Environment yet.",
296884
+ CLAUDE_USAGE_REFRESH_IN_PROGRESS: "A refresh for this account is already running.",
296885
+ CLAUDE_USAGE_PROBE_FAILED: "Your Environment could not read Claude usage.",
296886
+ CLAUDE_USAGE_RECONNECT_REQUIRED: "Reconnect this Claude account to refresh usage.",
296887
+ CLAUDE_USAGE_ACCOUNT_NOT_FOUND: "This Claude account is no longer connected.",
296888
+ CLAUDE_USAGE_REFRESH_UNAVAILABLE: "Claude usage refresh is unavailable."
296889
+ };
296890
+ /** Verbatim from CMMD `personal-claude-usage.ts` and `provider-usage-service.ts`. */
296891
+ const CODE_BY_UPSTREAM_REASON = {
296892
+ runtime_not_active: "CLAUDE_USAGE_ENVIRONMENT_UNAVAILABLE",
296893
+ claude_native_delivery_disabled: "CLAUDE_USAGE_ENVIRONMENT_UNAVAILABLE",
296894
+ runtime_identity_mismatch: "CLAUDE_USAGE_ENVIRONMENT_UNAVAILABLE",
296895
+ credential_generation_not_applied: "CLAUDE_USAGE_CREDENTIAL_NOT_READY",
296896
+ credential_generation_unavailable: "CLAUDE_USAGE_CREDENTIAL_NOT_READY",
296897
+ credential_changed: "CLAUDE_USAGE_CREDENTIAL_NOT_READY",
296898
+ refresh_in_progress: "CLAUDE_USAGE_REFRESH_IN_PROGRESS",
296899
+ usage_probe_failed: "CLAUDE_USAGE_PROBE_FAILED",
296900
+ usage_receipt_invalid: "CLAUDE_USAGE_PROBE_FAILED",
296901
+ usage_output_invalid: "CLAUDE_USAGE_PROBE_FAILED"
296902
+ };
296903
+ function failure(code, status) {
296904
+ return {
296905
+ status,
296906
+ body: {
296907
+ error: FAILURE_TEXT[code],
296908
+ code
296909
+ }
296910
+ };
296911
+ }
296912
+ /**
296913
+ * CMMD sends the reason as `{ "error": "<reason>" }`. Anything else, including
296914
+ * a longer sentence that happens to start with an allowlisted word, is treated
296915
+ * as unknown.
296916
+ */
296917
+ function readAllowlistedReasonCode(body) {
296918
+ if (body === null || typeof body !== "object") return null;
296919
+ const reason = body.error;
296920
+ if (typeof reason !== "string") return null;
296921
+ return Object.hasOwn(CODE_BY_UPSTREAM_REASON, reason) ? CODE_BY_UPSTREAM_REASON[reason] ?? null : null;
296922
+ }
296923
+ function describeClaudeUsageRefreshFailure(error) {
296924
+ if (!(error instanceof CmmdProxyRequestError)) return failure("CLAUDE_USAGE_REFRESH_UNAVAILABLE", 502);
296925
+ if (error.status === 409) return failure("CLAUDE_USAGE_RECONNECT_REQUIRED", 409);
296926
+ if (error.status === 404) return failure("CLAUDE_USAGE_ACCOUNT_NOT_FOUND", 404);
296927
+ const reasonCode = readAllowlistedReasonCode(error.body);
296928
+ if (reasonCode !== null) return failure(reasonCode, 503);
296929
+ return failure("CLAUDE_USAGE_REFRESH_UNAVAILABLE", error.status === 503 ? 503 : 502);
296930
+ }
296931
+ //#endregion
296798
296932
  //#region src/command/wsToken.ts
296799
296933
  /**
296800
296934
  * Build the WS token response given an optional PAT and the configured CMMD
@@ -298534,11 +298668,8 @@ function resolveProviderUsageOwnerUserId(session) {
298534
298668
  return Number.isSafeInteger(userId) && userId > 0 ? userId : null;
298535
298669
  }
298536
298670
  function buildClaudeUsageRefreshErrorResponse(error) {
298537
- const status = error instanceof CmmdProxyRequestError && error.status === 503 ? 503 : 502;
298538
- return require_HttpServer.jsonUnsafe({
298539
- error: "Claude usage refresh is unavailable.",
298540
- code: "CLAUDE_USAGE_REFRESH_UNAVAILABLE"
298541
- }, { status });
298671
+ const failure = describeClaudeUsageRefreshFailure(error);
298672
+ return require_HttpServer.jsonUnsafe(failure.body, { status: failure.status });
298542
298673
  }
298543
298674
  function makeRefreshProviderUsageHandler(dependencies = {}) {
298544
298675
  return require_Schema$1.gen(function* () {