codexuse-cli 3.9.2 → 3.9.8

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
@@ -2094,6 +2094,117 @@ function normalizeCommitMessagePrompt(value) {
2094
2094
  return normalized.length > 0 ? normalized : null;
2095
2095
  }
2096
2096
 
2097
+ // ../../packages/contracts/src/settings/auto-roll.ts
2098
+ var DEFAULT_AUTO_ROLL_ENABLED = false;
2099
+ var DEFAULT_AUTO_ROLL_REARM_REMAINING_THRESHOLD = 15;
2100
+ var DEFAULT_AUTO_ROLL_SWITCH_REMAINING_THRESHOLD = 5;
2101
+ var DEFAULT_RESTART_OFFICIAL_CODEX_ON_AUTO_ROLL = false;
2102
+ var DEFAULT_LAUNCH_OFFICIAL_CODEX_WHEN_CLOSED_ON_AUTO_ROLL = false;
2103
+ var DEFAULT_AUTO_ROLL_PRIORITY_ORDER = [];
2104
+ var DEFAULT_LOW_REMAINING_NOTIFICATION_ENABLED = false;
2105
+ var DEFAULT_LOW_REMAINING_NOTIFICATION_THRESHOLD = 1;
2106
+ var AUTO_ROLL_SWITCH_REMAINING_MIN = 0;
2107
+ var AUTO_ROLL_SWITCH_REMAINING_MAX = 50;
2108
+ var AUTO_ROLL_REARM_REMAINING_MAX = 100;
2109
+ var LOW_REMAINING_NOTIFICATION_MIN = 1;
2110
+ var LOW_REMAINING_NOTIFICATION_MAX = 50;
2111
+ function clampNumber(value, min, max) {
2112
+ return Math.min(max, Math.max(min, value));
2113
+ }
2114
+ function resolveFiniteNumber(value, fallback) {
2115
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
2116
+ }
2117
+ function legacyUsedThresholdToRemaining(value, fallbackUsed) {
2118
+ const used = clampNumber(resolveFiniteNumber(value, fallbackUsed), 0, 100);
2119
+ return 100 - used;
2120
+ }
2121
+ function sanitizeAutoRollSwitchRemainingThreshold(value) {
2122
+ const numeric = resolveFiniteNumber(
2123
+ value,
2124
+ DEFAULT_AUTO_ROLL_SWITCH_REMAINING_THRESHOLD
2125
+ );
2126
+ return clampNumber(
2127
+ numeric,
2128
+ AUTO_ROLL_SWITCH_REMAINING_MIN,
2129
+ AUTO_ROLL_SWITCH_REMAINING_MAX
2130
+ );
2131
+ }
2132
+ function sanitizeAutoRollRearmRemainingThreshold(value, switchRemainingThreshold) {
2133
+ const sanitizedSwitch = sanitizeAutoRollSwitchRemainingThreshold(
2134
+ switchRemainingThreshold
2135
+ );
2136
+ const numeric = resolveFiniteNumber(
2137
+ value,
2138
+ DEFAULT_AUTO_ROLL_REARM_REMAINING_THRESHOLD
2139
+ );
2140
+ return clampNumber(
2141
+ numeric,
2142
+ sanitizedSwitch + 1,
2143
+ AUTO_ROLL_REARM_REMAINING_MAX
2144
+ );
2145
+ }
2146
+ function sanitizeAutoRollThresholds(rearmRemainingThreshold, switchRemainingThreshold) {
2147
+ const sanitizedSwitch = sanitizeAutoRollSwitchRemainingThreshold(
2148
+ switchRemainingThreshold
2149
+ );
2150
+ const sanitizedRearm = sanitizeAutoRollRearmRemainingThreshold(
2151
+ rearmRemainingThreshold,
2152
+ sanitizedSwitch
2153
+ );
2154
+ return {
2155
+ rearmRemainingThreshold: sanitizedRearm,
2156
+ switchRemainingThreshold: sanitizedSwitch
2157
+ };
2158
+ }
2159
+ function sanitizeAutoRollPriorityOrder(value) {
2160
+ if (!Array.isArray(value)) {
2161
+ return [...DEFAULT_AUTO_ROLL_PRIORITY_ORDER];
2162
+ }
2163
+ const seen = /* @__PURE__ */ new Set();
2164
+ const normalized = [];
2165
+ for (const item of value) {
2166
+ if (typeof item !== "string") {
2167
+ continue;
2168
+ }
2169
+ const trimmed = item.trim();
2170
+ if (!trimmed || seen.has(trimmed)) {
2171
+ continue;
2172
+ }
2173
+ seen.add(trimmed);
2174
+ normalized.push(trimmed);
2175
+ }
2176
+ return normalized;
2177
+ }
2178
+ function sanitizeLowRemainingNotificationThreshold(value) {
2179
+ const numeric = resolveFiniteNumber(value, DEFAULT_LOW_REMAINING_NOTIFICATION_THRESHOLD);
2180
+ return clampNumber(
2181
+ numeric,
2182
+ LOW_REMAINING_NOTIFICATION_MIN,
2183
+ LOW_REMAINING_NOTIFICATION_MAX
2184
+ );
2185
+ }
2186
+ function normalizeAutoRollSettings(raw) {
2187
+ const enabled = typeof raw?.enabled === "boolean" ? raw.enabled : DEFAULT_AUTO_ROLL_ENABLED;
2188
+ const rawSwitchRemaining = typeof raw?.switchRemainingThreshold === "number" ? raw.switchRemainingThreshold : legacyUsedThresholdToRemaining(raw?.switchThreshold, 95);
2189
+ const rawRearmRemaining = typeof raw?.rearmRemainingThreshold === "number" ? raw.rearmRemainingThreshold : legacyUsedThresholdToRemaining(raw?.warningThreshold, 85);
2190
+ const {
2191
+ rearmRemainingThreshold: normalizedRearm,
2192
+ switchRemainingThreshold: normalizedSwitch
2193
+ } = sanitizeAutoRollThresholds(rawRearmRemaining, rawSwitchRemaining);
2194
+ return {
2195
+ enabled,
2196
+ rearmRemainingThreshold: normalizedRearm,
2197
+ switchRemainingThreshold: normalizedSwitch,
2198
+ restartOfficialCodexOnAutoRoll: raw?.restartOfficialCodexOnAutoRoll === true ? true : DEFAULT_RESTART_OFFICIAL_CODEX_ON_AUTO_ROLL,
2199
+ launchOfficialCodexWhenClosedOnAutoRoll: raw?.launchOfficialCodexWhenClosedOnAutoRoll === true ? true : DEFAULT_LAUNCH_OFFICIAL_CODEX_WHEN_CLOSED_ON_AUTO_ROLL,
2200
+ priorityOrder: sanitizeAutoRollPriorityOrder(raw?.priorityOrder),
2201
+ lowRemainingNotificationEnabled: raw?.lowRemainingNotificationEnabled === true ? true : DEFAULT_LOW_REMAINING_NOTIFICATION_ENABLED,
2202
+ lowRemainingNotificationThreshold: sanitizeLowRemainingNotificationThreshold(
2203
+ typeof raw?.lowRemainingNotificationThreshold === "number" ? raw.lowRemainingNotificationThreshold : Number.NaN
2204
+ )
2205
+ };
2206
+ }
2207
+
2097
2208
  // ../../packages/runtime-app-state/src/storage/documents.ts
2098
2209
  var import_node_fs = require("fs");
2099
2210
  var import_node_path = __toESM(require("path"), 1);
@@ -2351,9 +2462,25 @@ function createDefaultAppState() {
2351
2462
  schemaVersion: 1,
2352
2463
  autoRoll: {
2353
2464
  enabled: false,
2354
- warningThreshold: 85,
2355
- switchThreshold: 95,
2356
- restartOfficialCodexOnAutoRoll: false
2465
+ rearmRemainingThreshold: 15,
2466
+ switchRemainingThreshold: 5,
2467
+ restartOfficialCodexOnAutoRoll: false,
2468
+ launchOfficialCodexWhenClosedOnAutoRoll: false,
2469
+ priorityOrder: [],
2470
+ lowRemainingNotificationEnabled: false,
2471
+ lowRemainingNotificationThreshold: 1
2472
+ },
2473
+ officialCodex: {
2474
+ lastProfileSwitchAt: null,
2475
+ lastProfileSwitchProfileKey: null,
2476
+ lastVerifiedLaunchAt: null,
2477
+ lastVerifiedLaunchProfileKey: null,
2478
+ lastObservedPid: null,
2479
+ lastRestartStatus: null,
2480
+ lastRestartReason: null,
2481
+ activity: [],
2482
+ instancesByProfileName: {},
2483
+ pendingRestartDebt: null
2357
2484
  },
2358
2485
  app: {
2359
2486
  lastAppVersion: null,
@@ -2454,34 +2581,114 @@ function asString(value) {
2454
2581
  const trimmed = value.trim();
2455
2582
  return trimmed.length > 0 ? trimmed : null;
2456
2583
  }
2584
+ function asNumberOrNull(value) {
2585
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
2586
+ }
2587
+ function asBooleanOrNull(value) {
2588
+ return typeof value === "boolean" ? value : null;
2589
+ }
2457
2590
  function normalizeAppState(raw) {
2458
2591
  const defaults = createDefaultAppState();
2459
2592
  if (!isRecord2(raw)) {
2460
2593
  return defaults;
2461
2594
  }
2595
+ const rawAutoRoll = isRecord2(raw.autoRoll) ? raw.autoRoll : void 0;
2462
2596
  const next = deepMerge(defaults, raw);
2463
2597
  const merged = clone2(next);
2464
2598
  merged.schemaVersion = 1;
2465
- if (typeof merged.autoRoll.enabled !== "boolean") {
2466
- merged.autoRoll.enabled = false;
2467
- }
2468
- if (!Number.isFinite(merged.autoRoll.warningThreshold)) {
2469
- merged.autoRoll.warningThreshold = 85;
2470
- }
2471
- if (!Number.isFinite(merged.autoRoll.switchThreshold)) {
2472
- merged.autoRoll.switchThreshold = 95;
2473
- }
2474
- if (merged.autoRoll.warningThreshold < 50 || merged.autoRoll.warningThreshold > 99) {
2475
- merged.autoRoll.warningThreshold = 85;
2599
+ merged.autoRoll = normalizeAutoRollSettings(rawAutoRoll);
2600
+ if (!isRecord2(merged.officialCodex)) {
2601
+ merged.officialCodex = clone2(defaults.officialCodex);
2476
2602
  }
2477
- if (merged.autoRoll.switchThreshold <= merged.autoRoll.warningThreshold || merged.autoRoll.switchThreshold > 100) {
2478
- merged.autoRoll.switchThreshold = Math.min(
2479
- 100,
2480
- Math.max(merged.autoRoll.warningThreshold + 1, 95)
2481
- );
2482
- }
2483
- if (typeof merged.autoRoll.restartOfficialCodexOnAutoRoll !== "boolean") {
2484
- merged.autoRoll.restartOfficialCodexOnAutoRoll = false;
2603
+ merged.officialCodex.lastProfileSwitchAt = asNumberOrNull(
2604
+ merged.officialCodex.lastProfileSwitchAt
2605
+ );
2606
+ merged.officialCodex.lastProfileSwitchProfileKey = asString(
2607
+ merged.officialCodex.lastProfileSwitchProfileKey
2608
+ );
2609
+ merged.officialCodex.lastVerifiedLaunchAt = asNumberOrNull(
2610
+ merged.officialCodex.lastVerifiedLaunchAt
2611
+ );
2612
+ merged.officialCodex.lastVerifiedLaunchProfileKey = asString(
2613
+ merged.officialCodex.lastVerifiedLaunchProfileKey
2614
+ );
2615
+ merged.officialCodex.lastObservedPid = asNumberOrNull(
2616
+ merged.officialCodex.lastObservedPid
2617
+ );
2618
+ merged.officialCodex.lastRestartStatus = asString(
2619
+ merged.officialCodex.lastRestartStatus
2620
+ );
2621
+ merged.officialCodex.lastRestartReason = asString(
2622
+ merged.officialCodex.lastRestartReason
2623
+ );
2624
+ merged.officialCodex.activity = Array.isArray(merged.officialCodex.activity) ? merged.officialCodex.activity.filter((entry) => isRecord2(entry)).map((entry) => ({
2625
+ id: asString(entry.id) ?? "",
2626
+ at: asString(entry.at) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2627
+ kind: entry.kind === "auto-roll-eval" || entry.kind === "profile-switch" || entry.kind === "auth-verified" || entry.kind === "official-codex-restart" || entry.kind === "low-remaining-alert" ? entry.kind : "auto-roll-eval",
2628
+ status: asString(entry.status) ?? "unknown",
2629
+ reason: asString(entry.reason),
2630
+ decisionId: asString(entry.decisionId),
2631
+ profileName: asString(entry.profileName),
2632
+ sourceProfileName: asString(entry.sourceProfileName),
2633
+ targetProfileName: asString(entry.targetProfileName),
2634
+ remainingPercent: asNumberOrNull(entry.remainingPercent),
2635
+ threshold: asNumberOrNull(entry.threshold),
2636
+ snapshotAgeMs: asNumberOrNull(entry.snapshotAgeMs),
2637
+ snapshotSource: asString(entry.snapshotSource),
2638
+ phase: asString(entry.phase),
2639
+ pid: asNumberOrNull(entry.pid),
2640
+ profileKeyHash: asString(entry.profileKeyHash),
2641
+ switchVerified: asBooleanOrNull(entry.switchVerified),
2642
+ restartRequested: asBooleanOrNull(entry.restartRequested),
2643
+ restartResult: asString(entry.restartResult),
2644
+ observedProfileName: asString(entry.observedProfileName),
2645
+ observedProfileKeyHash: asString(entry.observedProfileKeyHash),
2646
+ observedProfileMatchSource: asString(entry.observedProfileMatchSource)
2647
+ })).filter((entry) => entry.id && entry.at).slice(-50) : [];
2648
+ if (!isRecord2(merged.officialCodex.instancesByProfileName)) {
2649
+ merged.officialCodex.instancesByProfileName = {};
2650
+ } else {
2651
+ const nextInstances = {};
2652
+ for (const [key, value] of Object.entries(
2653
+ merged.officialCodex.instancesByProfileName
2654
+ )) {
2655
+ if (!isRecord2(value)) {
2656
+ continue;
2657
+ }
2658
+ const profileName = asString(value.profileName) ?? asString(key);
2659
+ if (!profileName) {
2660
+ continue;
2661
+ }
2662
+ nextInstances[profileName] = {
2663
+ profileName,
2664
+ profileKey: asString(value.profileKey),
2665
+ profileHome: asString(value.profileHome),
2666
+ appPath: asString(value.appPath),
2667
+ bundleId: asString(value.bundleId),
2668
+ pid: asNumberOrNull(value.pid),
2669
+ appServerPid: asNumberOrNull(value.appServerPid),
2670
+ launchedAt: asNumberOrNull(value.launchedAt),
2671
+ lastVerifiedAt: asNumberOrNull(value.lastVerifiedAt),
2672
+ lastStatus: asString(value.lastStatus),
2673
+ lastError: asString(value.lastError)
2674
+ };
2675
+ }
2676
+ merged.officialCodex.instancesByProfileName = nextInstances;
2677
+ }
2678
+ if (isRecord2(merged.officialCodex.pendingRestartDebt)) {
2679
+ const debt = merged.officialCodex.pendingRestartDebt;
2680
+ const targetProfileName = asString(debt.targetProfileName);
2681
+ merged.officialCodex.pendingRestartDebt = targetProfileName ? {
2682
+ targetProfileName,
2683
+ targetProfileKey: asString(debt.targetProfileKey),
2684
+ sourceProfileName: asString(debt.sourceProfileName),
2685
+ sourceProfileKey: asString(debt.sourceProfileKey),
2686
+ decisionId: asString(debt.decisionId),
2687
+ attempts: asNumberOrNull(debt.attempts) ?? 0,
2688
+ lastReason: asString(debt.lastReason)
2689
+ } : null;
2690
+ } else {
2691
+ merged.officialCodex.pendingRestartDebt = null;
2485
2692
  }
2486
2693
  merged.app.lastAppVersion = asString(merged.app.lastAppVersion);
2487
2694
  merged.app.pendingUpdateVersion = asString(merged.app.pendingUpdateVersion);
@@ -2977,59 +3184,6 @@ async function writeAppSettings(settings, onUpdated) {
2977
3184
  // ../../packages/runtime-profiles/src/license/service.ts
2978
3185
  var import_node_crypto2 = __toESM(require("crypto"), 1);
2979
3186
 
2980
- // ../../packages/contracts/src/settings/auto-roll.ts
2981
- var DEFAULT_AUTO_ROLL_ENABLED = false;
2982
- var DEFAULT_AUTO_ROLL_WARNING_THRESHOLD = 85;
2983
- var DEFAULT_AUTO_ROLL_SWITCH_THRESHOLD = 95;
2984
- var DEFAULT_RESTART_OFFICIAL_CODEX_ON_AUTO_ROLL = false;
2985
- var AUTO_ROLL_WARNING_MIN = 50;
2986
- var AUTO_ROLL_WARNING_MAX = 99;
2987
- var AUTO_ROLL_SWITCH_MAX = 100;
2988
- function clampNumber(value, min, max) {
2989
- return Math.min(max, Math.max(min, value));
2990
- }
2991
- function resolveFiniteNumber(value, fallback) {
2992
- return Number.isFinite(value) ? value : fallback;
2993
- }
2994
- function sanitizeAutoRollWarningThreshold(value) {
2995
- const numeric = resolveFiniteNumber(value, DEFAULT_AUTO_ROLL_WARNING_THRESHOLD);
2996
- return clampNumber(numeric, AUTO_ROLL_WARNING_MIN, AUTO_ROLL_WARNING_MAX);
2997
- }
2998
- function sanitizeAutoRollSwitchThreshold(value, warningThreshold) {
2999
- const sanitizedWarning = sanitizeAutoRollWarningThreshold(warningThreshold);
3000
- const numeric = resolveFiniteNumber(value, DEFAULT_AUTO_ROLL_SWITCH_THRESHOLD);
3001
- return clampNumber(numeric, sanitizedWarning + 1, AUTO_ROLL_SWITCH_MAX);
3002
- }
3003
- function sanitizeAutoRollThresholds(warningThreshold, switchThreshold) {
3004
- const sanitizedWarning = sanitizeAutoRollWarningThreshold(warningThreshold);
3005
- const sanitizedSwitch = sanitizeAutoRollSwitchThreshold(switchThreshold, sanitizedWarning);
3006
- return {
3007
- warningThreshold: sanitizedWarning,
3008
- switchThreshold: sanitizedSwitch
3009
- };
3010
- }
3011
- function normalizeAutoRollSettings(raw) {
3012
- const enabled = typeof raw?.enabled === "boolean" ? raw.enabled : DEFAULT_AUTO_ROLL_ENABLED;
3013
- const rawWarning = resolveFiniteNumber(
3014
- typeof raw?.warningThreshold === "number" ? raw.warningThreshold : Number.NaN,
3015
- DEFAULT_AUTO_ROLL_WARNING_THRESHOLD
3016
- );
3017
- const rawSwitch = resolveFiniteNumber(
3018
- typeof raw?.switchThreshold === "number" ? raw.switchThreshold : Number.NaN,
3019
- DEFAULT_AUTO_ROLL_SWITCH_THRESHOLD
3020
- );
3021
- const {
3022
- warningThreshold: normalizedWarning,
3023
- switchThreshold: normalizedSwitch
3024
- } = sanitizeAutoRollThresholds(rawWarning, rawSwitch);
3025
- return {
3026
- enabled,
3027
- warningThreshold: normalizedWarning,
3028
- switchThreshold: normalizedSwitch,
3029
- restartOfficialCodexOnAutoRoll: raw?.restartOfficialCodexOnAutoRoll === true ? true : DEFAULT_RESTART_OFFICIAL_CODEX_ON_AUTO_ROLL
3030
- };
3031
- }
3032
-
3033
3187
  // ../../packages/runtime-codex/src/codex/settings.ts
3034
3188
  function asString2(value) {
3035
3189
  if (typeof value !== "string") {
@@ -3071,17 +3225,6 @@ function parseStoredLicense(raw) {
3071
3225
  );
3072
3226
  return hasValue ? license : null;
3073
3227
  }
3074
- async function getLastProfileName() {
3075
- const state = await getAppState();
3076
- return asString2(state.app.lastProfileName);
3077
- }
3078
- async function persistLastProfileName(profileName) {
3079
- await patchAppState({
3080
- app: {
3081
- lastProfileName: asString2(profileName)
3082
- }
3083
- });
3084
- }
3085
3228
  async function getStoredLicense() {
3086
3229
  const state = await getAppState();
3087
3230
  return parseStoredLicense(state.license);
@@ -3150,9 +3293,13 @@ async function writeCodexSettingsJsonRaw(payload) {
3150
3293
  },
3151
3294
  autoRoll: autoRoll ? {
3152
3295
  enabled: autoRoll.enabled,
3153
- warningThreshold: autoRoll.warningThreshold,
3154
- switchThreshold: autoRoll.switchThreshold,
3155
- restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll
3296
+ rearmRemainingThreshold: autoRoll.rearmRemainingThreshold,
3297
+ switchRemainingThreshold: autoRoll.switchRemainingThreshold,
3298
+ restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll,
3299
+ launchOfficialCodexWhenClosedOnAutoRoll: autoRoll.launchOfficialCodexWhenClosedOnAutoRoll,
3300
+ priorityOrder: autoRoll.priorityOrder,
3301
+ lowRemainingNotificationEnabled: autoRoll.lowRemainingNotificationEnabled,
3302
+ lowRemainingNotificationThreshold: autoRoll.lowRemainingNotificationThreshold
3156
3303
  } : void 0,
3157
3304
  license: license ? {
3158
3305
  licenseKey: license.licenseKey ?? null,
@@ -3882,10 +4029,10 @@ function logError(...args) {
3882
4029
  console.error(...args);
3883
4030
  }
3884
4031
  function logInfo(...args) {
3885
- if (isTestEnv && !isMocked(console.warn)) {
4032
+ if (isTestEnv && !isMocked(console.info)) {
3886
4033
  return;
3887
4034
  }
3888
- console.warn(...args);
4035
+ console.info(...args);
3889
4036
  }
3890
4037
 
3891
4038
  // ../../packages/runtime-codex/src/codex/rpc.ts
@@ -4245,21 +4392,6 @@ var ProfileManager = class {
4245
4392
  error && typeof error === "object" && "code" in error && error.code === "ENOENT"
4246
4393
  );
4247
4394
  }
4248
- async readPreferredProfileName() {
4249
- try {
4250
- return await getLastProfileName();
4251
- } catch (error) {
4252
- logWarn("Failed to read preferred profile name:", error);
4253
- return null;
4254
- }
4255
- }
4256
- async persistPreferredProfileName(name) {
4257
- try {
4258
- await persistLastProfileName(name);
4259
- } catch (error) {
4260
- logWarn("Failed to persist preferred profile name:", error);
4261
- }
4262
- }
4263
4395
  toStateRecord(record) {
4264
4396
  return {
4265
4397
  name: record.name,
@@ -4379,47 +4511,36 @@ var ProfileManager = class {
4379
4511
  return null;
4380
4512
  }
4381
4513
  }
4382
- async captureActiveAuthSnapshot() {
4383
- await this.initialize();
4514
+ emptyAuthSnapshot(fingerprint = null) {
4515
+ return {
4516
+ fingerprint,
4517
+ email: null,
4518
+ accountId: null,
4519
+ userId: null,
4520
+ chatgptUserId: null,
4521
+ chatgptAccountUserId: null,
4522
+ workspaceId: null
4523
+ };
4524
+ }
4525
+ async captureAuthSnapshotAtPath(authPath) {
4384
4526
  let raw;
4385
4527
  try {
4386
- raw = await import_fs.promises.readFile(this.activeAuth, "utf8");
4528
+ raw = await import_fs.promises.readFile(authPath, "utf8");
4387
4529
  } catch (error) {
4388
4530
  if (this.isNotFoundError(error)) {
4389
- return {
4390
- fingerprint: null,
4391
- email: null,
4392
- accountId: null,
4393
- userId: null,
4394
- chatgptUserId: null,
4395
- workspaceId: null
4396
- };
4531
+ return this.emptyAuthSnapshot();
4397
4532
  }
4398
4533
  const message = error instanceof Error ? error.message : "unknown error";
4399
- const signature = typeof message === "string" ? `${message}:${this.activeAuth}` : this.activeAuth;
4534
+ const signature = typeof message === "string" ? `${message}:${authPath}` : authPath;
4400
4535
  if (this.lastActiveAuthErrorSignature !== signature) {
4401
- logWarn("Failed to snapshot active auth file:", error);
4536
+ logWarn("Failed to snapshot auth file:", error);
4402
4537
  this.lastActiveAuthErrorSignature = signature;
4403
4538
  }
4404
- return {
4405
- fingerprint: null,
4406
- email: null,
4407
- accountId: null,
4408
- userId: null,
4409
- chatgptUserId: null,
4410
- workspaceId: null
4411
- };
4539
+ return this.emptyAuthSnapshot();
4412
4540
  }
4413
4541
  const trimmed = raw.trim();
4414
4542
  if (!trimmed) {
4415
- return {
4416
- fingerprint: null,
4417
- email: null,
4418
- accountId: null,
4419
- userId: null,
4420
- chatgptUserId: null,
4421
- workspaceId: null
4422
- };
4543
+ return this.emptyAuthSnapshot();
4423
4544
  }
4424
4545
  const fingerprint = (0, import_node_crypto3.createHash)("sha256").update(trimmed).digest("hex");
4425
4546
  try {
@@ -4433,18 +4554,26 @@ var ProfileManager = class {
4433
4554
  accountId: this.getAccountIdFromData(normalized) ?? null,
4434
4555
  userId: metadata?.userId ?? null,
4435
4556
  chatgptUserId: metadata?.chatgptUserId ?? null,
4557
+ chatgptAccountUserId: metadata?.chatgptAccountUserId ?? null,
4436
4558
  workspaceId: workspace.id ?? null
4437
4559
  };
4438
4560
  } catch {
4439
- return {
4440
- fingerprint,
4441
- email: null,
4442
- accountId: null,
4443
- userId: null,
4444
- chatgptUserId: null,
4445
- workspaceId: null
4446
- };
4561
+ return this.emptyAuthSnapshot(fingerprint);
4562
+ }
4563
+ }
4564
+ async captureAuthSnapshot(authPath) {
4565
+ const targetPath = authPath ?? this.activeAuth;
4566
+ if (targetPath === this.activeAuth) {
4567
+ return this.enqueueAuthSwap(async () => {
4568
+ await this.initialize();
4569
+ return this.captureAuthSnapshotAtPath(targetPath);
4570
+ });
4447
4571
  }
4572
+ await this.initialize();
4573
+ return this.captureAuthSnapshotAtPath(targetPath);
4574
+ }
4575
+ async captureActiveAuthSnapshot() {
4576
+ return this.captureAuthSnapshot(this.activeAuth);
4448
4577
  }
4449
4578
  /**
4450
4579
  * Decode a JWT payload into an object without validating the signature.
@@ -4842,6 +4971,130 @@ var ProfileManager = class {
4842
4971
  metadata: record.metadata
4843
4972
  }) ?? resolveFallbackAccountKey(record.name);
4844
4973
  }
4974
+ describeActiveAuth(auth) {
4975
+ const normalized = this.normalizeProfileData(auth);
4976
+ if (typeof normalized.auth_method === "string") {
4977
+ normalized.auth_method = normalized.auth_method.trim().toLowerCase();
4978
+ }
4979
+ normalized.auth_method = normalized.auth_method ?? "codex-cli";
4980
+ const metadata = this.extractProfileMetadata(normalized);
4981
+ const workspace = this.resolveWorkspaceIdentity(normalized, metadata);
4982
+ const workspaceId = workspace.id || DEFAULT_WORKSPACE_ID;
4983
+ const workspaceName = workspace.name ?? null;
4984
+ normalized.workspace_id = normalized.workspace_id ?? workspaceId;
4985
+ if (workspaceName) {
4986
+ normalized.workspace_name = normalized.workspace_name ?? workspaceName;
4987
+ }
4988
+ const email = this.resolveProfileEmail(normalized, metadata) ?? null;
4989
+ if (email) {
4990
+ normalized.email = email;
4991
+ }
4992
+ const accountId = this.getAccountIdFromData(normalized) ?? null;
4993
+ const identityKey = this.resolveProfileIdentityFromData(null, normalized, metadata);
4994
+ const hasTokenPayload = Boolean(
4995
+ normalized.id_token || normalized.access_token || normalized.refresh_token || accountId || email
4996
+ );
4997
+ return {
4998
+ normalized,
4999
+ metadata,
5000
+ workspaceId,
5001
+ workspaceName,
5002
+ email,
5003
+ accountId,
5004
+ identityKey,
5005
+ hasTokenPayload
5006
+ };
5007
+ }
5008
+ async findProfileForActiveAuth(identityKey, workspaceId) {
5009
+ return this.getProfileRowByIdentityAndWorkspace(identityKey, workspaceId, {
5010
+ preferAuthMethod: "codex-cli"
5011
+ });
5012
+ }
5013
+ async syncMatchedProfileFromActiveAuth(record, description) {
5014
+ if (!this.hasTokenChanges(record.data, description.normalized)) {
5015
+ return;
5016
+ }
5017
+ const { profile: merged, metadata } = this.mergeProfileRecords(record.data, description.normalized);
5018
+ delete merged.tokenAlert;
5019
+ await this.persistProfileRecord(record.name, merged, metadata);
5020
+ }
5021
+ async assertActiveAuthMatchesRecord(record, message) {
5022
+ const activeAuth = await this.readActiveAuthFile();
5023
+ if (!activeAuth) {
5024
+ throw new Error("Active Codex auth is missing. Refresh profiles and try again.");
5025
+ }
5026
+ const activeDescription = this.describeActiveAuth(activeAuth);
5027
+ const targetIdentity = this.resolveProfileIdentityFromRecord(record);
5028
+ const targetWorkspaceId = this.normalizeWorkspaceId(record.workspaceId ?? record.data.workspace_id);
5029
+ if (activeDescription.identityKey !== targetIdentity || this.normalizeWorkspaceId(activeDescription.workspaceId) !== targetWorkspaceId) {
5030
+ throw new Error(message);
5031
+ }
5032
+ }
5033
+ async writeActiveAuthForRecord(record) {
5034
+ const profileData = record.data;
5035
+ const targetIdentity = this.resolveProfileIdentityFromRecord(record);
5036
+ const targetWorkspaceId = this.normalizeWorkspaceId(record.workspaceId ?? profileData.workspace_id);
5037
+ const codexFormat = this.convertToCodexFormat(profileData);
5038
+ await this.writeAtomic(this.activeAuth, JSON.stringify(codexFormat, null, 2));
5039
+ const written = await this.readActiveAuthFile();
5040
+ if (!written) {
5041
+ throw new Error("Codex auth file was not written.");
5042
+ }
5043
+ const writtenDescription = this.describeActiveAuth(written);
5044
+ if (writtenDescription.identityKey !== targetIdentity) {
5045
+ throw new Error("Codex auth verification failed after profile switch.");
5046
+ }
5047
+ if (this.normalizeWorkspaceId(writtenDescription.workspaceId) !== targetWorkspaceId) {
5048
+ throw new Error("Codex auth workspace verification failed after profile switch.");
5049
+ }
5050
+ }
5051
+ async syncActiveAuthFromProfileIfCurrent(name) {
5052
+ const profileName = this.normalizeProfileName(name);
5053
+ const record = await this.getProfileRowByName(profileName);
5054
+ if (!record) {
5055
+ return;
5056
+ }
5057
+ const activeAuth = await this.readActiveAuthFile();
5058
+ if (!activeAuth) {
5059
+ return;
5060
+ }
5061
+ const activeDescription = this.describeActiveAuth(activeAuth);
5062
+ const targetIdentity = this.resolveProfileIdentityFromRecord(record);
5063
+ const targetWorkspaceId = this.normalizeWorkspaceId(record.workspaceId ?? record.data.workspace_id);
5064
+ if (activeDescription.identityKey !== targetIdentity || this.normalizeWorkspaceId(activeDescription.workspaceId) !== targetWorkspaceId) {
5065
+ return;
5066
+ }
5067
+ await this.writeActiveAuthForRecord(record);
5068
+ }
5069
+ async removeActiveAuthFiles() {
5070
+ await import_fs.promises.rm(this.activeAuth, { force: true });
5071
+ await import_fs.promises.rm(this.activeAuthBackup, { force: true });
5072
+ await import_fs.promises.rm(`${this.activeAuth}.tmp`, { force: true });
5073
+ }
5074
+ resolveAutoImportedProfileName(email, existingRecords) {
5075
+ const source = email ? email.split("@")[0] : "codex-account";
5076
+ const base = source.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "codex-account";
5077
+ const used = new Set(existingRecords.map((record) => record.name));
5078
+ let candidate = this.normalizeProfileName(base);
5079
+ let suffix = 2;
5080
+ while (used.has(candidate)) {
5081
+ candidate = this.normalizeProfileName(`${base}-${suffix}`);
5082
+ suffix += 1;
5083
+ }
5084
+ return candidate;
5085
+ }
5086
+ buildActiveCodexAuthStatus(state, description, options) {
5087
+ return {
5088
+ state,
5089
+ profileName: options?.profileName ?? null,
5090
+ email: description?.email ?? null,
5091
+ accountId: description?.accountId ?? null,
5092
+ workspaceId: description?.workspaceId ?? null,
5093
+ workspaceName: description?.workspaceName ?? null,
5094
+ importedProfileName: options?.importedProfileName ?? null,
5095
+ importBlockedReason: options?.importBlockedReason ?? null
5096
+ };
5097
+ }
4845
5098
  resolveWorkspaceIdentity(data, metadata) {
4846
5099
  const directId = "workspace_id" in data && typeof data.workspace_id === "string" ? data.workspace_id.trim() : void 0;
4847
5100
  const directName = "workspace_name" in data && typeof data.workspace_name === "string" ? data.workspace_name.trim() : void 0;
@@ -4862,22 +5115,6 @@ var ProfileManager = class {
4862
5115
  const record = await this.readProfileRecord(normalized);
4863
5116
  return record ?? void 0;
4864
5117
  }
4865
- async getProfileRowByIdentityKey(identityKey, options) {
4866
- const records = await this.listProfileRecords();
4867
- const matches = records.filter((record) => this.resolveProfileIdentityFromRecord(record) === identityKey);
4868
- if (matches.length === 0) {
4869
- return void 0;
4870
- }
4871
- const mustMatch = typeof options?.authMethod === "string" ? options.authMethod.trim().toLowerCase() : null;
4872
- if (mustMatch) {
4873
- return matches.find((record) => this.resolveAuthMethod(record.data) === mustMatch);
4874
- }
4875
- const prefer = typeof options?.preferAuthMethod === "string" ? options.preferAuthMethod.trim().toLowerCase() : null;
4876
- if (prefer) {
4877
- return matches.find((record) => this.resolveAuthMethod(record.data) === prefer) ?? matches[0];
4878
- }
4879
- return matches[0];
4880
- }
4881
5118
  async getProfileRowByIdentityAndWorkspace(identityKey, workspaceId, options) {
4882
5119
  const workspaceKey = workspaceId && workspaceId.trim().length > 0 ? workspaceId.trim() : DEFAULT_WORKSPACE_ID;
4883
5120
  const records = await this.listProfileRecords();
@@ -4901,6 +5138,9 @@ var ProfileManager = class {
4901
5138
  }
4902
5139
  return matches[0];
4903
5140
  }
5141
+ normalizeWorkspaceId(value) {
5142
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : DEFAULT_WORKSPACE_ID;
5143
+ }
4904
5144
  async persistProfileRecord(name, data, metadata, options) {
4905
5145
  const resolvedName = this.normalizeProfileName(name);
4906
5146
  const existingRecord = await this.readProfileRecord(resolvedName);
@@ -4960,6 +5200,22 @@ var ProfileManager = class {
4960
5200
  });
4961
5201
  return profile;
4962
5202
  }
5203
+ async deleteProfileRecordAndHome(record) {
5204
+ const profileName = this.normalizeProfileName(record.name);
5205
+ if (!record.accountId) {
5206
+ const dashboardState = await this.readProfileDashboardState();
5207
+ delete dashboardState.customGroupsByAccountKey[resolveFallbackAccountKey(profileName)];
5208
+ await this.writeProfileDashboardState(dashboardState);
5209
+ }
5210
+ try {
5211
+ await import_fs.promises.rm(this.getProfileHomePath(profileName), { recursive: true, force: true });
5212
+ } catch (error) {
5213
+ if (!this.isNotFoundError(error)) {
5214
+ logWarn(`Failed to remove profile home for '${profileName}':`, error);
5215
+ }
5216
+ }
5217
+ await this.deleteProfileRecord(profileName);
5218
+ }
4963
5219
  buildProfileFromData(name, data, fallback) {
4964
5220
  const metadata = fallback?.metadata ?? this.extractProfileMetadata(data);
4965
5221
  const workspace = this.resolveWorkspaceIdentity(data, metadata);
@@ -5019,6 +5275,18 @@ var ProfileManager = class {
5019
5275
  if (normalizedLastRefresh) {
5020
5276
  codexAuth.last_refresh = normalizedLastRefresh;
5021
5277
  }
5278
+ const email = typeof data.email === "string" ? data.email.trim() : "";
5279
+ if (email) {
5280
+ codexAuth.email = email;
5281
+ }
5282
+ const workspaceId = typeof data.workspace_id === "string" ? data.workspace_id.trim() : "";
5283
+ if (workspaceId) {
5284
+ codexAuth.workspace_id = workspaceId;
5285
+ }
5286
+ const workspaceName = typeof data.workspace_name === "string" ? data.workspace_name.trim() : "";
5287
+ if (workspaceName) {
5288
+ codexAuth.workspace_name = workspaceName;
5289
+ }
5022
5290
  return codexAuth;
5023
5291
  }
5024
5292
  /**
@@ -5041,8 +5309,17 @@ var ProfileManager = class {
5041
5309
  if (normalizedLastRefresh) {
5042
5310
  profile.last_refresh = normalizedLastRefresh;
5043
5311
  }
5044
- if (data.email) {
5045
- profile.email = data.email;
5312
+ const email = typeof data.email === "string" ? data.email.trim() : "";
5313
+ if (email) {
5314
+ profile.email = email;
5315
+ }
5316
+ const workspaceId = typeof data.workspace_id === "string" ? data.workspace_id.trim() : "";
5317
+ if (workspaceId) {
5318
+ profile.workspace_id = workspaceId;
5319
+ }
5320
+ const workspaceName = typeof data.workspace_name === "string" ? data.workspace_name.trim() : "";
5321
+ if (workspaceName) {
5322
+ profile.workspace_name = workspaceName;
5046
5323
  }
5047
5324
  return profile;
5048
5325
  }
@@ -5169,11 +5446,17 @@ var ProfileManager = class {
5169
5446
  const existingIdentity = this.resolveProfileIdentityFromData(profileName, existing, existingMetadata);
5170
5447
  const nextIdentity = this.resolveProfileIdentityFromData(profileName, normalized, metadata);
5171
5448
  if (existingIdentity && nextIdentity && existingIdentity !== nextIdentity) {
5172
- logWarn(
5449
+ logInfo(
5173
5450
  `Skipped syncing tokens for profile '${profileName}' because Codex auth switched to a different OpenAI identity.`
5174
5451
  );
5175
5452
  return;
5176
5453
  }
5454
+ if (this.normalizeWorkspaceId(existing.workspace_id) !== this.normalizeWorkspaceId(normalized.workspace_id)) {
5455
+ logInfo(
5456
+ `Skipped syncing tokens for profile '${profileName}' because Codex auth switched to a different workspace.`
5457
+ );
5458
+ return;
5459
+ }
5177
5460
  if (!this.hasTokenChanges(existing, normalized)) {
5178
5461
  return;
5179
5462
  }
@@ -5371,94 +5654,80 @@ var ProfileManager = class {
5371
5654
  }
5372
5655
  if (finalAuthContent) {
5373
5656
  await this.syncProfileTokensFromAuthContent(profileName, record.data, finalAuthContent);
5657
+ await this.syncActiveAuthFromProfileIfCurrent(profileName);
5374
5658
  return;
5375
5659
  }
5376
5660
  await this.syncProfileTokensFromActiveAuth(profileName, record.data, authPath);
5661
+ await this.syncActiveAuthFromProfileIfCurrent(profileName);
5377
5662
  })
5378
5663
  );
5379
5664
  }
5380
5665
  /**
5381
5666
  * Create a new profile by running codex login
5382
5667
  */
5383
- async createProfile(name) {
5384
- await this.initialize();
5385
- const profileName = this.normalizeProfileName(name);
5386
- if (await this.getProfileRowByName(profileName)) {
5387
- throw new Error(`Profile '${profileName}' already exists!`);
5388
- }
5389
- let authData;
5668
+ async readAuthFileForImport(authPath, actionLabel) {
5669
+ let authRaw;
5390
5670
  try {
5391
- const authRaw = await import_fs.promises.readFile(this.activeAuth, "utf8");
5392
- authData = JSON.parse(authRaw);
5671
+ authRaw = await import_fs.promises.readFile(authPath, "utf8");
5393
5672
  } catch (error) {
5394
- logError("Error creating profile:", error);
5395
- throw new Error("Failed to create profile. Make sure Codex CLI is installed and you are logged in.");
5396
- }
5397
- const normalizedProfile = this.normalizeProfileData(authData);
5398
- normalizedProfile.created_at = normalizedProfile.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
5399
- if (typeof normalizedProfile.auth_method === "string") {
5400
- normalizedProfile.auth_method = normalizedProfile.auth_method.trim().toLowerCase();
5401
- }
5402
- normalizedProfile.auth_method = normalizedProfile.auth_method ?? "codex-cli";
5403
- const metadata = this.extractProfileMetadata(normalizedProfile);
5404
- const workspace = this.resolveWorkspaceIdentity(normalizedProfile, metadata);
5405
- normalizedProfile.workspace_id = normalizedProfile.workspace_id ?? workspace.id ?? DEFAULT_WORKSPACE_ID;
5406
- if (workspace.name) {
5407
- normalizedProfile.workspace_name = normalizedProfile.workspace_name ?? workspace.name;
5408
- }
5409
- const resolvedEmail = this.resolveProfileEmail(normalizedProfile, metadata);
5410
- if (resolvedEmail) {
5411
- normalizedProfile.email = resolvedEmail;
5673
+ if (this.isNotFoundError(error)) {
5674
+ throw new Error(`Codex CLI did not produce an auth file. Complete the login before ${actionLabel}.`);
5675
+ }
5676
+ logError(`Failed to read Codex auth file during ${actionLabel}:`, error);
5677
+ throw new Error("Failed to read Codex auth file. Complete the login and try again.");
5412
5678
  }
5413
5679
  try {
5414
- await this.persistProfileRecord(profileName, normalizedProfile, metadata);
5415
- const stored = await this.getProfileRowByName(profileName);
5416
- return stored ? this.buildProfileFromRow(stored) : null;
5680
+ return JSON.parse(authRaw);
5417
5681
  } catch (error) {
5418
- logError("Error creating profile:", error);
5419
- throw new Error("Failed to create profile. Make sure Codex CLI is installed and you are logged in.");
5682
+ logError(`Failed to parse Codex auth file during ${actionLabel}:`, error);
5683
+ throw new Error("Failed to parse Codex auth file. Complete the login and try again.");
5420
5684
  }
5421
5685
  }
5422
- /**
5423
- * Switch to a different profile
5424
- */
5425
- async switchToProfile(name) {
5426
- await this.initialize();
5686
+ async persistNewProfileFromAuth(profileName, authData) {
5687
+ const description = this.describeActiveAuth(authData);
5688
+ const normalizedProfile = description.normalized;
5689
+ normalizedProfile.created_at = normalizedProfile.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
5690
+ await this.persistProfileRecord(profileName, normalizedProfile, description.metadata);
5691
+ const stored = await this.getProfileRowByName(profileName);
5692
+ return stored ? this.buildProfileFromRow(stored) : null;
5693
+ }
5694
+ async createProfile(name) {
5427
5695
  const profileName = this.normalizeProfileName(name);
5428
- const record = await this.getProfileRowByName(profileName);
5429
- if (!record) {
5430
- throw new Error(`Profile '${profileName}' not found!`);
5431
- }
5432
- try {
5433
- const profileData = record.data;
5434
- const codexFormat = this.convertToCodexFormat(profileData);
5435
- await this.writeAtomic(this.activeAuth, JSON.stringify(codexFormat, null, 2));
5436
- await this.persistPreferredProfileName(profileName);
5437
- return true;
5438
- } catch (error) {
5439
- logError("Error switching profile:", error);
5440
- throw new Error("Failed to switch profile");
5441
- }
5696
+ return this.enqueueProfileOperation(
5697
+ profileName,
5698
+ () => this.enqueueAuthSwap(async () => {
5699
+ await this.initialize();
5700
+ if (await this.getProfileRowByName(profileName)) {
5701
+ throw new Error(`Profile '${profileName}' already exists!`);
5702
+ }
5703
+ try {
5704
+ const authData = await this.readAuthFileForImport(this.activeAuth, "profile creation");
5705
+ return await this.persistNewProfileFromAuth(profileName, authData);
5706
+ } catch (error) {
5707
+ logError("Error creating profile:", error);
5708
+ throw new Error("Failed to create profile. Make sure Codex CLI is installed and you are logged in.");
5709
+ }
5710
+ })
5711
+ );
5442
5712
  }
5443
- async refreshProfileAuth(name) {
5444
- await this.initialize();
5713
+ async createProfileFromAuthFile(name, authPath) {
5445
5714
  const profileName = this.normalizeProfileName(name);
5446
- const record = await this.getProfileRowByName(profileName);
5447
- if (!record) {
5448
- throw new Error(`Profile '${profileName}' not found!`);
5449
- }
5450
- const existing = record.data;
5451
- let activeAuth;
5452
- try {
5453
- const authContent = await import_fs.promises.readFile(this.activeAuth, "utf8");
5454
- activeAuth = JSON.parse(authContent);
5455
- } catch (error) {
5456
- if (this.isNotFoundError(error)) {
5457
- throw new Error("Codex CLI did not produce an auth file. Complete the login before refreshing this profile.");
5715
+ return this.enqueueProfileOperation(profileName, async () => {
5716
+ await this.initialize();
5717
+ if (await this.getProfileRowByName(profileName)) {
5718
+ throw new Error(`Profile '${profileName}' already exists!`);
5458
5719
  }
5459
- logError("Failed to read Codex auth file during refresh:", error);
5460
- throw new Error("Failed to read Codex auth file. Complete the login and try again.");
5461
- }
5720
+ try {
5721
+ const authData = await this.readAuthFileForImport(authPath, "profile creation");
5722
+ return await this.persistNewProfileFromAuth(profileName, authData);
5723
+ } catch (error) {
5724
+ logError("Error creating profile:", error);
5725
+ throw new Error("Failed to create profile. Make sure Codex CLI is installed and you are logged in.");
5726
+ }
5727
+ });
5728
+ }
5729
+ async refreshProfileAuthWithData(profileName, record, activeAuth) {
5730
+ const existing = record.data;
5462
5731
  const normalized = this.normalizeProfileData(activeAuth);
5463
5732
  const existingMetadata = record.metadata ?? this.extractProfileMetadata(existing);
5464
5733
  const currentWorkspace = this.resolveWorkspaceIdentity(existing, existingMetadata);
@@ -5507,6 +5776,60 @@ var ProfileManager = class {
5507
5776
  }
5508
5777
  return profile;
5509
5778
  }
5779
+ /**
5780
+ * Switch to a different profile
5781
+ */
5782
+ async switchToProfile(name) {
5783
+ const profileName = this.normalizeProfileName(name);
5784
+ return this.enqueueProfileOperation(
5785
+ profileName,
5786
+ () => this.enqueueAuthSwap(async () => {
5787
+ await this.initialize();
5788
+ const record = await this.getProfileRowByName(profileName);
5789
+ if (!record) {
5790
+ throw new Error(`Profile '${profileName}' not found!`);
5791
+ }
5792
+ try {
5793
+ await this.writeActiveAuthForRecord(record);
5794
+ return true;
5795
+ } catch (error) {
5796
+ logError("Error switching profile:", error);
5797
+ throw new Error("Failed to switch profile");
5798
+ }
5799
+ })
5800
+ );
5801
+ }
5802
+ async refreshProfileAuth(name) {
5803
+ const profileName = this.normalizeProfileName(name);
5804
+ return this.enqueueProfileOperation(
5805
+ profileName,
5806
+ () => this.enqueueAuthSwap(async () => {
5807
+ await this.initialize();
5808
+ const record = await this.getProfileRowByName(profileName);
5809
+ if (!record) {
5810
+ throw new Error(`Profile '${profileName}' not found!`);
5811
+ }
5812
+ const activeAuth = await this.readAuthFileForImport(this.activeAuth, "profile refresh");
5813
+ return this.refreshProfileAuthWithData(profileName, record, activeAuth);
5814
+ })
5815
+ );
5816
+ }
5817
+ async refreshProfileAuthFromFile(name, authPath) {
5818
+ const profileName = this.normalizeProfileName(name);
5819
+ return this.enqueueProfileOperation(profileName, async () => {
5820
+ await this.initialize();
5821
+ const record = await this.getProfileRowByName(profileName);
5822
+ if (!record) {
5823
+ throw new Error(`Profile '${profileName}' not found!`);
5824
+ }
5825
+ const activeAuth = await this.readAuthFileForImport(authPath, "profile refresh");
5826
+ const profile = await this.refreshProfileAuthWithData(profileName, record, activeAuth);
5827
+ await this.enqueueAuthSwap(async () => {
5828
+ await this.syncActiveAuthFromProfileIfCurrent(profileName);
5829
+ });
5830
+ return profile;
5831
+ });
5832
+ }
5510
5833
  /**
5511
5834
  * Execute an action with a profile's auth injected.
5512
5835
  * Creates an isolated CODEX_HOME with the profile's auth/config, invokes the action
@@ -5515,22 +5838,35 @@ var ProfileManager = class {
5515
5838
  async runWithProfileAuth(name, action) {
5516
5839
  return this.enqueueProfileOperation(
5517
5840
  name,
5518
- () => this.enqueueAuthSwap(
5519
- () => this.runWithPreparedProfileHome(name, action, {
5520
- syncFromActiveAuthBeforeAction: true
5521
- })
5522
- )
5841
+ () => this.enqueueAuthSwap(async () => {
5842
+ try {
5843
+ return await this.runWithPreparedProfileHome(name, action, {
5844
+ syncFromActiveAuthBeforeAction: true
5845
+ });
5846
+ } finally {
5847
+ await this.syncActiveAuthFromProfileIfCurrent(name);
5848
+ }
5849
+ })
5523
5850
  );
5524
5851
  }
5525
5852
  async readLiveRateLimits(name, options = {}) {
5526
- return this.enqueueProfileOperation(
5527
- name,
5528
- () => this.runWithPreparedProfileHome(
5529
- name,
5530
- (env) => fetchRateLimitsViaRpc(env, { codexPath: options.codexPath }),
5531
- { syncFromActiveAuthBeforeAction: false }
5532
- )
5533
- );
5853
+ const profileName = this.normalizeProfileName(name);
5854
+ try {
5855
+ return await this.enqueueProfileOperation(
5856
+ profileName,
5857
+ () => this.runWithPreparedProfileHome(
5858
+ profileName,
5859
+ (env) => fetchRateLimitsViaRpc(env, { codexPath: options.codexPath }),
5860
+ { syncFromActiveAuthBeforeAction: false }
5861
+ )
5862
+ );
5863
+ } finally {
5864
+ await this.enqueueAuthSwap(async () => {
5865
+ await this.syncActiveAuthFromProfileIfCurrent(profileName);
5866
+ }).catch((error) => {
5867
+ logWarn(`Failed to sync active auth after rate-limit probe for '${profileName}':`, error);
5868
+ });
5869
+ }
5534
5870
  }
5535
5871
  /**
5536
5872
  * Rename a profile
@@ -5546,10 +5882,6 @@ var ProfileManager = class {
5546
5882
  if (await this.getProfileRowByName(targetName)) {
5547
5883
  throw new Error(`Profile '${targetName}' already exists!`);
5548
5884
  }
5549
- const preferred = await this.readPreferredProfileName();
5550
- if (preferred && preferred === sourceName) {
5551
- await this.persistPreferredProfileName(targetName);
5552
- }
5553
5885
  const oldHome = this.getProfileHomePath(sourceName);
5554
5886
  const newHome = this.getProfileHomePath(targetName);
5555
5887
  try {
@@ -5618,25 +5950,51 @@ var ProfileManager = class {
5618
5950
  if (!record) {
5619
5951
  throw new Error(`Profile '${profileName}' not found!`);
5620
5952
  }
5621
- if (!record.accountId) {
5622
- const dashboardState = await this.readProfileDashboardState();
5623
- delete dashboardState.customGroupsByAccountKey[resolveFallbackAccountKey(profileName)];
5624
- await this.writeProfileDashboardState(dashboardState);
5625
- }
5626
- const preferred = await this.readPreferredProfileName();
5627
- if (preferred && preferred === profileName) {
5628
- await this.persistPreferredProfileName(null);
5629
- }
5630
- try {
5631
- await import_fs.promises.rm(this.getProfileHomePath(profileName), { recursive: true, force: true });
5632
- } catch (error) {
5633
- if (!this.isNotFoundError(error)) {
5634
- logWarn(`Failed to remove profile home for '${profileName}':`, error);
5635
- }
5636
- }
5637
- await this.deleteProfileRecord(profileName);
5953
+ await this.deleteProfileRecordAndHome(record);
5638
5954
  return true;
5639
5955
  }
5956
+ async deleteActiveProfileAndSwitch(name, replacementName) {
5957
+ const profileName = this.normalizeProfileName(name);
5958
+ const nextProfileName = this.normalizeProfileName(replacementName);
5959
+ if (profileName === nextProfileName) {
5960
+ throw new Error("Replacement profile must be different from the deleted profile.");
5961
+ }
5962
+ return this.enqueueAuthSwap(async () => {
5963
+ await this.initialize();
5964
+ const record = await this.getProfileRowByName(profileName);
5965
+ if (!record) {
5966
+ throw new Error(`Profile '${profileName}' not found!`);
5967
+ }
5968
+ const replacement = await this.getProfileRowByName(nextProfileName);
5969
+ if (!replacement) {
5970
+ throw new Error(`Profile '${nextProfileName}' not found!`);
5971
+ }
5972
+ await this.assertActiveAuthMatchesRecord(
5973
+ record,
5974
+ "Active Codex login changed before deletion. Refresh profiles and try again."
5975
+ );
5976
+ await this.writeActiveAuthForRecord(replacement);
5977
+ await this.deleteProfileRecordAndHome(record);
5978
+ return true;
5979
+ });
5980
+ }
5981
+ async deleteActiveProfileAndAuth(name) {
5982
+ const profileName = this.normalizeProfileName(name);
5983
+ return this.enqueueAuthSwap(async () => {
5984
+ await this.initialize();
5985
+ const record = await this.getProfileRowByName(profileName);
5986
+ if (!record) {
5987
+ throw new Error(`Profile '${profileName}' not found!`);
5988
+ }
5989
+ await this.assertActiveAuthMatchesRecord(
5990
+ record,
5991
+ "Active Codex login changed before deletion. Refresh profiles and try again."
5992
+ );
5993
+ await this.removeActiveAuthFiles();
5994
+ await this.deleteProfileRecordAndHome(record);
5995
+ return true;
5996
+ });
5997
+ }
5640
5998
  /**
5641
5999
  * Delete every profile that does not appear in the allow-list.
5642
6000
  * Used to enforce plan limits when local storage was tampered with.
@@ -5673,64 +6031,61 @@ var ProfileManager = class {
5673
6031
  }
5674
6032
  }
5675
6033
  }
5676
- const preferred = await this.readPreferredProfileName();
5677
- if (preferred && removed.includes(preferred)) {
5678
- await this.persistPreferredProfileName(null);
5679
- }
5680
6034
  return removed;
5681
6035
  }
5682
- /**
5683
- * Check if the current auth matches a profile (for smart detection)
5684
- */
5685
- async getCurrentProfile() {
5686
- await this.initialize();
5687
- const preferredName = await this.readPreferredProfileName();
5688
- if (preferredName) {
5689
- try {
5690
- const normalizedPreferred = this.normalizeProfileName(preferredName);
5691
- const preferredRecord = await this.getProfileRowByName(normalizedPreferred);
5692
- if (preferredRecord) {
5693
- return { name: normalizedPreferred, trusted: true };
5694
- }
5695
- } catch {
6036
+ async getActiveCodexAuthStatus(options = {}) {
6037
+ return this.enqueueAuthSwap(async () => {
6038
+ await this.initialize();
6039
+ const activeAuth = await this.readActiveAuthFile();
6040
+ if (!activeAuth) {
6041
+ return this.buildActiveCodexAuthStatus("missing");
5696
6042
  }
5697
- }
5698
- const activeAuth = await this.readActiveAuthFile();
5699
- if (activeAuth) {
5700
- const normalizedAuth = this.normalizeProfileData(activeAuth);
5701
- const authMetadata = this.extractProfileMetadata(normalizedAuth);
5702
- const workspace = this.resolveWorkspaceIdentity(normalizedAuth, authMetadata);
5703
- const activeIdentityKey = this.resolveProfileIdentityFromData(null, normalizedAuth, authMetadata);
5704
- if (activeIdentityKey) {
5705
- const scoped = await this.getProfileRowByIdentityAndWorkspace(activeIdentityKey, workspace.id ?? DEFAULT_WORKSPACE_ID, {
5706
- preferAuthMethod: "codex-cli"
6043
+ const description = this.describeActiveAuth(activeAuth);
6044
+ if (!description.hasTokenPayload || !description.identityKey) {
6045
+ return this.buildActiveCodexAuthStatus("unknown", description);
6046
+ }
6047
+ const matching = await this.findProfileForActiveAuth(
6048
+ description.identityKey,
6049
+ description.workspaceId
6050
+ );
6051
+ if (matching) {
6052
+ const normalized = this.normalizeProfileName(matching.name);
6053
+ await this.syncMatchedProfileFromActiveAuth(matching, description);
6054
+ return this.buildActiveCodexAuthStatus("matched", description, {
6055
+ profileName: normalized
5707
6056
  });
5708
- if (scoped) {
5709
- const normalized = this.normalizeProfileName(scoped.name);
5710
- await this.persistPreferredProfileName(normalized);
5711
- return { name: normalized, trusted: true };
5712
- }
5713
- const matching = await this.getProfileRowByIdentityKey(activeIdentityKey, { preferAuthMethod: "codex-cli" });
5714
- if (matching) {
5715
- const normalized = this.normalizeProfileName(matching.name);
5716
- await this.persistPreferredProfileName(normalized);
5717
- return { name: normalized, trusted: true };
5718
- }
5719
6057
  }
5720
- }
5721
- const preferred = await this.readPreferredProfileName();
5722
- if (preferred) {
5723
- try {
5724
- const normalized = this.normalizeProfileName(preferred);
5725
- const exists = await this.getProfileRowByName(normalized);
5726
- if (exists) {
5727
- return { name: normalized, trusted: true };
5728
- }
5729
- } catch {
5730
- await this.persistPreferredProfileName(null);
5731
- return { name: null, trusted: false };
6058
+ if (!options.autoImport) {
6059
+ return this.buildActiveCodexAuthStatus("unmanaged", description);
6060
+ }
6061
+ if (options.canAutoImport === false) {
6062
+ return this.buildActiveCodexAuthStatus("unmanaged", description, {
6063
+ importBlockedReason: options.importBlockedReason ?? "Profile limit reached. Upgrade to CodexUse Pro or remove a profile before importing this Codex login."
6064
+ });
5732
6065
  }
5733
- await this.persistPreferredProfileName(null);
6066
+ const records = await this.listProfileRecords();
6067
+ const profileName = this.resolveAutoImportedProfileName(description.email, records);
6068
+ const profileData = {
6069
+ ...description.normalized,
6070
+ auth_method: "codex-cli",
6071
+ created_at: description.normalized.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
6072
+ };
6073
+ await this.persistProfileRecord(profileName, profileData, description.metadata, {
6074
+ displayName: description.email ?? profileName
6075
+ });
6076
+ return this.buildActiveCodexAuthStatus("matched", description, {
6077
+ profileName,
6078
+ importedProfileName: profileName
6079
+ });
6080
+ });
6081
+ }
6082
+ /**
6083
+ * Check if the real Codex auth file matches a saved profile.
6084
+ */
6085
+ async getCurrentProfile() {
6086
+ const status = await this.getActiveCodexAuthStatus({ autoImport: false });
6087
+ if (status.state === "matched" && status.profileName) {
6088
+ return { name: status.profileName, trusted: true };
5734
6089
  }
5735
6090
  return { name: null, trusted: false };
5736
6091
  }
@@ -5831,17 +6186,6 @@ var ProfileManager = class {
5831
6186
  }
5832
6187
  }
5833
6188
  }
5834
- const preferred = await this.readPreferredProfileName();
5835
- if (preferred) {
5836
- try {
5837
- const normalizedPreferred = this.normalizeProfileName(preferred);
5838
- if (!normalized.has(normalizedPreferred)) {
5839
- await this.persistPreferredProfileName(null);
5840
- }
5841
- } catch {
5842
- await this.persistPreferredProfileName(null);
5843
- }
5844
- }
5845
6189
  return {
5846
6190
  imported: normalized.size,
5847
6191
  removed
@@ -5874,8 +6218,8 @@ Usage:
5874
6218
  codexuse profile current
5875
6219
  codexuse profile add <name> [--skip-login] [--device-auth] [--login=browser|device]
5876
6220
  codexuse profile refresh <name> [--skip-login] [--device-auth] [--login=browser|device]
5877
- codexuse profile switch <name>
5878
- codexuse profile autoroll [--threshold=50-100] [--dry-run] [--watch] [--interval=seconds]
6221
+ codexuse profile switch <name> [--restart-codex]
6222
+ codexuse profile autoroll [--switch-left=0-50] [--threshold=50-100] [--dry-run] [--watch] [--interval=seconds] [--restart-codex|--no-restart-codex]
5879
6223
  codexuse profile delete <name>
5880
6224
  codexuse profile rename <old> <new>
5881
6225
 
@@ -5895,22 +6239,26 @@ Flags:
5895
6239
  --compact Names only
5896
6240
  --device-auth Use device auth for Codex login
5897
6241
  --login=MODE Login mode: browser | device
5898
- --threshold=NN Auto-roll switch threshold percent (50-100)
6242
+ --switch-left=NN Auto-roll switch threshold by percent left (0-50)
6243
+ --threshold=NN Legacy auto-roll switch threshold by percent used (50-100)
5899
6244
  --watch Keep checking and auto-switch when threshold is reached
5900
6245
  --interval=SEC Watch interval in seconds (default: 30)
5901
6246
  --dry-run Print planned switch without changing active profile
6247
+ --restart-codex Restart official Codex after a profile switch
6248
+ --no-restart-codex Override saved auto-roll restart setting
5902
6249
  --profile=NAME Run Codex with one saved profile
5903
6250
  --runtime=NAME Accounts Pool runtime store: desktop | daemon
5904
6251
  --state-dir=PATH Override the runtime state dir for Accounts Pool inspection
5905
6252
  --port=NNNN Stable daemon/API port (or use with account-pool status for daemon base URL)
5906
6253
  --telegram-bot-token=TOKEN Enable Telegram remote control for daemon mode
5907
6254
  --project-path=PATH Optional path to bootstrap as the default Projects root in daemon mode
6255
+
6256
+ Note: profile autoroll requires Pro.
5908
6257
  `);
5909
6258
  }
5910
6259
 
5911
6260
  // src/platform/args.ts
5912
6261
  var DEFAULT_AUTOROLL_INTERVAL_SECONDS = 30;
5913
- var DEFAULT_AUTOROLL_THRESHOLD = 95;
5914
6262
  function hasFlag(args, flag) {
5915
6263
  return args.includes(flag);
5916
6264
  }
@@ -7694,77 +8042,976 @@ async function assertProfileCreationAllowed(profileManager) {
7694
8042
  }
7695
8043
  }
7696
8044
 
7697
- // src/commands/profile.ts
7698
- function formatProfileLabel(name, displayName) {
7699
- if (displayName && displayName.trim() && displayName !== name) {
7700
- return `${displayName} (${name})`;
8045
+ // ../../packages/runtime-codex/src/codex/officialAppRestart.ts
8046
+ var import_node_child_process7 = require("child_process");
8047
+ var import_node_crypto6 = require("crypto");
8048
+ var import_node_fs8 = require("fs");
8049
+ var import_node_os5 = require("os");
8050
+ var import_node_path11 = __toESM(require("path"), 1);
8051
+ var COMMAND_TIMEOUT_MS = 3e3;
8052
+ var EXIT_WAIT_MS = 1e4;
8053
+ var LAUNCH_WAIT_MS = 15e3;
8054
+ var POLL_INTERVAL_MS = 250;
8055
+ var APP_DISCOVERY_CACHE_TTL_MS = 6e4;
8056
+ var APP_DISCOVERY_MISS_CACHE_TTL_MS = 5e3;
8057
+ var OFFICIAL_CODEX_ACTIVITY_LIMIT = 50;
8058
+ var profileActionLock = Promise.resolve();
8059
+ var appDiscoveryCache = null;
8060
+ function logOfficialCodexRestart(level, message, context) {
8061
+ const logger = level === "warn" ? console.warn : level === "error" ? console.error : console.info;
8062
+ if (context && Object.keys(context).length > 0) {
8063
+ logger(`[official-codex-restart] ${message}`, context);
8064
+ return;
7701
8065
  }
7702
- return name;
8066
+ logger(`[official-codex-restart] ${message}`);
7703
8067
  }
7704
- function profileRateLimitKey(profile) {
7705
- return resolveProfileIdentityKeyFromProfile(profile);
8068
+ function hashProfileKey(profileKey) {
8069
+ if (!profileKey) {
8070
+ return null;
8071
+ }
8072
+ return (0, import_node_crypto6.createHash)("sha256").update(profileKey).digest("hex").slice(0, 12);
8073
+ }
8074
+ async function recordOfficialCodexActivity(input) {
8075
+ const now = /* @__PURE__ */ new Date();
8076
+ const entry = {
8077
+ id: `${now.getTime().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
8078
+ at: now.toISOString(),
8079
+ kind: input.kind,
8080
+ status: input.status,
8081
+ reason: input.reason ?? null,
8082
+ decisionId: input.decisionId ?? null,
8083
+ profileName: input.profileName ?? null,
8084
+ sourceProfileName: input.sourceProfileName ?? null,
8085
+ targetProfileName: input.targetProfileName ?? null,
8086
+ remainingPercent: input.remainingPercent ?? null,
8087
+ threshold: input.threshold ?? null,
8088
+ snapshotAgeMs: input.snapshotAgeMs ?? null,
8089
+ snapshotSource: input.snapshotSource ?? null,
8090
+ phase: input.phase ?? null,
8091
+ pid: input.pid ?? null,
8092
+ profileKeyHash: input.profileKeyHash ?? hashProfileKey(input.profileKey),
8093
+ switchVerified: input.switchVerified ?? null,
8094
+ restartRequested: input.restartRequested ?? null,
8095
+ restartResult: input.restartResult ?? null,
8096
+ observedProfileName: input.observedProfileName ?? null,
8097
+ observedProfileKeyHash: input.observedProfileKeyHash ?? hashProfileKey(input.observedProfileKey),
8098
+ observedProfileMatchSource: input.observedProfileMatchSource ?? null
8099
+ };
8100
+ try {
8101
+ await updateAppState((state) => ({
8102
+ officialCodex: {
8103
+ activity: [...state.officialCodex.activity, entry].slice(
8104
+ -OFFICIAL_CODEX_ACTIVITY_LIMIT
8105
+ )
8106
+ }
8107
+ }));
8108
+ } catch (error) {
8109
+ logOfficialCodexRestart("warn", "Failed to record official Codex activity.", {
8110
+ kind: entry.kind,
8111
+ status: entry.status,
8112
+ profileKeyHash: entry.profileKeyHash,
8113
+ error: error instanceof Error ? error.message : String(error)
8114
+ });
8115
+ }
7706
8116
  }
7707
- function toTitleCase(value) {
7708
- return value.replace(/\w\S*/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
8117
+ function runCommand2(command, args, timeoutMs = COMMAND_TIMEOUT_MS) {
8118
+ return new Promise((resolve) => {
8119
+ const child = (0, import_node_child_process7.spawn)(command, args, {
8120
+ stdio: ["ignore", "pipe", "pipe"]
8121
+ });
8122
+ let stdout = "";
8123
+ let stderr = "";
8124
+ let timedOut = false;
8125
+ const timer = setTimeout(() => {
8126
+ timedOut = true;
8127
+ child.kill("SIGTERM");
8128
+ }, timeoutMs);
8129
+ child.stdout?.on("data", (chunk) => {
8130
+ stdout += String(chunk);
8131
+ });
8132
+ child.stderr?.on("data", (chunk) => {
8133
+ stderr += String(chunk);
8134
+ });
8135
+ child.on("error", (error) => {
8136
+ clearTimeout(timer);
8137
+ resolve({
8138
+ code: -1,
8139
+ stdout,
8140
+ stderr: error instanceof Error ? error.message : String(error),
8141
+ timedOut
8142
+ });
8143
+ });
8144
+ child.on("close", (code) => {
8145
+ clearTimeout(timer);
8146
+ resolve({ code, stdout, stderr, timedOut });
8147
+ });
8148
+ });
7709
8149
  }
7710
- function formatShortDate(value) {
7711
- if (!value) return null;
7712
- const date = new Date(value);
7713
- if (Number.isNaN(date.getTime())) return null;
7714
- if (typeof Intl !== "undefined" && Intl.DateTimeFormat) {
7715
- return new Intl.DateTimeFormat(void 0, { month: "short", day: "numeric" }).format(date);
7716
- }
7717
- return date.toLocaleDateString();
8150
+ function enqueueProfileAction(work) {
8151
+ const run = profileActionLock.then(work, work);
8152
+ profileActionLock = run.then(
8153
+ () => void 0,
8154
+ () => void 0
8155
+ );
8156
+ return run;
7718
8157
  }
7719
- function formatRelativeTime(value) {
7720
- if (!value) return null;
7721
- const parsed = Date.parse(value);
7722
- if (Number.isNaN(parsed)) return null;
7723
- const diffMs = parsed - Date.now();
7724
- const absMs = Math.abs(diffMs);
7725
- if (absMs < 6e4) {
7726
- return null;
7727
- }
7728
- const minutes = Math.round(absMs / 6e4);
7729
- if (minutes < 60) {
7730
- return diffMs >= 0 ? `in ${minutes}m` : `${minutes}m ago`;
8158
+ async function readProcessRows() {
8159
+ const result = await runCommand2(
8160
+ "/bin/ps",
8161
+ ["-axo", "pid=,ppid=,lstart=,args="],
8162
+ 2e3
8163
+ );
8164
+ if (result.code !== 0) {
8165
+ return [];
7731
8166
  }
7732
- const hours = Math.round(minutes / 60);
7733
- if (hours < 24) {
7734
- return diffMs >= 0 ? `in ${hours}h` : `${hours}h ago`;
8167
+ return result.stdout.split(/\r?\n/).flatMap((line) => {
8168
+ const match = line.match(
8169
+ /^\s*(\d+)\s+(\d+)\s+([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d+\s+\d+:\d+:\d+\s+\d{4})\s+(.+)$/
8170
+ );
8171
+ if (!match) {
8172
+ return [];
8173
+ }
8174
+ const pid = Number(match[1]);
8175
+ const ppid = Number(match[2]);
8176
+ const parsedStartedAt = Date.parse(match[3] ?? "");
8177
+ const startedAt = Number.isFinite(parsedStartedAt) ? parsedStartedAt : null;
8178
+ const args = match[4] ?? "";
8179
+ if (!Number.isInteger(pid) || !Number.isInteger(ppid) || !args) {
8180
+ return [];
8181
+ }
8182
+ return [{ pid, ppid, startedAt, args }];
8183
+ });
8184
+ }
8185
+ function getMainExecutablePath(candidate) {
8186
+ return import_node_path11.default.join(candidate.appPath, "Contents", "MacOS", candidate.executableName);
8187
+ }
8188
+ function isMainProcessRow(row, candidate) {
8189
+ return row.args.includes(getMainExecutablePath(candidate));
8190
+ }
8191
+ function findAppServerPid(rows, mainPid) {
8192
+ return rows.find(
8193
+ (row) => row.ppid === mainPid && row.args.includes("/Contents/Resources/codex app-server")
8194
+ )?.pid ?? null;
8195
+ }
8196
+ async function processMatchesProfileHome(pid, profileHome) {
8197
+ const result = await runCommand2(
8198
+ "/bin/ps",
8199
+ ["eww", "-p", String(pid), "-o", "command="],
8200
+ 2e3
8201
+ );
8202
+ if (result.code !== 0) {
8203
+ return false;
7735
8204
  }
7736
- const days = Math.round(hours / 24);
7737
- return diffMs >= 0 ? `in ${days}d` : `${days}d ago`;
8205
+ return result.stdout.includes(`CODEX_HOME=${profileHome}`) || result.stdout.includes(profileHome);
7738
8206
  }
7739
- function formatOrganizations(organizations) {
7740
- if (!organizations || !Array.isArray(organizations) || organizations.length === 0) {
7741
- return null;
8207
+ async function findAppServerPidForProfile(rows, mainPid, profileHome) {
8208
+ const candidates = rows.filter(
8209
+ (row) => row.ppid === mainPid && row.args.includes("/Contents/Resources/codex app-server")
8210
+ );
8211
+ for (const row of candidates) {
8212
+ if (row.args.includes(profileHome) || await processMatchesProfileHome(row.pid, profileHome)) {
8213
+ return row.pid;
8214
+ }
7742
8215
  }
7743
- const parts = organizations.map((org) => {
7744
- if (!org) return null;
7745
- const name = org.title || org.id;
7746
- if (!name) return null;
7747
- return org.role ? `${name} (${org.role})` : name;
7748
- }).filter((value) => Boolean(value));
7749
- return parts.length > 0 ? parts.join(", ") : null;
8216
+ return null;
7750
8217
  }
7751
- function formatWindowLabel(windowMinutes) {
7752
- if (typeof windowMinutes !== "number" || Number.isNaN(windowMinutes)) {
7753
- return null;
8218
+ async function describeUnmanagedProfileHint(appServerPid) {
8219
+ if (!appServerPid) {
8220
+ return {};
7754
8221
  }
7755
- if (windowMinutes >= 2880) {
7756
- const days = windowMinutes / 1440;
7757
- const rounded = Math.round(days * 10) / 10;
7758
- const display = Number.isInteger(rounded) ? rounded.toFixed(0) : rounded.toFixed(1);
7759
- return `${display.replace(/\.0$/, "")}d window`;
8222
+ const result = await runCommand2(
8223
+ "/bin/ps",
8224
+ ["eww", "-p", String(appServerPid), "-o", "command="],
8225
+ 2e3
8226
+ );
8227
+ if (result.code !== 0) {
8228
+ return {};
7760
8229
  }
7761
- if (windowMinutes >= 60) {
7762
- const hours = windowMinutes / 60;
7763
- const rounded = Math.round(hours * 10) / 10;
7764
- const display = Number.isInteger(rounded) ? rounded.toFixed(0) : rounded.toFixed(1);
7765
- return `${display.replace(/\.0$/, "")}h window`;
8230
+ const telemetryMatch = result.stdout.match(
8231
+ /CODEX_TELEMETRY_LABEL=codexuse-profile-([^\s]+)/
8232
+ );
8233
+ if (telemetryMatch?.[1]) {
8234
+ const encodedProfileName = telemetryMatch[1];
8235
+ let profileName = encodedProfileName;
8236
+ try {
8237
+ profileName = decodeURIComponent(encodedProfileName);
8238
+ } catch {
8239
+ profileName = encodedProfileName;
8240
+ }
8241
+ return {
8242
+ profileName,
8243
+ profileMatchSource: "telemetry-label"
8244
+ };
7766
8245
  }
7767
- const minutes = Math.round(windowMinutes);
8246
+ const profileHomeMatch = result.stdout.match(/profile-homes\/([^\s]+)/);
8247
+ if (profileHomeMatch?.[1]) {
8248
+ return {
8249
+ profileName: profileHomeMatch[1],
8250
+ profileMatchSource: "profile-home"
8251
+ };
8252
+ }
8253
+ return {};
8254
+ }
8255
+ function getDescendantPids(rootPid, rows) {
8256
+ const descendants = [];
8257
+ const queue = [rootPid];
8258
+ const seen = /* @__PURE__ */ new Set();
8259
+ while (queue.length > 0) {
8260
+ const current = queue.shift();
8261
+ if (seen.has(current)) {
8262
+ continue;
8263
+ }
8264
+ seen.add(current);
8265
+ for (const row of rows) {
8266
+ if (row.ppid !== current || seen.has(row.pid)) {
8267
+ continue;
8268
+ }
8269
+ descendants.push(row.pid);
8270
+ queue.push(row.pid);
8271
+ }
8272
+ }
8273
+ return descendants;
8274
+ }
8275
+ async function waitForNewMainPid(candidate, previousPids) {
8276
+ const deadline = Date.now() + LAUNCH_WAIT_MS;
8277
+ while (Date.now() < deadline) {
8278
+ const rows = await readProcessRows();
8279
+ const pid = rows.find((row) => isMainProcessRow(row, candidate) && !previousPids.has(row.pid))?.pid ?? null;
8280
+ if (pid !== null) {
8281
+ return pid;
8282
+ }
8283
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
8284
+ }
8285
+ return null;
8286
+ }
8287
+ async function waitForMainPid(candidate, pid, timeoutMs) {
8288
+ const deadline = Date.now() + timeoutMs;
8289
+ while (Date.now() < deadline) {
8290
+ const rows = await readProcessRows();
8291
+ if (rows.some((row) => row.pid === pid && isMainProcessRow(row, candidate))) {
8292
+ return true;
8293
+ }
8294
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
8295
+ }
8296
+ return false;
8297
+ }
8298
+ async function waitForAppServerPid(mainPid, profileHome) {
8299
+ const deadline = Date.now() + LAUNCH_WAIT_MS;
8300
+ while (Date.now() < deadline) {
8301
+ const pid = await findAppServerPidForProfile(
8302
+ await readProcessRows(),
8303
+ mainPid,
8304
+ profileHome
8305
+ );
8306
+ if (pid !== null) {
8307
+ return pid;
8308
+ }
8309
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
8310
+ }
8311
+ return null;
8312
+ }
8313
+ async function waitForMainPidExit(pid, timeoutMs) {
8314
+ const deadline = Date.now() + timeoutMs;
8315
+ while (Date.now() < deadline) {
8316
+ const rows = await readProcessRows();
8317
+ if (!rows.some((row) => row.pid === pid)) {
8318
+ return true;
8319
+ }
8320
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
8321
+ }
8322
+ return false;
8323
+ }
8324
+ function managedInstanceToPayload(instance, running, appServerPid = instance.appServerPid, startedAt = null) {
8325
+ const runningStartedAt = startedAt ?? instance.launchedAt;
8326
+ return {
8327
+ profileName: instance.profileName,
8328
+ profileKey: instance.profileKey,
8329
+ appPath: instance.appPath,
8330
+ bundleId: instance.bundleId,
8331
+ pid: running ? instance.pid : null,
8332
+ appServerPid: running ? appServerPid : null,
8333
+ running,
8334
+ startedAt: running ? toIso(runningStartedAt) : null,
8335
+ uptimeMs: running && typeof runningStartedAt === "number" ? Math.max(0, Date.now() - runningStartedAt) : null,
8336
+ launchedAt: toIso(instance.launchedAt),
8337
+ lastVerifiedAt: toIso(instance.lastVerifiedAt),
8338
+ lastStatus: instance.lastStatus,
8339
+ lastError: instance.lastError
8340
+ };
8341
+ }
8342
+ async function patchManagedInstance(instance) {
8343
+ await patchAppState({
8344
+ officialCodex: {
8345
+ instancesByProfileName: {
8346
+ [instance.profileName]: instance
8347
+ }
8348
+ }
8349
+ });
8350
+ }
8351
+ async function readManagedInstance(profileName) {
8352
+ const state = await getAppState();
8353
+ return state.officialCodex.instancesByProfileName[profileName] ?? null;
8354
+ }
8355
+ async function resolveInstanceRuntimeState(args) {
8356
+ const pid = args.instance.pid;
8357
+ if (!pid) {
8358
+ return { running: false, appServerPid: null, startedAt: null };
8359
+ }
8360
+ const mainRow = args.rows.find((row) => row.pid === pid);
8361
+ if (!mainRow || !isMainProcessRow(mainRow, args.candidate)) {
8362
+ return { running: false, appServerPid: null, startedAt: null };
8363
+ }
8364
+ const appServerPid = args.instance.profileHome ? await findAppServerPidForProfile(args.rows, pid, args.instance.profileHome) : findAppServerPid(args.rows, pid);
8365
+ if (args.instance.profileHome && appServerPid === null) {
8366
+ return { running: false, appServerPid: null, startedAt: null };
8367
+ }
8368
+ return {
8369
+ running: true,
8370
+ appServerPid,
8371
+ startedAt: mainRow.startedAt
8372
+ };
8373
+ }
8374
+ async function openCodexWithProfileHome(candidate, profileHome, profileName, previousPids) {
8375
+ const executablePath = getMainExecutablePath(candidate);
8376
+ if (!(0, import_node_fs8.existsSync)(executablePath)) {
8377
+ return { opened: false, pid: null };
8378
+ }
8379
+ const telemetryLabel = `codexuse-profile-${encodeURIComponent(profileName)}`;
8380
+ let child;
8381
+ try {
8382
+ child = (0, import_node_child_process7.spawn)(executablePath, [], {
8383
+ detached: true,
8384
+ env: {
8385
+ ...process.env,
8386
+ CODEX_HOME: profileHome,
8387
+ CODEX_TELEMETRY_LABEL: telemetryLabel
8388
+ },
8389
+ stdio: "ignore"
8390
+ });
8391
+ } catch {
8392
+ return { opened: false, pid: null };
8393
+ }
8394
+ const childPid = child.pid ?? null;
8395
+ child.unref();
8396
+ const spawnError = await new Promise((resolve) => {
8397
+ let settled = false;
8398
+ const settle = (failed) => {
8399
+ if (settled) {
8400
+ return;
8401
+ }
8402
+ settled = true;
8403
+ resolve(failed);
8404
+ };
8405
+ child.once("error", () => settle(true));
8406
+ setTimeout(() => settle(false), POLL_INTERVAL_MS);
8407
+ });
8408
+ if (spawnError) {
8409
+ return { opened: false, pid: null };
8410
+ }
8411
+ if (childPid !== null) {
8412
+ const verified = await waitForMainPid(candidate, childPid, 2e3);
8413
+ if (verified) {
8414
+ return { opened: true, pid: childPid };
8415
+ }
8416
+ }
8417
+ return {
8418
+ opened: true,
8419
+ pid: await waitForNewMainPid(candidate, previousPids)
8420
+ };
8421
+ }
8422
+ async function readBundleString(appPath, key) {
8423
+ const plistPath = import_node_path11.default.join(appPath, "Contents", "Info.plist");
8424
+ if (!(0, import_node_fs8.existsSync)(plistPath)) {
8425
+ return null;
8426
+ }
8427
+ const result = await runCommand2("/usr/bin/plutil", [
8428
+ "-extract",
8429
+ key,
8430
+ "raw",
8431
+ "-o",
8432
+ "-",
8433
+ plistPath
8434
+ ]);
8435
+ if (result.code !== 0) {
8436
+ return null;
8437
+ }
8438
+ const bundleId = result.stdout.trim();
8439
+ return bundleId.length > 0 ? bundleId : null;
8440
+ }
8441
+ async function discoverCodexAppCandidates() {
8442
+ const now = Date.now();
8443
+ const envPath = process.env.CODEXUSE_OFFICIAL_CODEX_APP_PATH?.trim() || null;
8444
+ if (appDiscoveryCache && appDiscoveryCache.envPath === envPath && appDiscoveryCache.expiresAt > now) {
8445
+ return appDiscoveryCache.candidates;
8446
+ }
8447
+ const rawPaths = /* @__PURE__ */ new Set();
8448
+ if (envPath) {
8449
+ rawPaths.add(envPath);
8450
+ }
8451
+ rawPaths.add("/Applications/Codex.app");
8452
+ rawPaths.add(import_node_path11.default.join((0, import_node_os5.homedir)(), "Applications", "Codex.app"));
8453
+ const mdfind = await runCommand2(
8454
+ "/usr/bin/mdfind",
8455
+ ["kMDItemFSName == 'Codex.app'"],
8456
+ 1500
8457
+ );
8458
+ if (mdfind.code === 0) {
8459
+ for (const line of mdfind.stdout.split(/\r?\n/)) {
8460
+ const trimmed = line.trim();
8461
+ if (trimmed.endsWith("/Codex.app")) {
8462
+ rawPaths.add(trimmed);
8463
+ }
8464
+ }
8465
+ }
8466
+ const candidates = [];
8467
+ for (const appPath of rawPaths) {
8468
+ if (!(0, import_node_fs8.existsSync)(appPath) || import_node_path11.default.basename(appPath) !== "Codex.app") {
8469
+ continue;
8470
+ }
8471
+ const bundleId = await readBundleString(appPath, "CFBundleIdentifier");
8472
+ if (!bundleId || bundleId.toLowerCase().includes("codexuse")) {
8473
+ continue;
8474
+ }
8475
+ candidates.push({
8476
+ appPath,
8477
+ bundleId,
8478
+ version: await readBundleString(appPath, "CFBundleShortVersionString"),
8479
+ executableName: await readBundleString(appPath, "CFBundleExecutable") ?? "Codex"
8480
+ });
8481
+ }
8482
+ const sorted = candidates.sort((a, b) => a.appPath.localeCompare(b.appPath));
8483
+ appDiscoveryCache = {
8484
+ envPath,
8485
+ expiresAt: now + (sorted.length > 0 ? APP_DISCOVERY_CACHE_TTL_MS : APP_DISCOVERY_MISS_CACHE_TTL_MS),
8486
+ candidates: sorted
8487
+ };
8488
+ return sorted;
8489
+ }
8490
+ async function getRunningCodexPids(candidate) {
8491
+ const result = await runCommand2("/bin/ps", ["-axo", "pid=,args="], 2e3);
8492
+ const pids = /* @__PURE__ */ new Set();
8493
+ const appContentsRoot = `${import_node_path11.default.join(candidate.appPath, "Contents")}${import_node_path11.default.sep}`;
8494
+ if (result.code === 0) {
8495
+ for (const line of result.stdout.split(/\r?\n/)) {
8496
+ const match = line.match(/^\s*(\d+)\s+(.+)$/);
8497
+ if (!match) {
8498
+ continue;
8499
+ }
8500
+ const pid = Number(match[1]);
8501
+ const args = match[2] ?? "";
8502
+ if (Number.isInteger(pid) && args.includes(appContentsRoot)) {
8503
+ pids.add(pid);
8504
+ }
8505
+ }
8506
+ }
8507
+ return Array.from(pids).sort((a, b) => a - b);
8508
+ }
8509
+ async function getRunningCodexMainPids(candidate) {
8510
+ const result = await runCommand2("/bin/ps", ["-axo", "pid=,args="], 2e3);
8511
+ const pids = /* @__PURE__ */ new Set();
8512
+ const executablePath = import_node_path11.default.join(
8513
+ candidate.appPath,
8514
+ "Contents",
8515
+ "MacOS",
8516
+ candidate.executableName
8517
+ );
8518
+ if (result.code === 0) {
8519
+ for (const line of result.stdout.split(/\r?\n/)) {
8520
+ const match = line.match(/^\s*(\d+)\s+(.+)$/);
8521
+ if (!match) {
8522
+ continue;
8523
+ }
8524
+ const pid = Number(match[1]);
8525
+ const args = match[2] ?? "";
8526
+ if (Number.isInteger(pid) && args.includes(executablePath)) {
8527
+ pids.add(pid);
8528
+ }
8529
+ }
8530
+ }
8531
+ return Array.from(pids).sort((a, b) => a - b);
8532
+ }
8533
+ async function resolveCodexAppTarget() {
8534
+ const candidates = await discoverCodexAppCandidates();
8535
+ const fallback = candidates[0] ?? null;
8536
+ if (!fallback) {
8537
+ return { candidate: null, runningPids: [], mainPids: [] };
8538
+ }
8539
+ for (const candidate of candidates) {
8540
+ const [runningPids2, mainPids2] = await Promise.all([
8541
+ getRunningCodexPids(candidate),
8542
+ getRunningCodexMainPids(candidate)
8543
+ ]);
8544
+ if (mainPids2.length > 0) {
8545
+ return { candidate, runningPids: runningPids2, mainPids: mainPids2 };
8546
+ }
8547
+ }
8548
+ const [runningPids, mainPids] = await Promise.all([
8549
+ getRunningCodexPids(fallback),
8550
+ getRunningCodexMainPids(fallback)
8551
+ ]);
8552
+ return { candidate: fallback, runningPids, mainPids };
8553
+ }
8554
+ async function signalPids(pids, signal) {
8555
+ const signaled = [];
8556
+ for (const pid of pids) {
8557
+ try {
8558
+ process.kill(pid, signal);
8559
+ signaled.push(pid);
8560
+ } catch {
8561
+ }
8562
+ }
8563
+ return signaled;
8564
+ }
8565
+ function toIso(value) {
8566
+ return typeof value === "number" && Number.isFinite(value) ? new Date(value).toISOString() : null;
8567
+ }
8568
+ async function rememberProfileSwitch(profileKey) {
8569
+ await patchAppState({
8570
+ officialCodex: {
8571
+ lastProfileSwitchAt: Date.now(),
8572
+ lastProfileSwitchProfileKey: profileKey
8573
+ }
8574
+ });
8575
+ }
8576
+ async function rememberRestartResult(args) {
8577
+ const now = Date.now();
8578
+ const verified = args.status === "restarted" || args.status === "started";
8579
+ await patchAppState({
8580
+ officialCodex: {
8581
+ lastVerifiedLaunchAt: verified ? now : void 0,
8582
+ lastVerifiedLaunchProfileKey: verified ? args.profileKey ?? null : void 0,
8583
+ lastObservedPid: verified ? args.pid ?? null : void 0,
8584
+ lastRestartStatus: args.status,
8585
+ lastRestartReason: args.reason ?? null
8586
+ }
8587
+ });
8588
+ }
8589
+ function recordOfficialCodexRestartResult(args) {
8590
+ return rememberRestartResult(args);
8591
+ }
8592
+ function recordOfficialCodexProfileSwitch(profileKey) {
8593
+ return rememberProfileSwitch(profileKey);
8594
+ }
8595
+ async function getOfficialCodexProfileInstances() {
8596
+ if (process.platform !== "darwin") {
8597
+ return {
8598
+ state: "unsupported",
8599
+ appPath: null,
8600
+ bundleId: null,
8601
+ version: null,
8602
+ instances: [],
8603
+ unmanaged: []
8604
+ };
8605
+ }
8606
+ const { candidate, mainPids } = await resolveCodexAppTarget();
8607
+ if (!candidate) {
8608
+ return {
8609
+ state: "not-found",
8610
+ appPath: null,
8611
+ bundleId: null,
8612
+ version: null,
8613
+ instances: [],
8614
+ unmanaged: []
8615
+ };
8616
+ }
8617
+ const [state, rows] = await Promise.all([getAppState(), readProcessRows()]);
8618
+ const managed = [];
8619
+ for (const instance of Object.values(state.officialCodex.instancesByProfileName)) {
8620
+ const runtime = await resolveInstanceRuntimeState({ instance, candidate, rows });
8621
+ managed.push(
8622
+ managedInstanceToPayload(
8623
+ instance,
8624
+ runtime.running,
8625
+ runtime.appServerPid,
8626
+ runtime.startedAt
8627
+ )
8628
+ );
8629
+ }
8630
+ managed.sort((a, b) => a.profileName.localeCompare(b.profileName));
8631
+ const managedPids = new Set(
8632
+ managed.flatMap((entry) => entry.running && entry.pid ? [entry.pid] : [])
8633
+ );
8634
+ const unmanaged = [];
8635
+ for (const pid of mainPids.filter((entry) => !managedPids.has(entry))) {
8636
+ const appServerPid = findAppServerPid(rows, pid);
8637
+ const startedAt = rows.find((row) => row.pid === pid)?.startedAt ?? null;
8638
+ unmanaged.push({
8639
+ pid,
8640
+ appServerPid,
8641
+ startedAt: toIso(startedAt),
8642
+ uptimeMs: typeof startedAt === "number" ? Math.max(0, Date.now() - startedAt) : null,
8643
+ ...await describeUnmanagedProfileHint(appServerPid)
8644
+ });
8645
+ }
8646
+ return {
8647
+ state: "found",
8648
+ appPath: candidate.appPath,
8649
+ bundleId: candidate.bundleId,
8650
+ version: candidate.version,
8651
+ instances: managed,
8652
+ unmanaged
8653
+ };
8654
+ }
8655
+ function launchOfficialCodexProfileInstance(options) {
8656
+ return enqueueProfileAction(async () => {
8657
+ if (process.platform !== "darwin") {
8658
+ return {
8659
+ status: "skipped",
8660
+ profileName: options.profileName,
8661
+ instance: null,
8662
+ reason: "unsupported-platform"
8663
+ };
8664
+ }
8665
+ const { candidate, mainPids } = await resolveCodexAppTarget();
8666
+ if (!candidate) {
8667
+ return {
8668
+ status: "skipped",
8669
+ profileName: options.profileName,
8670
+ instance: null,
8671
+ reason: "official-codex-app-not-found"
8672
+ };
8673
+ }
8674
+ const existing = await readManagedInstance(options.profileName);
8675
+ if (existing) {
8676
+ const rows = await readProcessRows();
8677
+ const runtime = await resolveInstanceRuntimeState({ instance: existing, candidate, rows });
8678
+ if (runtime.running) {
8679
+ const verified = {
8680
+ ...existing,
8681
+ profileHome: existing.profileHome ?? options.profileHome,
8682
+ appServerPid: runtime.appServerPid,
8683
+ lastVerifiedAt: Date.now(),
8684
+ lastStatus: "already-running",
8685
+ lastError: null
8686
+ };
8687
+ await patchManagedInstance(verified);
8688
+ return {
8689
+ status: "already-running",
8690
+ profileName: options.profileName,
8691
+ instance: managedInstanceToPayload(
8692
+ verified,
8693
+ true,
8694
+ runtime.appServerPid,
8695
+ runtime.startedAt
8696
+ ),
8697
+ reason: null
8698
+ };
8699
+ }
8700
+ }
8701
+ const launch = await openCodexWithProfileHome(
8702
+ candidate,
8703
+ options.profileHome,
8704
+ options.profileName,
8705
+ new Set(mainPids)
8706
+ );
8707
+ if (!launch.opened) {
8708
+ const failed = {
8709
+ profileName: options.profileName,
8710
+ profileKey: options.profileKey,
8711
+ profileHome: options.profileHome,
8712
+ appPath: candidate.appPath,
8713
+ bundleId: candidate.bundleId,
8714
+ pid: null,
8715
+ appServerPid: null,
8716
+ launchedAt: null,
8717
+ lastVerifiedAt: null,
8718
+ lastStatus: "failed",
8719
+ lastError: "open-failed"
8720
+ };
8721
+ await patchManagedInstance(failed);
8722
+ return {
8723
+ status: "failed",
8724
+ profileName: options.profileName,
8725
+ instance: managedInstanceToPayload(failed, false),
8726
+ reason: "open-failed"
8727
+ };
8728
+ }
8729
+ const launchedPid = launch.pid;
8730
+ if (launchedPid === null) {
8731
+ const failed = {
8732
+ profileName: options.profileName,
8733
+ profileKey: options.profileKey,
8734
+ profileHome: options.profileHome,
8735
+ appPath: candidate.appPath,
8736
+ bundleId: candidate.bundleId,
8737
+ pid: null,
8738
+ appServerPid: null,
8739
+ launchedAt: null,
8740
+ lastVerifiedAt: null,
8741
+ lastStatus: "failed",
8742
+ lastError: "launch-timeout"
8743
+ };
8744
+ await patchManagedInstance(failed);
8745
+ return {
8746
+ status: "failed",
8747
+ profileName: options.profileName,
8748
+ instance: managedInstanceToPayload(failed, false),
8749
+ reason: "launch-timeout"
8750
+ };
8751
+ }
8752
+ const appServerPid = await waitForAppServerPid(
8753
+ launchedPid,
8754
+ options.profileHome
8755
+ );
8756
+ if (appServerPid === null) {
8757
+ const failed = {
8758
+ profileName: options.profileName,
8759
+ profileKey: options.profileKey,
8760
+ profileHome: options.profileHome,
8761
+ appPath: candidate.appPath,
8762
+ bundleId: candidate.bundleId,
8763
+ pid: null,
8764
+ appServerPid: null,
8765
+ launchedAt: null,
8766
+ lastVerifiedAt: Date.now(),
8767
+ lastStatus: "failed",
8768
+ lastError: "app-server-not-verified"
8769
+ };
8770
+ await patchManagedInstance(failed);
8771
+ return {
8772
+ status: "failed",
8773
+ profileName: options.profileName,
8774
+ instance: managedInstanceToPayload(failed, false),
8775
+ reason: "app-server-not-verified"
8776
+ };
8777
+ }
8778
+ const now = Date.now();
8779
+ const instance = {
8780
+ profileName: options.profileName,
8781
+ profileKey: options.profileKey,
8782
+ profileHome: options.profileHome,
8783
+ appPath: candidate.appPath,
8784
+ bundleId: candidate.bundleId,
8785
+ pid: launchedPid,
8786
+ appServerPid,
8787
+ launchedAt: now,
8788
+ lastVerifiedAt: now,
8789
+ lastStatus: "started",
8790
+ lastError: null
8791
+ };
8792
+ await patchManagedInstance(instance);
8793
+ return {
8794
+ status: "started",
8795
+ profileName: options.profileName,
8796
+ instance: managedInstanceToPayload(instance, true, appServerPid),
8797
+ reason: null
8798
+ };
8799
+ });
8800
+ }
8801
+ function stopOfficialCodexProfileInstance(profileName) {
8802
+ return enqueueProfileAction(async () => {
8803
+ const existing = await readManagedInstance(profileName);
8804
+ if (!existing) {
8805
+ return {
8806
+ status: "not-running",
8807
+ profileName,
8808
+ instance: null,
8809
+ reason: "not-managed"
8810
+ };
8811
+ }
8812
+ const { candidate } = await resolveCodexAppTarget();
8813
+ if (!candidate) {
8814
+ const stopped2 = {
8815
+ ...existing,
8816
+ pid: null,
8817
+ appServerPid: null,
8818
+ lastVerifiedAt: Date.now(),
8819
+ lastStatus: "not-running",
8820
+ lastError: "official-codex-app-not-found"
8821
+ };
8822
+ await patchManagedInstance(stopped2);
8823
+ return {
8824
+ status: "not-running",
8825
+ profileName,
8826
+ instance: managedInstanceToPayload(stopped2, false),
8827
+ reason: "official-codex-app-not-found"
8828
+ };
8829
+ }
8830
+ const rows = await readProcessRows();
8831
+ const runtime = await resolveInstanceRuntimeState({ instance: existing, candidate, rows });
8832
+ if (!runtime.running || !existing.pid) {
8833
+ const stopped2 = {
8834
+ ...existing,
8835
+ pid: null,
8836
+ appServerPid: null,
8837
+ lastVerifiedAt: Date.now(),
8838
+ lastStatus: "not-running",
8839
+ lastError: null
8840
+ };
8841
+ await patchManagedInstance(stopped2);
8842
+ return {
8843
+ status: "not-running",
8844
+ profileName,
8845
+ instance: managedInstanceToPayload(stopped2, false),
8846
+ reason: null
8847
+ };
8848
+ }
8849
+ const tree = [existing.pid, ...getDescendantPids(existing.pid, rows)].filter((pid, index, all) => all.indexOf(pid) === index).sort((a, b) => b - a);
8850
+ await signalPids(tree, "SIGTERM");
8851
+ const exited = await waitForMainPidExit(existing.pid, 5e3);
8852
+ if (!exited) {
8853
+ await signalPids(tree, "SIGKILL");
8854
+ await waitForMainPidExit(existing.pid, EXIT_WAIT_MS);
8855
+ }
8856
+ const stillRunning = (await readProcessRows()).some((row) => row.pid === existing.pid);
8857
+ const stopped = {
8858
+ ...existing,
8859
+ pid: stillRunning ? existing.pid : null,
8860
+ appServerPid: stillRunning ? runtime.appServerPid : null,
8861
+ lastVerifiedAt: Date.now(),
8862
+ lastStatus: stillRunning ? "failed" : "stopped",
8863
+ lastError: stillRunning ? "stop-timeout" : null
8864
+ };
8865
+ await patchManagedInstance(stopped);
8866
+ return {
8867
+ status: stillRunning ? "failed" : "stopped",
8868
+ profileName,
8869
+ instance: managedInstanceToPayload(
8870
+ stopped,
8871
+ stillRunning,
8872
+ stopped.appServerPid,
8873
+ runtime.startedAt
8874
+ ),
8875
+ reason: stillRunning ? "stop-timeout" : null
8876
+ };
8877
+ });
8878
+ }
8879
+ function stopOfficialCodexObservedProfileInstance(profileName, pid, appServerPid) {
8880
+ return enqueueProfileAction(async () => {
8881
+ const { candidate } = await resolveCodexAppTarget();
8882
+ if (!candidate) {
8883
+ return {
8884
+ status: "not-running",
8885
+ profileName,
8886
+ instance: null,
8887
+ reason: "official-codex-app-not-found"
8888
+ };
8889
+ }
8890
+ const rows = await readProcessRows();
8891
+ const mainRow = rows.find((row) => row.pid === pid);
8892
+ if (!mainRow || !isMainProcessRow(mainRow, candidate)) {
8893
+ return {
8894
+ status: "not-running",
8895
+ profileName,
8896
+ instance: null,
8897
+ reason: null
8898
+ };
8899
+ }
8900
+ const runtimeAppServerPid = findAppServerPid(rows, pid) ?? appServerPid;
8901
+ const tree = [pid, ...getDescendantPids(pid, rows)].filter((entry, index, all) => all.indexOf(entry) === index).sort((a, b) => b - a);
8902
+ await signalPids(tree, "SIGTERM");
8903
+ const exited = await waitForMainPidExit(pid, 5e3);
8904
+ if (!exited) {
8905
+ await signalPids(tree, "SIGKILL");
8906
+ await waitForMainPidExit(pid, EXIT_WAIT_MS);
8907
+ }
8908
+ const stillRunning = (await readProcessRows()).some((row) => row.pid === pid);
8909
+ const instance = {
8910
+ profileName,
8911
+ profileKey: null,
8912
+ appPath: candidate.appPath,
8913
+ bundleId: candidate.bundleId,
8914
+ pid: stillRunning ? pid : null,
8915
+ appServerPid: stillRunning ? runtimeAppServerPid : null,
8916
+ running: stillRunning,
8917
+ startedAt: stillRunning ? toIso(mainRow.startedAt) : null,
8918
+ uptimeMs: stillRunning && typeof mainRow.startedAt === "number" ? Math.max(0, Date.now() - mainRow.startedAt) : null,
8919
+ launchedAt: null,
8920
+ lastVerifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
8921
+ lastStatus: stillRunning ? "failed" : "stopped",
8922
+ lastError: stillRunning ? "stop-timeout" : null
8923
+ };
8924
+ return {
8925
+ status: stillRunning ? "failed" : "stopped",
8926
+ profileName,
8927
+ instance,
8928
+ reason: stillRunning ? "stop-timeout" : null
8929
+ };
8930
+ });
8931
+ }
8932
+ async function restartOfficialCodexProfileInstance(options) {
8933
+ const stopped = await stopOfficialCodexProfileInstance(options.profileName);
8934
+ if (stopped.status === "failed") {
8935
+ return stopped;
8936
+ }
8937
+ const launched = await launchOfficialCodexProfileInstance(options);
8938
+ return {
8939
+ ...launched,
8940
+ status: launched.status === "started" ? "restarted" : launched.status
8941
+ };
8942
+ }
8943
+
8944
+ // src/commands/profile.ts
8945
+ function formatProfileLabel(name, displayName) {
8946
+ if (displayName && displayName.trim() && displayName !== name) {
8947
+ return `${displayName} (${name})`;
8948
+ }
8949
+ return name;
8950
+ }
8951
+ function profileRateLimitKey(profile) {
8952
+ return resolveProfileIdentityKeyFromProfile(profile);
8953
+ }
8954
+ function toTitleCase(value) {
8955
+ return value.replace(/\w\S*/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
8956
+ }
8957
+ function formatShortDate(value) {
8958
+ if (!value) return null;
8959
+ const date = new Date(value);
8960
+ if (Number.isNaN(date.getTime())) return null;
8961
+ if (typeof Intl !== "undefined" && Intl.DateTimeFormat) {
8962
+ return new Intl.DateTimeFormat(void 0, { month: "short", day: "numeric" }).format(date);
8963
+ }
8964
+ return date.toLocaleDateString();
8965
+ }
8966
+ function formatRelativeTime(value) {
8967
+ if (!value) return null;
8968
+ const parsed = Date.parse(value);
8969
+ if (Number.isNaN(parsed)) return null;
8970
+ const diffMs = parsed - Date.now();
8971
+ const absMs = Math.abs(diffMs);
8972
+ if (absMs < 6e4) {
8973
+ return null;
8974
+ }
8975
+ const minutes = Math.round(absMs / 6e4);
8976
+ if (minutes < 60) {
8977
+ return diffMs >= 0 ? `in ${minutes}m` : `${minutes}m ago`;
8978
+ }
8979
+ const hours = Math.round(minutes / 60);
8980
+ if (hours < 24) {
8981
+ return diffMs >= 0 ? `in ${hours}h` : `${hours}h ago`;
8982
+ }
8983
+ const days = Math.round(hours / 24);
8984
+ return diffMs >= 0 ? `in ${days}d` : `${days}d ago`;
8985
+ }
8986
+ function formatOrganizations(organizations) {
8987
+ if (!organizations || !Array.isArray(organizations) || organizations.length === 0) {
8988
+ return null;
8989
+ }
8990
+ const parts = organizations.map((org) => {
8991
+ if (!org) return null;
8992
+ const name = org.title || org.id;
8993
+ if (!name) return null;
8994
+ return org.role ? `${name} (${org.role})` : name;
8995
+ }).filter((value) => Boolean(value));
8996
+ return parts.length > 0 ? parts.join(", ") : null;
8997
+ }
8998
+ function formatWindowLabel(windowMinutes) {
8999
+ if (typeof windowMinutes !== "number" || Number.isNaN(windowMinutes)) {
9000
+ return null;
9001
+ }
9002
+ if (windowMinutes >= 2880) {
9003
+ const days = windowMinutes / 1440;
9004
+ const rounded = Math.round(days * 10) / 10;
9005
+ const display = Number.isInteger(rounded) ? rounded.toFixed(0) : rounded.toFixed(1);
9006
+ return `${display.replace(/\.0$/, "")}d window`;
9007
+ }
9008
+ if (windowMinutes >= 60) {
9009
+ const hours = windowMinutes / 60;
9010
+ const rounded = Math.round(hours * 10) / 10;
9011
+ const display = Number.isInteger(rounded) ? rounded.toFixed(0) : rounded.toFixed(1);
9012
+ return `${display.replace(/\.0$/, "")}h window`;
9013
+ }
9014
+ const minutes = Math.round(windowMinutes);
7768
9015
  return `${minutes}m window`;
7769
9016
  }
7770
9017
  function formatDurationFromSeconds(seconds) {
@@ -7812,6 +9059,7 @@ function formatWindowUsage(window) {
7812
9059
  var EMPTY_USAGE_SUMMARY = {
7813
9060
  windowKey: "primary",
7814
9061
  usagePercent: 0,
9062
+ remainingPercent: 100,
7815
9063
  hasUsage: false,
7816
9064
  resetsInSeconds: null,
7817
9065
  windowMinutes: null
@@ -7835,6 +9083,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7835
9083
  }
7836
9084
  const usageValueRaw = toFiniteNumberOrNull(entry.window.usedPercent);
7837
9085
  const usageValue = usageValueRaw === null ? 0 : Math.max(0, Math.min(100, usageValueRaw));
9086
+ const remainingValue = 100 - usageValue;
7838
9087
  const hasUsage = usageValueRaw !== null;
7839
9088
  const resetsInSeconds = toFiniteNumberOrNull(entry.window.resetsInSeconds);
7840
9089
  const windowMinutes = toFiniteNumberOrNull(entry.window.windowMinutes);
@@ -7843,6 +9092,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7843
9092
  summary = {
7844
9093
  windowKey: entry.key,
7845
9094
  usagePercent: usageValue,
9095
+ remainingPercent: remainingValue,
7846
9096
  hasUsage,
7847
9097
  resetsInSeconds,
7848
9098
  windowMinutes
@@ -7853,6 +9103,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7853
9103
  summary = {
7854
9104
  windowKey: entry.key,
7855
9105
  usagePercent: usageValue,
9106
+ remainingPercent: remainingValue,
7856
9107
  hasUsage,
7857
9108
  resetsInSeconds,
7858
9109
  windowMinutes
@@ -7864,6 +9115,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7864
9115
  summary = {
7865
9116
  windowKey: entry.key,
7866
9117
  usagePercent: usageValue,
9118
+ remainingPercent: remainingValue,
7867
9119
  hasUsage,
7868
9120
  resetsInSeconds,
7869
9121
  windowMinutes
@@ -7875,6 +9127,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7875
9127
  summary = {
7876
9128
  windowKey: entry.key,
7877
9129
  usagePercent: usageValue,
9130
+ remainingPercent: remainingValue,
7878
9131
  hasUsage,
7879
9132
  resetsInSeconds,
7880
9133
  windowMinutes
@@ -7885,6 +9138,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7885
9138
  summary = {
7886
9139
  windowKey: entry.key,
7887
9140
  usagePercent: usageValue,
9141
+ remainingPercent: remainingValue,
7888
9142
  hasUsage,
7889
9143
  resetsInSeconds,
7890
9144
  windowMinutes
@@ -7929,9 +9183,16 @@ async function resolveProfileUsage(manager, profile, codexPath) {
7929
9183
  return fallbackUsage(profile);
7930
9184
  }
7931
9185
  }
7932
- function compareAutoRollCandidates(a, b) {
7933
- if (a.usageSummary.usagePercent !== b.usageSummary.usagePercent) {
7934
- return a.usageSummary.usagePercent - b.usageSummary.usagePercent;
9186
+ function compareAutoRollCandidates(a, b, priorityRanks) {
9187
+ const aRank = priorityRanks.get(resolveProfileIdentityKeyFromProfile(a.profile));
9188
+ const bRank = priorityRanks.get(resolveProfileIdentityKeyFromProfile(b.profile));
9189
+ if (aRank !== void 0 || bRank !== void 0) {
9190
+ if (aRank === void 0) return 1;
9191
+ if (bRank === void 0) return -1;
9192
+ if (aRank !== bRank) return aRank - bRank;
9193
+ }
9194
+ if (a.usageSummary.remainingPercent !== b.usageSummary.remainingPercent) {
9195
+ return b.usageSummary.remainingPercent - a.usageSummary.remainingPercent;
7935
9196
  }
7936
9197
  const aReset = a.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
7937
9198
  const bReset = b.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
@@ -7940,37 +9201,38 @@ function compareAutoRollCandidates(a, b) {
7940
9201
  }
7941
9202
  return a.profile.name.localeCompare(b.profile.name);
7942
9203
  }
7943
- function computeAutoRollCandidate(profiles, usageMap, currentProfileName, currentSummary, switchThreshold) {
7944
- const candidates = profiles.filter((profile) => profile.name !== currentProfileName && profile.isValid && !profile.tokenStatus?.requiresUserAction).map((profile) => ({
9204
+ function computeAutoRollCandidate(profiles, usageMap, currentProfileName, currentSummary, switchRemainingThreshold, priorityOrder = [], blockedProfileNames = /* @__PURE__ */ new Set()) {
9205
+ const priorityRanks = /* @__PURE__ */ new Map();
9206
+ for (const [index, key] of priorityOrder.entries()) {
9207
+ const normalized = key.trim();
9208
+ if (normalized && !priorityRanks.has(normalized)) {
9209
+ priorityRanks.set(normalized, index);
9210
+ }
9211
+ }
9212
+ const candidates = profiles.filter(
9213
+ (profile) => profile.name !== currentProfileName && !blockedProfileNames.has(profile.name) && profile.isValid && !profile.tokenStatus?.requiresUserAction
9214
+ ).map((profile) => ({
7945
9215
  profile,
7946
9216
  usageSummary: usageMap.get(profile.name)?.usageSummary ?? summarizeRateLimitSnapshot(profile.rateLimit ?? null)
7947
9217
  }));
7948
9218
  if (candidates.length === 0) {
7949
9219
  return null;
7950
9220
  }
7951
- const belowThreshold = candidates.filter((candidate) => candidate.usageSummary.hasUsage && candidate.usageSummary.usagePercent < switchThreshold).sort(compareAutoRollCandidates);
9221
+ const belowThreshold = candidates.filter(
9222
+ (candidate) => candidate.usageSummary.hasUsage && candidate.usageSummary.remainingPercent > switchRemainingThreshold
9223
+ ).sort((a, b) => compareAutoRollCandidates(a, b, priorityRanks));
7952
9224
  let selected = belowThreshold[0] ?? null;
7953
9225
  if (!selected) {
7954
- selected = candidates.filter((candidate) => candidate.usageSummary.hasUsage).sort(compareAutoRollCandidates)[0] ?? null;
7955
- }
7956
- if (!selected) {
7957
- selected = candidates.filter((candidate) => !candidate.usageSummary.hasUsage).sort((a, b) => {
7958
- const aReset = a.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
7959
- const bReset = b.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
7960
- if (aReset !== bReset) {
7961
- return aReset - bReset;
7962
- }
7963
- return a.profile.name.localeCompare(b.profile.name);
7964
- })[0] ?? null;
9226
+ selected = candidates.filter((candidate) => !candidate.usageSummary.hasUsage).sort((a, b) => compareAutoRollCandidates(a, b, priorityRanks))[0] ?? null;
7965
9227
  }
7966
9228
  if (!selected) {
7967
9229
  return null;
7968
9230
  }
7969
9231
  if (selected.usageSummary.hasUsage && currentSummary.hasUsage) {
7970
- if (selected.usageSummary.usagePercent > currentSummary.usagePercent) {
9232
+ if (selected.usageSummary.remainingPercent < currentSummary.remainingPercent) {
7971
9233
  return null;
7972
9234
  }
7973
- if (selected.usageSummary.usagePercent === currentSummary.usagePercent) {
9235
+ if (selected.usageSummary.remainingPercent === currentSummary.remainingPercent) {
7974
9236
  const selectedReset = selected.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
7975
9237
  const currentReset = currentSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
7976
9238
  if (selectedReset >= currentReset) {
@@ -8054,18 +9316,31 @@ async function mapWithConcurrency(items, limit, fn) {
8054
9316
  await Promise.all(workers);
8055
9317
  return results;
8056
9318
  }
8057
- function parseAutoRollThreshold(flags, fallbackThreshold) {
8058
- const explicitThreshold = parseNumericFlag(flags, "--threshold");
8059
- if (explicitThreshold === null) {
8060
- return fallbackThreshold;
9319
+ function parseAutoRollSwitchRemainingThreshold(flags, fallbackThreshold) {
9320
+ const explicitLeft = parseNumericFlag(flags, "--switch-left");
9321
+ const explicitLegacyUsed = parseNumericFlag(flags, "--threshold");
9322
+ if (explicitLeft !== null && explicitLegacyUsed !== null) {
9323
+ throw new Error("Use either --switch-left or legacy --threshold, not both.");
8061
9324
  }
8062
- if (Number.isNaN(explicitThreshold)) {
8063
- throw new Error("Invalid --threshold value. Use --threshold=50-100.");
9325
+ if (explicitLeft !== null) {
9326
+ if (Number.isNaN(explicitLeft)) {
9327
+ throw new Error("Invalid --switch-left value. Use --switch-left=0-50.");
9328
+ }
9329
+ if (explicitLeft < AUTO_ROLL_SWITCH_REMAINING_MIN || explicitLeft > AUTO_ROLL_SWITCH_REMAINING_MAX) {
9330
+ throw new Error("Invalid --switch-left value. Use a number between 0 and 50.");
9331
+ }
9332
+ return sanitizeAutoRollSwitchRemainingThreshold(explicitLeft);
8064
9333
  }
8065
- if (explicitThreshold < 50 || explicitThreshold > 100) {
8066
- throw new Error("Invalid --threshold value. Use a number between 50 and 100.");
9334
+ if (explicitLegacyUsed !== null) {
9335
+ if (Number.isNaN(explicitLegacyUsed)) {
9336
+ throw new Error("Invalid --threshold value. Use --threshold=50-100.");
9337
+ }
9338
+ if (explicitLegacyUsed < 50 || explicitLegacyUsed > 100) {
9339
+ throw new Error("Invalid --threshold value. Use a number between 50 and 100.");
9340
+ }
9341
+ return sanitizeAutoRollSwitchRemainingThreshold(100 - explicitLegacyUsed);
8067
9342
  }
8068
- return Math.round(explicitThreshold);
9343
+ return sanitizeAutoRollSwitchRemainingThreshold(fallbackThreshold);
8069
9344
  }
8070
9345
  function parseAutoRollIntervalSeconds(flags) {
8071
9346
  const explicitInterval = parseIntegerFlag(flags, "--interval");
@@ -8077,6 +9352,231 @@ function parseAutoRollIntervalSeconds(flags) {
8077
9352
  }
8078
9353
  return explicitInterval;
8079
9354
  }
9355
+ function resolveRestartCodexFlag(flags, fallback) {
9356
+ const restart = hasFlag(flags, "--restart-codex");
9357
+ const noRestart = hasFlag(flags, "--no-restart-codex");
9358
+ if (restart && noRestart) {
9359
+ throw new Error("Use either --restart-codex or --no-restart-codex, not both.");
9360
+ }
9361
+ if (restart) {
9362
+ return true;
9363
+ }
9364
+ if (noRestart) {
9365
+ return false;
9366
+ }
9367
+ return fallback;
9368
+ }
9369
+ function findProfileKey(profiles, name) {
9370
+ const profile = profiles.find((entry) => entry.name === name);
9371
+ return profile ? profileRateLimitKey(profile) : null;
9372
+ }
9373
+ function rearmBlockedProfiles(blockedProfiles, usageMap, rearmRemainingThreshold) {
9374
+ if (!blockedProfiles) {
9375
+ return;
9376
+ }
9377
+ for (const profileName of Array.from(blockedProfiles)) {
9378
+ const summary = usageMap.get(profileName)?.usageSummary;
9379
+ if (!summary || !summary.hasUsage || summary.remainingPercent > rearmRemainingThreshold) {
9380
+ blockedProfiles.delete(profileName);
9381
+ }
9382
+ }
9383
+ }
9384
+ async function rememberOfficialCodexSwitch(profileKey) {
9385
+ try {
9386
+ await recordOfficialCodexProfileSwitch(profileKey);
9387
+ } catch (error) {
9388
+ console.warn(
9389
+ `Could not record official Codex sync state: ${error instanceof Error ? error.message : String(error)}`
9390
+ );
9391
+ }
9392
+ }
9393
+ function formatOfficialCodexProfileAction(result) {
9394
+ if (result.status === "restarted") {
9395
+ return "Official Codex restarted.";
9396
+ }
9397
+ if (result.status === "started") {
9398
+ return "Official Codex started.";
9399
+ }
9400
+ if (result.status === "already-running") {
9401
+ return "Official Codex already running.";
9402
+ }
9403
+ if (result.status === "failed") {
9404
+ return `Official Codex restart failed: ${result.reason}.`;
9405
+ }
9406
+ if (result.status === "skipped" || result.status === "not-running") {
9407
+ return `Official Codex restart skipped: ${result.reason}.`;
9408
+ }
9409
+ return "Official Codex restart status unknown.";
9410
+ }
9411
+ function findUnmanagedOfficialCodexRestartTarget(instances, profileName, profileKey) {
9412
+ const restartable = instances.unmanaged.filter((instance) => instance.pid);
9413
+ const matched = restartable.find((instance) => {
9414
+ if (profileKey && instance.profileKey) {
9415
+ return instance.profileKey === profileKey;
9416
+ }
9417
+ if (instance.profileKey) {
9418
+ return false;
9419
+ }
9420
+ return instance.profileName === profileName;
9421
+ }) ?? null;
9422
+ if (matched) {
9423
+ return { target: matched, ambiguous: false, unverified: false };
9424
+ }
9425
+ if (restartable.length > 1) {
9426
+ return { target: null, ambiguous: true, unverified: false };
9427
+ }
9428
+ return { target: null, ambiguous: false, unverified: restartable.length === 1 };
9429
+ }
9430
+ async function restartUnmanagedOfficialCodexWindow(profileName, launchOptions, target) {
9431
+ const stopped = await stopOfficialCodexObservedProfileInstance(
9432
+ target.profileName ?? profileName,
9433
+ target.pid,
9434
+ target.appServerPid
9435
+ );
9436
+ if (stopped.status === "failed") {
9437
+ return stopped;
9438
+ }
9439
+ const launched = await launchOfficialCodexProfileInstance(launchOptions);
9440
+ return {
9441
+ ...launched,
9442
+ status: stopped.status === "stopped" && launched.status === "started" ? "restarted" : launched.status
9443
+ };
9444
+ }
9445
+ async function recordCliOfficialCodexRestartResult(profileName, profileKey, action) {
9446
+ const pid = action.status === "started" || action.status === "restarted" ? action.instance?.pid ?? null : null;
9447
+ const status = action.status === "started" || action.status === "restarted" ? action.status : action.status === "failed" ? "failed" : "skipped";
9448
+ const reason = action.status === "started" || action.status === "restarted" ? null : action.reason ?? action.status;
9449
+ try {
9450
+ await recordOfficialCodexRestartResult({
9451
+ status,
9452
+ reason,
9453
+ profileKey,
9454
+ pid
9455
+ });
9456
+ await recordOfficialCodexActivity({
9457
+ kind: "official-codex-restart",
9458
+ status,
9459
+ reason,
9460
+ targetProfileName: profileName,
9461
+ profileKey,
9462
+ phase: "cli",
9463
+ pid,
9464
+ restartRequested: true,
9465
+ restartResult: status
9466
+ });
9467
+ } catch (error) {
9468
+ console.warn(
9469
+ `Could not record official Codex restart state: ${error instanceof Error ? error.message : String(error)}`
9470
+ );
9471
+ }
9472
+ }
9473
+ async function recordCliOfficialCodexRestartSkipped(profileName, profileKey, reason) {
9474
+ await recordCliOfficialCodexRestartResult(profileName, profileKey, {
9475
+ status: "skipped",
9476
+ profileName,
9477
+ instance: null,
9478
+ reason
9479
+ });
9480
+ }
9481
+ async function maybeRestartOfficialCodex(options) {
9482
+ if (!options.enabled) {
9483
+ return null;
9484
+ }
9485
+ try {
9486
+ const runtime = await options.manager.prepareProfileRuntime(options.profileName);
9487
+ const launchOptions = {
9488
+ profileName: options.profileName,
9489
+ profileKey: options.profileKey,
9490
+ profileHome: runtime.profileHome
9491
+ };
9492
+ const instances = await getOfficialCodexProfileInstances();
9493
+ if (instances.state === "unsupported") {
9494
+ await recordCliOfficialCodexRestartSkipped(
9495
+ options.profileName,
9496
+ options.profileKey,
9497
+ "unsupported-platform"
9498
+ );
9499
+ return "Official Codex restart skipped: unsupported-platform.";
9500
+ }
9501
+ if (instances.state === "not-found") {
9502
+ await recordCliOfficialCodexRestartSkipped(
9503
+ options.profileName,
9504
+ options.profileKey,
9505
+ "official-codex-app-not-found"
9506
+ );
9507
+ return "Official Codex restart skipped: official-codex-app-not-found.";
9508
+ }
9509
+ const managedRunning = instances.instances.some(
9510
+ (instance) => instance.profileName === options.profileName && instance.running
9511
+ );
9512
+ const unmanaged = findUnmanagedOfficialCodexRestartTarget(
9513
+ instances,
9514
+ options.profileName,
9515
+ options.profileKey
9516
+ );
9517
+ if (unmanaged.target && !managedRunning) {
9518
+ const action2 = await restartUnmanagedOfficialCodexWindow(
9519
+ options.profileName,
9520
+ launchOptions,
9521
+ unmanaged.target
9522
+ );
9523
+ await recordCliOfficialCodexRestartResult(
9524
+ options.profileName,
9525
+ options.profileKey,
9526
+ action2
9527
+ );
9528
+ return formatOfficialCodexProfileAction(action2);
9529
+ }
9530
+ if (unmanaged.ambiguous && !managedRunning) {
9531
+ await recordCliOfficialCodexRestartSkipped(
9532
+ options.profileName,
9533
+ options.profileKey,
9534
+ "ambiguous-official-codex-windows"
9535
+ );
9536
+ return "Official Codex restart skipped: ambiguous-official-codex-windows.";
9537
+ }
9538
+ if (unmanaged.unverified && !managedRunning) {
9539
+ await recordCliOfficialCodexRestartSkipped(
9540
+ options.profileName,
9541
+ options.profileKey,
9542
+ "unverified-official-codex-window"
9543
+ );
9544
+ return "Official Codex restart skipped: unverified-official-codex-window.";
9545
+ }
9546
+ if (options.launchIfNotRunning === false) {
9547
+ if (!managedRunning) {
9548
+ await recordCliOfficialCodexRestartSkipped(
9549
+ options.profileName,
9550
+ options.profileKey,
9551
+ "official-codex-app-not-running"
9552
+ );
9553
+ return "Official Codex restart skipped: official-codex-app-not-running.";
9554
+ }
9555
+ const action2 = await restartOfficialCodexProfileInstance(launchOptions);
9556
+ await recordCliOfficialCodexRestartResult(
9557
+ options.profileName,
9558
+ options.profileKey,
9559
+ action2
9560
+ );
9561
+ return formatOfficialCodexProfileAction(action2);
9562
+ }
9563
+ const action = await restartOfficialCodexProfileInstance(launchOptions);
9564
+ await recordCliOfficialCodexRestartResult(
9565
+ options.profileName,
9566
+ options.profileKey,
9567
+ action
9568
+ );
9569
+ return formatOfficialCodexProfileAction(action);
9570
+ } catch (error) {
9571
+ await recordCliOfficialCodexRestartResult(options.profileName, options.profileKey, {
9572
+ status: "failed",
9573
+ profileName: options.profileName,
9574
+ instance: null,
9575
+ reason: error instanceof Error ? error.message : "restart-threw"
9576
+ });
9577
+ return `Official Codex restart failed: ${error instanceof Error ? error.message : String(error)}.`;
9578
+ }
9579
+ }
8080
9580
  async function collectUsageMap(manager, profiles, codexPath) {
8081
9581
  const usageMap = /* @__PURE__ */ new Map();
8082
9582
  const uniqueProfilesByAccount = /* @__PURE__ */ new Map();
@@ -8126,6 +9626,11 @@ async function runAutoRollPass(manager, options) {
8126
9626
  throw new Error("Codex CLI binary not found on PATH. Auto-roll requires codex for live rate limits.");
8127
9627
  }
8128
9628
  const usageMap = await collectUsageMap(manager, profiles, codexPath);
9629
+ rearmBlockedProfiles(
9630
+ options.blockedProfiles,
9631
+ usageMap,
9632
+ options.rearmRemainingThreshold
9633
+ );
8129
9634
  const currentProfile = profiles.find((profile) => profile.name === current.name);
8130
9635
  if (!currentProfile) {
8131
9636
  return {
@@ -8144,10 +9649,31 @@ async function runAutoRollPass(manager, options) {
8144
9649
  target: null
8145
9650
  };
8146
9651
  }
8147
- if (currentUsage.usagePercent < options.threshold) {
9652
+ if (options.blockedProfiles?.has(current.name)) {
9653
+ if (currentUsage.remainingPercent > options.rearmRemainingThreshold) {
9654
+ options.blockedProfiles.delete(current.name);
9655
+ } else {
9656
+ return {
9657
+ switched: false,
9658
+ message: `'${current.name}' at ${Math.round(currentUsage.remainingPercent)}% left; waiting for re-arm above ${options.rearmRemainingThreshold}% left.`,
9659
+ source: current.name,
9660
+ target: null
9661
+ };
9662
+ }
9663
+ }
9664
+ if (options.blockCurrentUntilRearm && currentUsage.remainingPercent <= options.switchRemainingThreshold) {
9665
+ options.blockedProfiles?.add(current.name);
9666
+ return {
9667
+ switched: false,
9668
+ message: `'${current.name}' at ${Math.round(currentUsage.remainingPercent)}% left; waiting for re-arm above ${options.rearmRemainingThreshold}% left.`,
9669
+ source: current.name,
9670
+ target: null
9671
+ };
9672
+ }
9673
+ if (currentUsage.remainingPercent > options.switchRemainingThreshold) {
8148
9674
  return {
8149
9675
  switched: false,
8150
- message: `'${current.name}' at ${Math.round(currentUsage.usagePercent)}%, below threshold ${options.threshold}%.`,
9676
+ message: `'${current.name}' at ${Math.round(currentUsage.remainingPercent)}% left, above switch threshold ${options.switchRemainingThreshold}% left.`,
8151
9677
  source: current.name,
8152
9678
  target: null
8153
9679
  };
@@ -8157,29 +9683,42 @@ async function runAutoRollPass(manager, options) {
8157
9683
  usageMap,
8158
9684
  current.name,
8159
9685
  currentUsage,
8160
- options.threshold
9686
+ options.switchRemainingThreshold,
9687
+ options.priorityOrder ?? [],
9688
+ options.blockedProfiles
8161
9689
  );
8162
9690
  if (!candidate) {
8163
9691
  return {
8164
9692
  switched: false,
8165
- message: `No eligible profile to switch from '${current.name}' at ${Math.round(currentUsage.usagePercent)}%.`,
9693
+ message: `No eligible profile to switch from '${current.name}' at ${Math.round(currentUsage.remainingPercent)}% left.`,
8166
9694
  source: current.name,
8167
9695
  target: null
8168
9696
  };
8169
9697
  }
8170
- const candidateUsage = candidate.usageSummary.hasUsage ? `${Math.round(candidate.usageSummary.usagePercent)}%` : "n/a";
9698
+ const candidateUsage = candidate.usageSummary.hasUsage ? `${Math.round(candidate.usageSummary.remainingPercent)}% left` : "n/a";
8171
9699
  if (options.dryRun) {
9700
+ const restartSuffix = options.restartOfficialCodex ? " Official Codex would restart after the switch if running." : "";
8172
9701
  return {
8173
9702
  switched: false,
8174
- message: `[dry-run] Would switch '${current.name}' (${Math.round(currentUsage.usagePercent)}%) -> '${candidate.profile.name}' (${candidateUsage}).`,
9703
+ message: `[dry-run] Would switch '${current.name}' (${Math.round(currentUsage.remainingPercent)}% left) -> '${candidate.profile.name}' (${candidateUsage}).${restartSuffix}`,
8175
9704
  source: current.name,
8176
9705
  target: candidate.profile.name
8177
9706
  };
8178
9707
  }
8179
9708
  await manager.switchToProfile(candidate.profile.name);
9709
+ const targetProfileKey = profileRateLimitKey(candidate.profile);
9710
+ await rememberOfficialCodexSwitch(targetProfileKey);
9711
+ const restartMessage = await maybeRestartOfficialCodex({
9712
+ manager,
9713
+ enabled: options.restartOfficialCodex,
9714
+ profileName: candidate.profile.name,
9715
+ profileKey: targetProfileKey,
9716
+ launchIfNotRunning: options.launchOfficialCodexWhenClosed === true
9717
+ });
9718
+ options.blockedProfiles?.add(current.name);
8180
9719
  return {
8181
9720
  switched: true,
8182
- message: `Auto-rolled '${current.name}' (${Math.round(currentUsage.usagePercent)}%) -> '${candidate.profile.name}' (${candidateUsage}).`,
9721
+ message: `Auto-rolled '${current.name}' (${Math.round(currentUsage.remainingPercent)}% left) -> '${candidate.profile.name}' (${candidateUsage}).${restartMessage ? ` ${restartMessage}` : ""}`,
8183
9722
  source: current.name,
8184
9723
  target: candidate.profile.name
8185
9724
  };
@@ -8279,20 +9818,52 @@ async function handleProfileCommand(args, version) {
8279
9818
  if (!name) {
8280
9819
  throw new Error("Profile name is required.");
8281
9820
  }
9821
+ const restartOfficialCodex = resolveRestartCodexFlag(flags, false);
9822
+ const profiles = await manager.listProfiles();
9823
+ const targetProfileKey = findProfileKey(profiles, name);
8282
9824
  await manager.switchToProfile(name);
8283
9825
  console.log(`Switched to profile: ${name}`);
9826
+ await rememberOfficialCodexSwitch(targetProfileKey);
9827
+ const restartMessage = await maybeRestartOfficialCodex({
9828
+ manager,
9829
+ enabled: restartOfficialCodex,
9830
+ profileName: name,
9831
+ profileKey: targetProfileKey
9832
+ });
9833
+ if (restartMessage) {
9834
+ console.log(restartMessage);
9835
+ }
8284
9836
  return;
8285
9837
  }
8286
9838
  case "autoroll":
8287
9839
  case "auto-roll": {
9840
+ const license = await licenseService.getStatus();
9841
+ if (!license.isPro) {
9842
+ throw new Error("Auto-roll requires a Pro license.");
9843
+ }
8288
9844
  const watch = hasFlag(flags, "--watch");
8289
9845
  const dryRun = hasFlag(flags, "--dry-run");
8290
9846
  const settings = await getStoredAutoRollSettings().catch(() => null);
8291
- const fallbackThreshold = typeof settings?.switchThreshold === "number" && Number.isFinite(settings.switchThreshold) ? Math.round(settings.switchThreshold) : DEFAULT_AUTOROLL_THRESHOLD;
8292
- const threshold = parseAutoRollThreshold(flags, fallbackThreshold);
9847
+ const restartOfficialCodex = resolveRestartCodexFlag(
9848
+ flags,
9849
+ settings?.restartOfficialCodexOnAutoRoll === true
9850
+ );
9851
+ const fallbackSwitchRemainingThreshold = typeof settings?.switchRemainingThreshold === "number" && Number.isFinite(settings.switchRemainingThreshold) ? settings.switchRemainingThreshold : DEFAULT_AUTO_ROLL_SWITCH_REMAINING_THRESHOLD;
9852
+ const switchRemainingThreshold = parseAutoRollSwitchRemainingThreshold(
9853
+ flags,
9854
+ fallbackSwitchRemainingThreshold
9855
+ );
9856
+ const rearmRemainingThreshold = sanitizeAutoRollRearmRemainingThreshold(
9857
+ settings?.rearmRemainingThreshold ?? DEFAULT_AUTO_ROLL_REARM_REMAINING_THRESHOLD,
9858
+ switchRemainingThreshold
9859
+ );
8293
9860
  const intervalSeconds = parseAutoRollIntervalSeconds(flags);
8294
9861
  if (watch) {
8295
- console.log(`Auto-roll watch: threshold ${threshold}% | interval ${intervalSeconds}s${dryRun ? " | dry-run" : ""}`);
9862
+ console.log(
9863
+ `Auto-roll watch: switch at/below ${switchRemainingThreshold}% left | re-arm above ${rearmRemainingThreshold}% left | interval ${intervalSeconds}s${dryRun ? " | dry-run" : ""}${restartOfficialCodex ? " | restart-codex" : ""}`
9864
+ );
9865
+ const blockedProfiles = /* @__PURE__ */ new Set();
9866
+ let lastObservedProfile = null;
8296
9867
  let stop = false;
8297
9868
  process.on("SIGINT", () => {
8298
9869
  stop = true;
@@ -8301,8 +9872,23 @@ async function handleProfileCommand(args, version) {
8301
9872
  stop = true;
8302
9873
  });
8303
9874
  while (!stop) {
8304
- const result2 = await runAutoRollPass(manager, { threshold, dryRun });
9875
+ const current = await manager.getCurrentProfile();
9876
+ const currentName = current.name ?? null;
9877
+ const blockCurrentUntilRearm = Boolean(
9878
+ currentName && lastObservedProfile && currentName !== lastObservedProfile
9879
+ );
9880
+ const result2 = await runAutoRollPass(manager, {
9881
+ switchRemainingThreshold,
9882
+ rearmRemainingThreshold,
9883
+ dryRun,
9884
+ priorityOrder: settings?.priorityOrder ?? [],
9885
+ restartOfficialCodex,
9886
+ launchOfficialCodexWhenClosed: settings?.launchOfficialCodexWhenClosedOnAutoRoll === true,
9887
+ blockedProfiles,
9888
+ blockCurrentUntilRearm
9889
+ });
8305
9890
  console.log(result2.message);
9891
+ lastObservedProfile = result2.switched ? result2.target ?? currentName : currentName ?? result2.source;
8306
9892
  if (stop) {
8307
9893
  break;
8308
9894
  }
@@ -8311,7 +9897,14 @@ async function handleProfileCommand(args, version) {
8311
9897
  console.log("Auto-roll watch stopped.");
8312
9898
  return;
8313
9899
  }
8314
- const result = await runAutoRollPass(manager, { threshold, dryRun });
9900
+ const result = await runAutoRollPass(manager, {
9901
+ switchRemainingThreshold,
9902
+ rearmRemainingThreshold,
9903
+ dryRun,
9904
+ priorityOrder: settings?.priorityOrder ?? [],
9905
+ restartOfficialCodex,
9906
+ launchOfficialCodexWhenClosed: settings?.launchOfficialCodexWhenClosedOnAutoRoll === true
9907
+ });
8315
9908
  console.log(result.message);
8316
9909
  return;
8317
9910
  }
@@ -8341,8 +9934,8 @@ async function handleProfileCommand(args, version) {
8341
9934
  }
8342
9935
 
8343
9936
  // ../../packages/runtime-profiles/src/cloud-sync/service.ts
8344
- var import_node_fs8 = require("fs");
8345
- var import_node_path14 = __toESM(require("path"), 1);
9937
+ var import_node_fs9 = require("fs");
9938
+ var import_node_path15 = __toESM(require("path"), 1);
8346
9939
 
8347
9940
  // ../../packages/contracts/src/cloud-sync/types.ts
8348
9941
  var CLOUD_SYNC_SCHEMA_VERSION = 1;
@@ -8502,9 +10095,9 @@ async function pushRemoteSnapshot(licenseKey, snapshot, options) {
8502
10095
  var import_toml2 = __toESM(require_toml(), 1);
8503
10096
 
8504
10097
  // ../../packages/runtime-codex/src/codex/config-metadata.ts
8505
- var import_node_child_process7 = require("child_process");
10098
+ var import_node_child_process8 = require("child_process");
8506
10099
  var import_promises = require("fs/promises");
8507
- var import_node_path11 = __toESM(require("path"), 1);
10100
+ var import_node_path12 = __toESM(require("path"), 1);
8508
10101
  var CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/openai/codex/main/codex-rs/core/config.schema.json";
8509
10102
  var METADATA_CACHE_TTL_MS = 6e4;
8510
10103
  var FALLBACK_SCHEMA_TOP_LEVEL_KEYS = [
@@ -8612,9 +10205,9 @@ function isRecord5(value) {
8612
10205
  function uniqueSorted(values) {
8613
10206
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
8614
10207
  }
8615
- async function runCommand2(command, args, timeoutMs = 3e3) {
10208
+ async function runCommand3(command, args, timeoutMs = 3e3) {
8616
10209
  return new Promise((resolve, reject) => {
8617
- const child = (0, import_node_child_process7.spawn)(command, args, {
10210
+ const child = (0, import_node_child_process8.spawn)(command, args, {
8618
10211
  stdio: ["ignore", "pipe", "pipe"],
8619
10212
  env: process.env
8620
10213
  });
@@ -8649,7 +10242,7 @@ async function runCodexCommand(args) {
8649
10242
  const isJsLauncher = codexPath.endsWith(".js");
8650
10243
  const command = isJsLauncher ? process.execPath : codexPath;
8651
10244
  const commandArgs = isJsLauncher ? [codexPath, ...args] : args;
8652
- return runCommand2(command, commandArgs);
10245
+ return runCommand3(command, commandArgs);
8653
10246
  }
8654
10247
  function parseFeatureCatalog(output) {
8655
10248
  const entries = [];
@@ -8690,8 +10283,8 @@ function parseSchemaKeys(schemaRaw) {
8690
10283
  async function readSchemaFromLocal() {
8691
10284
  const candidates = [
8692
10285
  process.env.CODEX_CONFIG_SCHEMA_PATH,
8693
- import_node_path11.default.join(process.cwd(), "codex-rs", "core", "config.schema.json"),
8694
- import_node_path11.default.join(process.cwd(), "node_modules", "@openai", "codex", "codex-rs", "core", "config.schema.json")
10286
+ import_node_path12.default.join(process.cwd(), "codex-rs", "core", "config.schema.json"),
10287
+ import_node_path12.default.join(process.cwd(), "node_modules", "@openai", "codex", "codex-rs", "core", "config.schema.json")
8695
10288
  ].filter((candidate) => Boolean(candidate));
8696
10289
  for (const candidate of candidates) {
8697
10290
  try {
@@ -8796,20 +10389,20 @@ async function getCodexConfigMetadata(forceRefresh = false) {
8796
10389
 
8797
10390
  // ../../packages/runtime-codex/src/codex/config-io.ts
8798
10391
  var import_promises2 = require("fs/promises");
8799
- var import_node_path13 = __toESM(require("path"), 1);
10392
+ var import_node_path14 = __toESM(require("path"), 1);
8800
10393
 
8801
10394
  // ../../packages/runtime-codex/src/codex/home.ts
8802
- var import_node_os5 = __toESM(require("os"), 1);
8803
- var import_node_path12 = __toESM(require("path"), 1);
10395
+ var import_node_os6 = __toESM(require("os"), 1);
10396
+ var import_node_path13 = __toESM(require("path"), 1);
8804
10397
  function resolveHomeDir() {
8805
- return process.env.HOME || process.env.USERPROFILE || import_node_os5.default.homedir();
10398
+ return process.env.HOME || process.env.USERPROFILE || import_node_os6.default.homedir();
8806
10399
  }
8807
10400
  function expandHomePrefix(input, homeDir) {
8808
10401
  if (input === "~") {
8809
10402
  return homeDir;
8810
10403
  }
8811
10404
  if (input.startsWith("~/") || input.startsWith("~\\")) {
8812
- return import_node_path12.default.join(homeDir, input.slice(2));
10405
+ return import_node_path13.default.join(homeDir, input.slice(2));
8813
10406
  }
8814
10407
  return input;
8815
10408
  }
@@ -8825,13 +10418,10 @@ function normalizeCodexHomePath(input) {
8825
10418
  if (!configured) {
8826
10419
  return null;
8827
10420
  }
8828
- return import_node_path12.default.resolve(expandHomePrefix(configured, resolveHomeDir()));
10421
+ return import_node_path13.default.resolve(expandHomePrefix(configured, resolveHomeDir()));
8829
10422
  }
8830
10423
  function resolveDefaultCodexHomeDir() {
8831
- return import_node_path12.default.join(resolveHomeDir(), ".codex");
8832
- }
8833
- function resolveProfileCodexHomeDir(profileName) {
8834
- return import_node_path12.default.join(getUserDataDir(), "profile-homes", profileName);
10424
+ return import_node_path13.default.join(resolveHomeDir(), ".codex");
8835
10425
  }
8836
10426
  function resolveCodexHomeDir(options) {
8837
10427
  return normalizeCodexHomePath(options?.codexHomePath) ?? resolveDefaultCodexHomeDir();
@@ -8846,14 +10436,6 @@ async function resolveCodexRuntimeContext(options) {
8846
10436
  };
8847
10437
  }
8848
10438
  const state = await getAppState();
8849
- const profileName = normalizePathCandidate(state.app.lastProfileName);
8850
- if (profileName && state.profilesByName[profileName]) {
8851
- return {
8852
- codexHomePath: resolveProfileCodexHomeDir(profileName),
8853
- profileName,
8854
- source: "profile"
8855
- };
8856
- }
8857
10439
  const configured = normalizeCodexHomePath(
8858
10440
  state.runtimeSettings?.codexHome
8859
10441
  );
@@ -8912,10 +10494,10 @@ var YOLO_PRESET = {
8912
10494
 
8913
10495
  // ../../packages/runtime-codex/src/codex/config-io.ts
8914
10496
  function getConfigPath(options) {
8915
- return import_node_path13.default.join(resolveCodexHomeDir(options), "config.toml");
10497
+ return import_node_path14.default.join(resolveCodexHomeDir(options), "config.toml");
8916
10498
  }
8917
10499
  async function ensureConfigDirExists(filePath) {
8918
- const dir = import_node_path13.default.dirname(filePath);
10500
+ const dir = import_node_path14.default.dirname(filePath);
8919
10501
  await (0, import_promises2.mkdir)(dir, { recursive: true });
8920
10502
  }
8921
10503
  function normalizeLineEndings2(content) {
@@ -9569,13 +11151,13 @@ function formatMegabytes(bytes) {
9569
11151
  return (bytes / MB_DIVISOR).toFixed(2);
9570
11152
  }
9571
11153
  function resolveSyncBackupsDir() {
9572
- return import_node_path14.default.join(getUserDataDir(), SYNC_BACKUP_DIR);
11154
+ return import_node_path15.default.join(getUserDataDir(), SYNC_BACKUP_DIR);
9573
11155
  }
9574
11156
  function toSafeIsoForFileName(iso) {
9575
11157
  return iso.replace(/:/g, "-");
9576
11158
  }
9577
11159
  async function pruneOldPrePullBackups(backupsDir) {
9578
- const entries = await import_node_fs8.promises.readdir(backupsDir, { withFileTypes: true });
11160
+ const entries = await import_node_fs9.promises.readdir(backupsDir, { withFileTypes: true });
9579
11161
  const files = entries.filter(
9580
11162
  (entry) => entry.isFile() && entry.name.startsWith(PRE_PULL_BACKUP_PREFIX) && entry.name.endsWith(PRE_PULL_BACKUP_SUFFIX)
9581
11163
  ).map((entry) => entry.name).sort((a, b) => b.localeCompare(a));
@@ -9583,7 +11165,7 @@ async function pruneOldPrePullBackups(backupsDir) {
9583
11165
  return;
9584
11166
  }
9585
11167
  for (const stale of files.slice(PRE_PULL_BACKUP_KEEP_COUNT)) {
9586
- await import_node_fs8.promises.rm(import_node_path14.default.join(backupsDir, stale), { force: true });
11168
+ await import_node_fs9.promises.rm(import_node_path15.default.join(backupsDir, stale), { force: true });
9587
11169
  }
9588
11170
  }
9589
11171
  async function createPrePullBackup(profileManager) {
@@ -9591,13 +11173,13 @@ async function createPrePullBackup(profileManager) {
9591
11173
  enforcePushGuards: false
9592
11174
  });
9593
11175
  const backupsDir = resolveSyncBackupsDir();
9594
- await import_node_fs8.promises.mkdir(backupsDir, { recursive: true });
11176
+ await import_node_fs9.promises.mkdir(backupsDir, { recursive: true });
9595
11177
  const timestamp = toSafeIsoForFileName((/* @__PURE__ */ new Date()).toISOString());
9596
- const backupPath = import_node_path14.default.join(
11178
+ const backupPath = import_node_path15.default.join(
9597
11179
  backupsDir,
9598
11180
  `${PRE_PULL_BACKUP_PREFIX}${timestamp}${PRE_PULL_BACKUP_SUFFIX}`
9599
11181
  );
9600
- await import_node_fs8.promises.writeFile(backupPath, `${JSON.stringify(snapshot, null, 2)}
11182
+ await import_node_fs9.promises.writeFile(backupPath, `${JSON.stringify(snapshot, null, 2)}
9601
11183
  `, "utf8");
9602
11184
  await pruneOldPrePullBackups(backupsDir);
9603
11185
  logInfo("[cloud-sync] created pre-pull backup", { backupPath });
@@ -9866,9 +11448,9 @@ async function handleSync(args, version) {
9866
11448
  }
9867
11449
 
9868
11450
  // ../../packages/runtime-app-state/src/storage/migrations/v1.ts
9869
- var import_node_fs9 = require("fs");
9870
- var import_node_path15 = __toESM(require("path"), 1);
9871
- var import_node_os6 = __toESM(require("os"), 1);
11451
+ var import_node_fs10 = require("fs");
11452
+ var import_node_path16 = __toESM(require("path"), 1);
11453
+ var import_node_os7 = __toESM(require("os"), 1);
9872
11454
 
9873
11455
  // ../../packages/contracts/src/settings/legacy-localstorage-keys.ts
9874
11456
  var LEGACY_LOCALSTORAGE_KEYS = [
@@ -9907,20 +11489,20 @@ function isMissingPathError(error) {
9907
11489
  return error.code === "ENOENT";
9908
11490
  }
9909
11491
  function resolveHomeDir2() {
9910
- const home = process.env.HOME || process.env.USERPROFILE || import_node_os6.default.homedir();
11492
+ const home = process.env.HOME || process.env.USERPROFILE || import_node_os7.default.homedir();
9911
11493
  if (!home) {
9912
11494
  throw new Error("HOME is not set.");
9913
11495
  }
9914
11496
  return home;
9915
11497
  }
9916
11498
  function resolveCodexDir() {
9917
- return import_node_path15.default.join(resolveHomeDir2(), ".codex");
11499
+ return import_node_path16.default.join(resolveHomeDir2(), ".codex");
9918
11500
  }
9919
11501
  function resolveLegacyPath(...segments) {
9920
- return import_node_path15.default.join(resolveCodexDir(), ...segments);
11502
+ return import_node_path16.default.join(resolveCodexDir(), ...segments);
9921
11503
  }
9922
11504
  async function readJsonFile(filePath) {
9923
- const raw = await import_node_fs9.promises.readFile(filePath, "utf8");
11505
+ const raw = await import_node_fs10.promises.readFile(filePath, "utf8");
9924
11506
  return JSON.parse(raw);
9925
11507
  }
9926
11508
  async function readJsonFileIfExists(filePath) {
@@ -9935,14 +11517,14 @@ async function readJsonFileIfExists(filePath) {
9935
11517
  }
9936
11518
  }
9937
11519
  async function copyDirectoryManual(source, destination) {
9938
- await import_node_fs9.promises.mkdir(destination, { recursive: true });
9939
- const entries = await import_node_fs9.promises.readdir(source, { withFileTypes: true });
11520
+ await import_node_fs10.promises.mkdir(destination, { recursive: true });
11521
+ const entries = await import_node_fs10.promises.readdir(source, { withFileTypes: true });
9940
11522
  for (const entry of entries) {
9941
- const srcPath = import_node_path15.default.join(source, entry.name);
9942
- const destPath = import_node_path15.default.join(destination, entry.name);
11523
+ const srcPath = import_node_path16.default.join(source, entry.name);
11524
+ const destPath = import_node_path16.default.join(destination, entry.name);
9943
11525
  if (entry.isSymbolicLink()) {
9944
- const linkTarget = await import_node_fs9.promises.readlink(srcPath);
9945
- await import_node_fs9.promises.symlink(linkTarget, destPath).catch((error) => {
11526
+ const linkTarget = await import_node_fs10.promises.readlink(srcPath);
11527
+ await import_node_fs10.promises.symlink(linkTarget, destPath).catch((error) => {
9946
11528
  if (error.code !== "EEXIST") {
9947
11529
  throw error;
9948
11530
  }
@@ -9954,14 +11536,14 @@ async function copyDirectoryManual(source, destination) {
9954
11536
  continue;
9955
11537
  }
9956
11538
  if (entry.isFile()) {
9957
- await import_node_fs9.promises.copyFile(srcPath, destPath);
11539
+ await import_node_fs10.promises.copyFile(srcPath, destPath);
9958
11540
  }
9959
11541
  }
9960
11542
  }
9961
11543
  async function copyDirIfExists(source, destination) {
9962
11544
  let stats = null;
9963
11545
  try {
9964
- stats = await import_node_fs9.promises.stat(source);
11546
+ stats = await import_node_fs10.promises.stat(source);
9965
11547
  } catch (error) {
9966
11548
  if (error.code === "ENOENT") {
9967
11549
  return;
@@ -9971,10 +11553,10 @@ async function copyDirIfExists(source, destination) {
9971
11553
  if (!stats.isDirectory()) {
9972
11554
  return;
9973
11555
  }
9974
- await import_node_fs9.promises.rm(destination, { recursive: true, force: true });
9975
- await import_node_fs9.promises.mkdir(import_node_path15.default.dirname(destination), { recursive: true });
11556
+ await import_node_fs10.promises.rm(destination, { recursive: true, force: true });
11557
+ await import_node_fs10.promises.mkdir(import_node_path16.default.dirname(destination), { recursive: true });
9976
11558
  try {
9977
- await import_node_fs9.promises.cp(source, destination, {
11559
+ await import_node_fs10.promises.cp(source, destination, {
9978
11560
  recursive: true,
9979
11561
  errorOnExist: false,
9980
11562
  force: true,
@@ -9991,7 +11573,7 @@ async function copyDirIfExists(source, destination) {
9991
11573
  }
9992
11574
  async function copyFileIfExists(source, destination, options) {
9993
11575
  try {
9994
- const sourceStat = await import_node_fs9.promises.stat(source);
11576
+ const sourceStat = await import_node_fs10.promises.stat(source);
9995
11577
  if (!sourceStat.isFile()) {
9996
11578
  return;
9997
11579
  }
@@ -10002,7 +11584,7 @@ async function copyFileIfExists(source, destination, options) {
10002
11584
  throw error;
10003
11585
  }
10004
11586
  try {
10005
- const destinationStat = await import_node_fs9.promises.stat(destination);
11587
+ const destinationStat = await import_node_fs10.promises.stat(destination);
10006
11588
  if (destinationStat.isFile()) {
10007
11589
  return;
10008
11590
  }
@@ -10011,17 +11593,17 @@ async function copyFileIfExists(source, destination, options) {
10011
11593
  throw error;
10012
11594
  }
10013
11595
  }
10014
- await import_node_fs9.promises.mkdir(import_node_path15.default.dirname(destination), { recursive: true });
10015
- await import_node_fs9.promises.copyFile(source, destination);
11596
+ await import_node_fs10.promises.mkdir(import_node_path16.default.dirname(destination), { recursive: true });
11597
+ await import_node_fs10.promises.copyFile(source, destination);
10016
11598
  if (typeof options?.mode === "number") {
10017
- await import_node_fs9.promises.chmod(destination, options.mode).catch(() => void 0);
11599
+ await import_node_fs10.promises.chmod(destination, options.mode).catch(() => void 0);
10018
11600
  }
10019
11601
  }
10020
11602
  async function cleanupCopiedSkillsMetadata(skillsDir) {
10021
- await removeIfExists(import_node_path15.default.join(skillsDir, LEGACY_SKILLS_REPOS_FILE));
11603
+ await removeIfExists(import_node_path16.default.join(skillsDir, LEGACY_SKILLS_REPOS_FILE));
10022
11604
  let entries = [];
10023
11605
  try {
10024
- entries = await import_node_fs9.promises.readdir(skillsDir, { withFileTypes: true });
11606
+ entries = await import_node_fs10.promises.readdir(skillsDir, { withFileTypes: true });
10025
11607
  } catch {
10026
11608
  return;
10027
11609
  }
@@ -10029,30 +11611,30 @@ async function cleanupCopiedSkillsMetadata(skillsDir) {
10029
11611
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
10030
11612
  continue;
10031
11613
  }
10032
- await removeIfExists(import_node_path15.default.join(skillsDir, entry.name, LEGACY_SKILL_MANIFEST));
11614
+ await removeIfExists(import_node_path16.default.join(skillsDir, entry.name, LEGACY_SKILL_MANIFEST));
10033
11615
  }
10034
11616
  }
10035
11617
  async function migrateLegacyDirectoriesToUserData() {
10036
11618
  const userDataDir = getUserDataDir();
10037
11619
  const legacyCodexDir = resolveCodexDir();
10038
11620
  await copyDirIfExists(
10039
- import_node_path15.default.join(legacyCodexDir, LEGACY_PROFILE_HOMES_DIR),
10040
- import_node_path15.default.join(userDataDir, LEGACY_PROFILE_HOMES_DIR)
11621
+ import_node_path16.default.join(legacyCodexDir, LEGACY_PROFILE_HOMES_DIR),
11622
+ import_node_path16.default.join(userDataDir, LEGACY_PROFILE_HOMES_DIR)
10041
11623
  );
10042
11624
  await copyDirIfExists(
10043
- import_node_path15.default.join(legacyCodexDir, LEGACY_SKILLS_DIR),
10044
- import_node_path15.default.join(userDataDir, LEGACY_SKILLS_DIR)
11625
+ import_node_path16.default.join(legacyCodexDir, LEGACY_SKILLS_DIR),
11626
+ import_node_path16.default.join(userDataDir, LEGACY_SKILLS_DIR)
10045
11627
  );
10046
11628
  await copyDirIfExists(
10047
- import_node_path15.default.join(legacyCodexDir, LEGACY_SKILL_CACHE_DIR),
10048
- import_node_path15.default.join(userDataDir, LEGACY_SKILL_CACHE_DIR)
11629
+ import_node_path16.default.join(legacyCodexDir, LEGACY_SKILL_CACHE_DIR),
11630
+ import_node_path16.default.join(userDataDir, LEGACY_SKILL_CACHE_DIR)
10049
11631
  );
10050
11632
  await copyFileIfExists(
10051
- import_node_path15.default.join(legacyCodexDir, LEGACY_LICENSE_SECRET_FILE2),
10052
- import_node_path15.default.join(userDataDir, LEGACY_LICENSE_SECRET_FILE2),
11633
+ import_node_path16.default.join(legacyCodexDir, LEGACY_LICENSE_SECRET_FILE2),
11634
+ import_node_path16.default.join(userDataDir, LEGACY_LICENSE_SECRET_FILE2),
10053
11635
  { mode: 384 }
10054
11636
  );
10055
- await cleanupCopiedSkillsMetadata(import_node_path15.default.join(userDataDir, LEGACY_SKILLS_DIR));
11637
+ await cleanupCopiedSkillsMetadata(import_node_path16.default.join(userDataDir, LEGACY_SKILLS_DIR));
10056
11638
  }
10057
11639
  function pickAutoRoll(raw) {
10058
11640
  if (!raw) {
@@ -10137,9 +11719,13 @@ async function loadLegacySettingsPatch() {
10137
11719
  license,
10138
11720
  autoRoll: autoRoll ? {
10139
11721
  enabled: autoRoll.enabled,
10140
- warningThreshold: autoRoll.warningThreshold,
10141
- switchThreshold: autoRoll.switchThreshold,
10142
- restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll
11722
+ rearmRemainingThreshold: autoRoll.rearmRemainingThreshold,
11723
+ switchRemainingThreshold: autoRoll.switchRemainingThreshold,
11724
+ restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll,
11725
+ launchOfficialCodexWhenClosedOnAutoRoll: autoRoll.launchOfficialCodexWhenClosedOnAutoRoll,
11726
+ priorityOrder: autoRoll.priorityOrder,
11727
+ lowRemainingNotificationEnabled: autoRoll.lowRemainingNotificationEnabled,
11728
+ lowRemainingNotificationThreshold: autoRoll.lowRemainingNotificationThreshold
10143
11729
  } : void 0
10144
11730
  };
10145
11731
  }
@@ -10159,7 +11745,7 @@ async function loadLegacySyncPatch() {
10159
11745
  };
10160
11746
  }
10161
11747
  async function loadLegacyAppSettingsParityPatch() {
10162
- const filePath = import_node_path15.default.join(getUserDataDir(), LEGACY_APP_SETTINGS_PARITY_FILE);
11748
+ const filePath = import_node_path16.default.join(getUserDataDir(), LEGACY_APP_SETTINGS_PARITY_FILE);
10163
11749
  const raw = await readJsonFileIfExists(filePath);
10164
11750
  if (!isRecord6(raw)) {
10165
11751
  return {};
@@ -10172,7 +11758,7 @@ async function loadLegacyProfilesPatch() {
10172
11758
  const root = resolveLegacyPath(LEGACY_PROFILE_HOMES_DIR);
10173
11759
  let entries = [];
10174
11760
  try {
10175
- entries = await import_node_fs9.promises.readdir(root, { withFileTypes: true });
11761
+ entries = await import_node_fs10.promises.readdir(root, { withFileTypes: true });
10176
11762
  } catch (error) {
10177
11763
  if (error.code === "ENOENT") {
10178
11764
  return {};
@@ -10184,7 +11770,7 @@ async function loadLegacyProfilesPatch() {
10184
11770
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
10185
11771
  continue;
10186
11772
  }
10187
- const profileFile = import_node_path15.default.join(root, entry.name, "profile.json");
11773
+ const profileFile = import_node_path16.default.join(root, entry.name, "profile.json");
10188
11774
  const raw = await readJsonFileIfExists(profileFile);
10189
11775
  if (!raw) {
10190
11776
  continue;
@@ -10220,12 +11806,12 @@ function parseSkillInstallMetadata(raw) {
10220
11806
  }
10221
11807
  async function loadLegacySkillsPatch() {
10222
11808
  const skillsRoot = resolveLegacyPath(LEGACY_SKILLS_DIR);
10223
- const reposPath = import_node_path15.default.join(skillsRoot, LEGACY_SKILLS_REPOS_FILE);
11809
+ const reposPath = import_node_path16.default.join(skillsRoot, LEGACY_SKILLS_REPOS_FILE);
10224
11810
  const reposRaw = await readJsonFileIfExists(reposPath);
10225
11811
  const installsBySlug = {};
10226
11812
  let dirEntries = [];
10227
11813
  try {
10228
- dirEntries = await import_node_fs9.promises.readdir(skillsRoot, { withFileTypes: true });
11814
+ dirEntries = await import_node_fs10.promises.readdir(skillsRoot, { withFileTypes: true });
10229
11815
  } catch (error) {
10230
11816
  if (error.code !== "ENOENT") {
10231
11817
  throw error;
@@ -10238,7 +11824,7 @@ async function loadLegacySkillsPatch() {
10238
11824
  if (entry.name.startsWith(".")) {
10239
11825
  continue;
10240
11826
  }
10241
- const manifestPath = import_node_path15.default.join(skillsRoot, entry.name, LEGACY_SKILL_MANIFEST);
11827
+ const manifestPath = import_node_path16.default.join(skillsRoot, entry.name, LEGACY_SKILL_MANIFEST);
10242
11828
  const manifestRaw = await readJsonFileIfExists(manifestPath);
10243
11829
  const parsed = parseSkillInstallMetadata(manifestRaw);
10244
11830
  if (!parsed) {
@@ -10334,9 +11920,13 @@ function mergeLegacyLocalStoragePatch(payload) {
10334
11920
  if (autoRoll) {
10335
11921
  patch.autoRoll = {
10336
11922
  enabled: autoRoll.enabled,
10337
- warningThreshold: autoRoll.warningThreshold,
10338
- switchThreshold: autoRoll.switchThreshold,
10339
- restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll
11923
+ rearmRemainingThreshold: autoRoll.rearmRemainingThreshold,
11924
+ switchRemainingThreshold: autoRoll.switchRemainingThreshold,
11925
+ restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll,
11926
+ launchOfficialCodexWhenClosedOnAutoRoll: autoRoll.launchOfficialCodexWhenClosedOnAutoRoll,
11927
+ priorityOrder: autoRoll.priorityOrder,
11928
+ lowRemainingNotificationEnabled: autoRoll.lowRemainingNotificationEnabled,
11929
+ lowRemainingNotificationThreshold: autoRoll.lowRemainingNotificationThreshold
10340
11930
  };
10341
11931
  markSkippedIfPresent("codex:auto-roll-settings", true);
10342
11932
  } else {
@@ -10349,13 +11939,13 @@ function mergeLegacyLocalStoragePatch(payload) {
10349
11939
  };
10350
11940
  }
10351
11941
  async function removeIfExists(target) {
10352
- await import_node_fs9.promises.rm(target, { recursive: true, force: true });
11942
+ await import_node_fs10.promises.rm(target, { recursive: true, force: true });
10353
11943
  }
10354
11944
  async function cleanupLegacyCanonicalSources() {
10355
11945
  const homeDir = resolveHomeDir2();
10356
11946
  await removeIfExists(resolveLegacyAppStatePath());
10357
- await removeIfExists(import_node_path15.default.join(getUserDataDir(), LEGACY_APP_SETTINGS_PARITY_FILE));
10358
- await removeIfExists(import_node_path15.default.join(homeDir, SQLITE_STORAGE_DIR));
11947
+ await removeIfExists(import_node_path16.default.join(getUserDataDir(), LEGACY_APP_SETTINGS_PARITY_FILE));
11948
+ await removeIfExists(import_node_path16.default.join(homeDir, SQLITE_STORAGE_DIR));
10359
11949
  await removeIfExists(resolveLegacyPath(LEGACY_SETTINGS_FILE));
10360
11950
  await removeIfExists(resolveLegacyPath(LEGACY_SETTINGS_BACKUP_FILE));
10361
11951
  await removeIfExists(resolveLegacyPath(LEGACY_SYNC_STATE_FILE));
@@ -10363,15 +11953,15 @@ async function cleanupLegacyCanonicalSources() {
10363
11953
  await removeIfExists(resolveLegacyPath(LEGACY_SKILLS_DIR, LEGACY_SKILLS_REPOS_FILE));
10364
11954
  const profileHomesRoot = resolveLegacyPath(LEGACY_PROFILE_HOMES_DIR);
10365
11955
  try {
10366
- const profileHomes = await import_node_fs9.promises.readdir(profileHomesRoot, { withFileTypes: true });
11956
+ const profileHomes = await import_node_fs10.promises.readdir(profileHomesRoot, { withFileTypes: true });
10367
11957
  for (const profileHome of profileHomes) {
10368
11958
  if (!profileHome.isDirectory()) {
10369
11959
  continue;
10370
11960
  }
10371
- const profileDir = import_node_path15.default.join(profileHomesRoot, profileHome.name);
11961
+ const profileDir = import_node_path16.default.join(profileHomesRoot, profileHome.name);
10372
11962
  let files = [];
10373
11963
  try {
10374
- files = await import_node_fs9.promises.readdir(profileDir);
11964
+ files = await import_node_fs10.promises.readdir(profileDir);
10375
11965
  } catch {
10376
11966
  continue;
10377
11967
  }
@@ -10379,7 +11969,7 @@ async function cleanupLegacyCanonicalSources() {
10379
11969
  if (!file.startsWith("profile.json")) {
10380
11970
  continue;
10381
11971
  }
10382
- await removeIfExists(import_node_path15.default.join(profileDir, file));
11972
+ await removeIfExists(import_node_path16.default.join(profileDir, file));
10383
11973
  }
10384
11974
  }
10385
11975
  } catch (error) {
@@ -10392,12 +11982,12 @@ async function cleanupLegacyCanonicalSources() {
10392
11982
  }
10393
11983
  const skillsRoot = resolveLegacyPath(LEGACY_SKILLS_DIR);
10394
11984
  try {
10395
- const skillDirs = await import_node_fs9.promises.readdir(skillsRoot, { withFileTypes: true });
11985
+ const skillDirs = await import_node_fs10.promises.readdir(skillsRoot, { withFileTypes: true });
10396
11986
  for (const entry of skillDirs) {
10397
11987
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
10398
11988
  continue;
10399
11989
  }
10400
- await removeIfExists(import_node_path15.default.join(skillsRoot, entry.name, LEGACY_SKILL_MANIFEST));
11990
+ await removeIfExists(import_node_path16.default.join(skillsRoot, entry.name, LEGACY_SKILL_MANIFEST));
10401
11991
  }
10402
11992
  } catch (error) {
10403
11993
  if (!isMissingPathError(error)) {
@@ -10610,7 +12200,7 @@ async function ensureCliStorageReady() {
10610
12200
  }
10611
12201
 
10612
12202
  // src/app/main.ts
10613
- var VERSION = true ? "3.9.2" : "0.0.0";
12203
+ var VERSION = true ? "3.9.8" : "0.0.0";
10614
12204
  async function runCli() {
10615
12205
  const args = process.argv.slice(2);
10616
12206
  if (args.length === 0) {