codexuse-cli 5.0.5 → 5.0.7

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/index.js CHANGED
@@ -2054,6 +2054,7 @@ var DEFAULT_AUTO_ROLL_ENABLED = false;
2054
2054
  var DEFAULT_AUTO_ROLL_REARM_REMAINING_THRESHOLD = 15;
2055
2055
  var DEFAULT_AUTO_ROLL_SWITCH_REMAINING_THRESHOLD = 5;
2056
2056
  var DEFAULT_LAUNCH_OFFICIAL_CODEX_WHEN_CLOSED_ON_AUTO_ROLL = false;
2057
+ var DEFAULT_HANDOFF_RUNNING_THREADS_ON_ACCOUNT_SWITCH = false;
2057
2058
  var DEFAULT_AUTO_ROLL_PRIORITY_ORDER = [];
2058
2059
  var DEFAULT_LOW_REMAINING_NOTIFICATION_ENABLED = false;
2059
2060
  var DEFAULT_LOW_REMAINING_NOTIFICATION_THRESHOLD = 1;
@@ -2157,6 +2158,7 @@ function normalizeAutoRollSettings(raw) {
2157
2158
  rearmRemainingThreshold: normalizedRearm,
2158
2159
  switchRemainingThreshold: normalizedSwitch,
2159
2160
  launchOfficialCodexWhenClosedOnAutoRoll: raw?.launchOfficialCodexWhenClosedOnAutoRoll === true ? true : DEFAULT_LAUNCH_OFFICIAL_CODEX_WHEN_CLOSED_ON_AUTO_ROLL,
2161
+ handoffRunningThreadsOnAccountSwitch: raw?.handoffRunningThreadsOnAccountSwitch === true ? true : DEFAULT_HANDOFF_RUNNING_THREADS_ON_ACCOUNT_SWITCH,
2160
2162
  priorityOrder: sanitizeAutoRollPriorityOrder(raw?.priorityOrder),
2161
2163
  lowRemainingNotificationEnabled: raw?.lowRemainingNotificationEnabled === true ? true : DEFAULT_LOW_REMAINING_NOTIFICATION_ENABLED,
2162
2164
  lowRemainingNotificationThreshold: sanitizeLowRemainingNotificationThreshold(
@@ -2530,6 +2532,7 @@ function createDefaultAppState() {
2530
2532
  rearmRemainingThreshold: 15,
2531
2533
  switchRemainingThreshold: 5,
2532
2534
  launchOfficialCodexWhenClosedOnAutoRoll: false,
2535
+ handoffRunningThreadsOnAccountSwitch: false,
2533
2536
  priorityOrder: [],
2534
2537
  lowRemainingNotificationEnabled: false,
2535
2538
  lowRemainingNotificationThreshold: 1
@@ -2613,13 +2616,15 @@ function createDefaultAppState() {
2613
2616
  lastPushAt: null,
2614
2617
  lastPullAt: null,
2615
2618
  lastError: null,
2616
- remoteUpdatedAt: null
2619
+ remoteUpdatedAt: null,
2620
+ remoteKind: "none"
2617
2621
  },
2618
2622
  analytics: {
2619
2623
  anonymousId: null,
2620
2624
  enabled: true,
2621
2625
  lastFlushAt: null,
2622
- lastError: null
2626
+ lastError: null,
2627
+ reportedNativeCrashIncidentIds: []
2623
2628
  },
2624
2629
  profilesByName: {}
2625
2630
  };
@@ -2631,6 +2636,20 @@ function asString(value) {
2631
2636
  const trimmed = value.trim();
2632
2637
  return trimmed.length > 0 ? trimmed : null;
2633
2638
  }
2639
+ function asStringArray(value) {
2640
+ if (!Array.isArray(value)) {
2641
+ return [];
2642
+ }
2643
+ const seen = /* @__PURE__ */ new Set();
2644
+ return value.flatMap((entry) => {
2645
+ const normalized = asString(entry);
2646
+ if (!normalized || seen.has(normalized)) {
2647
+ return [];
2648
+ }
2649
+ seen.add(normalized);
2650
+ return [normalized];
2651
+ });
2652
+ }
2634
2653
  function asNumberOrNull(value) {
2635
2654
  return typeof value === "number" && Number.isFinite(value) ? value : null;
2636
2655
  }
@@ -2954,7 +2973,10 @@ function normalizeAppState(raw) {
2954
2973
  sourceProfileKey: asString(debt.sourceProfileKey),
2955
2974
  decisionId: asString(debt.decisionId),
2956
2975
  attempts: asNumberOrNull(debt.attempts) ?? 0,
2957
- lastReason: asString(debt.lastReason)
2976
+ lastReason: asString(debt.lastReason),
2977
+ handoffReason: debt.handoffReason === "manual" || debt.handoffReason === "auto-roll" ? debt.handoffReason : null,
2978
+ handoffThreadIds: asStringArray(debt.handoffThreadIds),
2979
+ handoffSentThreadIds: asStringArray(debt.handoffSentThreadIds)
2958
2980
  } : null;
2959
2981
  } else {
2960
2982
  merged.officialCodex.pendingActivationDebt = null;
@@ -3183,6 +3205,9 @@ function normalizeAppState(raw) {
3183
3205
  merged.sync.lastPullAt = asString(merged.sync.lastPullAt);
3184
3206
  merged.sync.lastError = asString(merged.sync.lastError);
3185
3207
  merged.sync.remoteUpdatedAt = asString(merged.sync.remoteUpdatedAt);
3208
+ if (merged.sync.remoteKind !== "legacy-plaintext" && merged.sync.remoteKind !== "encrypted") {
3209
+ merged.sync.remoteKind = "none";
3210
+ }
3186
3211
  if (!isRecord2(merged.analytics)) {
3187
3212
  merged.analytics = clone2(defaults.analytics);
3188
3213
  }
@@ -3192,6 +3217,9 @@ function normalizeAppState(raw) {
3192
3217
  }
3193
3218
  merged.analytics.lastFlushAt = asString(merged.analytics.lastFlushAt);
3194
3219
  merged.analytics.lastError = asString(merged.analytics.lastError);
3220
+ merged.analytics.reportedNativeCrashIncidentIds = asStringArray(
3221
+ merged.analytics.reportedNativeCrashIncidentIds
3222
+ ).slice(-20);
3195
3223
  if ("telemetry" in merged) {
3196
3224
  delete merged.telemetry;
3197
3225
  }
@@ -3496,6 +3524,7 @@ async function writeCodexSettingsJsonRaw(payload) {
3496
3524
  rearmRemainingThreshold: autoRoll.rearmRemainingThreshold,
3497
3525
  switchRemainingThreshold: autoRoll.switchRemainingThreshold,
3498
3526
  launchOfficialCodexWhenClosedOnAutoRoll: autoRoll.launchOfficialCodexWhenClosedOnAutoRoll,
3527
+ handoffRunningThreadsOnAccountSwitch: autoRoll.handoffRunningThreadsOnAccountSwitch,
3499
3528
  priorityOrder: autoRoll.priorityOrder,
3500
3529
  lowRemainingNotificationEnabled: autoRoll.lowRemainingNotificationEnabled,
3501
3530
  lowRemainingNotificationThreshold: autoRoll.lowRemainingNotificationThreshold
@@ -3927,7 +3956,7 @@ var import_node_crypto3 = require("crypto");
3927
3956
  var import_fs = require("fs");
3928
3957
  var import_path = __toESM(require("path"), 1);
3929
3958
  var import_path2 = require("path");
3930
- var import_toml = __toESM(require_toml(), 1);
3959
+ var import_toml2 = __toESM(require_toml(), 1);
3931
3960
 
3932
3961
  // ../../packages/contracts/src/profiles/identity.ts
3933
3962
  function normalizeValue(value) {
@@ -3981,6 +4010,7 @@ var import_node_readline = __toESM(require("readline"), 1);
3981
4010
  var import_node_fs3 = require("fs");
3982
4011
  var import_node_os2 = __toESM(require("os"), 1);
3983
4012
  var import_node_path5 = __toESM(require("path"), 1);
4013
+ var import_toml = __toESM(require_toml(), 1);
3984
4014
 
3985
4015
  // ../../packages/runtime-codex/src/codex/cli.ts
3986
4016
  var import_node_fs2 = require("fs");
@@ -4206,6 +4236,12 @@ var ACTIVATION_MODEL = "gpt-5.1-codex-mini";
4206
4236
  var MAX_STDERR_CAPTURE_CHARS = 32768;
4207
4237
  var REFRESH_TOKEN_REDEEMED_SNIPPET = "refresh token was already used";
4208
4238
  var authFileCache = /* @__PURE__ */ new Map();
4239
+ var CODEX_OAUTH_REFRESH_URL = "https://auth.openai.com/oauth/token";
4240
+ var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
4241
+ var CODEX_OAUTH_REFRESH_MAX_AGE_MS = 8 * 24 * 60 * 60 * 1e3;
4242
+ var CODEX_OAUTH_ACCESS_TOKEN_REFRESH_SKEW_MS = 2 * 60 * 1e3;
4243
+ var CODEX_USAGE_DEFAULT_BASE_URL = "https://chatgpt.com/backend-api";
4244
+ var CODEX_USAGE_TIMEOUT_MS = 3e4;
4209
4245
  function isRecord4(value) {
4210
4246
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
4211
4247
  }
@@ -4429,6 +4465,17 @@ function extractJwtIssuedAtMs(token) {
4429
4465
  }
4430
4466
  return issuedAt * 1e3;
4431
4467
  }
4468
+ function extractJwtExpiresAtMs(token) {
4469
+ const payload = decodeJwtPayload(token);
4470
+ if (!payload) {
4471
+ return null;
4472
+ }
4473
+ const expiresAt = payload["exp"];
4474
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) {
4475
+ return null;
4476
+ }
4477
+ return expiresAt * 1e3;
4478
+ }
4432
4479
  function parseAuthRecord(content) {
4433
4480
  try {
4434
4481
  const parsed = JSON.parse(content);
@@ -4517,6 +4564,353 @@ async function writeAuthFileCached(filePath, content) {
4517
4564
  authFileCache.delete(filePath);
4518
4565
  }
4519
4566
  }
4567
+ function nonEmptyString(value) {
4568
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
4569
+ }
4570
+ function resolveCodexAuthPathForEnv(envOverride) {
4571
+ return envOverride?.CODEX_HOME ? import_node_path5.default.join(envOverride.CODEX_HOME, "auth.json") : import_node_path5.default.join(
4572
+ envOverride?.HOME ?? process.env.HOME ?? import_node_os2.default.homedir(),
4573
+ ".codex",
4574
+ "auth.json"
4575
+ );
4576
+ }
4577
+ function parseCodexOAuthCredentials(content) {
4578
+ const parsed = parseAuthRecord(content);
4579
+ if (!parsed) {
4580
+ return null;
4581
+ }
4582
+ const tokens = isRecord4(parsed.tokens) ? parsed.tokens : {};
4583
+ const accessToken = nonEmptyString(tokens.access_token) ?? nonEmptyString(parsed.access_token);
4584
+ const refreshToken = nonEmptyString(tokens.refresh_token) ?? nonEmptyString(parsed.refresh_token);
4585
+ if (!accessToken && !refreshToken) {
4586
+ return null;
4587
+ }
4588
+ return {
4589
+ accessToken: accessToken ?? "",
4590
+ refreshToken: refreshToken ?? "",
4591
+ idToken: nonEmptyString(tokens.id_token) ?? nonEmptyString(parsed.id_token),
4592
+ accountId: nonEmptyString(tokens.account_id) ?? nonEmptyString(parsed.account_id),
4593
+ lastRefreshMs: parseTimestamp2(tokens.last_refresh) ?? parseTimestamp2(parsed.last_refresh)
4594
+ };
4595
+ }
4596
+ function shouldRefreshCodexOAuthToken(credentials) {
4597
+ if (!credentials.refreshToken) {
4598
+ return false;
4599
+ }
4600
+ const accessExpiresAt = extractJwtExpiresAtMs(credentials.accessToken);
4601
+ if (accessExpiresAt !== null && accessExpiresAt - Date.now() <= CODEX_OAUTH_ACCESS_TOKEN_REFRESH_SKEW_MS) {
4602
+ return true;
4603
+ }
4604
+ if (credentials.lastRefreshMs === null) {
4605
+ return true;
4606
+ }
4607
+ return Date.now() - credentials.lastRefreshMs > CODEX_OAUTH_REFRESH_MAX_AGE_MS;
4608
+ }
4609
+ async function writeCodexOAuthCredentials(authPath, content, credentials) {
4610
+ let json = {};
4611
+ try {
4612
+ const parsed = JSON.parse(content);
4613
+ if (isRecord4(parsed)) {
4614
+ json = { ...parsed };
4615
+ }
4616
+ } catch {
4617
+ json = {};
4618
+ }
4619
+ const existingTokens = isRecord4(json.tokens) ? json.tokens : {};
4620
+ json.tokens = {
4621
+ ...existingTokens,
4622
+ access_token: credentials.accessToken,
4623
+ refresh_token: credentials.refreshToken,
4624
+ ...credentials.idToken ? { id_token: credentials.idToken } : {},
4625
+ ...credentials.accountId ? { account_id: credentials.accountId } : {},
4626
+ last_refresh: (/* @__PURE__ */ new Date()).toISOString()
4627
+ };
4628
+ json.last_refresh = (/* @__PURE__ */ new Date()).toISOString();
4629
+ await writeAuthFileCached(authPath, `${JSON.stringify(json, null, 2)}
4630
+ `);
4631
+ }
4632
+ async function refreshCodexOAuthCredentials(credentials) {
4633
+ if (!credentials.refreshToken) {
4634
+ return credentials;
4635
+ }
4636
+ const response = await fetch(CODEX_OAUTH_REFRESH_URL, {
4637
+ method: "POST",
4638
+ headers: { "Content-Type": "application/json" },
4639
+ signal: AbortSignal.timeout(CODEX_USAGE_TIMEOUT_MS),
4640
+ body: JSON.stringify({
4641
+ client_id: CODEX_OAUTH_CLIENT_ID,
4642
+ grant_type: "refresh_token",
4643
+ refresh_token: credentials.refreshToken,
4644
+ scope: "openid profile email"
4645
+ })
4646
+ });
4647
+ const body = await response.text();
4648
+ if (!response.ok) {
4649
+ let code = "";
4650
+ try {
4651
+ const parsed2 = JSON.parse(body);
4652
+ if (isRecord4(parsed2)) {
4653
+ const error = isRecord4(parsed2.error) ? parsed2.error : {};
4654
+ code = nonEmptyString(error.code) ?? nonEmptyString(parsed2.error) ?? nonEmptyString(parsed2.code) ?? "";
4655
+ }
4656
+ } catch {
4657
+ code = "";
4658
+ }
4659
+ if (code === "refresh_token_reused") {
4660
+ throw new Error("refresh token was already used");
4661
+ }
4662
+ if (code === "refresh_token_expired") {
4663
+ throw new Error("refresh token expired");
4664
+ }
4665
+ if (code === "invalid_grant" || code === "refresh_token_invalidated") {
4666
+ throw new Error("refresh token was revoked");
4667
+ }
4668
+ throw new Error(`Codex OAuth refresh failed HTTP ${response.status}`);
4669
+ }
4670
+ let parsed;
4671
+ try {
4672
+ parsed = JSON.parse(body);
4673
+ } catch {
4674
+ throw new Error("Codex OAuth refresh returned invalid JSON");
4675
+ }
4676
+ if (!isRecord4(parsed)) {
4677
+ throw new Error("Codex OAuth refresh returned invalid data");
4678
+ }
4679
+ return {
4680
+ accessToken: nonEmptyString(parsed.access_token) ?? credentials.accessToken,
4681
+ refreshToken: nonEmptyString(parsed.refresh_token) ?? credentials.refreshToken,
4682
+ idToken: nonEmptyString(parsed.id_token) ?? credentials.idToken,
4683
+ accountId: credentials.accountId,
4684
+ lastRefreshMs: Date.now()
4685
+ };
4686
+ }
4687
+ async function loadCodexConfigContents(envOverride) {
4688
+ const codexHome = envOverride?.CODEX_HOME?.trim();
4689
+ const root = codexHome || import_node_path5.default.join(envOverride?.HOME ?? process.env.HOME ?? import_node_os2.default.homedir(), ".codex");
4690
+ try {
4691
+ return await import_node_fs3.promises.readFile(import_node_path5.default.join(root, "config.toml"), "utf8");
4692
+ } catch {
4693
+ return null;
4694
+ }
4695
+ }
4696
+ function parseChatGPTBaseUrl(contents) {
4697
+ if (!contents) {
4698
+ return CODEX_USAGE_DEFAULT_BASE_URL;
4699
+ }
4700
+ try {
4701
+ const parsed = (0, import_toml.parse)(contents);
4702
+ if (isRecord4(parsed)) {
4703
+ return nonEmptyString(parsed.chatgpt_base_url) ?? CODEX_USAGE_DEFAULT_BASE_URL;
4704
+ }
4705
+ } catch {
4706
+ return CODEX_USAGE_DEFAULT_BASE_URL;
4707
+ }
4708
+ return CODEX_USAGE_DEFAULT_BASE_URL;
4709
+ }
4710
+ async function resolveCodexUsageUrl(envOverride) {
4711
+ let base = parseChatGPTBaseUrl(await loadCodexConfigContents(envOverride));
4712
+ while (base.endsWith("/")) {
4713
+ base = base.slice(0, -1);
4714
+ }
4715
+ if ((base.startsWith("https://chatgpt.com") || base.startsWith("https://chat.openai.com")) && !base.includes("/backend-api")) {
4716
+ base += "/backend-api";
4717
+ }
4718
+ return `${base}${base.includes("/backend-api") ? "/wham/usage" : "/api/codex/usage"}`;
4719
+ }
4720
+ function numberFromUnknown(value) {
4721
+ if (typeof value === "number" && Number.isFinite(value)) {
4722
+ return value;
4723
+ }
4724
+ if (typeof value === "string" && value.trim()) {
4725
+ const parsed = Number(value.trim());
4726
+ return Number.isFinite(parsed) ? parsed : null;
4727
+ }
4728
+ return null;
4729
+ }
4730
+ function toOAuthWindow(window) {
4731
+ if (!window) {
4732
+ return void 0;
4733
+ }
4734
+ const usedPercent = numberFromUnknown(window.used_percent);
4735
+ const resetAt = numberFromUnknown(window.reset_at);
4736
+ const limitWindowSeconds = numberFromUnknown(window.limit_window_seconds);
4737
+ if (usedPercent === null && resetAt === null && limitWindowSeconds === null) {
4738
+ return void 0;
4739
+ }
4740
+ const result = {};
4741
+ if (usedPercent !== null) {
4742
+ result.usedPercent = Math.max(0, Math.min(100, usedPercent));
4743
+ }
4744
+ if (limitWindowSeconds !== null) {
4745
+ result.windowMinutes = Math.round(limitWindowSeconds / 60);
4746
+ }
4747
+ if (resetAt !== null) {
4748
+ const resetMs = resetAt > 1e10 ? resetAt : resetAt * 1e3;
4749
+ result.resetsAt = new Date(resetMs).toISOString();
4750
+ result.resetsInSeconds = Math.max(0, Math.round((resetMs - Date.now()) / 1e3));
4751
+ }
4752
+ return result;
4753
+ }
4754
+ function firstNonEmptyString(...values) {
4755
+ for (const value of values) {
4756
+ const text = nonEmptyString(value);
4757
+ if (text) {
4758
+ return text;
4759
+ }
4760
+ }
4761
+ return null;
4762
+ }
4763
+ function slugForRateLimitId(value) {
4764
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4765
+ }
4766
+ function isSparkRateLimit(limitName, meteredFeature) {
4767
+ return [limitName, meteredFeature].filter((value) => Boolean(value)).some((value) => value.toLowerCase().includes("spark"));
4768
+ }
4769
+ function sparkWindowIdentity(window, fallback) {
4770
+ const limitWindowSeconds = numberFromUnknown(window.limit_window_seconds);
4771
+ const windowMinutes = limitWindowSeconds !== null ? Math.round(limitWindowSeconds / 60) : null;
4772
+ const kind = windowMinutes !== null && windowMinutes > 0 && windowMinutes <= 6 * 60 ? "five-hour" : windowMinutes !== null && windowMinutes >= 6 * 24 * 60 ? "weekly" : fallback;
4773
+ return kind === "weekly" ? { id: "codex-spark-weekly", label: "Codex Spark Weekly" } : { id: "codex-spark", label: "Codex Spark 5-hour" };
4774
+ }
4775
+ function toExtraRateLimitWindow(args) {
4776
+ const window = toOAuthWindow(args.window);
4777
+ if (!window) {
4778
+ return null;
4779
+ }
4780
+ return {
4781
+ id: args.id,
4782
+ label: args.label,
4783
+ ...args.meteredFeature ? { meteredFeature: args.meteredFeature } : {},
4784
+ ...window
4785
+ };
4786
+ }
4787
+ function parseExtraRateLimitWindows(additionalRateLimits) {
4788
+ if (!Array.isArray(additionalRateLimits)) {
4789
+ return [];
4790
+ }
4791
+ const usedIds = /* @__PURE__ */ new Set();
4792
+ const extraWindows = [];
4793
+ for (const entry of additionalRateLimits) {
4794
+ if (!isRecord4(entry)) {
4795
+ continue;
4796
+ }
4797
+ const limitName = firstNonEmptyString(entry.limit_name);
4798
+ const meteredFeature = firstNonEmptyString(entry.metered_feature);
4799
+ const rateLimit = isRecord4(entry.rate_limit) ? entry.rate_limit : null;
4800
+ if (!rateLimit) {
4801
+ continue;
4802
+ }
4803
+ const primary = isRecord4(rateLimit.primary_window) ? rateLimit.primary_window : null;
4804
+ const secondary = isRecord4(rateLimit.secondary_window) ? rateLimit.secondary_window : null;
4805
+ if (isSparkRateLimit(limitName, meteredFeature)) {
4806
+ const candidates = [
4807
+ { window: primary, fallback: "five-hour" },
4808
+ { window: secondary, fallback: "weekly" }
4809
+ ];
4810
+ for (const candidate of candidates) {
4811
+ if (!candidate.window) {
4812
+ continue;
4813
+ }
4814
+ const identity = sparkWindowIdentity(candidate.window, candidate.fallback);
4815
+ if (usedIds.has(identity.id)) {
4816
+ continue;
4817
+ }
4818
+ const extraWindow2 = toExtraRateLimitWindow({
4819
+ ...identity,
4820
+ meteredFeature,
4821
+ window: candidate.window
4822
+ });
4823
+ if (extraWindow2) {
4824
+ usedIds.add(identity.id);
4825
+ extraWindows.push(extraWindow2);
4826
+ }
4827
+ }
4828
+ continue;
4829
+ }
4830
+ const idSource = firstNonEmptyString(meteredFeature, limitName);
4831
+ const slug = idSource ? slugForRateLimitId(idSource) : "";
4832
+ if (!slug) {
4833
+ continue;
4834
+ }
4835
+ const id = `codex-${slug}`;
4836
+ if (usedIds.has(id)) {
4837
+ continue;
4838
+ }
4839
+ const extraWindow = toExtraRateLimitWindow({
4840
+ id,
4841
+ label: firstNonEmptyString(limitName, meteredFeature) ?? "Codex extra limit",
4842
+ meteredFeature,
4843
+ window: primary ?? secondary
4844
+ });
4845
+ if (extraWindow) {
4846
+ usedIds.add(id);
4847
+ extraWindows.push(extraWindow);
4848
+ }
4849
+ }
4850
+ return extraWindows;
4851
+ }
4852
+ function parseRateLimitSnapshotFromOAuthUsage(value) {
4853
+ if (!isRecord4(value)) {
4854
+ throw new Error("Codex usage API returned invalid data");
4855
+ }
4856
+ const usage = value;
4857
+ const primary = toOAuthWindow(usage.rate_limit?.primary_window ?? null);
4858
+ const secondary = toOAuthWindow(usage.rate_limit?.secondary_window ?? null);
4859
+ const extraWindows = parseExtraRateLimitWindows(usage.additional_rate_limits);
4860
+ if (!primary && !secondary && extraWindows.length === 0) {
4861
+ return null;
4862
+ }
4863
+ return {
4864
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
4865
+ source: "codex-oauth",
4866
+ ...primary ? { primary } : {},
4867
+ ...secondary ? { secondary } : {},
4868
+ ...extraWindows.length > 0 ? { extraWindows } : {}
4869
+ };
4870
+ }
4871
+ async function fetchRateLimitsViaOAuth(envOverride) {
4872
+ const authPath = resolveCodexAuthPathForEnv(envOverride);
4873
+ const authContent = await readAuthFileCached(authPath);
4874
+ if (!authContent) {
4875
+ return null;
4876
+ }
4877
+ let credentials = parseCodexOAuthCredentials(authContent);
4878
+ if (!credentials) {
4879
+ return null;
4880
+ }
4881
+ if (shouldRefreshCodexOAuthToken(credentials)) {
4882
+ credentials = await refreshCodexOAuthCredentials(credentials);
4883
+ await writeCodexOAuthCredentials(authPath, authContent, credentials);
4884
+ }
4885
+ if (!credentials.accessToken) {
4886
+ return null;
4887
+ }
4888
+ const usageUrl = await resolveCodexUsageUrl(envOverride);
4889
+ const response = await fetch(usageUrl, {
4890
+ method: "GET",
4891
+ headers: {
4892
+ Authorization: `Bearer ${credentials.accessToken}`,
4893
+ Accept: "application/json",
4894
+ "User-Agent": "CodexUse",
4895
+ ...credentials.accountId ? { "ChatGPT-Account-Id": credentials.accountId } : {}
4896
+ },
4897
+ signal: AbortSignal.timeout(CODEX_USAGE_TIMEOUT_MS)
4898
+ });
4899
+ const body = await response.text();
4900
+ if (response.status === 401 || response.status === 403) {
4901
+ throw new Error(`Codex OAuth usage failed HTTP ${response.status} (${usageUrl})`);
4902
+ }
4903
+ if (!response.ok) {
4904
+ throw new Error(`Codex usage API failed HTTP ${response.status} (${usageUrl}): ${body.slice(0, 500)}`);
4905
+ }
4906
+ let parsed;
4907
+ try {
4908
+ parsed = JSON.parse(body);
4909
+ } catch {
4910
+ throw new Error("Codex usage API returned invalid JSON");
4911
+ }
4912
+ return parseRateLimitSnapshotFromOAuthUsage(parsed);
4913
+ }
4520
4914
  function inferRefreshFailureHint(stderrOutput) {
4521
4915
  if (!stderrOutput) {
4522
4916
  return null;
@@ -6129,7 +6523,7 @@ var ProfileManager = class {
6129
6523
  }
6130
6524
  let parsed;
6131
6525
  try {
6132
- parsed = (0, import_toml.parse)(raw);
6526
+ parsed = (0, import_toml2.parse)(raw);
6133
6527
  } catch (error) {
6134
6528
  logWarn(`Failed to parse config.toml for profile home '${profileHome}':`, error);
6135
6529
  return;
@@ -6139,7 +6533,7 @@ var ProfileManager = class {
6139
6533
  }
6140
6534
  delete parsed["model_reasoning_effort"];
6141
6535
  const sanitized = this.ensureTrailingNewline(
6142
- (0, import_toml.stringify)(parsed)
6536
+ (0, import_toml2.stringify)(parsed)
6143
6537
  );
6144
6538
  try {
6145
6539
  await import_fs.promises.writeFile(configPath, sanitized, "utf8");
@@ -6503,7 +6897,16 @@ var ProfileManager = class {
6503
6897
  profileName,
6504
6898
  () => this.runWithPreparedProfileHome(
6505
6899
  profileName,
6506
- (env) => fetchRateLimitsViaRpc(env, { codexPath: options.codexPath }),
6900
+ async (env) => {
6901
+ try {
6902
+ const oauthSnapshot = await fetchRateLimitsViaOAuth(env);
6903
+ if (oauthSnapshot) {
6904
+ return oauthSnapshot;
6905
+ }
6906
+ } catch {
6907
+ }
6908
+ return fetchRateLimitsViaRpc(env, { codexPath: options.codexPath });
6909
+ },
6507
6910
  { syncFromActiveAuthBeforeAction: false }
6508
6911
  )
6509
6912
  );
@@ -6899,8 +7302,8 @@ Usage:
6899
7302
  codexuse license activate <license-key>
6900
7303
 
6901
7304
  codexuse sync status
6902
- codexuse sync pull
6903
- codexuse sync push
7305
+ codexuse sync pull [--passphrase-stdin]
7306
+ codexuse sync push [--passphrase-stdin]
6904
7307
 
6905
7308
  Flags:
6906
7309
  -h, --help Show help
@@ -6918,7 +7321,9 @@ Flags:
6918
7321
  --profile=NAME Run Codex with one saved profile
6919
7322
  --runtime=NAME Accounts Pool runtime store: desktop
6920
7323
  --state-dir=PATH Override the runtime state dir for Accounts Pool inspection
7324
+ --passphrase-stdin Read Cloud Sync passphrase from stdin
6921
7325
  Note: profile autoroll requires Pro.
7326
+ Cloud Sync: set CODEXUSE_SYNC_PASSPHRASE or use --passphrase-stdin unless the passphrase is saved in Keychain.
6922
7327
  `);
6923
7328
  }
6924
7329
 
@@ -7877,7 +8282,8 @@ function maxUsedPercent(snapshot) {
7877
8282
  }
7878
8283
  const candidates = [
7879
8284
  snapshot.primary?.usedPercent,
7880
- snapshot.secondary?.usedPercent
8285
+ snapshot.secondary?.usedPercent,
8286
+ ...(snapshot.extraWindows ?? []).map((window) => window.usedPercent)
7881
8287
  ].filter((value) => typeof value === "number" && Number.isFinite(value));
7882
8288
  if (candidates.length === 0) {
7883
8289
  return null;
@@ -8006,7 +8412,8 @@ function formatUsagePercent(value) {
8006
8412
  function snapshotResetSeconds(snapshot) {
8007
8413
  const values = [
8008
8414
  snapshot?.primary?.resetsInSeconds,
8009
- snapshot?.secondary?.resetsInSeconds
8415
+ snapshot?.secondary?.resetsInSeconds,
8416
+ ...(snapshot?.extraWindows ?? []).map((window) => window.resetsInSeconds)
8010
8417
  ].filter((value) => typeof value === "number" && Number.isFinite(value));
8011
8418
  return values.length > 0 ? Math.min(...values) : null;
8012
8419
  }
@@ -9147,7 +9554,7 @@ function buildInCodexStatusOverlayCssRules() {
9147
9554
  `${id}[data-open='false']:not([data-collapsed='true']):hover .codexuse-status-trigger{padding:8px 10px;gap:8px;}`,
9148
9555
  `${id}[data-open='false']:not([data-collapsed='true']):hover .codexuse-status-label{max-width:160px;font-size:12px;}`,
9149
9556
  `${id}[data-open='false']:not([data-collapsed='true']):hover .codexuse-status-chip{display:inline-flex;}`,
9150
- `${id} .codexuse-action-menu{display:none;position:absolute;right:0;bottom:calc(100% + 8px);width:min(280px,calc(100vw - 24px));border:1px solid var(--cu-border);border-radius:12px;background:var(--cu-popover);padding:6px;box-shadow:0 12px 36px rgba(0,0,0,.32);}`,
9557
+ `${id} .codexuse-action-menu{display:none;position:absolute;right:0;bottom:calc(100% + 8px);width:min(280px,calc(100vw - 24px));max-height:min(420px,calc(100vh - 32px));overflow-y:auto;border:1px solid var(--cu-border);border-radius:12px;background:var(--cu-popover);padding:6px;box-shadow:0 12px 36px rgba(0,0,0,.32);}`,
9151
9558
  `${id}[data-drop='down'] .codexuse-action-menu{bottom:auto;top:calc(100% + 8px);}`,
9152
9559
  `${id}[data-open='true'] .codexuse-action-menu{display:flex;flex-direction:column;gap:2px;}`,
9153
9560
  `${id} .codexuse-target-list{display:flex;max-height:210px;flex-direction:column;gap:2px;overflow-y:auto;}`,
@@ -9169,10 +9576,10 @@ function buildInCodexStatusOverlayCssRules() {
9169
9576
  `${id} .codexuse-intro-detail{margin-top:2px;font-size:10px;color:var(--cu-muted);}`,
9170
9577
  `${id} .codexuse-decision-secondary{border:1px solid var(--cu-border)!important;color:var(--cu-muted)!important;}`,
9171
9578
  `${id} .codexuse-menu-divider{height:1px;margin:4px 2px;background:var(--cu-border);}`,
9172
- `${id} .codexuse-command-row{display:flex;width:100%;align-items:center;justify-content:space-between;gap:10px;padding:6px 9px;border-radius:8px;cursor:pointer;text-align:left;transition:background 120ms ease-out;}`,
9173
- `${id} .codexuse-command-row:hover{background:var(--cu-hover);}`,
9174
9579
  `${id} .codexuse-setting-row{display:flex;width:100%;align-items:center;justify-content:space-between;gap:10px;padding:6px 9px;border-radius:8px;cursor:pointer;text-align:left;transition:background 120ms ease-out;}`,
9175
9580
  `${id} .codexuse-setting-row:hover{background:var(--cu-hover);}`,
9581
+ `${id} .codexuse-setting-copy{display:flex;min-width:0;flex:1;flex-direction:column;gap:1px;}`,
9582
+ `${id} .codexuse-setting-copy .codexuse-target-detail{min-width:0;flex-shrink:1;overflow:hidden;text-overflow:ellipsis;}`,
9176
9583
  `${id} .codexuse-toggle{position:relative;flex-shrink:0;width:26px;height:15px;border-radius:999px;background:var(--cu-border);transition:background 120ms ease-out;}`,
9177
9584
  `${id} .codexuse-toggle::after{content:'';position:absolute;left:2px;top:2px;width:11px;height:11px;border-radius:50%;background:var(--cu-fg,#ececf1);transition:transform 120ms ease-out;}`,
9178
9585
  `${id} .codexuse-setting-row[data-on='true'] .codexuse-toggle{background:var(--cu-accent);}`,
@@ -9181,13 +9588,17 @@ function buildInCodexStatusOverlayCssRules() {
9181
9588
  `${id} .codexuse-action-status{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--cu-muted);font-size:10px;}`,
9182
9589
  `${id} .codexuse-overlay-hide{flex-shrink:0;padding:2px 6px;border-radius:6px;color:var(--cu-muted);font-size:10px;cursor:pointer;}`,
9183
9590
  `${id} .codexuse-overlay-hide:hover{background:var(--cu-hover);color:var(--cu-fg,#ececf1);}`,
9184
- `${chip}{${palette}position:fixed;z-index:2147483001;display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border-radius:999px;border:1px solid var(--cu-border);background:var(--cu-surface);color:var(--cu-fg,#ececf1);font-family:inherit;font-size:11px;font-weight:600;line-height:1.2;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.2);transition:background 120ms ease-out;}`,
9185
- `${chip}:hover{background:var(--cu-hover);}`,
9186
- `${chip}:disabled{opacity:.6;cursor:default;}`,
9591
+ `${chip}{${palette}position:fixed;z-index:2147483002;display:inline-flex;align-items:center;gap:4px;border-radius:999px;border:1px solid var(--cu-border);background:var(--cu-surface);color:var(--cu-fg,#ececf1);font-family:inherit;font-size:11px;font-weight:600;line-height:1.2;white-space:nowrap;box-shadow:0 4px 16px rgba(0,0,0,.2);transition:background 120ms ease-out;pointer-events:auto;user-select:none;-webkit-user-select:none;}`,
9592
+ `${chip}:hover,${chip}[data-open='true']{background:var(--cu-hover);}`,
9593
+ `${chip}[data-disabled='true']{opacity:.6;}`,
9594
+ `${chip} button{font:inherit;font-family:inherit;color:inherit;border:0;background:transparent;}`,
9595
+ `${chip} .codexuse-handoff-main{display:inline-flex;align-items:center;padding:5px 8px 5px 10px;border-radius:999px;cursor:pointer;}`,
9596
+ `${chip} .codexuse-handoff-hide{display:inline-flex;width:20px;height:20px;align-items:center;justify-content:center;margin-right:3px;border-radius:50%;color:var(--cu-muted);cursor:pointer;font-size:12px;line-height:1;}`,
9597
+ `${chip} .codexuse-handoff-hide:hover{background:var(--cu-border);color:var(--cu-fg,#ececf1);}`,
9187
9598
  // Hand-off account picker is a standalone body child (not inside the pill),
9188
9599
  // so the pill-scoped popover rules don't reach it — give it its own palette
9189
9600
  // + popover styling, mirroring the chip.
9190
- `${picker}{${palette}position:fixed;z-index:2147483001;display:flex;flex-direction:column;gap:2px;width:min(280px,calc(100vw - 24px));max-height:240px;overflow-y:auto;border:1px solid var(--cu-border);border-radius:12px;background:var(--cu-popover);color:var(--cu-fg,#ececf1);font-family:inherit;font-size:12px;line-height:1.4;padding:6px;box-shadow:0 12px 36px rgba(0,0,0,.32);}`,
9601
+ `${picker}{${palette}position:fixed;z-index:2147483003;display:flex;flex-direction:column;gap:2px;width:min(280px,calc(100vw - 24px));max-height:240px;overflow-y:auto;border:1px solid var(--cu-border);border-radius:12px;background:var(--cu-popover);color:var(--cu-fg,#ececf1);font-family:inherit;font-size:12px;line-height:1.4;padding:6px;box-shadow:0 12px 36px rgba(0,0,0,.32);pointer-events:auto;}`,
9191
9602
  `${picker} button{font:inherit;font-family:inherit;color:inherit;border:0;background:transparent;}`,
9192
9603
  `${picker} .codexuse-target-list{display:flex;flex-direction:column;gap:2px;}`,
9193
9604
  `${picker} .codexuse-target-row{display:flex;width:100%;align-items:center;justify-content:space-between;gap:10px;padding:7px 9px;border-radius:8px;cursor:pointer;text-align:left;transition:background 120ms ease-out;}`,
@@ -9219,6 +9630,9 @@ function createBridgeApplyQolExpression(settings) {
9219
9630
  )};
9220
9631
  const overlayAutoRollEnabled = ${JSON.stringify(
9221
9632
  typeof settings.overlayAutoRollEnabled === "boolean" ? settings.overlayAutoRollEnabled : null
9633
+ )};
9634
+ const overlayRunningThreadHandoffEnabled = ${JSON.stringify(
9635
+ typeof settings.overlayRunningThreadHandoffEnabled === "boolean" ? settings.overlayRunningThreadHandoffEnabled : null
9222
9636
  )};
9223
9637
  const versions = {
9224
9638
  wideView: "3",
@@ -9560,6 +9974,8 @@ function createBridgeApplyQolExpression(settings) {
9560
9974
  const overlayHiddenKey = "codexuse:overlay:hidden";
9561
9975
  const overlayIntroKey = "codexuse:overlay:intro-shown";
9562
9976
  const overlayPosKey = "codexuse:overlay:pos";
9977
+ const handoffHiddenKey = "codexuse:handoff:hidden";
9978
+ const handoffPosKey = "codexuse:handoff:pos:v2";
9563
9979
  const readOverlayStore = (key) => {
9564
9980
  try {
9565
9981
  return window.localStorage.getItem(key);
@@ -9658,6 +10074,41 @@ function createBridgeApplyQolExpression(settings) {
9658
10074
  const y = Math.max(72, Math.min(140, Math.round(window.innerHeight * 0.16)));
9659
10075
  placeOverlay(pill, x, y);
9660
10076
  };
10077
+ const rectsOverlap = (a, b, gap = 0) =>
10078
+ a.left < b.right + gap &&
10079
+ a.right > b.left - gap &&
10080
+ a.top < b.bottom + gap &&
10081
+ a.bottom > b.top - gap;
10082
+ const positionedRect = (rect, pos) => ({
10083
+ left: pos.x,
10084
+ top: pos.y,
10085
+ right: pos.x + rect.width,
10086
+ bottom: pos.y + rect.height
10087
+ });
10088
+ const placeInitialHandoffPosition = (chip, anchorRect = null) => {
10089
+ const rect = chip.getBoundingClientRect();
10090
+ if (anchorRect && rect.width > 0 && rect.height > 0) {
10091
+ const gap = 8;
10092
+ const middleY = Math.round(anchorRect.top + (anchorRect.height - rect.height) / 2);
10093
+ const candidates = [
10094
+ { x: anchorRect.right + gap, y: middleY },
10095
+ { x: anchorRect.left - rect.width - gap, y: middleY },
10096
+ { x: anchorRect.right - rect.width, y: anchorRect.top - rect.height - gap },
10097
+ { x: anchorRect.left, y: anchorRect.top - rect.height - gap },
10098
+ { x: anchorRect.right - rect.width, y: anchorRect.bottom + gap }
10099
+ ];
10100
+ for (const candidate of candidates) {
10101
+ const next = clampOverlayXY(chip, candidate.x, candidate.y);
10102
+ if (!rectsOverlap(positionedRect(rect, next), anchorRect, 6)) {
10103
+ placeOverlay(chip, next.x, next.y);
10104
+ return;
10105
+ }
10106
+ }
10107
+ }
10108
+ const x = Math.max(8, window.innerWidth - rect.width - 16);
10109
+ const y = Math.max(64, Math.min(128, Math.round(window.innerHeight * 0.14)));
10110
+ placeOverlay(chip, x, y);
10111
+ };
9661
10112
  const setOverlayMessage = (value) => {
9662
10113
  const node = document.querySelector("#" + statusId + " [data-codexuse-action-status]");
9663
10114
  if (node) node.textContent = value || "";
@@ -9701,7 +10152,7 @@ function createBridgeApplyQolExpression(settings) {
9701
10152
  state.lastActionRequest = payload;
9702
10153
  setOverlayMessage(
9703
10154
  action === "switch" ? "Switching\u2026"
9704
- : action === "continue" ? "Handing off\u2026"
10155
+ : action === "continue" ? "Continuing in another account\u2026"
9705
10156
  : action === "auto-roll-accept" ? "Switching\u2026"
9706
10157
  : action === "auto-roll-cancel" ? "Staying here"
9707
10158
  : action === "restart" ? "Waiting to restart; keeping draft\u2026"
@@ -9862,7 +10313,7 @@ function createBridgeApplyQolExpression(settings) {
9862
10313
  introTitle.textContent = "CodexUse controls";
9863
10314
  const introDetail = document.createElement("div");
9864
10315
  introDetail.className = "codexuse-intro-detail";
9865
- introDetail.textContent = "Switch accounts, restart Codex, and confirm auto-roll from here. Drag to move, or Hide to dismiss.";
10316
+ introDetail.textContent = "Switch accounts, tune CodexUse controls, and confirm Auto-roll from here. Drag to move, or Hide to dismiss.";
9866
10317
  intro.appendChild(introTitle);
9867
10318
  intro.appendChild(introDetail);
9868
10319
  menu.appendChild(intro);
@@ -9986,34 +10437,92 @@ function createBridgeApplyQolExpression(settings) {
9986
10437
  empty.textContent = "No other account ready";
9987
10438
  menu.appendChild(empty);
9988
10439
  }
9989
- const restartRow = document.createElement("button");
9990
- restartRow.type = "button";
9991
- restartRow.className = "codexuse-command-row";
9992
- restartRow.title = "Restart Official Codex";
9993
- const restartLabel = document.createElement("span");
9994
- restartLabel.textContent = "Restart Codex";
9995
- restartRow.appendChild(restartLabel);
9996
- const restartDetail = document.createElement("span");
9997
- restartDetail.className = "codexuse-target-detail";
9998
- restartDetail.textContent = "Keeps draft when possible";
9999
- restartRow.appendChild(restartDetail);
10000
- restartRow.addEventListener("click", (event) => {
10001
- event.preventDefault();
10002
- event.stopPropagation();
10003
- pill.dataset.open = "false";
10004
- state.overlayMenuOpen = false;
10005
- requestAction("restart");
10006
- }, true);
10007
- menu.appendChild(restartRow);
10008
- // In-app tweaks (auto-roll, wide view, scroll restore, timeline, hand-off)
10009
- // are configured in the CodexUse cockpit's Advanced section, not here \u2014
10010
- // the overlay stays focused on account status, switching, and recovery.
10440
+ const appendSettingRow = (item) => {
10441
+ const row = document.createElement("button");
10442
+ row.type = "button";
10443
+ row.className = "codexuse-setting-row";
10444
+ row.dataset.key = item.key;
10445
+ row.dataset.on = item.enabled ? "true" : "false";
10446
+ row.title = item.detail;
10447
+ const copy = document.createElement("span");
10448
+ copy.className = "codexuse-setting-copy";
10449
+ const label = document.createElement("span");
10450
+ label.className = "codexuse-target-label";
10451
+ label.textContent = item.label;
10452
+ copy.appendChild(label);
10453
+ const detail = document.createElement("span");
10454
+ detail.className = "codexuse-target-detail";
10455
+ detail.textContent = item.detail;
10456
+ copy.appendChild(detail);
10457
+ const toggle = document.createElement("span");
10458
+ toggle.className = "codexuse-toggle";
10459
+ row.appendChild(copy);
10460
+ row.appendChild(toggle);
10461
+ row.addEventListener("click", (event) => {
10462
+ event.preventDefault();
10463
+ event.stopPropagation();
10464
+ const next = row.dataset.on !== "true";
10465
+ row.dataset.on = next ? "true" : "false";
10466
+ requestAction("setting", null, { key: item.key, value: next });
10467
+ }, true);
10468
+ menu.appendChild(row);
10469
+ };
10470
+ const settingRows = [
10471
+ {
10472
+ key: "autoRoll",
10473
+ label: "Auto-roll",
10474
+ detail: "Switch when quota is low",
10475
+ enabled: overlayAutoRollEnabled === true,
10476
+ },
10477
+ {
10478
+ key: "runningThreadHandoff",
10479
+ label: "Continue running threads",
10480
+ detail: "Beta \xB7 after account switch",
10481
+ enabled: overlayRunningThreadHandoffEnabled === true,
10482
+ },
10483
+ {
10484
+ key: "wideView",
10485
+ label: "Wide conversations",
10486
+ detail: "More message room",
10487
+ enabled: wideViewEnabled,
10488
+ },
10489
+ {
10490
+ key: "scrollRestore",
10491
+ label: "Remember position",
10492
+ detail: "Return to last spot",
10493
+ enabled: scrollRestoreEnabled,
10494
+ },
10495
+ {
10496
+ key: "conversationTimeline",
10497
+ label: "Jump to user turns",
10498
+ detail: "Show turn markers",
10499
+ enabled: conversationTimelineEnabled,
10500
+ },
10501
+ ];
10502
+ const divider = document.createElement("div");
10503
+ divider.className = "codexuse-menu-divider";
10504
+ menu.appendChild(divider);
10505
+ settingRows.forEach(appendSettingRow);
10011
10506
  const footer = document.createElement("div");
10012
10507
  footer.className = "codexuse-menu-footer";
10013
10508
  const actionStatus = document.createElement("span");
10014
10509
  actionStatus.className = "codexuse-action-status";
10015
10510
  actionStatus.setAttribute("data-codexuse-action-status", "true");
10016
10511
  footer.appendChild(actionStatus);
10512
+ if (handoffChipEnabled && readOverlayStore(handoffHiddenKey) === "1") {
10513
+ const showHandoff = document.createElement("button");
10514
+ showHandoff.type = "button";
10515
+ showHandoff.className = "codexuse-overlay-hide";
10516
+ showHandoff.textContent = "Show handoff";
10517
+ showHandoff.addEventListener("click", (event) => {
10518
+ event.preventDefault();
10519
+ event.stopPropagation();
10520
+ writeOverlayStore(handoffHiddenKey, null);
10521
+ renderStatus();
10522
+ renderHandoff();
10523
+ }, true);
10524
+ footer.appendChild(showHandoff);
10525
+ }
10017
10526
  const hide = document.createElement("button");
10018
10527
  hide.type = "button";
10019
10528
  hide.className = "codexuse-overlay-hide";
@@ -10039,7 +10548,7 @@ function createBridgeApplyQolExpression(settings) {
10039
10548
  }
10040
10549
  return true;
10041
10550
  };
10042
- // Thread-scoped hand-off control: pinned to the composer, only shown when
10551
+ // Thread-scoped hand-off control: separate draggable chip, only shown when
10043
10552
  // a conversation is on screen and another account is ready to take over.
10044
10553
  // Log the hand-off chip's visibility gate only when it CHANGES \u2014 this runs
10045
10554
  // on every mutation tick, and the reason is the first question every
@@ -10049,9 +10558,17 @@ function createBridgeApplyQolExpression(settings) {
10049
10558
  state.lastHandoffReason = reason;
10050
10559
  cuLog("handoff.render", { reason, targets: switchable.length });
10051
10560
  };
10561
+ let cleanupHandoffPicker = null;
10052
10562
  const renderHandoff = () => {
10053
- document.getElementById(handoffId)?.remove();
10563
+ const previous = document.getElementById(handoffId);
10564
+ if (previous?.dataset.dragging === "true") return true;
10565
+ const wasOpen = previous?.dataset.open === "true";
10566
+ previous?.remove();
10054
10567
  document.getElementById(handoffPickerId)?.remove();
10568
+ if (typeof cleanupHandoffPicker === "function") {
10569
+ cleanupHandoffPicker();
10570
+ cleanupHandoffPicker = null;
10571
+ }
10055
10572
  if (!inCodexStatusEnabled || !handoffChipEnabled) {
10056
10573
  reportHandoff("disabled");
10057
10574
  return false;
@@ -10060,6 +10577,10 @@ function createBridgeApplyQolExpression(settings) {
10060
10577
  reportHandoff("overlay-hidden");
10061
10578
  return false;
10062
10579
  }
10580
+ if (readOverlayStore(handoffHiddenKey) === "1") {
10581
+ reportHandoff("handoff-hidden");
10582
+ return false;
10583
+ }
10063
10584
  // Codex desktop marks turns with data-turn-key / data-user-message-bubble;
10064
10585
  // the ChatGPT-style selectors are kept as a fallback.
10065
10586
  const inThread = Boolean(domActiveThreadId() || document.querySelector(
@@ -10076,22 +10597,28 @@ function createBridgeApplyQolExpression(settings) {
10076
10597
  reportHandoff("no-composer");
10077
10598
  return false;
10078
10599
  }
10079
- const composer = textbox.closest("form,[class*='composer'],[data-testid*='composer']") || textbox;
10080
- const rect = composer.getBoundingClientRect();
10600
+ const rect = textbox.getBoundingClientRect();
10081
10601
  if (rect.width <= 0 || rect.height <= 0) {
10082
10602
  reportHandoff("composer-hidden");
10083
10603
  return false;
10084
10604
  }
10085
- const chipTop = Math.max(8, rect.top - 36);
10086
- const chipRight = Math.max(8, window.innerWidth - rect.right);
10087
- const chip = document.createElement("button");
10088
- chip.type = "button";
10605
+ const chip = document.createElement("div");
10089
10606
  chip.id = handoffId;
10090
10607
  applyOverlayTheme(chip);
10091
- chip.textContent = "Continue there";
10092
- chip.title = "Continue this thread on another account";
10093
- chip.style.top = chipTop + "px";
10094
- chip.style.right = chipRight + "px";
10608
+ chip.title = "Choose an account, then continue this thread there";
10609
+ chip.dataset.open = wasOpen ? "true" : "false";
10610
+ const chipMain = document.createElement("button");
10611
+ chipMain.type = "button";
10612
+ chipMain.className = "codexuse-handoff-main";
10613
+ chipMain.textContent = "Continue in another account";
10614
+ const chipHide = document.createElement("button");
10615
+ chipHide.type = "button";
10616
+ chipHide.className = "codexuse-handoff-hide";
10617
+ chipHide.textContent = "x";
10618
+ chipHide.title = "Hide Continue in another account";
10619
+ chipHide.setAttribute("aria-label", "Hide Continue in another account");
10620
+ chip.appendChild(chipMain);
10621
+ chip.appendChild(chipHide);
10095
10622
  // Hand off needs BOTH a thread (this chip only shows in-thread) and a
10096
10623
  // target account. Clicking opens a picker of ready accounts; the chosen
10097
10624
  // account becomes the explicit continue target (the request also carries
@@ -10099,26 +10626,47 @@ function createBridgeApplyQolExpression(settings) {
10099
10626
  // the "continue" message is auto-sent there).
10100
10627
  const closePicker = () => {
10101
10628
  document.getElementById(handoffPickerId)?.remove();
10629
+ if (typeof cleanupHandoffPicker === "function") {
10630
+ cleanupHandoffPicker();
10631
+ cleanupHandoffPicker = null;
10632
+ }
10102
10633
  chip.dataset.open = "false";
10103
10634
  };
10104
10635
  const handOffTo = (target) => {
10105
10636
  closePicker();
10106
- chip.disabled = true;
10107
- chip.textContent = "Handing off\u2026";
10637
+ chip.dataset.disabled = "true";
10638
+ chipMain.textContent = "Continuing...";
10108
10639
  requestAction("continue", target);
10109
10640
  };
10641
+ const placePicker = (picker) => {
10642
+ const chipRect = chip.getBoundingClientRect();
10643
+ const pickerRect = picker.getBoundingClientRect();
10644
+ const width = pickerRect.width || Math.min(280, window.innerWidth - 24);
10645
+ const height = pickerRect.height || 120;
10646
+ const left = Math.min(
10647
+ Math.max(8, chipRect.right - width),
10648
+ Math.max(8, window.innerWidth - width - 8)
10649
+ );
10650
+ const below = chipRect.bottom + 6;
10651
+ const above = chipRect.top - height - 6;
10652
+ const top = below + height <= window.innerHeight - 8
10653
+ ? below
10654
+ : Math.max(8, above);
10655
+ picker.style.left = left + "px";
10656
+ picker.style.top = Math.min(top, Math.max(8, window.innerHeight - height - 8)) + "px";
10657
+ picker.style.right = "auto";
10658
+ picker.style.bottom = "auto";
10659
+ };
10110
10660
  const openPicker = (skipRefresh = false) => {
10111
10661
  document.getElementById(handoffPickerId)?.remove();
10662
+ if (typeof cleanupHandoffPicker === "function") {
10663
+ cleanupHandoffPicker();
10664
+ cleanupHandoffPicker = null;
10665
+ }
10112
10666
  const picker = document.createElement("div");
10113
10667
  picker.id = handoffPickerId;
10114
10668
  picker.className = "codexuse-action-menu";
10115
10669
  applyOverlayTheme(picker);
10116
- picker.style.position = "fixed";
10117
- picker.style.right = chipRight + "px";
10118
- picker.style.bottom = Math.max(8, window.innerHeight - chipTop + 4) + "px";
10119
- picker.style.maxHeight = "240px";
10120
- picker.style.overflowY = "auto";
10121
- picker.style.zIndex = "2147483647";
10122
10670
  const list = document.createElement("div");
10123
10671
  list.className = "codexuse-target-list";
10124
10672
  if (switchable.length === 0) {
@@ -10131,7 +10679,7 @@ function createBridgeApplyQolExpression(settings) {
10131
10679
  const row = document.createElement("button");
10132
10680
  row.type = "button";
10133
10681
  row.className = "codexuse-target-row";
10134
- row.title = "Continue this thread on " + (target.label || "account");
10682
+ row.title = "Continue this thread in " + (target.label || "account");
10135
10683
  const label = document.createElement("span");
10136
10684
  label.className = "codexuse-target-label";
10137
10685
  label.textContent = typeof target.label === "string" && target.label.trim() ? target.label.trim() : "Account";
@@ -10145,12 +10693,14 @@ function createBridgeApplyQolExpression(settings) {
10145
10693
  row.addEventListener("click", (event) => {
10146
10694
  event.preventDefault();
10147
10695
  event.stopPropagation();
10696
+ event.stopImmediatePropagation?.();
10148
10697
  handOffTo(target);
10149
10698
  }, true);
10150
10699
  list.appendChild(row);
10151
10700
  });
10152
10701
  picker.appendChild(list);
10153
10702
  document.body.appendChild(picker);
10703
+ placePicker(picker);
10154
10704
  chip.dataset.open = "true";
10155
10705
  // Rebuild the open picker once fresh targets arrive; skipRefresh stops
10156
10706
  // the rebuilt picker from requesting again in a loop.
@@ -10161,14 +10711,56 @@ function createBridgeApplyQolExpression(settings) {
10161
10711
  }
10162
10712
  const onDocPointer = (event) => {
10163
10713
  if (picker.contains(event.target) || chip.contains(event.target)) return;
10164
- document.removeEventListener("pointerdown", onDocPointer, true);
10165
10714
  closePicker();
10166
10715
  };
10716
+ const onKeyDown = (event) => {
10717
+ if (event.key === "Escape") closePicker();
10718
+ };
10167
10719
  document.addEventListener("pointerdown", onDocPointer, true);
10720
+ document.addEventListener("keydown", onKeyDown, true);
10721
+ cleanupHandoffPicker = () => {
10722
+ document.removeEventListener("pointerdown", onDocPointer, true);
10723
+ document.removeEventListener("keydown", onKeyDown, true);
10724
+ };
10168
10725
  };
10169
- chip.addEventListener("click", (event) => {
10726
+ let dragMoved = false;
10727
+ chip.addEventListener("pointerdown", (event) => {
10728
+ if (event.button !== 0 || event.target?.closest?.(".codexuse-handoff-hide")) return;
10729
+ event.stopPropagation();
10730
+ event.stopImmediatePropagation?.();
10731
+ const startX = event.clientX;
10732
+ const startY = event.clientY;
10733
+ const startRect = chip.getBoundingClientRect();
10734
+ const offsetX = startX - startRect.left;
10735
+ const offsetY = startY - startRect.top;
10736
+ dragMoved = false;
10737
+ const onMove = (moveEvent) => {
10738
+ if (!dragMoved && Math.hypot(moveEvent.clientX - startX, moveEvent.clientY - startY) < 4) return;
10739
+ dragMoved = true;
10740
+ chip.dataset.dragging = "true";
10741
+ closePicker();
10742
+ placeOverlay(chip, moveEvent.clientX - offsetX, moveEvent.clientY - offsetY);
10743
+ };
10744
+ const onUp = () => {
10745
+ window.removeEventListener("pointermove", onMove, true);
10746
+ window.removeEventListener("pointerup", onUp, true);
10747
+ if (chip.dataset.dragging === "true") {
10748
+ delete chip.dataset.dragging;
10749
+ const chipRect = chip.getBoundingClientRect();
10750
+ writeOverlayStore(handoffPosKey, JSON.stringify({ x: chipRect.left, y: chipRect.top }));
10751
+ }
10752
+ };
10753
+ window.addEventListener("pointermove", onMove, true);
10754
+ window.addEventListener("pointerup", onUp, true);
10755
+ }, true);
10756
+ chipMain.addEventListener("click", (event) => {
10170
10757
  event.preventDefault();
10171
10758
  event.stopPropagation();
10759
+ event.stopImmediatePropagation?.();
10760
+ if (dragMoved || chip.dataset.disabled === "true") {
10761
+ dragMoved = false;
10762
+ return;
10763
+ }
10172
10764
  // Always make the user pick the destination account explicitly \u2014 never
10173
10765
  // auto-hand-off to a server-chosen account (which could be out of quota).
10174
10766
  if (chip.dataset.open === "true") {
@@ -10177,7 +10769,38 @@ function createBridgeApplyQolExpression(settings) {
10177
10769
  openPicker();
10178
10770
  }
10179
10771
  }, true);
10772
+ chipHide.addEventListener("click", (event) => {
10773
+ event.preventDefault();
10774
+ event.stopPropagation();
10775
+ event.stopImmediatePropagation?.();
10776
+ writeOverlayStore(handoffHiddenKey, "1");
10777
+ closePicker();
10778
+ renderHandoff();
10779
+ renderStatus();
10780
+ }, true);
10180
10781
  document.body.appendChild(chip);
10782
+ const rawPos = readOverlayStore(handoffPosKey);
10783
+ if (rawPos) {
10784
+ try {
10785
+ const pos = JSON.parse(rawPos);
10786
+ if (typeof pos?.x === "number" && typeof pos?.y === "number") {
10787
+ const next = clampOverlayXY(chip, pos.x, pos.y);
10788
+ const chipRect = chip.getBoundingClientRect();
10789
+ if (rectsOverlap(positionedRect(chipRect, next), rect, 6)) {
10790
+ placeInitialHandoffPosition(chip, rect);
10791
+ } else {
10792
+ placeOverlay(chip, next.x, next.y);
10793
+ }
10794
+ } else {
10795
+ placeInitialHandoffPosition(chip, rect);
10796
+ }
10797
+ } catch {
10798
+ placeInitialHandoffPosition(chip, rect);
10799
+ }
10800
+ } else {
10801
+ placeInitialHandoffPosition(chip, rect);
10802
+ }
10803
+ if (wasOpen) openPicker();
10181
10804
  reportHandoff("rendered");
10182
10805
  return true;
10183
10806
  };
@@ -10229,12 +10852,16 @@ function createBridgeApplyQolExpression(settings) {
10229
10852
  clearTimelineTargets();
10230
10853
  document.getElementById(statusId)?.remove();
10231
10854
  document.getElementById(handoffId)?.remove();
10855
+ document.getElementById(handoffPickerId)?.remove();
10856
+ if (typeof cleanupHandoffPicker === "function") cleanupHandoffPicker();
10232
10857
  };
10233
10858
  } else {
10234
10859
  document.querySelectorAll("." + timelineClass).forEach((node) => node.remove());
10235
10860
  clearTimelineTargets();
10236
10861
  document.getElementById(statusId)?.remove();
10237
10862
  document.getElementById(handoffId)?.remove();
10863
+ document.getElementById(handoffPickerId)?.remove();
10864
+ if (typeof cleanupHandoffPicker === "function") cleanupHandoffPicker();
10238
10865
  }
10239
10866
 
10240
10867
  state.wideViewEnabled = wideViewEnabled;
@@ -11529,7 +12156,8 @@ function normalizeOfficialCodexQolSettings2(value) {
11529
12156
  actionTargets,
11530
12157
  canonicalSyncLine: typeof value.canonicalSyncLine === "string" && value.canonicalSyncLine.trim() ? value.canonicalSyncLine.trim().slice(0, 40) : null,
11531
12158
  autoRollDecision: normalizeOverlayAutoRollDecision(value.autoRollDecision),
11532
- overlayAutoRollEnabled: typeof value.overlayAutoRollEnabled === "boolean" ? value.overlayAutoRollEnabled : null
12159
+ overlayAutoRollEnabled: typeof value.overlayAutoRollEnabled === "boolean" ? value.overlayAutoRollEnabled : null,
12160
+ overlayRunningThreadHandoffEnabled: typeof value.overlayRunningThreadHandoffEnabled === "boolean" ? value.overlayRunningThreadHandoffEnabled : null
11533
12161
  };
11534
12162
  }
11535
12163
  function hasActiveQolSettings(settings) {
@@ -12356,7 +12984,7 @@ function startOfficialCodexBridgeActionListener(args) {
12356
12984
  if (!action) {
12357
12985
  return;
12358
12986
  }
12359
- const settingKey = payload.settingKey === "autoRoll" || payload.settingKey === "wideView" || payload.settingKey === "scrollRestore" || payload.settingKey === "conversationTimeline" || payload.settingKey === "handoffChip" ? payload.settingKey : null;
12987
+ const settingKey = payload.settingKey === "autoRoll" || payload.settingKey === "runningThreadHandoff" || payload.settingKey === "wideView" || payload.settingKey === "scrollRestore" || payload.settingKey === "conversationTimeline" || payload.settingKey === "handoffChip" ? payload.settingKey : null;
12360
12988
  const settingValue = typeof payload.settingValue === "boolean" ? payload.settingValue : null;
12361
12989
  if (action === "setting" && (!settingKey || settingValue === null)) {
12362
12990
  return;
@@ -12743,9 +13371,12 @@ function isMainProcessRow(row, candidate) {
12743
13371
  }
12744
13372
  function findAppServerPid(rows, mainPid) {
12745
13373
  return rows.find(
12746
- (row) => row.ppid === mainPid && row.args.includes("/Contents/Resources/codex app-server")
13374
+ (row) => row.ppid === mainPid && isAppServerProcessRow(row)
12747
13375
  )?.pid ?? null;
12748
13376
  }
13377
+ function isAppServerProcessRow(row) {
13378
+ return row.args.includes("/Contents/Resources/codex app-server");
13379
+ }
12749
13380
  async function processMatchesProfileHome(pid, profileHome) {
12750
13381
  const result = await runCommand2(
12751
13382
  "/bin/ps",
@@ -12759,7 +13390,7 @@ async function processMatchesProfileHome(pid, profileHome) {
12759
13390
  }
12760
13391
  async function findAppServerPidForProfile(rows, mainPid, profileHome) {
12761
13392
  const candidates = rows.filter(
12762
- (row) => row.ppid === mainPid && row.args.includes("/Contents/Resources/codex app-server")
13393
+ (row) => row.ppid === mainPid && isAppServerProcessRow(row)
12763
13394
  );
12764
13395
  for (const row of candidates) {
12765
13396
  if (row.args.includes(profileHome) || await processMatchesProfileHome(row.pid, profileHome)) {
@@ -12768,6 +13399,19 @@ async function findAppServerPidForProfile(rows, mainPid, profileHome) {
12768
13399
  }
12769
13400
  return null;
12770
13401
  }
13402
+ async function resolveStoredAppServerPid(instance, rows) {
13403
+ if (!instance.appServerPid) {
13404
+ return null;
13405
+ }
13406
+ const row = rows.find((entry) => entry.pid === instance.appServerPid);
13407
+ if (!row || !isAppServerProcessRow(row)) {
13408
+ return null;
13409
+ }
13410
+ if (instance.profileHome && !row.args.includes(instance.profileHome) && !await processMatchesProfileHome(row.pid, instance.profileHome)) {
13411
+ return null;
13412
+ }
13413
+ return row.pid;
13414
+ }
12771
13415
  function parseRemoteDebuggingPort(args) {
12772
13416
  const match = args.match(/--remote-debugging-port(?:=|\s+)(\d{2,5})\b/);
12773
13417
  const port = Number(match?.[1]);
@@ -12896,11 +13540,27 @@ async function readManagedInstance(profileName) {
12896
13540
  async function resolveInstanceRuntimeState(args) {
12897
13541
  const pid = args.instance.pid;
12898
13542
  if (!pid) {
12899
- return { running: false, appServerPid: null, startedAt: null };
13543
+ const appServerPid2 = await resolveStoredAppServerPid(
13544
+ args.instance,
13545
+ args.rows
13546
+ );
13547
+ return {
13548
+ running: appServerPid2 !== null,
13549
+ appServerPid: appServerPid2,
13550
+ startedAt: null
13551
+ };
12900
13552
  }
12901
13553
  const mainRow = args.rows.find((row) => row.pid === pid);
12902
13554
  if (!mainRow || !isMainProcessRow(mainRow, args.candidate)) {
12903
- return { running: false, appServerPid: null, startedAt: null };
13555
+ const appServerPid2 = await resolveStoredAppServerPid(
13556
+ args.instance,
13557
+ args.rows
13558
+ );
13559
+ return {
13560
+ running: appServerPid2 !== null,
13561
+ appServerPid: appServerPid2,
13562
+ startedAt: null
13563
+ };
12904
13564
  }
12905
13565
  const appServerPid = args.instance.profileHome ? await findAppServerPidForProfile(args.rows, pid, args.instance.profileHome) : findAppServerPid(args.rows, pid);
12906
13566
  if (args.instance.profileHome && appServerPid === null) {
@@ -13813,8 +14473,8 @@ function formatResetText(window) {
13813
14473
  if (durationText === "now") return "now";
13814
14474
  return `in ${durationText}`;
13815
14475
  }
13816
- function formatWindowUsage(window) {
13817
- const label = formatWindowLabel(window.windowMinutes) ?? "window";
14476
+ function formatWindowUsage(window, labelOverride) {
14477
+ const label = labelOverride ?? formatWindowLabel(window.windowMinutes) ?? "window";
13818
14478
  const usedPercent = typeof window.usedPercent === "number" && Number.isFinite(window.usedPercent) ? `${Math.round(Math.max(0, Math.min(100, window.usedPercent)))}%` : "n/a";
13819
14479
  const resetText = formatResetText(window);
13820
14480
  return {
@@ -13943,6 +14603,10 @@ async function resolveProfileUsage(manager, profile, codexPath) {
13943
14603
  const row = formatWindowUsage(snapshot.secondary);
13944
14604
  if (row) rows.push(row);
13945
14605
  }
14606
+ for (const window of snapshot.extraWindows ?? []) {
14607
+ const row = formatWindowUsage(window, window.label);
14608
+ if (row) rows.push(row);
14609
+ }
13946
14610
  const maxPercent = maxUsedPercent(snapshot);
13947
14611
  const summary = typeof maxPercent === "number" ? `${Math.round(maxPercent)}%` : "n/a";
13948
14612
  return { summary, rows: rows.length > 0 ? rows : null, usageSummary: summarizeRateLimitSnapshot(snapshot) };
@@ -14685,7 +15349,8 @@ async function handleProfileCommand(args, version) {
14685
15349
  }
14686
15350
 
14687
15351
  // ../../packages/contracts/src/cloud-sync/types.ts
14688
- var CLOUD_SYNC_SCHEMA_VERSION = 1;
15352
+ var CLOUD_SYNC_PLAINTEXT_SCHEMA_VERSION = 1;
15353
+ var CLOUD_SYNC_ENCRYPTED_SCHEMA_VERSION = 2;
14689
15354
 
14690
15355
  // ../../packages/shared/src/core/type-guards.ts
14691
15356
  function isRecord6(value) {
@@ -14725,6 +15390,12 @@ function buildSyncUrl(pathname) {
14725
15390
  const normalizedPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
14726
15391
  return `${baseUrl}${normalizedPath}`;
14727
15392
  }
15393
+ function normalizeSnapshotKind(value) {
15394
+ if (value === "encrypted" || value === "legacy-plaintext") {
15395
+ return value;
15396
+ }
15397
+ return "none";
15398
+ }
14728
15399
  function normalizeSnapshot(value) {
14729
15400
  if (!isRecord6(value)) {
14730
15401
  return null;
@@ -14734,7 +15405,37 @@ function normalizeSnapshot(value) {
14734
15405
  return null;
14735
15406
  }
14736
15407
  const schemaVersion = Number(value.schemaVersion);
14737
- if (!Number.isFinite(schemaVersion) || schemaVersion !== CLOUD_SYNC_SCHEMA_VERSION) {
15408
+ if (schemaVersion === CLOUD_SYNC_ENCRYPTED_SCHEMA_VERSION) {
15409
+ if (!isRecord6(value.encryption)) {
15410
+ return null;
15411
+ }
15412
+ const encryption = value.encryption;
15413
+ const params = isRecord6(encryption.kdfParams) ? encryption.kdfParams : null;
15414
+ if (!params) {
15415
+ return null;
15416
+ }
15417
+ const N = Number(params.N);
15418
+ const r = Number(params.r);
15419
+ const p = Number(params.p);
15420
+ const keyLength = Number(params.keyLength);
15421
+ if (encryption.cipher !== "aes-256-gcm" || encryption.kdf !== "scrypt" || !Number.isSafeInteger(N) || !Number.isSafeInteger(r) || !Number.isSafeInteger(p) || !Number.isSafeInteger(keyLength) || keyLength !== 32 || typeof encryption.salt !== "string" || typeof encryption.iv !== "string" || typeof encryption.tag !== "string" || typeof encryption.ciphertext !== "string") {
15422
+ return null;
15423
+ }
15424
+ return {
15425
+ schemaVersion: CLOUD_SYNC_ENCRYPTED_SCHEMA_VERSION,
15426
+ updatedAt,
15427
+ encryption: {
15428
+ cipher: "aes-256-gcm",
15429
+ kdf: "scrypt",
15430
+ kdfParams: { N, r, p, keyLength },
15431
+ salt: encryption.salt,
15432
+ iv: encryption.iv,
15433
+ tag: encryption.tag,
15434
+ ciphertext: encryption.ciphertext
15435
+ }
15436
+ };
15437
+ }
15438
+ if (schemaVersion !== CLOUD_SYNC_PLAINTEXT_SCHEMA_VERSION) {
14738
15439
  return null;
14739
15440
  }
14740
15441
  const rawProfiles = Array.isArray(value.profiles) ? value.profiles : [];
@@ -14758,7 +15459,7 @@ function normalizeSnapshot(value) {
14758
15459
  const rawSettings = value.settingsJson;
14759
15460
  const settingsJson = isRecord6(rawSettings) ? rawSettings : null;
14760
15461
  return {
14761
- schemaVersion: CLOUD_SYNC_SCHEMA_VERSION,
15462
+ schemaVersion: CLOUD_SYNC_PLAINTEXT_SCHEMA_VERSION,
14762
15463
  updatedAt,
14763
15464
  profiles,
14764
15465
  configTomlContent: typeof value.configTomlContent === "string" ? value.configTomlContent : null,
@@ -14810,7 +15511,12 @@ async function fetchRemoteSnapshotMeta(licenseKey) {
14810
15511
  "/v1/sync/snapshot/meta",
14811
15512
  licenseKey
14812
15513
  );
14813
- return toIsoOrNull(response.updatedAt);
15514
+ const updatedAt = toIsoOrNull(response.updatedAt);
15515
+ const snapshotKind = normalizeSnapshotKind(response.snapshotKind);
15516
+ return {
15517
+ updatedAt,
15518
+ snapshotKind: updatedAt && response.snapshotKind === void 0 ? "legacy-plaintext" : snapshotKind
15519
+ };
14814
15520
  }
14815
15521
  async function pushRemoteSnapshot(licenseKey, snapshot, options) {
14816
15522
  const serializedSnapshot = typeof options?.serializedSnapshot === "string" ? options.serializedSnapshot : JSON.stringify(snapshot);
@@ -14833,7 +15539,7 @@ async function pushRemoteSnapshot(licenseKey, snapshot, options) {
14833
15539
  }
14834
15540
 
14835
15541
  // ../../packages/runtime-codex/src/codex/config.ts
14836
- var import_toml2 = __toESM(require_toml(), 1);
15542
+ var import_toml3 = __toESM(require_toml(), 1);
14837
15543
 
14838
15544
  // ../../packages/runtime-codex/src/codex/config-metadata.ts
14839
15545
  var import_node_child_process6 = require("child_process");
@@ -15472,7 +16178,7 @@ function formatUserFacingError(error, options = {}) {
15472
16178
  // ../../packages/runtime-codex/src/codex/config.ts
15473
16179
  function tryParseToml(content) {
15474
16180
  try {
15475
- const parsed = (0, import_toml2.parse)(content);
16181
+ const parsed = (0, import_toml3.parse)(content);
15476
16182
  return { data: parsed ?? {}, error: null };
15477
16183
  } catch (error) {
15478
16184
  const maybe = error;
@@ -15791,6 +16497,170 @@ async function saveCodexConfigContent(content, options) {
15791
16497
  return buildSnapshot(normalized, true, options);
15792
16498
  }
15793
16499
 
16500
+ // ../../packages/runtime-profiles/src/cloud-sync/encryption.ts
16501
+ var import_node_child_process7 = require("child_process");
16502
+ var import_node_crypto6 = require("crypto");
16503
+ var KEYCHAIN_SERVICE = "CODEXUSE_CLOUD_SYNC_PASSPHRASE";
16504
+ var KEYCHAIN_ACCOUNT_PREFIX = "license-sha256:";
16505
+ var ENCRYPTION_AAD = Buffer.from("codexuse-cloud-sync-v2", "utf8");
16506
+ var SCRYPT_PARAMS = {
16507
+ N: 32768,
16508
+ r: 8,
16509
+ p: 1,
16510
+ keyLength: 32
16511
+ };
16512
+ function normalizePassphrase(value) {
16513
+ if (typeof value !== "string" || value.length === 0) {
16514
+ return null;
16515
+ }
16516
+ return value;
16517
+ }
16518
+ function requireCloudSyncPassphrase(value) {
16519
+ const normalized = normalizePassphrase(value);
16520
+ if (!normalized) {
16521
+ throw new Error("Cloud Sync passphrase is required.");
16522
+ }
16523
+ return normalized;
16524
+ }
16525
+ async function deriveKey(passphrase, salt, params = SCRYPT_PARAMS) {
16526
+ return new Promise((resolve, reject) => {
16527
+ (0, import_node_crypto6.scrypt)(passphrase, salt, params.keyLength, {
16528
+ N: params.N,
16529
+ r: params.r,
16530
+ p: params.p,
16531
+ maxmem: 64 * 1024 * 1024
16532
+ }, (error, key) => {
16533
+ if (error) {
16534
+ reject(error);
16535
+ return;
16536
+ }
16537
+ resolve(Buffer.from(key));
16538
+ });
16539
+ });
16540
+ }
16541
+ function toBase64(value) {
16542
+ return value.toString("base64");
16543
+ }
16544
+ function fromBase64(value) {
16545
+ return Buffer.from(value, "base64");
16546
+ }
16547
+ function normalizeDecryptedSnapshot(value) {
16548
+ if (!isRecord6(value) || Number(value.schemaVersion) !== CLOUD_SYNC_PLAINTEXT_SCHEMA_VERSION) {
16549
+ throw new Error("Cloud Sync snapshot payload is invalid.");
16550
+ }
16551
+ const updatedAt = toIsoOrNull(value.updatedAt);
16552
+ if (!updatedAt) {
16553
+ throw new Error("Cloud Sync snapshot timestamp is invalid.");
16554
+ }
16555
+ return {
16556
+ schemaVersion: CLOUD_SYNC_PLAINTEXT_SCHEMA_VERSION,
16557
+ updatedAt,
16558
+ profiles: Array.isArray(value.profiles) ? value.profiles : [],
16559
+ configTomlContent: typeof value.configTomlContent === "string" ? value.configTomlContent : null,
16560
+ settingsJson: isRecord6(value.settingsJson) ? value.settingsJson : null
16561
+ };
16562
+ }
16563
+ async function encryptCloudSyncSnapshot(snapshot, passphrase) {
16564
+ const salt = (0, import_node_crypto6.randomBytes)(16);
16565
+ const iv = (0, import_node_crypto6.randomBytes)(12);
16566
+ const key = await deriveKey(requireCloudSyncPassphrase(passphrase), salt);
16567
+ const cipher = (0, import_node_crypto6.createCipheriv)("aes-256-gcm", key, iv);
16568
+ cipher.setAAD(ENCRYPTION_AAD);
16569
+ const plaintext = Buffer.from(JSON.stringify(snapshot), "utf8");
16570
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
16571
+ const tag = cipher.getAuthTag();
16572
+ const encryption = {
16573
+ cipher: "aes-256-gcm",
16574
+ kdf: "scrypt",
16575
+ kdfParams: SCRYPT_PARAMS,
16576
+ salt: toBase64(salt),
16577
+ iv: toBase64(iv),
16578
+ tag: toBase64(tag),
16579
+ ciphertext: toBase64(ciphertext)
16580
+ };
16581
+ return {
16582
+ schemaVersion: CLOUD_SYNC_ENCRYPTED_SCHEMA_VERSION,
16583
+ updatedAt: snapshot.updatedAt,
16584
+ encryption
16585
+ };
16586
+ }
16587
+ async function decryptCloudSyncSnapshot(snapshot, passphrase) {
16588
+ try {
16589
+ const salt = fromBase64(snapshot.encryption.salt);
16590
+ const iv = fromBase64(snapshot.encryption.iv);
16591
+ const tag = fromBase64(snapshot.encryption.tag);
16592
+ const ciphertext = fromBase64(snapshot.encryption.ciphertext);
16593
+ const key = await deriveKey(requireCloudSyncPassphrase(passphrase), salt, snapshot.encryption.kdfParams);
16594
+ const decipher = (0, import_node_crypto6.createDecipheriv)("aes-256-gcm", key, iv);
16595
+ decipher.setAAD(ENCRYPTION_AAD);
16596
+ decipher.setAuthTag(tag);
16597
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
16598
+ return normalizeDecryptedSnapshot(JSON.parse(plaintext.toString("utf8")));
16599
+ } catch (error) {
16600
+ if (error instanceof Error && error.message === "Cloud Sync passphrase is required.") {
16601
+ throw error;
16602
+ }
16603
+ const wrapped = new Error("Cloud Sync passphrase is incorrect or snapshot is corrupted.");
16604
+ wrapped.cause = error;
16605
+ throw wrapped;
16606
+ }
16607
+ }
16608
+ function keychainAccountForLicense(licenseKey) {
16609
+ const digest = (0, import_node_crypto6.createHash)("sha256").update(licenseKey).digest("hex");
16610
+ return KEYCHAIN_ACCOUNT_PREFIX + digest;
16611
+ }
16612
+ function runSecurityCommand(args) {
16613
+ return new Promise((resolve, reject) => {
16614
+ const child = (0, import_node_child_process7.spawn)("security", args, { stdio: ["ignore", "pipe", "ignore"] });
16615
+ let stdout = "";
16616
+ child.stdout?.on("data", (chunk) => {
16617
+ stdout += String(chunk);
16618
+ });
16619
+ child.on("error", () => {
16620
+ reject(new Error("keychain-command-failed"));
16621
+ });
16622
+ child.on("close", (code) => {
16623
+ resolve({ code, stdout });
16624
+ });
16625
+ });
16626
+ }
16627
+ async function readRememberedCloudSyncPassphrase(licenseKey) {
16628
+ if (process.platform !== "darwin") {
16629
+ return null;
16630
+ }
16631
+ const result = await runSecurityCommand([
16632
+ "find-generic-password",
16633
+ "-a",
16634
+ keychainAccountForLicense(licenseKey),
16635
+ "-s",
16636
+ KEYCHAIN_SERVICE,
16637
+ "-w"
16638
+ ]);
16639
+ if (result.code !== 0) {
16640
+ return null;
16641
+ }
16642
+ return result.stdout.length > 0 ? result.stdout.replace(/\n$/, "") : null;
16643
+ }
16644
+ async function writeRememberedCloudSyncPassphrase(licenseKey, passphrase) {
16645
+ if (process.platform !== "darwin") {
16646
+ throw new Error("keychain-unsupported");
16647
+ }
16648
+ const value = requireCloudSyncPassphrase(passphrase);
16649
+ const result = await runSecurityCommand([
16650
+ "add-generic-password",
16651
+ "-U",
16652
+ "-a",
16653
+ keychainAccountForLicense(licenseKey),
16654
+ "-s",
16655
+ KEYCHAIN_SERVICE,
16656
+ "-w",
16657
+ value
16658
+ ]);
16659
+ if (result.code !== 0) {
16660
+ throw new Error("keychain-write-failed");
16661
+ }
16662
+ }
16663
+
15794
16664
  // ../../packages/runtime-profiles/src/cloud-sync/service.ts
15795
16665
  var SYNC_SIZE_WARN_BYTES = 1 * 1024 * 1024;
15796
16666
  var SYNC_SIZE_MAX_BYTES = 5 * 1024 * 1024;
@@ -15843,7 +16713,8 @@ async function readState() {
15843
16713
  lastPushAt: state.sync.lastPushAt ?? void 0,
15844
16714
  lastPullAt: state.sync.lastPullAt ?? void 0,
15845
16715
  lastError: state.sync.lastError ?? void 0,
15846
- remoteUpdatedAt: state.sync.remoteUpdatedAt ?? void 0
16716
+ remoteUpdatedAt: state.sync.remoteUpdatedAt ?? void 0,
16717
+ remoteKind: state.sync.remoteKind ?? "none"
15847
16718
  };
15848
16719
  }
15849
16720
  async function writeState(patch) {
@@ -15852,7 +16723,8 @@ async function writeState(patch) {
15852
16723
  ...typeof patch.lastPushAt === "string" ? { lastPushAt: patch.lastPushAt } : {},
15853
16724
  ...typeof patch.lastPullAt === "string" ? { lastPullAt: patch.lastPullAt } : {},
15854
16725
  ...typeof patch.lastError === "string" ? { lastError: patch.lastError } : patch.lastError === void 0 ? { lastError: null } : {},
15855
- ...typeof patch.remoteUpdatedAt === "string" ? { remoteUpdatedAt: patch.remoteUpdatedAt } : {}
16726
+ ...typeof patch.remoteUpdatedAt === "string" ? { remoteUpdatedAt: patch.remoteUpdatedAt } : patch.remoteUpdatedAt === void 0 ? {} : { remoteUpdatedAt: null },
16727
+ ...patch.remoteKind ? { remoteKind: patch.remoteKind } : {}
15856
16728
  }
15857
16729
  });
15858
16730
  }
@@ -15905,7 +16777,7 @@ async function buildLocalSnapshot(profileManager, options = {}) {
15905
16777
  throw new Error("Refusing to push an empty cloud sync snapshot.");
15906
16778
  }
15907
16779
  const snapshot = {
15908
- schemaVersion: CLOUD_SYNC_SCHEMA_VERSION,
16780
+ schemaVersion: CLOUD_SYNC_PLAINTEXT_SCHEMA_VERSION,
15909
16781
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
15910
16782
  profiles,
15911
16783
  configTomlContent: config.exists ? config.content : null,
@@ -15930,6 +16802,34 @@ async function applyRemoteSnapshot(profileManager, snapshot) {
15930
16802
  await writeCodexSettingsJsonRaw({});
15931
16803
  }
15932
16804
  }
16805
+ function getSnapshotKind(snapshot) {
16806
+ if (!snapshot) {
16807
+ return "none";
16808
+ }
16809
+ return snapshot.schemaVersion === CLOUD_SYNC_ENCRYPTED_SCHEMA_VERSION ? "encrypted" : "legacy-plaintext";
16810
+ }
16811
+ async function resolvePassphrase(licenseKey, options) {
16812
+ if (typeof options?.passphrase === "string" && options.passphrase.length > 0) {
16813
+ return { value: requireCloudSyncPassphrase(options.passphrase), source: "input" };
16814
+ }
16815
+ const remembered = await readRememberedCloudSyncPassphrase(licenseKey);
16816
+ if (remembered) {
16817
+ return { value: remembered, source: "keychain" };
16818
+ }
16819
+ throw new Error("Cloud Sync passphrase is required.");
16820
+ }
16821
+ async function rememberPassphraseIfRequested(licenseKey, options, passphrase) {
16822
+ if (options?.rememberPassphrase !== true || passphrase.source !== "input") {
16823
+ return;
16824
+ }
16825
+ try {
16826
+ await writeRememberedCloudSyncPassphrase(licenseKey, passphrase.value);
16827
+ } catch (error) {
16828
+ logWarn("[cloud-sync] failed to remember passphrase in keychain", {
16829
+ error: error instanceof Error ? error.message : String(error)
16830
+ });
16831
+ }
16832
+ }
15933
16833
  function toRunError(mode, error) {
15934
16834
  const message = formatUserFacingError(error, {
15935
16835
  fallback: `Cloud sync ${mode} failed.`
@@ -15946,14 +16846,23 @@ async function getCloudSyncStatus() {
15946
16846
  const eligibility = await resolveEligibility();
15947
16847
  const state = await readState();
15948
16848
  let remoteUpdatedAt = state.remoteUpdatedAt ?? null;
16849
+ let remoteKind = state.remoteKind ?? "none";
16850
+ let hasRememberedPassphrase = false;
15949
16851
  if (eligibility.canSync && eligibility.licenseKey) {
16852
+ hasRememberedPassphrase = Boolean(
16853
+ await readRememberedCloudSyncPassphrase(eligibility.licenseKey).catch(() => null)
16854
+ );
15950
16855
  try {
15951
- remoteUpdatedAt = await fetchRemoteSnapshotMeta(eligibility.licenseKey);
16856
+ const remoteMeta = await fetchRemoteSnapshotMeta(eligibility.licenseKey);
16857
+ remoteUpdatedAt = remoteMeta.updatedAt;
16858
+ remoteKind = remoteMeta.snapshotKind;
16859
+ await writeState({ remoteUpdatedAt, remoteKind });
15952
16860
  } catch (error) {
15953
16861
  if (error instanceof CloudSyncClientError && error.status === 404) {
15954
16862
  try {
15955
16863
  const remote = await fetchRemoteSnapshot(eligibility.licenseKey);
15956
16864
  remoteUpdatedAt = remote?.updatedAt ?? null;
16865
+ remoteKind = getSnapshotKind(remote);
15957
16866
  } catch {
15958
16867
  }
15959
16868
  }
@@ -15964,13 +16873,15 @@ async function getCloudSyncStatus() {
15964
16873
  reason: eligibility.reason,
15965
16874
  licenseState: eligibility.licenseState,
15966
16875
  hasLicenseKey: Boolean(eligibility.licenseKey),
16876
+ hasRememberedPassphrase,
15967
16877
  lastPushAt: state.lastPushAt ?? null,
15968
16878
  lastPullAt: state.lastPullAt ?? null,
15969
16879
  lastError: state.lastError ?? null,
15970
- remoteUpdatedAt
16880
+ remoteUpdatedAt,
16881
+ remoteKind
15971
16882
  };
15972
16883
  }
15973
- async function pushCloudSync() {
16884
+ async function pushCloudSync(options = {}) {
15974
16885
  const eligibility = await resolveEligibility();
15975
16886
  if (!eligibility.canSync || !eligibility.licenseKey) {
15976
16887
  return {
@@ -15985,7 +16896,9 @@ async function pushCloudSync() {
15985
16896
  try {
15986
16897
  await profileManager.initialize();
15987
16898
  const localSnapshot = await buildLocalSnapshot(profileManager, { enforcePushGuards: true });
15988
- const serializedSnapshot = JSON.stringify(localSnapshot);
16899
+ const passphrase = await resolvePassphrase(eligibility.licenseKey, options);
16900
+ const encryptedSnapshot = await encryptCloudSyncSnapshot(localSnapshot, passphrase.value);
16901
+ const serializedSnapshot = JSON.stringify(encryptedSnapshot);
15989
16902
  const snapshotBytes = Buffer.byteLength(serializedSnapshot, "utf8");
15990
16903
  if (snapshotBytes > SYNC_SIZE_WARN_BYTES) {
15991
16904
  logWarn("[cloud-sync] push snapshot is large", {
@@ -16001,12 +16914,14 @@ async function pushCloudSync() {
16001
16914
  if (process.env.NODE_ENV !== "production") {
16002
16915
  logInfo("[cloud-sync] push snapshot summary", summarizeSnapshot(localSnapshot));
16003
16916
  }
16004
- const remote = await pushRemoteSnapshot(eligibility.licenseKey, localSnapshot, {
16917
+ const remote = await pushRemoteSnapshot(eligibility.licenseKey, encryptedSnapshot, {
16005
16918
  serializedSnapshot
16006
16919
  });
16920
+ const remoteKind = getSnapshotKind(remote.snapshot);
16007
16921
  await writeState({
16008
16922
  lastPushAt: (/* @__PURE__ */ new Date()).toISOString(),
16009
16923
  remoteUpdatedAt: remote.snapshot.updatedAt,
16924
+ remoteKind,
16010
16925
  lastError: void 0
16011
16926
  });
16012
16927
  if (remote.status === "stale") {
@@ -16018,6 +16933,7 @@ async function pushCloudSync() {
16018
16933
  remoteUpdatedAt: remote.snapshot.updatedAt
16019
16934
  };
16020
16935
  }
16936
+ await rememberPassphraseIfRequested(eligibility.licenseKey, options, passphrase);
16021
16937
  return {
16022
16938
  mode: "push",
16023
16939
  status: "applied",
@@ -16032,7 +16948,7 @@ async function pushCloudSync() {
16032
16948
  return result;
16033
16949
  }
16034
16950
  }
16035
- async function pullCloudSync() {
16951
+ async function pullCloudSync(options = {}) {
16036
16952
  const eligibility = await resolveEligibility();
16037
16953
  if (!eligibility.canSync || !eligibility.licenseKey) {
16038
16954
  return {
@@ -16048,6 +16964,7 @@ async function pullCloudSync() {
16048
16964
  await profileManager.initialize();
16049
16965
  const remote = await fetchRemoteSnapshot(eligibility.licenseKey);
16050
16966
  if (!remote) {
16967
+ await writeState({ remoteUpdatedAt: null, remoteKind: "none" });
16051
16968
  return {
16052
16969
  mode: "pull",
16053
16970
  status: "skipped",
@@ -16056,17 +16973,30 @@ async function pullCloudSync() {
16056
16973
  remoteUpdatedAt: null
16057
16974
  };
16058
16975
  }
16059
- await applyRemoteSnapshot(profileManager, remote);
16976
+ const remoteKind = getSnapshotKind(remote);
16977
+ let passphrase = null;
16978
+ let plainSnapshot;
16979
+ if (remote.schemaVersion === CLOUD_SYNC_ENCRYPTED_SCHEMA_VERSION) {
16980
+ passphrase = await resolvePassphrase(eligibility.licenseKey, options);
16981
+ plainSnapshot = await decryptCloudSyncSnapshot(remote, passphrase.value);
16982
+ } else {
16983
+ plainSnapshot = remote;
16984
+ }
16985
+ await applyRemoteSnapshot(profileManager, plainSnapshot);
16060
16986
  await writeState({
16061
16987
  lastPullAt: (/* @__PURE__ */ new Date()).toISOString(),
16062
16988
  remoteUpdatedAt: remote.updatedAt,
16989
+ remoteKind,
16063
16990
  lastError: void 0
16064
16991
  });
16992
+ if (passphrase) {
16993
+ await rememberPassphraseIfRequested(eligibility.licenseKey, options, passphrase);
16994
+ }
16065
16995
  return {
16066
16996
  mode: "pull",
16067
16997
  status: "applied",
16068
- message: "Cloud sync pull completed.",
16069
- localUpdatedAt: remote.updatedAt,
16998
+ message: remoteKind === "legacy-plaintext" ? "Legacy cloud sync pull completed. Push again to encrypt the remote snapshot." : "Cloud sync pull completed.",
16999
+ localUpdatedAt: plainSnapshot.updatedAt,
16070
17000
  remoteUpdatedAt: remote.updatedAt
16071
17001
  };
16072
17002
  } catch (error) {
@@ -16078,6 +17008,20 @@ async function pullCloudSync() {
16078
17008
  }
16079
17009
 
16080
17010
  // src/commands/sync.ts
17011
+ async function readPassphraseFromStdin() {
17012
+ let value = "";
17013
+ for await (const chunk of process.stdin) {
17014
+ value += String(chunk);
17015
+ }
17016
+ return value.replace(/\r?\n$/, "");
17017
+ }
17018
+ async function resolvePassphraseOptions(flags) {
17019
+ if (hasFlag(flags, "--passphrase-stdin")) {
17020
+ return { passphrase: await readPassphraseFromStdin() };
17021
+ }
17022
+ const fromEnv = process.env.CODEXUSE_SYNC_PASSPHRASE;
17023
+ return { passphrase: typeof fromEnv === "string" && fromEnv.length > 0 ? fromEnv : null };
17024
+ }
16081
17025
  function printSyncResult(result) {
16082
17026
  console.log(result.message);
16083
17027
  if (result.localUpdatedAt) {
@@ -16109,6 +17053,8 @@ async function handleSync(args, version) {
16109
17053
  if (status.remoteUpdatedAt) {
16110
17054
  console.log(`Remote snapshot: ${status.remoteUpdatedAt}`);
16111
17055
  }
17056
+ console.log(`Remote kind: ${status.remoteKind}`);
17057
+ console.log(`Remembered passphrase: ${status.hasRememberedPassphrase ? "yes" : "no"}`);
16112
17058
  if (status.lastPushAt) {
16113
17059
  console.log(`Last push: ${status.lastPushAt}`);
16114
17060
  }
@@ -16121,7 +17067,7 @@ async function handleSync(args, version) {
16121
17067
  return;
16122
17068
  }
16123
17069
  case "pull": {
16124
- const result = await pullCloudSync();
17070
+ const result = await pullCloudSync(await resolvePassphraseOptions(flags));
16125
17071
  printSyncResult(result);
16126
17072
  if (result.status === "error") {
16127
17073
  process.exitCode = 1;
@@ -16129,7 +17075,7 @@ async function handleSync(args, version) {
16129
17075
  return;
16130
17076
  }
16131
17077
  case "push": {
16132
- const result = await pushCloudSync();
17078
+ const result = await pushCloudSync(await resolvePassphraseOptions(flags));
16133
17079
  printSyncResult(result);
16134
17080
  if (result.status === "error") {
16135
17081
  process.exitCode = 1;
@@ -16157,7 +17103,7 @@ async function ensureCliStorageReady() {
16157
17103
  }
16158
17104
 
16159
17105
  // src/app/main.ts
16160
- var VERSION = true ? "5.0.5" : "0.0.0";
17106
+ var VERSION = true ? "5.0.7" : "0.0.0";
16161
17107
  async function runCli() {
16162
17108
  const args = process.argv.slice(2);
16163
17109
  if (args.length === 0) {