codexuse-cli 2.5.4 → 2.5.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
@@ -743,6 +743,7 @@ var REFRESH_TOKEN_REDEEMED_SNIPPET = "refresh token was already used";
743
743
  var REFRESH_TOKEN_REDEEMED_REASON = "Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.";
744
744
  var AUTH_BACKUP_MISSING_MARKER = "__codexuse_missing_auth__";
745
745
  var DEFAULT_WORKSPACE_ID = "__default__";
746
+ var globalAuthSwapLock = Promise.resolve();
746
747
  var ProfileManager = class {
747
748
  constructor() {
748
749
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
@@ -751,7 +752,6 @@ var ProfileManager = class {
751
752
  this.activeAuth = (0, import_path2.join)(this.codexDir, "auth.json");
752
753
  this.activeAuthBackup = `${this.activeAuth}.swap`;
753
754
  this.lastActiveAuthErrorSignature = null;
754
- this.authSwapLock = Promise.resolve();
755
755
  }
756
756
  computeExpiryIso(data) {
757
757
  if (!data) {
@@ -1195,8 +1195,8 @@ var ProfileManager = class {
1195
1195
  await this.recoverActiveAuthBackup();
1196
1196
  }
1197
1197
  enqueueAuthSwap(task) {
1198
- const run = this.authSwapLock.then(task, task);
1199
- this.authSwapLock = run.then(
1198
+ const run = globalAuthSwapLock.then(task, task);
1199
+ globalAuthSwapLock = run.then(
1200
1200
  () => void 0,
1201
1201
  () => void 0
1202
1202
  );
@@ -2266,17 +2266,51 @@ async function getLicenseSecret() {
2266
2266
  return secret;
2267
2267
  }
2268
2268
 
2269
+ // ../../lib/offer-config.ts
2270
+ function getActiveOffer() {
2271
+ const basePriceUsd = 39;
2272
+ const discountPercent = 50;
2273
+ const salePriceUsd = basePriceUsd * (100 - discountPercent) / 100;
2274
+ return {
2275
+ basePriceUsd,
2276
+ basePriceDisplay: `$${basePriceUsd}`,
2277
+ couponCode: "SPRING50",
2278
+ discountPercent,
2279
+ salePriceUsd,
2280
+ salePriceDisplay: `$${salePriceUsd.toFixed(2)}`,
2281
+ isActive: true,
2282
+ campaign: "spring-2026",
2283
+ productPermalink: "codex-use",
2284
+ checkoutBaseUrl: "https://hweihwang.gumroad.com/l/codex-use"
2285
+ };
2286
+ }
2287
+ function buildCheckoutUrl(offer2, utm) {
2288
+ const base = offer2.checkoutBaseUrl;
2289
+ const withCoupon = offer2.isActive && offer2.couponCode ? `${base}/${offer2.couponCode}` : base;
2290
+ if (!utm) {
2291
+ return withCoupon;
2292
+ }
2293
+ const params = new URLSearchParams();
2294
+ params.set("utm_source", utm.source);
2295
+ params.set("utm_medium", utm.medium);
2296
+ if (utm.campaign) {
2297
+ params.set("utm_campaign", utm.campaign);
2298
+ } else if (offer2.campaign) {
2299
+ params.set("utm_campaign", offer2.campaign);
2300
+ }
2301
+ return `${withCoupon}?${params.toString()}`;
2302
+ }
2303
+
2269
2304
  // ../../lib/license-service.ts
2270
- var PRODUCT_PERMALINK = "codex-use";
2305
+ var offer = getActiveOffer();
2306
+ var PRODUCT_PERMALINK = offer.productPermalink;
2271
2307
  var PRODUCT_ID = "3_CcyVEXt2FOMiEpPx8xzw==";
2272
- var BASE_PRICE = 19;
2273
- var BASE_PRICE_DISPLAY = "$19";
2274
- var PROMO_CODE = "NOELNEWYEAR50";
2275
- var PROMO_PERCENT = 50;
2276
- var PROMO_ACTIVE = true;
2277
- var PROMO_PRICE_DISPLAY = `$${(BASE_PRICE * (100 - PROMO_PERCENT) / 100).toFixed(2)}`;
2278
- var PRODUCT_URL = PROMO_ACTIVE ? `https://hweihwang.gumroad.com/l/${PRODUCT_PERMALINK}/${PROMO_CODE}` : "https://hweihwang.gumroad.com/l/codex-use";
2279
- var LICENSE_PRICE_DISPLAY = PROMO_ACTIVE ? PROMO_PRICE_DISPLAY : BASE_PRICE_DISPLAY;
2308
+ var PRODUCT_URL = buildCheckoutUrl(offer);
2309
+ var LICENSE_PRICE_DISPLAY = offer.isActive ? offer.salePriceDisplay : offer.basePriceDisplay;
2310
+ var BASE_PRICE_DISPLAY = offer.basePriceDisplay;
2311
+ var PROMO_CODE = offer.couponCode;
2312
+ var PROMO_PERCENT = offer.discountPercent;
2313
+ var PROMO_ACTIVE = offer.isActive;
2280
2314
  var LICENSE_MAX_USES = 5;
2281
2315
  var FREE_PROFILE_LIMIT = 2;
2282
2316
  var LICENSE_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
@@ -2438,7 +2472,8 @@ function toLicenseStatus(stored, overrides = {}) {
2438
2472
  priceDisplay: LICENSE_PRICE_DISPLAY,
2439
2473
  basePriceDisplay: BASE_PRICE_DISPLAY,
2440
2474
  promoCode: PROMO_ACTIVE ? PROMO_CODE : null,
2441
- promoPercent: PROMO_ACTIVE ? PROMO_PERCENT : null
2475
+ promoPercent: PROMO_ACTIVE ? PROMO_PERCENT : null,
2476
+ maxDevices: LICENSE_MAX_USES
2442
2477
  };
2443
2478
  return { ...base, ...overrides };
2444
2479
  }
@@ -2607,6 +2642,25 @@ var LicenseService = class {
2607
2642
  }
2608
2643
  };
2609
2644
  var licenseService = new LicenseService();
2645
+ function isExplicitInvalidation(message) {
2646
+ if (!message) {
2647
+ return false;
2648
+ }
2649
+ const normalized = message.toLowerCase();
2650
+ return normalized.includes("revoked") || normalized.includes("refunded") || normalized.includes("invalid") || normalized.includes("no longer active");
2651
+ }
2652
+ function shouldEnforceProfileLimit(status) {
2653
+ if (status.isPro) {
2654
+ return false;
2655
+ }
2656
+ if (status.state === "grace" || status.state === "verifying" || status.state === "error") {
2657
+ return false;
2658
+ }
2659
+ if (isExplicitInvalidation(status.error)) {
2660
+ return true;
2661
+ }
2662
+ return status.state === "inactive";
2663
+ }
2610
2664
 
2611
2665
  // ../../lib/cloud-sync-service.ts
2612
2666
  var import_node_fs4 = require("fs");
@@ -2780,6 +2834,7 @@ var import_node_path4 = __toESM(require("path"));
2780
2834
  var import_node_fs3 = require("fs");
2781
2835
  var import_node_path3 = __toESM(require("path"));
2782
2836
  var STABLE_CHANNEL = "stable";
2837
+ var ENV_HINTS = ["CODEX_BINARY", "CODEX_CLI_PATH", "CODEX_PATH"];
2783
2838
  var cachedStatus = null;
2784
2839
  function fileExists(candidate) {
2785
2840
  if (!candidate) {
@@ -2819,16 +2874,68 @@ function resolveBundledCodexBinary() {
2819
2874
  }
2820
2875
  return null;
2821
2876
  }
2822
- function buildUnavailableStatus() {
2877
+ function isElectronRuntime() {
2878
+ return typeof process.versions?.electron === "string" && process.versions.electron.length > 0;
2879
+ }
2880
+ function resolveCodexFromEnv() {
2881
+ for (const key of ENV_HINTS) {
2882
+ const value = process.env[key];
2883
+ if (!value) {
2884
+ continue;
2885
+ }
2886
+ const resolved = fileExists(value);
2887
+ if (resolved) {
2888
+ return resolved;
2889
+ }
2890
+ }
2891
+ return null;
2892
+ }
2893
+ function resolveCodexFromNodeModules() {
2894
+ const packageSegments = ["@openai", "codex", "bin"];
2895
+ const fileNames = ["codex.js", "codex"];
2896
+ let current = import_node_path3.default.resolve(__dirname);
2897
+ let last = "";
2898
+ while (current !== last) {
2899
+ for (const fileName of fileNames) {
2900
+ const candidate = fileExists(
2901
+ import_node_path3.default.join(current, "node_modules", ...packageSegments, fileName)
2902
+ );
2903
+ if (candidate) {
2904
+ return candidate;
2905
+ }
2906
+ }
2907
+ last = current;
2908
+ current = import_node_path3.default.dirname(current);
2909
+ }
2910
+ return null;
2911
+ }
2912
+ function resolveCodexFromPath() {
2913
+ const pathValue = process.env.PATH ?? "";
2914
+ if (!pathValue) {
2915
+ return null;
2916
+ }
2917
+ const entries = pathValue.split(import_node_path3.default.delimiter).map((entry) => entry.trim()).filter(Boolean);
2918
+ const names = process.platform === "win32" ? ["codex.exe", "codex.cmd", "codex.bat", "codex"] : ["codex"];
2919
+ for (const entry of entries) {
2920
+ for (const name of names) {
2921
+ const candidate = fileExists(import_node_path3.default.join(entry, name));
2922
+ if (candidate) {
2923
+ return candidate;
2924
+ }
2925
+ }
2926
+ }
2927
+ return null;
2928
+ }
2929
+ function buildUnavailableStatus(reason) {
2823
2930
  return {
2824
2931
  available: false,
2825
2932
  path: null,
2826
- reason: "Bundled Codex CLI is missing. Reinstall CodexUse to restore the CLI.",
2933
+ reason: reason ?? "Codex CLI not found. Install @openai/codex or set CODEX_BINARY.",
2827
2934
  source: null,
2828
2935
  channel: STABLE_CHANNEL
2829
2936
  };
2830
2937
  }
2831
- function evaluateCodexCliStatus() {
2938
+ function evaluateBundledOnlyStatus() {
2832
2939
  const resolvedPath = resolveBundledCodexBinary();
2833
2940
  if (resolvedPath) {
2834
2941
  return {
@@ -2839,8 +2946,55 @@ function evaluateCodexCliStatus() {
2839
2946
  channel: STABLE_CHANNEL
2840
2947
  };
2841
2948
  }
2949
+ return buildUnavailableStatus("Bundled Codex CLI is missing. Reinstall CodexUse.");
2950
+ }
2951
+ function evaluateExternalStatus() {
2952
+ const envPath = resolveCodexFromEnv();
2953
+ if (envPath) {
2954
+ return {
2955
+ available: true,
2956
+ path: envPath,
2957
+ reason: null,
2958
+ source: "env",
2959
+ channel: STABLE_CHANNEL
2960
+ };
2961
+ }
2962
+ const nodeModulesPath = resolveCodexFromNodeModules();
2963
+ if (nodeModulesPath) {
2964
+ return {
2965
+ available: true,
2966
+ path: nodeModulesPath,
2967
+ reason: null,
2968
+ source: "node_modules",
2969
+ channel: STABLE_CHANNEL
2970
+ };
2971
+ }
2972
+ const pathResolved = resolveCodexFromPath();
2973
+ if (pathResolved) {
2974
+ return {
2975
+ available: true,
2976
+ path: pathResolved,
2977
+ reason: null,
2978
+ source: "path",
2979
+ channel: STABLE_CHANNEL
2980
+ };
2981
+ }
2842
2982
  return buildUnavailableStatus();
2843
2983
  }
2984
+ function evaluateCodexCliStatus() {
2985
+ const bundledStatus = evaluateBundledOnlyStatus();
2986
+ if (bundledStatus.available) {
2987
+ return bundledStatus;
2988
+ }
2989
+ const externalStatus = evaluateExternalStatus();
2990
+ if (externalStatus.available) {
2991
+ return externalStatus;
2992
+ }
2993
+ if (isElectronRuntime()) {
2994
+ return bundledStatus;
2995
+ }
2996
+ return externalStatus;
2997
+ }
2844
2998
  async function refreshCodexStatus() {
2845
2999
  cachedStatus = evaluateCodexCliStatus();
2846
3000
  return { ...cachedStatus };
@@ -3950,6 +4104,7 @@ async function pullCloudSync() {
3950
4104
  }
3951
4105
 
3952
4106
  // ../../lib/license-guard.ts
4107
+ var PROJECT_LIMIT_MESSAGE = "CodexUse Free supports up to 2 projects. Upgrade to CodexUse Pro for unlimited projects.";
3953
4108
  async function assertProfileCreationAllowed(profileManager) {
3954
4109
  const manager = profileManager ?? new ProfileManager();
3955
4110
  await manager.initialize();
@@ -3960,13 +4115,25 @@ async function assertProfileCreationAllowed(profileManager) {
3960
4115
  throw new Error("CodexUse Free supports up to 2 profiles. Upgrade to CodexUse Pro for unlimited profiles.");
3961
4116
  }
3962
4117
  }
4118
+ function countMainProjects(workspaces) {
4119
+ return workspaces.filter((workspace) => (workspace.kind ?? "main") !== "worktree").length;
4120
+ }
4121
+ async function assertProjectCreationAllowed(workspaces) {
4122
+ const license = await licenseService.getStatus();
4123
+ if (!shouldEnforceProfileLimit(license)) {
4124
+ return;
4125
+ }
4126
+ const mainProjectCount = countMainProjects(workspaces);
4127
+ if (mainProjectCount >= 2) {
4128
+ throw new Error(PROJECT_LIMIT_MESSAGE);
4129
+ }
4130
+ }
3963
4131
 
3964
4132
  // src/codex-cli.ts
3965
4133
  var import_node_child_process2 = require("child_process");
3966
4134
  var import_node_fs5 = require("fs");
3967
4135
  var import_node_path7 = __toESM(require("path"));
3968
- var ENV_HINTS = ["CODEX_BINARY", "CODEX_CLI_PATH", "CODEX_PATH"];
3969
- var NODE_MODULE_CANDIDATES = [["@openai", "codex"]];
4136
+ var ENV_HINTS2 = ["CODEX_BINARY", "CODEX_CLI_PATH", "CODEX_PATH"];
3970
4137
  function fileExists2(candidate) {
3971
4138
  if (!candidate) return null;
3972
4139
  const resolved = import_node_path7.default.resolve(candidate);
@@ -3979,7 +4146,7 @@ function fileExists2(candidate) {
3979
4146
  return null;
3980
4147
  }
3981
4148
  function resolveFromEnv() {
3982
- for (const key of ENV_HINTS) {
4149
+ for (const key of ENV_HINTS2) {
3983
4150
  const value = process.env[key];
3984
4151
  if (!value) continue;
3985
4152
  const resolved = fileExists2(value);
@@ -3987,37 +4154,6 @@ function resolveFromEnv() {
3987
4154
  }
3988
4155
  return null;
3989
4156
  }
3990
- function resolveFromAppResources() {
3991
- const roots = [
3992
- import_node_path7.default.resolve(__dirname, ".."),
3993
- import_node_path7.default.resolve(__dirname, "..", "..")
3994
- ];
3995
- for (const root of roots) {
3996
- const base = import_node_path7.default.join(root, "app.asar.unpacked", "node_modules");
3997
- for (const segments of NODE_MODULE_CANDIDATES) {
3998
- const candidate = fileExists2(import_node_path7.default.join(base, ...segments, "bin", "codex"));
3999
- if (candidate) return candidate;
4000
- const jsCandidate = fileExists2(import_node_path7.default.join(base, ...segments, "bin", "codex.js"));
4001
- if (jsCandidate) return jsCandidate;
4002
- }
4003
- }
4004
- return null;
4005
- }
4006
- function resolveFromNodeModules() {
4007
- let current = import_node_path7.default.resolve(__dirname, "..");
4008
- let last = "";
4009
- while (current !== last) {
4010
- for (const segments of NODE_MODULE_CANDIDATES) {
4011
- const candidate = fileExists2(import_node_path7.default.join(current, "node_modules", ...segments, "bin", "codex"));
4012
- if (candidate) return candidate;
4013
- const jsCandidate = fileExists2(import_node_path7.default.join(current, "node_modules", ...segments, "bin", "codex.js"));
4014
- if (jsCandidate) return jsCandidate;
4015
- }
4016
- last = current;
4017
- current = import_node_path7.default.dirname(current);
4018
- }
4019
- return null;
4020
- }
4021
4157
  function resolveFromPath() {
4022
4158
  const pathValue = process.env.PATH ?? "";
4023
4159
  const entries = pathValue.split(import_node_path7.default.delimiter).filter(Boolean);
@@ -4031,7 +4167,7 @@ function resolveFromPath() {
4031
4167
  return null;
4032
4168
  }
4033
4169
  function resolveCodexBinary() {
4034
- return resolveFromEnv() || resolveFromAppResources() || resolveFromNodeModules() || resolveFromPath();
4170
+ return resolveFromEnv() || resolveFromPath();
4035
4171
  }
4036
4172
  function requireCodexBinary(context) {
4037
4173
  const resolved = resolveCodexBinary();
@@ -4093,6 +4229,98 @@ var import_node_fs6 = require("fs");
4093
4229
  var import_node_os2 = __toESM(require("os"));
4094
4230
  var import_node_path8 = __toESM(require("path"));
4095
4231
  var RPC_TIMEOUT_MS = 1e4;
4232
+ var MAX_STDERR_CAPTURE_CHARS = 32768;
4233
+ var REFRESH_TOKEN_REDEEMED_SNIPPET2 = "refresh token was already used";
4234
+ function parseTimestamp2(value) {
4235
+ if (typeof value !== "string" || value.trim().length === 0) {
4236
+ return null;
4237
+ }
4238
+ const parsed = Date.parse(value);
4239
+ return Number.isNaN(parsed) ? null : parsed;
4240
+ }
4241
+ function decodeJwtPayload(token) {
4242
+ if (typeof token !== "string" || token.trim().length === 0) {
4243
+ return null;
4244
+ }
4245
+ const segments = token.split(".");
4246
+ if (segments.length < 2) {
4247
+ return null;
4248
+ }
4249
+ try {
4250
+ const payloadRaw = Buffer.from(segments[1], "base64url").toString("utf8");
4251
+ const payload = JSON.parse(payloadRaw);
4252
+ return payload && typeof payload === "object" ? payload : null;
4253
+ } catch {
4254
+ return null;
4255
+ }
4256
+ }
4257
+ function extractJwtIssuedAtMs(token) {
4258
+ const payload = decodeJwtPayload(token);
4259
+ if (!payload) {
4260
+ return null;
4261
+ }
4262
+ const issuedAt = payload["iat"];
4263
+ if (typeof issuedAt !== "number" || !Number.isFinite(issuedAt)) {
4264
+ return null;
4265
+ }
4266
+ return issuedAt * 1e3;
4267
+ }
4268
+ function parseAuthRecord(content) {
4269
+ try {
4270
+ const parsed = JSON.parse(content);
4271
+ return parsed && typeof parsed === "object" ? parsed : null;
4272
+ } catch {
4273
+ return null;
4274
+ }
4275
+ }
4276
+ function extractAuthRecencyMs(content) {
4277
+ const parsed = parseAuthRecord(content);
4278
+ if (!parsed) {
4279
+ return null;
4280
+ }
4281
+ const rootLastRefresh = parseTimestamp2(parsed.last_refresh);
4282
+ const nestedLastRefresh = parseTimestamp2(parsed.tokens?.last_refresh);
4283
+ const rootAccessIssuedAt = extractJwtIssuedAtMs(parsed.access_token);
4284
+ const nestedAccessIssuedAt = extractJwtIssuedAtMs(parsed.tokens?.access_token);
4285
+ const candidates = [rootLastRefresh, nestedLastRefresh, rootAccessIssuedAt, nestedAccessIssuedAt].filter((value) => typeof value === "number" && Number.isFinite(value));
4286
+ if (candidates.length === 0) {
4287
+ return null;
4288
+ }
4289
+ return Math.max(...candidates);
4290
+ }
4291
+ function shouldWriteBackAuth(initialSourceAuth, currentSourceAuth, updatedAuth) {
4292
+ if (updatedAuth.trim().length === 0) {
4293
+ return false;
4294
+ }
4295
+ if (!currentSourceAuth) {
4296
+ return true;
4297
+ }
4298
+ if (currentSourceAuth === updatedAuth) {
4299
+ return false;
4300
+ }
4301
+ if (currentSourceAuth === initialSourceAuth) {
4302
+ return true;
4303
+ }
4304
+ const currentRecency = extractAuthRecencyMs(currentSourceAuth);
4305
+ const updatedRecency = extractAuthRecencyMs(updatedAuth);
4306
+ if (typeof updatedRecency === "number" && typeof currentRecency === "number") {
4307
+ return updatedRecency > currentRecency;
4308
+ }
4309
+ if (typeof updatedRecency === "number" && typeof currentRecency !== "number") {
4310
+ return true;
4311
+ }
4312
+ return false;
4313
+ }
4314
+ function inferRefreshFailureHint(stderrOutput) {
4315
+ if (!stderrOutput) {
4316
+ return null;
4317
+ }
4318
+ const normalized = stderrOutput.toLowerCase();
4319
+ if (normalized.includes(REFRESH_TOKEN_REDEEMED_SNIPPET2)) {
4320
+ return REFRESH_TOKEN_REDEEMED_SNIPPET2;
4321
+ }
4322
+ return null;
4323
+ }
4096
4324
  async function sendPayload(child, payload) {
4097
4325
  child.stdin?.write(JSON.stringify(payload));
4098
4326
  child.stdin?.write("\n");
@@ -4198,17 +4426,18 @@ async function fetchRateLimitsViaRpc(envOverride, options = {}) {
4198
4426
  const binaryPath = options.codexPath ?? await requireCodexCli();
4199
4427
  const tempHome = await import_node_fs6.promises.mkdtemp(import_node_path8.default.join(import_node_os2.default.tmpdir(), "codex-rpc-"));
4200
4428
  const tempAuthPath = import_node_path8.default.join(tempHome, "auth.json");
4429
+ let initialSourceAuth = null;
4201
4430
  const sourceAuthPath = options.authPath ?? (envOverride?.CODEX_HOME ? import_node_path8.default.join(envOverride.CODEX_HOME, "auth.json") : import_node_path8.default.join(
4202
4431
  envOverride?.HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? import_node_os2.default.homedir(),
4203
4432
  ".codex",
4204
4433
  "auth.json"
4205
4434
  ));
4206
4435
  try {
4207
- const authContent = await import_node_fs6.promises.readFile(sourceAuthPath, "utf8").catch(() => null);
4208
- if (!authContent) {
4436
+ initialSourceAuth = await import_node_fs6.promises.readFile(sourceAuthPath, "utf8").catch(() => null);
4437
+ if (!initialSourceAuth) {
4209
4438
  return null;
4210
4439
  }
4211
- await import_node_fs6.promises.writeFile(tempAuthPath, authContent, "utf8");
4440
+ await import_node_fs6.promises.writeFile(tempAuthPath, initialSourceAuth, "utf8");
4212
4441
  } catch {
4213
4442
  await import_node_fs6.promises.rm(tempHome, { recursive: true, force: true }).catch(() => {
4214
4443
  });
@@ -4230,6 +4459,15 @@ async function fetchRateLimitsViaRpc(envOverride, options = {}) {
4230
4459
  input: child.stdout,
4231
4460
  crlfDelay: Infinity
4232
4461
  });
4462
+ let stderrOutput = "";
4463
+ child.stderr?.on("data", (chunk) => {
4464
+ if (stderrOutput.length >= MAX_STDERR_CAPTURE_CHARS) {
4465
+ return;
4466
+ }
4467
+ const text = chunk.toString("utf8");
4468
+ const remaining = MAX_STDERR_CAPTURE_CHARS - stderrOutput.length;
4469
+ stderrOutput += text.slice(0, Math.max(0, remaining));
4470
+ });
4233
4471
  try {
4234
4472
  await sendPayload(child, {
4235
4473
  id: 1,
@@ -4243,6 +4481,14 @@ async function fetchRateLimitsViaRpc(envOverride, options = {}) {
4243
4481
  await sendPayload(child, { method: "initialized", params: {} });
4244
4482
  await sendPayload(child, { id: 2, method: "account/rateLimits/read", params: {} });
4245
4483
  const message = await readRpcResponseById(rl, 2, RPC_TIMEOUT_MS);
4484
+ if (message.error) {
4485
+ const base = formatRpcError("account/rateLimits/read", message.error);
4486
+ const hint = inferRefreshFailureHint(stderrOutput);
4487
+ if (hint && !base.toLowerCase().includes(hint)) {
4488
+ throw new Error(`${base}; ${hint}`);
4489
+ }
4490
+ throw new Error(base);
4491
+ }
4246
4492
  return parseRateLimitSnapshotFromRpcMessage(message);
4247
4493
  } finally {
4248
4494
  child.kill();
@@ -4250,7 +4496,16 @@ async function fetchRateLimitsViaRpc(envOverride, options = {}) {
4250
4496
  try {
4251
4497
  const updatedAuth = await import_node_fs6.promises.readFile(tempAuthPath, "utf8");
4252
4498
  if (updatedAuth.trim().length > 0) {
4253
- await import_node_fs6.promises.writeFile(sourceAuthPath, updatedAuth, "utf8");
4499
+ const currentSourceAuth = await import_node_fs6.promises.readFile(sourceAuthPath, "utf8").catch(() => null);
4500
+ if (shouldWriteBackAuth(initialSourceAuth, currentSourceAuth, updatedAuth)) {
4501
+ await import_node_fs6.promises.writeFile(sourceAuthPath, updatedAuth, "utf8");
4502
+ } else if (currentSourceAuth && currentSourceAuth !== updatedAuth && currentSourceAuth !== initialSourceAuth) {
4503
+ logWarn("Skipped stale auth sync-back after rate-limit probe; source auth changed in flight.", {
4504
+ sourceAuthPath,
4505
+ currentRecencyMs: extractAuthRecencyMs(currentSourceAuth),
4506
+ updatedRecencyMs: extractAuthRecencyMs(updatedAuth)
4507
+ });
4508
+ }
4254
4509
  }
4255
4510
  } catch {
4256
4511
  }
@@ -5035,103 +5290,3795 @@ async function importLegacyLocalStorageOnce(payload) {
5035
5290
  return { completed: true, importedKeys: consumedKeys, skippedKeys };
5036
5291
  }
5037
5292
 
5038
- // src/index.ts
5039
- var VERSION = true ? "2.5.4" : "0.0.0";
5040
- var cliStorageReadyPromise = null;
5041
- async function ensureCliStorageReady() {
5042
- if (cliStorageReadyPromise) {
5043
- return cliStorageReadyPromise;
5044
- }
5045
- cliStorageReadyPromise = (async () => {
5046
- await initializeAppState(getUserDataDir());
5047
- const migrated = await runStorageMigrationV1();
5048
- if (migrated.migration.status === "pending_local_storage") {
5049
- await importLegacyLocalStorageOnce({});
5050
- }
5051
- const state = await getAppState();
5052
- if (state.migration.status !== "complete") {
5053
- throw new Error(
5054
- `Storage migration is not complete (status: ${state.migration.status}). CLI cannot continue until migration succeeds.`
5055
- );
5056
- }
5057
- })().catch((error) => {
5058
- cliStorageReadyPromise = null;
5059
- throw error;
5060
- });
5061
- return cliStorageReadyPromise;
5062
- }
5063
- function printHelp() {
5064
- console.log(`CodexUse CLI v${VERSION}
5065
-
5066
- Usage:
5067
- codexuse profile list [--no-usage] [--compact]
5068
- codexuse profile current
5069
- codexuse profile add <name> [--skip-login] [--device-auth] [--login=browser|device]
5070
- codexuse profile refresh <name> [--skip-login] [--device-auth] [--login=browser|device]
5071
- codexuse profile switch <name>
5072
- codexuse profile autoroll [--threshold=50-100] [--dry-run] [--watch] [--interval=seconds]
5073
- codexuse profile delete <name>
5074
- codexuse profile rename <old> <new>
5075
-
5076
- codexuse license status [--refresh]
5077
- codexuse license activate <license-key>
5293
+ // src/daemon.ts
5294
+ var import_node_path14 = __toESM(require("path"));
5078
5295
 
5079
- codexuse sync status
5080
- codexuse sync pull
5081
- codexuse sync push
5296
+ // ../../lib/codex-app-server.ts
5297
+ var import_node_child_process4 = require("child_process");
5298
+ var import_node_events = require("events");
5299
+ var import_node_readline2 = __toESM(require("readline"));
5300
+ var import_node_path10 = __toESM(require("path"));
5082
5301
 
5083
- Flags:
5084
- -h, --help Show help
5085
- -v, --version Show version
5086
- --no-usage Skip rate-limit usage fetch
5087
- --compact Names only
5088
- --device-auth Use device auth for Codex login
5089
- --login=MODE Login mode: browser | device
5090
- --threshold=NN Auto-roll switch threshold percent (50-100)
5091
- --watch Keep checking and auto-switch when threshold is reached
5092
- --interval=SEC Watch interval in seconds (default: 30)
5093
- --dry-run Print planned switch without changing active profile
5094
- `);
5302
+ // ../../lib/apps-list-rpc.ts
5303
+ var APPS_LIST_METHODS = /* @__PURE__ */ new Set(["app/list", "apps/list", "apps_list"]);
5304
+ function isAppsListMethod(method) {
5305
+ return APPS_LIST_METHODS.has(method);
5095
5306
  }
5096
- function hasFlag(args, flag) {
5097
- return args.includes(flag);
5307
+ function buildAppsListPayload(params) {
5308
+ return {
5309
+ threadId: params?.threadId ?? null,
5310
+ cursor: params?.cursor ?? null,
5311
+ limit: params?.limit ?? null
5312
+ };
5098
5313
  }
5099
- function stripFlags(args) {
5100
- return args.filter((arg) => !arg.startsWith("-"));
5314
+ function isMethodUnavailableError(error, method) {
5315
+ if (!error || typeof error !== "object") {
5316
+ return false;
5317
+ }
5318
+ const code = typeof error.code === "number" ? error.code : null;
5319
+ if (code !== -32601 && code !== -32600) {
5320
+ return false;
5321
+ }
5322
+ const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
5323
+ if (!message) {
5324
+ return false;
5325
+ }
5326
+ const normalizedMethod = method.toLowerCase();
5327
+ if (message.includes("method not found") || message.includes("unknown method")) {
5328
+ return true;
5329
+ }
5330
+ return message.includes("unknown variant") && message.includes(normalizedMethod);
5101
5331
  }
5102
- function parseLoginMode(flags) {
5103
- if (flags.includes("--device-auth")) return "device";
5104
- const explicit = flags.find((flag) => flag.startsWith("--login="));
5105
- if (!explicit) return null;
5106
- const value = explicit.split("=")[1];
5107
- if (value === "browser" || value === "device") {
5108
- return value;
5332
+ function isAppsListThreadNotFoundError(error) {
5333
+ if (!error || typeof error !== "object") {
5334
+ return false;
5109
5335
  }
5110
- return null;
5336
+ const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
5337
+ if (!message) {
5338
+ return false;
5339
+ }
5340
+ const mentionsThread = message.includes("thread");
5341
+ const mentionsNotFound = message.includes("not found") || message.includes("unknown thread");
5342
+ return mentionsThread && mentionsNotFound;
5111
5343
  }
5112
- var DEFAULT_AUTOROLL_INTERVAL_SECONDS = 30;
5113
- var DEFAULT_AUTOROLL_THRESHOLD = 95;
5114
- function parseNumericFlag(flags, name) {
5115
- const explicit = flags.find((flag) => flag.startsWith(`${name}=`));
5116
- if (!explicit) {
5344
+
5345
+ // ../../lib/cli-env.ts
5346
+ var import_path3 = __toESM(require("path"));
5347
+ function buildCliEnv(codexPath, overrides = {}) {
5348
+ const env = { ...process.env, ...overrides };
5349
+ const currentPath = env.PATH ?? "";
5350
+ const codexDir = import_path3.default.dirname(codexPath);
5351
+ const homeDir = process.env.HOME ?? "";
5352
+ const extraPathHints = (process.env.CODEX_PATH_HINTS ?? "").split(import_path3.default.delimiter).map((entry) => entry.trim()).filter(Boolean);
5353
+ const extras = [
5354
+ codexDir,
5355
+ "/usr/local/bin",
5356
+ "/opt/homebrew/bin",
5357
+ import_path3.default.join(homeDir, ".local", "bin"),
5358
+ import_path3.default.join(homeDir, ".fnm", "aliases", "default", "bin"),
5359
+ import_path3.default.join(homeDir, ".fnm", "current", "bin"),
5360
+ ...extraPathHints
5361
+ ].filter(Boolean);
5362
+ const segments = [
5363
+ ...extras,
5364
+ ...currentPath.split(import_path3.default.delimiter).filter(Boolean)
5365
+ ];
5366
+ env.PATH = Array.from(new Set(segments)).join(import_path3.default.delimiter);
5367
+ return env;
5368
+ }
5369
+
5370
+ // ../../lib/codex-app-server.ts
5371
+ function isRecord6(value) {
5372
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
5373
+ }
5374
+ function requestIdToToken(id) {
5375
+ return typeof id === "string" ? id : String(id);
5376
+ }
5377
+ function isAlreadyInitializedError(error) {
5378
+ if (!error || typeof error !== "object") return false;
5379
+ const message = error.message;
5380
+ return typeof message === "string" && message.toLowerCase().includes("already initialized");
5381
+ }
5382
+ function isThreadNotFoundError(error) {
5383
+ if (!error || typeof error !== "object") return false;
5384
+ const message = error.message;
5385
+ if (typeof message !== "string") return false;
5386
+ const lower = message.toLowerCase();
5387
+ return lower.includes("thread not found") || lower.includes("rollout");
5388
+ }
5389
+ function asString4(value) {
5390
+ return typeof value === "string" ? value : value != null ? String(value) : "";
5391
+ }
5392
+ function isInlineImageValue(value) {
5393
+ const normalized = value.trim().toLowerCase();
5394
+ return normalized.startsWith("data:") || normalized.startsWith("http://") || normalized.startsWith("https://");
5395
+ }
5396
+ function normalizeSendUserMessageItem(value) {
5397
+ if (!isRecord6(value)) {
5117
5398
  return null;
5118
5399
  }
5119
- const raw = explicit.slice(`${name}=`.length);
5120
- const value = Number.parseFloat(raw);
5121
- return Number.isFinite(value) ? value : Number.NaN;
5122
- }
5123
- function parseIntegerFlag(flags, name) {
5124
- const explicit = flags.find((flag) => flag.startsWith(`${name}=`));
5125
- if (!explicit) {
5400
+ const type = asString4(value.type).trim();
5401
+ if (!type) {
5126
5402
  return null;
5127
5403
  }
5128
- const raw = explicit.slice(`${name}=`.length);
5129
- const value = Number.parseInt(raw, 10);
5130
- return Number.isFinite(value) ? value : Number.NaN;
5131
- }
5132
- function formatProfileLabel(name, displayName) {
5133
- if (displayName && displayName.trim() && displayName !== name) {
5134
- return `${displayName} (${name})`;
5404
+ const data = isRecord6(value.data) ? value.data : {};
5405
+ if (type === "text") {
5406
+ const text = asString4(data.text ?? value.text);
5407
+ if (!text.trim()) {
5408
+ return null;
5409
+ }
5410
+ return { type: "text", data: { text } };
5411
+ }
5412
+ if (type === "image") {
5413
+ const imageUrl = asString4(
5414
+ data.image_url ?? data.imageUrl ?? data.url ?? value.image_url ?? value.imageUrl ?? value.url
5415
+ ).trim();
5416
+ if (!imageUrl) {
5417
+ return null;
5418
+ }
5419
+ return { type: "image", data: { image_url: imageUrl } };
5420
+ }
5421
+ if (type === "localImage") {
5422
+ const path17 = asString4(data.path ?? value.path).trim();
5423
+ if (!path17) {
5424
+ return null;
5425
+ }
5426
+ return { type: "localImage", data: { path: path17 } };
5427
+ }
5428
+ return value;
5429
+ }
5430
+ function normalizeTurnInputParams(params) {
5431
+ const normalized = { ...params };
5432
+ const threadId = asString4(
5433
+ params.threadId ?? params.thread_id ?? params.conversationId ?? params.conversation_id ?? ""
5434
+ ).trim();
5435
+ if (threadId && !asString4(params.threadId).trim()) {
5436
+ normalized.threadId = threadId;
5437
+ }
5438
+ const expectedTurnId = asString4(
5439
+ params.expectedTurnId ?? params.expected_turn_id ?? params.turnId ?? params.turn_id ?? ""
5440
+ ).trim();
5441
+ if (expectedTurnId && !asString4(params.expectedTurnId).trim()) {
5442
+ normalized.expectedTurnId = expectedTurnId;
5443
+ }
5444
+ const turnId = asString4(params.turnId ?? params.turn_id ?? expectedTurnId).trim();
5445
+ if (turnId && !asString4(params.turnId).trim()) {
5446
+ normalized.turnId = turnId;
5447
+ }
5448
+ const providedInput = Array.isArray(params.input) ? params.input.filter((entry) => isRecord6(entry)) : [];
5449
+ if (providedInput.length > 0) {
5450
+ normalized.input = providedInput;
5451
+ return normalized;
5452
+ }
5453
+ const providedItems = Array.isArray(params.items) ? params.items.map((item) => normalizeSendUserMessageItem(item)).filter((item) => item !== null) : [];
5454
+ if (providedItems.length > 0) {
5455
+ const input2 = providedItems.flatMap((item) => {
5456
+ const type = asString4(item.type).trim();
5457
+ const data = isRecord6(item.data) ? item.data : item;
5458
+ if (type === "text") {
5459
+ const text2 = asString4(data.text).trim();
5460
+ return text2 ? [{ type: "text", text: text2 }] : [];
5461
+ }
5462
+ if (type === "localImage") {
5463
+ const path17 = asString4(data.path).trim();
5464
+ return path17 ? [{ type: "localImage", path: path17 }] : [];
5465
+ }
5466
+ return [];
5467
+ });
5468
+ if (input2.length > 0) {
5469
+ normalized.input = input2;
5470
+ return normalized;
5471
+ }
5472
+ }
5473
+ const input = [];
5474
+ const text = asString4(params.text);
5475
+ if (text.trim()) {
5476
+ input.push({ type: "text", text });
5477
+ }
5478
+ if (Array.isArray(params.images)) {
5479
+ for (const candidate of params.images) {
5480
+ const image = asString4(candidate).trim();
5481
+ if (!image || isInlineImageValue(image)) {
5482
+ continue;
5483
+ }
5484
+ input.push({
5485
+ type: "localImage",
5486
+ path: image
5487
+ });
5488
+ }
5489
+ }
5490
+ if (input.length > 0) {
5491
+ normalized.input = input;
5492
+ }
5493
+ return normalized;
5494
+ }
5495
+ function pickMethodKind(method, params) {
5496
+ const normalized = method.toLowerCase();
5497
+ if (normalized.includes("requestapproval")) return "approval";
5498
+ if (normalized.includes("requestuserinput")) return "user_input";
5499
+ if (isRecord6(params)) {
5500
+ if ("file_changes" in params || "fileChanges" in params || "grant_root" in params || "grantRoot" in params) {
5501
+ return "patch";
5502
+ }
5503
+ if ("command" in params || "parsed_cmd" in params || "parsedCmd" in params || "cwd" in params) {
5504
+ return "exec";
5505
+ }
5506
+ }
5507
+ if (normalized.includes("apply") && normalized.includes("patch")) return "patch";
5508
+ if (normalized.includes("file") && normalized.includes("change")) return "patch";
5509
+ if (normalized.includes("exec") || normalized.includes("command")) return "exec";
5510
+ return null;
5511
+ }
5512
+ var DEFAULT_REQUEST_TIMEOUT_MS = 45e3;
5513
+ var THREAD_LIST_REQUEST_TIMEOUT_MS = 18e4;
5514
+ var MAX_PARSE_BUFFER_CHARS = 2e6;
5515
+ var DEFAULT_THREAD_LIVE_WORKSPACE_ID = "__default__";
5516
+ var REQUEST_TIMEOUT_WARNING_COOLDOWN_MS = 3e4;
5517
+ function isLikelyRecoverableJsonParseError(error) {
5518
+ if (!(error instanceof Error)) {
5519
+ return false;
5520
+ }
5521
+ const message = error.message.toLowerCase();
5522
+ return message.includes("unterminated string") || message.includes("unexpected end of json input") || message.includes("bad control character");
5523
+ }
5524
+ function isIgnorableAppsListTransportStderr(message) {
5525
+ const normalized = message.toLowerCase();
5526
+ if (!normalized.includes("backend-api/wham/apps")) {
5527
+ return false;
5528
+ }
5529
+ return normalized.includes("worker quit with fatal") && normalized.includes("transport channel closed");
5530
+ }
5531
+ function isIgnorableMissingSkillsSymlinkStderr(message) {
5532
+ const normalized = message.toLowerCase();
5533
+ return normalized.includes("codex_core::skills::loader") && normalized.includes("failed to stat skills entry") && normalized.includes("(symlink)") && normalized.includes("no such file or directory");
5534
+ }
5535
+ function isRequestTimeoutErrorMessage(value) {
5536
+ if (typeof value !== "string") {
5537
+ return false;
5538
+ }
5539
+ return value.toLowerCase().includes("request timed out");
5540
+ }
5541
+ function getRequestTimeoutMs(method) {
5542
+ if (method === "thread/list") {
5543
+ return THREAD_LIST_REQUEST_TIMEOUT_MS;
5544
+ }
5545
+ return DEFAULT_REQUEST_TIMEOUT_MS;
5546
+ }
5547
+ function normalizeThreadLiveWorkspaceId(value) {
5548
+ const workspaceId = asString4(value).trim();
5549
+ return workspaceId || DEFAULT_THREAD_LIVE_WORKSPACE_ID;
5550
+ }
5551
+ function threadLiveKey(workspaceId, threadId) {
5552
+ return `${workspaceId}:${threadId}`;
5553
+ }
5554
+ function escapeInvalidJsonStringChars(input) {
5555
+ let output = "";
5556
+ let inString = false;
5557
+ let escaped = false;
5558
+ for (let i = 0; i < input.length; i += 1) {
5559
+ const char = input[i];
5560
+ if (escaped) {
5561
+ output += char;
5562
+ escaped = false;
5563
+ continue;
5564
+ }
5565
+ if (char === "\\") {
5566
+ output += char;
5567
+ escaped = true;
5568
+ continue;
5569
+ }
5570
+ if (char === '"') {
5571
+ output += char;
5572
+ inString = !inString;
5573
+ continue;
5574
+ }
5575
+ if (inString) {
5576
+ if (char === "\n") {
5577
+ output += "\\n";
5578
+ continue;
5579
+ }
5580
+ if (char === "\r") {
5581
+ output += "\\r";
5582
+ continue;
5583
+ }
5584
+ if (char === " ") {
5585
+ output += "\\t";
5586
+ continue;
5587
+ }
5588
+ const code = char.charCodeAt(0);
5589
+ if (code >= 0 && code < 32) {
5590
+ output += `\\u${code.toString(16).padStart(4, "0")}`;
5591
+ continue;
5592
+ }
5593
+ }
5594
+ output += char;
5595
+ }
5596
+ return output;
5597
+ }
5598
+ var cachedRuntimeBinary = null;
5599
+ function resolveRuntimeBinary(env) {
5600
+ const override = env?.CODEX_NODE_RUNTIME?.trim() || env?.CODEX_NODE_BIN?.trim() || env?.CODEX_NODE?.trim() || process.env.CODEX_NODE_RUNTIME?.trim() || process.env.CODEX_NODE_BIN?.trim() || process.env.CODEX_NODE?.trim() || null;
5601
+ if (override) {
5602
+ return override;
5603
+ }
5604
+ if (cachedRuntimeBinary) {
5605
+ return cachedRuntimeBinary;
5606
+ }
5607
+ const runtimeEnv = { ...process.env, ...env };
5608
+ const homeDir = runtimeEnv.HOME ?? runtimeEnv.USERPROFILE ?? "";
5609
+ const pathHints = (runtimeEnv.CODEX_PATH_HINTS ?? process.env.CODEX_PATH_HINTS ?? "").split(import_node_path10.default.delimiter).map((entry) => entry.trim()).filter(Boolean);
5610
+ const extraPathEntries = [
5611
+ "/usr/local/bin",
5612
+ "/opt/homebrew/bin",
5613
+ import_node_path10.default.join(homeDir, ".local", "bin"),
5614
+ import_node_path10.default.join(homeDir, ".fnm", "aliases", "default", "bin"),
5615
+ import_node_path10.default.join(homeDir, ".fnm", "current", "bin"),
5616
+ ...pathHints
5617
+ ].filter(Boolean);
5618
+ const currentPath = runtimeEnv.PATH ?? "";
5619
+ runtimeEnv.PATH = Array.from(
5620
+ /* @__PURE__ */ new Set([...extraPathEntries, ...currentPath.split(import_node_path10.default.delimiter).filter(Boolean)])
5621
+ ).join(import_node_path10.default.delimiter);
5622
+ const candidates = Array.from(
5623
+ new Set(
5624
+ [
5625
+ runtimeEnv.CODEX_NODE_RUNTIME?.trim(),
5626
+ runtimeEnv.CODEX_NODE_BIN?.trim(),
5627
+ runtimeEnv.CODEX_NODE?.trim(),
5628
+ "/opt/homebrew/bin/node",
5629
+ "/usr/local/bin/node",
5630
+ "node"
5631
+ ].filter(Boolean)
5632
+ )
5633
+ );
5634
+ for (const candidate of candidates) {
5635
+ const probe = (0, import_node_child_process4.spawnSync)(candidate, ["-v"], { env: runtimeEnv, stdio: "ignore" });
5636
+ if (!probe.error && probe.status === 0) {
5637
+ cachedRuntimeBinary = candidate;
5638
+ return candidate;
5639
+ }
5640
+ }
5641
+ cachedRuntimeBinary = process.execPath;
5642
+ return process.execPath;
5643
+ }
5644
+ var CodexAppServer = class extends import_node_events.EventEmitter {
5645
+ constructor() {
5646
+ super();
5647
+ this.child = null;
5648
+ this.reader = null;
5649
+ this.nextRequestId = 1;
5650
+ this.pendingRequests = /* @__PURE__ */ new Map();
5651
+ this.pendingServerRequests = /* @__PURE__ */ new Map();
5652
+ this.timeoutRecoveryPromise = null;
5653
+ this.lastTimeoutWarningAtByMethod = /* @__PURE__ */ new Map();
5654
+ this.startPromise = null;
5655
+ this.initializePromise = null;
5656
+ this.initializeResponse = null;
5657
+ this.parseLineBuffer = null;
5658
+ this.threadLiveSubscriptionIdByKey = /* @__PURE__ */ new Map();
5659
+ this.threadLiveModeByKey = /* @__PURE__ */ new Map();
5660
+ this.addConversationListenerSupported = null;
5661
+ this.setMaxListeners(50);
5662
+ }
5663
+ async start() {
5664
+ if (this.child) {
5665
+ return;
5666
+ }
5667
+ if (!this.startPromise) {
5668
+ this.startPromise = this.spawnProcess().finally(() => {
5669
+ this.startPromise = null;
5670
+ });
5671
+ }
5672
+ await this.startPromise;
5673
+ }
5674
+ async stop() {
5675
+ if (this.reader) {
5676
+ this.reader.close();
5677
+ this.reader = null;
5678
+ }
5679
+ if (this.child) {
5680
+ const child = this.child;
5681
+ this.child = null;
5682
+ try {
5683
+ child.kill();
5684
+ } catch {
5685
+ }
5686
+ }
5687
+ this.initializePromise = null;
5688
+ this.initializeResponse = null;
5689
+ this.parseLineBuffer = null;
5690
+ this.threadLiveSubscriptionIdByKey.clear();
5691
+ this.threadLiveModeByKey.clear();
5692
+ this.addConversationListenerSupported = null;
5693
+ this.clearPendingRequests(new Error("codex app-server stopped"));
5694
+ }
5695
+ isRunning() {
5696
+ return Boolean(this.child && !this.child.killed);
5697
+ }
5698
+ async initialize(clientInfo) {
5699
+ if (this.initializeResponse) {
5700
+ return this.initializeResponse;
5701
+ }
5702
+ if (!this.initializePromise) {
5703
+ this.initializePromise = (async () => {
5704
+ await this.start();
5705
+ const payload = {
5706
+ clientInfo: clientInfo ?? {
5707
+ name: "codexuse",
5708
+ title: "CodexUse",
5709
+ version: "0.0.0"
5710
+ }
5711
+ };
5712
+ try {
5713
+ const response = await this.request("initialize", payload);
5714
+ await this.notify("initialized", {});
5715
+ this.initializeResponse = response;
5716
+ return response;
5717
+ } catch (error) {
5718
+ if (isAlreadyInitializedError(error)) {
5719
+ this.initializeResponse = this.initializeResponse ?? {};
5720
+ return this.initializeResponse;
5721
+ }
5722
+ throw error;
5723
+ }
5724
+ })().finally(() => {
5725
+ this.initializePromise = null;
5726
+ });
5727
+ }
5728
+ return this.initializePromise;
5729
+ }
5730
+ async getAccount(refreshToken = false) {
5731
+ return this.request("account/read", { refreshToken });
5732
+ }
5733
+ async getAccountRateLimits() {
5734
+ return this.request("account/rateLimits/read");
5735
+ }
5736
+ async loginAccount(params) {
5737
+ return this.request("account/login/start", params);
5738
+ }
5739
+ async cancelLoginAccount(loginId) {
5740
+ return this.request("account/login/cancel", { loginId });
5741
+ }
5742
+ async logoutAccount() {
5743
+ return this.request("account/logout");
5744
+ }
5745
+ async newConversation(params, overrides) {
5746
+ const merged = overrides ? { ...params, ...overrides } : params;
5747
+ return this.request("newConversation", merged);
5748
+ }
5749
+ async resumeConversation(params, overrides) {
5750
+ const merged = overrides ? { ...params, ...overrides } : params;
5751
+ return this.request("resumeConversation", merged);
5752
+ }
5753
+ async addConversationListenerWithResumeFallback(threadId, workspaceId = null) {
5754
+ if (this.addConversationListenerSupported === false) {
5755
+ return {};
5756
+ }
5757
+ const listenerParams = {
5758
+ conversationId: threadId
5759
+ };
5760
+ const resolvedWorkspaceId = workspaceId ? workspaceId.trim() : "";
5761
+ if (resolvedWorkspaceId) {
5762
+ listenerParams.workspaceId = resolvedWorkspaceId;
5763
+ listenerParams.workspace_id = resolvedWorkspaceId;
5764
+ }
5765
+ try {
5766
+ const response = await this.request("addConversationListener", listenerParams);
5767
+ this.addConversationListenerSupported = true;
5768
+ return isRecord6(response) ? response : null;
5769
+ } catch (error) {
5770
+ if (isMethodUnavailableError(error, "addConversationListener")) {
5771
+ this.addConversationListenerSupported = false;
5772
+ return {};
5773
+ }
5774
+ if (!isThreadNotFoundError(error)) {
5775
+ throw error;
5776
+ }
5777
+ await this.threadResume(
5778
+ resolvedWorkspaceId ? { threadId, workspaceId: resolvedWorkspaceId, workspace_id: resolvedWorkspaceId } : { threadId }
5779
+ );
5780
+ try {
5781
+ const response = await this.request("addConversationListener", listenerParams);
5782
+ this.addConversationListenerSupported = true;
5783
+ return isRecord6(response) ? response : null;
5784
+ } catch (resumeError) {
5785
+ if (isMethodUnavailableError(resumeError, "addConversationListener")) {
5786
+ this.addConversationListenerSupported = false;
5787
+ return {};
5788
+ }
5789
+ throw resumeError;
5790
+ }
5791
+ }
5792
+ }
5793
+ async removeConversationListenerBySubscriptionId(subscriptionId) {
5794
+ if (!subscriptionId || this.addConversationListenerSupported === false) {
5795
+ return;
5796
+ }
5797
+ try {
5798
+ await this.request("removeConversationListener", { subscriptionId });
5799
+ } catch (error) {
5800
+ if (isMethodUnavailableError(error, "removeConversationListener")) {
5801
+ this.addConversationListenerSupported = false;
5802
+ return;
5803
+ }
5804
+ throw error;
5805
+ }
5806
+ }
5807
+ async addConversationListener(params) {
5808
+ if (this.addConversationListenerSupported === false) {
5809
+ return {};
5810
+ }
5811
+ const conversationId = asString4(
5812
+ params.conversationId ?? params.threadId ?? params.thread_id ?? ""
5813
+ ).trim();
5814
+ const workspaceId = asString4(
5815
+ params.workspaceId ?? params.workspace_id ?? ""
5816
+ ).trim();
5817
+ if (!conversationId) {
5818
+ try {
5819
+ const response = await this.request("addConversationListener", params);
5820
+ this.addConversationListenerSupported = true;
5821
+ return response;
5822
+ } catch (error) {
5823
+ if (isMethodUnavailableError(error, "addConversationListener")) {
5824
+ this.addConversationListenerSupported = false;
5825
+ return {};
5826
+ }
5827
+ throw error;
5828
+ }
5829
+ }
5830
+ return this.addConversationListenerWithResumeFallback(
5831
+ conversationId,
5832
+ workspaceId || null
5833
+ );
5834
+ }
5835
+ async removeConversationListener(params) {
5836
+ if (this.addConversationListenerSupported === false) {
5837
+ return {};
5838
+ }
5839
+ try {
5840
+ return await this.request("removeConversationListener", params);
5841
+ } catch (error) {
5842
+ if (isMethodUnavailableError(error, "removeConversationListener")) {
5843
+ this.addConversationListenerSupported = false;
5844
+ return {};
5845
+ }
5846
+ throw error;
5847
+ }
5848
+ }
5849
+ async threadLiveSubscribe(params) {
5850
+ const workspaceId = normalizeThreadLiveWorkspaceId(
5851
+ params.workspaceId ?? params.workspace_id
5852
+ );
5853
+ const threadId = asString4(
5854
+ params.threadId ?? params.thread_id ?? params.conversationId
5855
+ ).trim();
5856
+ if (!threadId) {
5857
+ return {};
5858
+ }
5859
+ const key = threadLiveKey(workspaceId, threadId);
5860
+ this.threadLiveModeByKey.set(key, "listener");
5861
+ this.threadLiveSubscriptionIdByKey.delete(key);
5862
+ return {};
5863
+ }
5864
+ async threadLiveUnsubscribe(params) {
5865
+ const workspaceId = normalizeThreadLiveWorkspaceId(
5866
+ params.workspaceId ?? params.workspace_id
5867
+ );
5868
+ const threadId = asString4(
5869
+ params.threadId ?? params.thread_id ?? params.conversationId
5870
+ ).trim();
5871
+ if (!threadId) {
5872
+ return {};
5873
+ }
5874
+ const key = threadLiveKey(workspaceId, threadId);
5875
+ this.threadLiveModeByKey.delete(key);
5876
+ const subscriptionId = this.threadLiveSubscriptionIdByKey.get(key) ?? null;
5877
+ this.threadLiveSubscriptionIdByKey.delete(key);
5878
+ if (subscriptionId) {
5879
+ await this.removeConversationListenerBySubscriptionId(subscriptionId);
5880
+ }
5881
+ return {};
5882
+ }
5883
+ async sendUserMessage(params) {
5884
+ return this.request("turn/start", normalizeTurnInputParams(params));
5885
+ }
5886
+ async threadStart(params) {
5887
+ return this.request("thread/start", params);
5888
+ }
5889
+ async threadResume(params) {
5890
+ const sanitized = { ...params };
5891
+ delete sanitized.rolloutPath;
5892
+ delete sanitized.rollout_path;
5893
+ return this.request("thread/resume", sanitized);
5894
+ }
5895
+ async turnStart(params) {
5896
+ return this.request("turn/start", normalizeTurnInputParams(params));
5897
+ }
5898
+ async steerTurn(params) {
5899
+ return this.request("turn/steer", normalizeTurnInputParams(params));
5900
+ }
5901
+ async interruptTurn(params) {
5902
+ return this.request("turn/interrupt", params);
5903
+ }
5904
+ async turnSteer(params) {
5905
+ return this.steerTurn(params);
5906
+ }
5907
+ async turnInterrupt(params) {
5908
+ return this.interruptTurn(params);
5909
+ }
5910
+ async interruptConversation(params) {
5911
+ return this.request("interruptConversation", params);
5912
+ }
5913
+ async archiveConversation(conversationId) {
5914
+ try {
5915
+ await this.request("thread/archive", { threadId: conversationId });
5916
+ return;
5917
+ } catch {
5918
+ }
5919
+ try {
5920
+ await this.request("archiveConversation", { conversationId });
5921
+ } catch {
5922
+ }
5923
+ }
5924
+ async listConversations(params) {
5925
+ return this.request("thread/list", {
5926
+ cursor: params.cursor ?? null,
5927
+ limit: params.limit ?? 20,
5928
+ cwd: params.cwd ?? null,
5929
+ sortKey: params.sortKey ?? null,
5930
+ searchQuery: params.searchQuery ?? null,
5931
+ sourceKinds: params.sourceKinds ?? null,
5932
+ archived: params.archived ?? null,
5933
+ modelProviders: params.modelProviders ?? null
5934
+ });
5935
+ }
5936
+ async listSkillsCore(params) {
5937
+ const cwd = typeof params?.cwd === "string" ? params.cwd.trim() : "";
5938
+ return this.request("skills/list", {
5939
+ cwds: cwd ? [cwd] : [],
5940
+ forceReload: Boolean(params?.forceReload)
5941
+ });
5942
+ }
5943
+ async listApps(params) {
5944
+ const payload = buildAppsListPayload(params);
5945
+ const methods = ["app/list", "apps/list", "apps_list"];
5946
+ for (let index = 0; index < methods.length; index += 1) {
5947
+ const method = methods[index];
5948
+ try {
5949
+ return await this.request(method, payload);
5950
+ } catch (error) {
5951
+ const hasLegacyFallback = index < methods.length - 1;
5952
+ if (hasLegacyFallback && isMethodUnavailableError(error, method)) {
5953
+ continue;
5954
+ }
5955
+ if (isAppsListThreadNotFoundError(error)) {
5956
+ return { data: [] };
5957
+ }
5958
+ throw error;
5959
+ }
5960
+ }
5961
+ throw new Error("Apps list is unavailable from the connected Codex backend.");
5962
+ }
5963
+ async setSkillEnabledCore(params) {
5964
+ const skillPath = typeof params.path === "string" ? params.path.trim() : "";
5965
+ if (!skillPath) {
5966
+ throw new Error("Skill path is required.");
5967
+ }
5968
+ return this.request("skills/config/write", {
5969
+ path: skillPath,
5970
+ enabled: Boolean(params.enabled)
5971
+ });
5972
+ }
5973
+ async readThread(params) {
5974
+ return this.request("thread/read", {
5975
+ threadId: params.threadId,
5976
+ includeTurns: Boolean(params.includeTurns)
5977
+ });
5978
+ }
5979
+ async setThreadName(threadId, name) {
5980
+ return this.request("thread/name/set", { threadId, name });
5981
+ }
5982
+ async startReview(params) {
5983
+ return this.request("review/start", params);
5984
+ }
5985
+ async startCompact(params) {
5986
+ try {
5987
+ return await this.request("thread/compact/start", params);
5988
+ } catch {
5989
+ try {
5990
+ return await this.request("thread/compact", params);
5991
+ } catch {
5992
+ return this.request("compactThread", params);
5993
+ }
5994
+ }
5995
+ }
5996
+ async respondExecCommandRequest(requestToken, decision) {
5997
+ await this.respondReviewDecision(requestToken, "exec", decision);
5998
+ }
5999
+ async respondApplyPatchRequest(requestToken, decision) {
6000
+ await this.respondReviewDecision(requestToken, "patch", decision);
6001
+ }
6002
+ async respondToServerRequest(requestId, result) {
6003
+ const token = requestIdToToken(requestId);
6004
+ const pending = this.pendingServerRequests.get(token);
6005
+ if (!pending) {
6006
+ throw new Error(`Unknown server request: ${requestId}`);
6007
+ }
6008
+ this.pendingServerRequests.delete(token);
6009
+ await this.sendResponse(pending.id, result ?? {});
6010
+ }
6011
+ async spawnProcess() {
6012
+ const codexPath = await requireCodexCli();
6013
+ const env = buildCliEnv(codexPath, { ELECTRON_RUN_AS_NODE: "1" });
6014
+ const runtimeBin = resolveRuntimeBinary(env);
6015
+ const child = (0, import_node_child_process4.spawn)(runtimeBin, [codexPath, "app-server"], {
6016
+ stdio: ["pipe", "pipe", "pipe"],
6017
+ env
6018
+ });
6019
+ this.child = child;
6020
+ this.initializeResponse = null;
6021
+ this.initializePromise = null;
6022
+ this.parseLineBuffer = null;
6023
+ this.addConversationListenerSupported = null;
6024
+ child.on("exit", (code, signal) => {
6025
+ logWarn("[app-server] exited", { code, signal });
6026
+ this.child = null;
6027
+ this.initializeResponse = null;
6028
+ this.initializePromise = null;
6029
+ this.threadLiveSubscriptionIdByKey.clear();
6030
+ this.threadLiveModeByKey.clear();
6031
+ this.addConversationListenerSupported = null;
6032
+ this.clearPendingRequests(new Error("codex app-server exited"));
6033
+ this.emit("codex:process-exited", {});
6034
+ });
6035
+ child.on("error", (error) => {
6036
+ logError("[app-server] process error", error);
6037
+ this.threadLiveSubscriptionIdByKey.clear();
6038
+ this.threadLiveModeByKey.clear();
6039
+ this.addConversationListenerSupported = null;
6040
+ this.emit("codex:process-exited", {});
6041
+ });
6042
+ if (child.stderr) {
6043
+ child.stderr.on("data", (chunk) => {
6044
+ const lines = chunk.toString().split(/\r?\n/);
6045
+ for (const line of lines) {
6046
+ const message = line.trim();
6047
+ if (!message) continue;
6048
+ const normalized = message.toLowerCase();
6049
+ if (normalized.includes("state db missing rollout path for thread")) {
6050
+ continue;
6051
+ }
6052
+ if (normalized.includes("rollout::recorder") && normalized.includes("falling back on rollout system")) {
6053
+ continue;
6054
+ }
6055
+ if (isIgnorableAppsListTransportStderr(message)) {
6056
+ continue;
6057
+ }
6058
+ if (isIgnorableMissingSkillsSymlinkStderr(message)) {
6059
+ continue;
6060
+ }
6061
+ logWarn("[app-server] stderr", message);
6062
+ }
6063
+ });
6064
+ }
6065
+ if (!child.stdout) {
6066
+ throw new Error("codex app-server missing stdout");
6067
+ }
6068
+ this.reader = import_node_readline2.default.createInterface({
6069
+ input: child.stdout,
6070
+ crlfDelay: Infinity
6071
+ });
6072
+ this.reader.on("line", (line) => {
6073
+ this.handleIncomingLine(line);
6074
+ });
6075
+ logInfo("[app-server] started");
6076
+ }
6077
+ handleIncomingLine(line) {
6078
+ if (this.parseLineBuffer !== null) {
6079
+ const merged = `${this.parseLineBuffer}
6080
+ ${line}`;
6081
+ const handled2 = this.tryHandleMessage(merged);
6082
+ if (handled2) {
6083
+ this.parseLineBuffer = null;
6084
+ return;
6085
+ }
6086
+ if (merged.length < MAX_PARSE_BUFFER_CHARS) {
6087
+ this.parseLineBuffer = merged;
6088
+ return;
6089
+ }
6090
+ this.logParseFailure(
6091
+ merged,
6092
+ new Error("Buffered JSON message exceeded max size while recovering malformed output.")
6093
+ );
6094
+ this.parseLineBuffer = null;
6095
+ return;
6096
+ }
6097
+ if (!line.trim()) {
6098
+ return;
6099
+ }
6100
+ const handled = this.tryHandleMessage(line);
6101
+ if (handled) {
6102
+ return;
6103
+ }
6104
+ if (line.trimStart().startsWith("{")) {
6105
+ this.parseLineBuffer = line;
6106
+ }
6107
+ }
6108
+ tryHandleMessage(line) {
6109
+ let parsed;
6110
+ let parseError = null;
6111
+ try {
6112
+ parsed = JSON.parse(line);
6113
+ } catch (error) {
6114
+ parseError = error;
6115
+ const repaired = escapeInvalidJsonStringChars(line);
6116
+ if (repaired !== line) {
6117
+ try {
6118
+ parsed = JSON.parse(repaired);
6119
+ parseError = null;
6120
+ } catch (repairError) {
6121
+ parseError = repairError;
6122
+ }
6123
+ }
6124
+ }
6125
+ if (parseError) {
6126
+ if (isLikelyRecoverableJsonParseError(parseError)) {
6127
+ return false;
6128
+ }
6129
+ this.logParseFailure(line, parseError);
6130
+ return true;
6131
+ }
6132
+ if (!isRecord6(parsed)) {
6133
+ logWarn("[app-server] unexpected JSON message", parsed);
6134
+ return true;
6135
+ }
6136
+ const hasId = "id" in parsed;
6137
+ const hasMethod = typeof parsed.method === "string";
6138
+ const hasResult = "result" in parsed;
6139
+ const hasError = "error" in parsed;
6140
+ if (hasMethod && hasId) {
6141
+ this.handleServerRequest(parsed);
6142
+ return true;
6143
+ }
6144
+ if (hasMethod) {
6145
+ this.handleNotification(parsed);
6146
+ return true;
6147
+ }
6148
+ if (hasId && (hasResult || hasError)) {
6149
+ this.handleResponse(parsed);
6150
+ return true;
6151
+ }
6152
+ logWarn("[app-server] unknown message shape", parsed);
6153
+ return true;
6154
+ }
6155
+ logParseFailure(line, error) {
6156
+ const message = error instanceof Error ? error.message : String(error);
6157
+ logWarn("[app-server] failed to parse JSON", {
6158
+ error: message,
6159
+ length: line.length,
6160
+ startsWithJsonObject: line.trimStart().startsWith("{")
6161
+ });
6162
+ }
6163
+ handleResponse(response) {
6164
+ const token = requestIdToToken(response.id);
6165
+ const pending = this.pendingRequests.get(token);
6166
+ if (!pending) {
6167
+ logWarn("[app-server] response with no pending request", response.id);
6168
+ return;
6169
+ }
6170
+ this.pendingRequests.delete(token);
6171
+ if (response.error) {
6172
+ const error = new Error(response.error.message || "codex app-server error");
6173
+ error.code = response.error.code;
6174
+ error.data = response.error.data;
6175
+ const suppressBackendErrorEvent = isAppsListMethod(pending.method) || pending.method === "addConversationListener" && isThreadNotFoundError(error) || pending.method === "thread/resume" && isThreadNotFoundError(error);
6176
+ if (!suppressBackendErrorEvent) {
6177
+ this.emit("codex:backend-error", {
6178
+ code: response.error.code,
6179
+ message: response.error.message,
6180
+ data: response.error.data
6181
+ });
6182
+ }
6183
+ pending.reject(error);
6184
+ return;
6185
+ }
6186
+ pending.resolve(response.result);
6187
+ }
6188
+ handleNotification(notification) {
6189
+ const { method, params } = notification;
6190
+ if (method.startsWith("codex/event/")) {
6191
+ this.emit("codex:event", { method, params });
6192
+ return;
6193
+ }
6194
+ const normalized = method.toLowerCase();
6195
+ if (normalized.includes("auth") && normalized.includes("status")) {
6196
+ this.emit("codex:auth-status", params ?? {});
6197
+ return;
6198
+ }
6199
+ if (normalized.includes("login") && normalized.includes("complete")) {
6200
+ this.emit("codex:login-complete", params ?? {});
6201
+ return;
6202
+ }
6203
+ this.emit("codex:notification", { method, params });
6204
+ }
6205
+ handleServerRequest(request) {
6206
+ const kind = pickMethodKind(request.method, request.params);
6207
+ if (!kind) {
6208
+ void this.sendError(request.id, {
6209
+ code: -32601,
6210
+ message: `Unsupported request: ${request.method}`
6211
+ });
6212
+ return;
6213
+ }
6214
+ const token = requestIdToToken(request.id);
6215
+ this.pendingServerRequests.set(token, { id: request.id, kind, method: request.method });
6216
+ const params = isRecord6(request.params) ? request.params : {};
6217
+ if (kind === "exec") {
6218
+ this.emit("codex:exec-command-request", {
6219
+ requestToken: token,
6220
+ params
6221
+ });
6222
+ } else if (kind === "patch") {
6223
+ this.emit("codex:apply-patch-request", {
6224
+ requestToken: token,
6225
+ params
6226
+ });
6227
+ } else {
6228
+ this.emit("codex:server-request", {
6229
+ requestId: request.id,
6230
+ method: request.method,
6231
+ params
6232
+ });
6233
+ }
6234
+ }
6235
+ async respondReviewDecision(requestToken, kind, decision) {
6236
+ const pending = this.pendingServerRequests.get(requestToken);
6237
+ if (!pending) {
6238
+ throw new Error(`Unknown approval request: ${requestToken}`);
6239
+ }
6240
+ if (pending.kind !== kind) {
6241
+ throw new Error(`Mismatched approval request type for ${requestToken}`);
6242
+ }
6243
+ this.pendingServerRequests.delete(requestToken);
6244
+ await this.sendResponse(pending.id, { decision });
6245
+ }
6246
+ async request(method, params) {
6247
+ if (method !== "initialize") {
6248
+ await this.initialize();
6249
+ } else {
6250
+ await this.start();
6251
+ }
6252
+ const id = this.nextRequestId++;
6253
+ const token = requestIdToToken(id);
6254
+ const timeoutMs = getRequestTimeoutMs(method);
6255
+ const payload = params ? { id, method, params } : { id, method };
6256
+ const result = new Promise((resolve, reject) => {
6257
+ const timeout = setTimeout(() => {
6258
+ const pending = this.pendingRequests.get(token);
6259
+ if (!pending) {
6260
+ return;
6261
+ }
6262
+ this.pendingRequests.delete(token);
6263
+ const timeoutError = new Error(
6264
+ `codex app-server request timed out after ${timeoutMs}ms (${method})`
6265
+ );
6266
+ pending.reject(timeoutError);
6267
+ this.maybeRecoverFromTimeout(method, timeoutError);
6268
+ }, timeoutMs);
6269
+ this.pendingRequests.set(token, {
6270
+ method,
6271
+ resolve: (value) => {
6272
+ clearTimeout(timeout);
6273
+ resolve(value);
6274
+ },
6275
+ reject: (error) => {
6276
+ clearTimeout(timeout);
6277
+ reject(error);
6278
+ }
6279
+ });
6280
+ });
6281
+ await this.writeMessage(payload);
6282
+ return result;
6283
+ }
6284
+ async notify(method, params) {
6285
+ await this.start();
6286
+ const payload = params ? { method, params } : { method };
6287
+ await this.writeMessage(payload);
6288
+ }
6289
+ async sendResponse(id, result) {
6290
+ await this.writeMessage({ id, result });
6291
+ }
6292
+ async sendError(id, error) {
6293
+ await this.writeMessage({ id, error });
6294
+ }
6295
+ async writeMessage(message) {
6296
+ if (!this.child || !this.child.stdin) {
6297
+ throw new Error("codex app-server is not running");
6298
+ }
6299
+ const payload = JSON.stringify({ jsonrpc: "2.0", ...message });
6300
+ this.child.stdin.write(`${payload}
6301
+ `);
6302
+ }
6303
+ clearPendingRequests(error) {
6304
+ for (const pending of this.pendingRequests.values()) {
6305
+ pending.reject(error);
6306
+ }
6307
+ this.pendingRequests.clear();
6308
+ this.pendingServerRequests.clear();
6309
+ this.threadLiveSubscriptionIdByKey.clear();
6310
+ this.threadLiveModeByKey.clear();
6311
+ }
6312
+ maybeRecoverFromTimeout(method, error) {
6313
+ const message = error instanceof Error ? error.message : String(error ?? "");
6314
+ if (!isRequestTimeoutErrorMessage(message)) {
6315
+ return;
6316
+ }
6317
+ if (method !== "initialize") {
6318
+ const now = Date.now();
6319
+ const lastWarning = this.lastTimeoutWarningAtByMethod.get(method) ?? 0;
6320
+ if (now - lastWarning >= REQUEST_TIMEOUT_WARNING_COOLDOWN_MS) {
6321
+ this.lastTimeoutWarningAtByMethod.set(method, now);
6322
+ logWarn("[app-server] request timed out (keeping process alive)", { method });
6323
+ }
6324
+ return;
6325
+ }
6326
+ if (this.timeoutRecoveryPromise) {
6327
+ return;
6328
+ }
6329
+ this.timeoutRecoveryPromise = (async () => {
6330
+ logWarn("[app-server] recycling after request timeout", { method });
6331
+ try {
6332
+ await this.stop();
6333
+ } catch (stopError) {
6334
+ logWarn("[app-server] failed to recycle after timeout", stopError);
6335
+ }
6336
+ })().finally(() => {
6337
+ this.timeoutRecoveryPromise = null;
6338
+ });
6339
+ }
6340
+ };
6341
+
6342
+ // ../../lib/workspace-parity-store.ts
6343
+ var import_node_path12 = __toESM(require("path"));
6344
+ var import_promises3 = __toESM(require("fs/promises"));
6345
+ var import_node_crypto3 = require("crypto");
6346
+
6347
+ // ../../lib/git/git-service.ts
6348
+ var import_node_fs8 = __toESM(require("fs"));
6349
+ var import_node_path11 = __toESM(require("path"));
6350
+ var import_node_child_process6 = require("child_process");
6351
+ var import_node_util2 = require("util");
6352
+
6353
+ // ../../lib/git/git-cli.ts
6354
+ var import_node_child_process5 = require("child_process");
6355
+ var import_node_util = require("util");
6356
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process5.execFile);
6357
+ var MAX_BUFFER_BYTES = 10 * 1024 * 1024;
6358
+
6359
+ // ../../lib/git/git-service.ts
6360
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process6.execFile);
6361
+
6362
+ // ../../lib/workspace-parity-store.ts
6363
+ var STORE_FILE = "workspace-parity.json";
6364
+ var LEGACY_STORE_FILE = "workspace-parity.json";
6365
+ var PROFILE_ROOT_DIR = "profiles";
6366
+ var PROFILE_PARITY_DIR = "parity";
6367
+ var LEGACY_MIGRATION_MARKER_FILE = "workspace-parity-profile-migration.json";
6368
+ function defaultSettings() {
6369
+ return {
6370
+ sidebarCollapsed: false,
6371
+ sortOrder: null,
6372
+ groupId: null,
6373
+ cloneSourceWorkspaceId: null,
6374
+ gitRoot: null,
6375
+ launchScript: null,
6376
+ launchScripts: null,
6377
+ worktreeSetupScript: null
6378
+ };
6379
+ }
6380
+ function defaultStore() {
6381
+ return {
6382
+ version: 1,
6383
+ workspaces: []
6384
+ };
6385
+ }
6386
+ function normalizeProfileName(value) {
6387
+ if (typeof value !== "string") {
6388
+ return null;
6389
+ }
6390
+ const trimmed = value.trim();
6391
+ return trimmed.length > 0 ? trimmed : null;
6392
+ }
6393
+ function toProfileStorageKey(profileName) {
6394
+ const normalized = (profileName ?? "default").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
6395
+ return normalized || "default";
6396
+ }
6397
+ async function getActiveProfileContext() {
6398
+ let profileName = null;
6399
+ try {
6400
+ const state = await getAppState();
6401
+ profileName = normalizeProfileName(state.app.lastProfileName);
6402
+ } catch {
6403
+ profileName = null;
6404
+ }
6405
+ const profileKey = toProfileStorageKey(profileName);
6406
+ const profileRootDir = import_node_path12.default.join(getUserDataDir(), PROFILE_ROOT_DIR, profileKey);
6407
+ return {
6408
+ profileName,
6409
+ profileKey,
6410
+ profileRootDir,
6411
+ parityStoreDir: import_node_path12.default.join(profileRootDir, PROFILE_PARITY_DIR)
6412
+ };
6413
+ }
6414
+ function toWorkspaceName(inputPath) {
6415
+ const normalized = inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
6416
+ const parts = normalized.split("/").filter(Boolean);
6417
+ return parts[parts.length - 1] ?? inputPath;
6418
+ }
6419
+ async function ensureStoreDir() {
6420
+ const context = await getActiveProfileContext();
6421
+ const dir = context.parityStoreDir;
6422
+ await import_promises3.default.mkdir(dir, { recursive: true });
6423
+ return dir;
6424
+ }
6425
+ async function fileExists3(filePath) {
6426
+ try {
6427
+ await import_promises3.default.access(filePath);
6428
+ return true;
6429
+ } catch {
6430
+ return false;
6431
+ }
6432
+ }
6433
+ function legacyStorePath() {
6434
+ return import_node_path12.default.join(getUserDataDir(), LEGACY_STORE_FILE);
6435
+ }
6436
+ function legacyMigrationMarkerPath() {
6437
+ return import_node_path12.default.join(getUserDataDir(), LEGACY_MIGRATION_MARKER_FILE);
6438
+ }
6439
+ async function readLegacyMigrationMarker() {
6440
+ try {
6441
+ const raw = await import_promises3.default.readFile(legacyMigrationMarkerPath(), "utf8");
6442
+ const parsed = JSON.parse(raw);
6443
+ if (parsed && parsed.version === 1 && typeof parsed.migratedToProfileKey === "string" && parsed.migratedToProfileKey.trim()) {
6444
+ return parsed;
6445
+ }
6446
+ return null;
6447
+ } catch {
6448
+ return null;
6449
+ }
6450
+ }
6451
+ async function writeLegacyMigrationMarker(profileKey) {
6452
+ const marker = {
6453
+ version: 1,
6454
+ migratedToProfileKey: profileKey,
6455
+ migratedAt: (/* @__PURE__ */ new Date()).toISOString()
6456
+ };
6457
+ const markerPath = legacyMigrationMarkerPath();
6458
+ await import_promises3.default.mkdir(import_node_path12.default.dirname(markerPath), { recursive: true });
6459
+ await import_promises3.default.writeFile(markerPath, JSON.stringify(marker, null, 2), "utf8");
6460
+ }
6461
+ async function migrateLegacyStoreIfNeeded(scopedStorePath) {
6462
+ if (await fileExists3(scopedStorePath)) {
6463
+ return;
6464
+ }
6465
+ const context = await getActiveProfileContext();
6466
+ const legacyPath = legacyStorePath();
6467
+ if (!await fileExists3(legacyPath)) {
6468
+ return;
6469
+ }
6470
+ const marker = await readLegacyMigrationMarker();
6471
+ if (marker && marker.migratedToProfileKey && marker.migratedToProfileKey !== context.profileKey) {
6472
+ return;
6473
+ }
6474
+ await import_promises3.default.mkdir(import_node_path12.default.dirname(scopedStorePath), { recursive: true });
6475
+ await import_promises3.default.copyFile(legacyPath, scopedStorePath);
6476
+ await writeLegacyMigrationMarker(context.profileKey);
6477
+ }
6478
+ async function storePath() {
6479
+ const dir = await ensureStoreDir();
6480
+ const scopedPath = import_node_path12.default.join(dir, STORE_FILE);
6481
+ await migrateLegacyStoreIfNeeded(scopedPath);
6482
+ return scopedPath;
6483
+ }
6484
+ function sanitizeWorkspace(entry) {
6485
+ if (!entry || typeof entry !== "object") {
6486
+ return null;
6487
+ }
6488
+ const id = typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : null;
6489
+ const workspacePath = typeof entry.path === "string" && entry.path.trim() ? import_node_path12.default.resolve(entry.path) : null;
6490
+ if (!id || !workspacePath) {
6491
+ return null;
6492
+ }
6493
+ const name = typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : toWorkspaceName(workspacePath);
6494
+ const branch = entry.worktree && typeof entry.worktree === "object" && typeof entry.worktree.branch === "string" ? entry.worktree.branch.trim() || "main" : "main";
6495
+ return {
6496
+ id,
6497
+ name,
6498
+ path: workspacePath,
6499
+ connected: Boolean(entry.connected),
6500
+ kind: entry.kind === "worktree" ? "worktree" : "main",
6501
+ parentId: typeof entry.parentId === "string" && entry.parentId.trim() ? entry.parentId.trim() : null,
6502
+ worktree: entry.kind === "worktree" ? { branch } : null,
6503
+ settings: {
6504
+ ...defaultSettings(),
6505
+ ...entry.settings ?? {}
6506
+ }
6507
+ };
6508
+ }
6509
+ async function readStore() {
6510
+ const filePath = await storePath();
6511
+ try {
6512
+ const raw = await import_promises3.default.readFile(filePath, "utf8");
6513
+ const parsed = JSON.parse(raw);
6514
+ const source = Array.isArray(parsed?.workspaces) ? parsed.workspaces : [];
6515
+ const workspaces = source.map((entry) => sanitizeWorkspace(entry)).filter((entry) => Boolean(entry));
6516
+ return {
6517
+ version: 1,
6518
+ workspaces
6519
+ };
6520
+ } catch {
6521
+ return defaultStore();
6522
+ }
6523
+ }
6524
+ async function writeStore(store) {
6525
+ const filePath = await storePath();
6526
+ await import_promises3.default.writeFile(filePath, JSON.stringify(store, null, 2), "utf8");
6527
+ }
6528
+ function dedupeByPath(workspaces) {
6529
+ const seen = /* @__PURE__ */ new Set();
6530
+ const ordered = [];
6531
+ for (const workspace of workspaces) {
6532
+ const key = workspace.path.toLowerCase();
6533
+ if (seen.has(key)) {
6534
+ continue;
6535
+ }
6536
+ seen.add(key);
6537
+ ordered.push(workspace);
6538
+ }
6539
+ return ordered;
6540
+ }
6541
+ async function upsertStore(mutator) {
6542
+ const current = await readStore();
6543
+ const next = mutator(current);
6544
+ await writeStore(next);
6545
+ return next;
6546
+ }
6547
+ function isMainWorkspace(workspace) {
6548
+ return (workspace.kind ?? "main") !== "worktree";
6549
+ }
6550
+ function pruneToFreeProjectLimit(workspaces) {
6551
+ const mainWorkspaces = workspaces.filter((workspace) => isMainWorkspace(workspace));
6552
+ if (mainWorkspaces.length <= FREE_PROFILE_LIMIT) {
6553
+ return workspaces;
6554
+ }
6555
+ const keptMainIds = new Set(mainWorkspaces.slice(0, FREE_PROFILE_LIMIT).map((workspace) => workspace.id));
6556
+ return workspaces.filter((workspace) => {
6557
+ if (isMainWorkspace(workspace)) {
6558
+ return keptMainIds.has(workspace.id);
6559
+ }
6560
+ return typeof workspace.parentId === "string" && keptMainIds.has(workspace.parentId);
6561
+ });
6562
+ }
6563
+ async function enforceFreeProjectLimitIfNeeded(workspaces) {
6564
+ const license = await licenseService.getStatus().catch(() => null);
6565
+ if (!license || !shouldEnforceProfileLimit(license)) {
6566
+ return workspaces;
6567
+ }
6568
+ const pruned = pruneToFreeProjectLimit(workspaces);
6569
+ if (pruned.length === workspaces.length) {
6570
+ return workspaces;
6571
+ }
6572
+ await writeStore({
6573
+ version: 1,
6574
+ workspaces: pruned
6575
+ });
6576
+ return pruned;
6577
+ }
6578
+ async function listParityWorkspaces() {
6579
+ const store = await readStore();
6580
+ const deduped = dedupeByPath(store.workspaces);
6581
+ return enforceFreeProjectLimitIfNeeded(deduped);
6582
+ }
6583
+ async function getParityWorkspaceById(id) {
6584
+ const workspaces = await listParityWorkspaces();
6585
+ return workspaces.find((entry) => entry.id === id) ?? null;
6586
+ }
6587
+ async function addParityWorkspace(targetPath) {
6588
+ const resolved = import_node_path12.default.resolve(targetPath);
6589
+ const name = toWorkspaceName(resolved);
6590
+ await import_promises3.default.mkdir(resolved, { recursive: true });
6591
+ const nextWorkspace = {
6592
+ id: (0, import_node_crypto3.randomUUID)(),
6593
+ name,
6594
+ path: resolved,
6595
+ connected: true,
6596
+ kind: "main",
6597
+ parentId: null,
6598
+ worktree: null,
6599
+ settings: defaultSettings()
6600
+ };
6601
+ await upsertStore((store) => ({
6602
+ version: 1,
6603
+ workspaces: dedupeByPath([nextWorkspace, ...store.workspaces])
6604
+ }));
6605
+ return nextWorkspace;
6606
+ }
6607
+ async function connectParityWorkspace(id) {
6608
+ let updated = null;
6609
+ await upsertStore((store) => {
6610
+ const next = store.workspaces.map((entry) => {
6611
+ if (entry.id !== id) {
6612
+ return entry;
6613
+ }
6614
+ updated = {
6615
+ ...entry,
6616
+ connected: true
6617
+ };
6618
+ return updated;
6619
+ });
6620
+ return {
6621
+ version: 1,
6622
+ workspaces: next
6623
+ };
6624
+ });
6625
+ if (!updated) {
6626
+ throw new Error(`Workspace '${id}' not found.`);
6627
+ }
6628
+ return updated;
6629
+ }
6630
+
6631
+ // ../../electron/telegram-bridge.ts
6632
+ var import_node_crypto4 = require("crypto");
6633
+ var import_promises4 = __toESM(require("fs/promises"));
6634
+ var import_node_path13 = __toESM(require("path"));
6635
+
6636
+ // ../../lib/retryable-turn-error.ts
6637
+ var RETRY_FLAG_KEYS = [
6638
+ "willRetry",
6639
+ "will_retry",
6640
+ "retryable",
6641
+ "retryable_error",
6642
+ "shouldRetry",
6643
+ "should_retry",
6644
+ "retry"
6645
+ ];
6646
+ var RETRY_PROGRESS_MESSAGE_RE = /(?:reconnecting|retrying)(?:\.\.\.)?(?:\s+\d+\s*\/\s*\d+)?/i;
6647
+ function asRecord(value) {
6648
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
6649
+ }
6650
+ function asString5(value) {
6651
+ return typeof value === "string" ? value : value != null ? String(value) : "";
6652
+ }
6653
+ function parseBooleanLike(value) {
6654
+ if (typeof value === "boolean") {
6655
+ return value;
6656
+ }
6657
+ if (typeof value === "number") {
6658
+ if (value === 1) {
6659
+ return true;
6660
+ }
6661
+ if (value === 0) {
6662
+ return false;
6663
+ }
6664
+ return null;
6665
+ }
6666
+ if (typeof value === "string") {
6667
+ const normalized = value.trim().toLowerCase();
6668
+ if (!normalized) {
6669
+ return null;
6670
+ }
6671
+ if (normalized === "true" || normalized === "1") {
6672
+ return true;
6673
+ }
6674
+ if (normalized === "false" || normalized === "0") {
6675
+ return false;
6676
+ }
6677
+ }
6678
+ return null;
6679
+ }
6680
+ function extractRetryFlag(payload) {
6681
+ const root = asRecord(payload);
6682
+ const nestedCandidates = [
6683
+ root,
6684
+ asRecord(root.error),
6685
+ asRecord(root.details),
6686
+ asRecord(root.data),
6687
+ asRecord(root.meta)
6688
+ ];
6689
+ for (const candidate of nestedCandidates) {
6690
+ for (const key of RETRY_FLAG_KEYS) {
6691
+ const parsed = parseBooleanLike(candidate[key]);
6692
+ if (parsed !== null) {
6693
+ return parsed;
6694
+ }
6695
+ }
6696
+ }
6697
+ return null;
6698
+ }
6699
+ function isRetryProgressMessage(message) {
6700
+ const trimmed = message.trim();
6701
+ if (!trimmed) {
6702
+ return false;
6703
+ }
6704
+ return RETRY_PROGRESS_MESSAGE_RE.test(trimmed);
6705
+ }
6706
+ function extractErrorMessageCandidates(payload) {
6707
+ const root = asRecord(payload);
6708
+ const error = asRecord(root.error);
6709
+ return [
6710
+ asString5(root.message),
6711
+ asString5(root.detail),
6712
+ asString5(root.reason),
6713
+ asString5(root.error),
6714
+ asString5(error.message),
6715
+ asString5(error.detail),
6716
+ asString5(error.reason),
6717
+ asString5(error.error)
6718
+ ].map((value) => value.trim()).filter(Boolean);
6719
+ }
6720
+ function isRetryableTurnErrorPayload(payload) {
6721
+ const retryFlag = extractRetryFlag(payload);
6722
+ if (retryFlag !== null) {
6723
+ return retryFlag;
6724
+ }
6725
+ return extractErrorMessageCandidates(payload).some(isRetryProgressMessage);
6726
+ }
6727
+
6728
+ // ../../electron/telegram-bridge.ts
6729
+ var TELEGRAM_API_BASE = "https://api.telegram.org";
6730
+ var TELEGRAM_POLL_TIMEOUT_SECONDS = 30;
6731
+ var TELEGRAM_API_TIMEOUT_MS = 4e4;
6732
+ var TELEGRAM_STREAM_FLUSH_MS = 300;
6733
+ var TELEGRAM_MAX_MESSAGE_LENGTH = 4096;
6734
+ var TELEGRAM_PENDING_TTL_MS = 30 * 60 * 1e3;
6735
+ var TELEGRAM_RETRY_LIMIT_MS = 3e4;
6736
+ var TELEGRAM_MIN_SEND_INTERVAL_MS = 500;
6737
+ var TELEGRAM_FINAL_TEXT_DEDUPE_MS = 2500;
6738
+ var TELEGRAM_RECENT_FINALIZED_TURNS_MAX = 24;
6739
+ var TELEGRAM_FINAL_SIGNATURE_DEDUPE_MS = 2e4;
6740
+ var TELEGRAM_RECENT_FINAL_SIGNATURES_MAX = 40;
6741
+ var TELEGRAM_RECENT_INCOMING_MESSAGES_MAX = 32;
6742
+ var TELEGRAM_TYPING_HEARTBEAT_MS = 4e3;
6743
+ var TELEGRAM_BRIDGE_LOCK_FILE_PREFIX = "telegram-bridge";
6744
+ function asRecord2(value) {
6745
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
6746
+ }
6747
+ function asString6(value) {
6748
+ return typeof value === "string" ? value : value != null ? String(value) : "";
6749
+ }
6750
+ function asTrimmedString(value) {
6751
+ return asString6(value).trim();
6752
+ }
6753
+ function asNumber(value) {
6754
+ if (typeof value === "number" && Number.isFinite(value)) {
6755
+ return value;
6756
+ }
6757
+ if (typeof value === "string" && value.trim()) {
6758
+ const parsed = Number(value.trim());
6759
+ if (Number.isFinite(parsed)) {
6760
+ return parsed;
6761
+ }
6762
+ }
6763
+ return null;
6764
+ }
6765
+ function normalizeChatId(value) {
6766
+ const trimmed = asTrimmedString(value);
6767
+ if (!trimmed) {
6768
+ return null;
6769
+ }
6770
+ if (!/^-?[0-9]+$/.test(trimmed)) {
6771
+ return null;
6772
+ }
6773
+ return trimmed;
6774
+ }
6775
+ function extractThreadId(value) {
6776
+ const params = asRecord2(value);
6777
+ const turn = asRecord2(params.turn);
6778
+ const thread = asRecord2(params.thread);
6779
+ const item = asRecord2(params.item);
6780
+ const msg = asRecord2(params.msg);
6781
+ const msgItem = asRecord2(msg.item);
6782
+ return asTrimmedString(
6783
+ params.threadId ?? params.thread_id ?? params.conversationId ?? params.conversation_id ?? turn.threadId ?? turn.thread_id ?? thread.id ?? thread.threadId ?? thread.thread_id ?? item.threadId ?? item.thread_id ?? item.conversationId ?? item.conversation_id ?? msg.threadId ?? msg.thread_id ?? msg.conversationId ?? msg.conversation_id ?? msgItem.threadId ?? msgItem.thread_id ?? msgItem.conversationId ?? msgItem.conversation_id ?? ""
6784
+ );
6785
+ }
6786
+ function extractTurnId(value) {
6787
+ const params = asRecord2(value);
6788
+ const turn = asRecord2(params.turn);
6789
+ const msg = asRecord2(params.msg);
6790
+ const msgTurn = asRecord2(msg.turn);
6791
+ return asTrimmedString(
6792
+ turn.id ?? turn.turnId ?? turn.turn_id ?? params.turnId ?? params.turn_id ?? msg.turnId ?? msg.turn_id ?? msgTurn.id ?? msgTurn.turnId ?? msgTurn.turn_id ?? ""
6793
+ );
6794
+ }
6795
+ function extractText(value, depth = 0) {
6796
+ if (typeof value === "string") {
6797
+ return value;
6798
+ }
6799
+ if (depth > 4 || value == null) {
6800
+ return "";
6801
+ }
6802
+ if (Array.isArray(value)) {
6803
+ return value.map((entry) => extractText(entry, depth + 1)).join("");
6804
+ }
6805
+ if (typeof value !== "object") {
6806
+ return "";
6807
+ }
6808
+ const record = value;
6809
+ return extractText(record.text, depth + 1) || extractText(record.delta, depth + 1) || extractText(record.message, depth + 1) || extractText(record.content, depth + 1) || extractText(record.value, depth + 1);
6810
+ }
6811
+ function extractDeltaFromNotification(params) {
6812
+ const msg = asRecord2(params.msg);
6813
+ return asString6(params.delta) || extractText(params.delta) || extractText(msg.delta ?? msg.message ?? msg.content ?? msg.value);
6814
+ }
6815
+ function looksLikeAssistantItem(item) {
6816
+ const itemType = asTrimmedString(item.type).toLowerCase();
6817
+ if (itemType === "agentmessage" || itemType === "agent_message" || itemType === "assistantmessage" || itemType === "assistant_message") {
6818
+ return true;
6819
+ }
6820
+ const itemRole = asTrimmedString(
6821
+ item.role ?? item.authorRole ?? item.author_role ?? item.senderRole ?? item.sender_role
6822
+ ).toLowerCase();
6823
+ if (itemRole === "assistant" || itemRole === "agent") {
6824
+ return true;
6825
+ }
6826
+ const itemId = asTrimmedString(item.id);
6827
+ return itemId.startsWith("msg_");
6828
+ }
6829
+ function extractAssistantTextFromItemCompleted(params) {
6830
+ const item = asRecord2(params.item);
6831
+ if (!looksLikeAssistantItem(item)) {
6832
+ return "";
6833
+ }
6834
+ return asString6(item.text) || extractText(item.content) || extractText(params.msg) || "";
6835
+ }
6836
+ function extractCommandText(params) {
6837
+ const command = params.command;
6838
+ if (typeof command === "string") {
6839
+ return command.trim();
6840
+ }
6841
+ if (Array.isArray(command)) {
6842
+ const parts = command.map((entry) => asString6(entry).trim()).filter(Boolean);
6843
+ if (parts.length > 0) {
6844
+ return parts.join(" ");
6845
+ }
6846
+ }
6847
+ const parsed = asRecord2(params.parsed_cmd ?? params.parsedCmd);
6848
+ const parsedText = asTrimmedString(parsed.text ?? parsed.command);
6849
+ if (parsedText) {
6850
+ return parsedText;
6851
+ }
6852
+ return asTrimmedString(params.cmd ?? "");
6853
+ }
6854
+ function summarizePatchRequest(params) {
6855
+ const fileChanges = params.file_changes ?? params.fileChanges;
6856
+ if (Array.isArray(fileChanges)) {
6857
+ const count = fileChanges.length;
6858
+ return `${count} file ${count === 1 ? "change" : "changes"}`;
6859
+ }
6860
+ const summary = extractText(params);
6861
+ return summary.trim() || "Apply patch request";
6862
+ }
6863
+ function splitMessage(text, maxLength = TELEGRAM_MAX_MESSAGE_LENGTH) {
6864
+ const normalized = text.trim();
6865
+ if (!normalized) {
6866
+ return [];
6867
+ }
6868
+ if (normalized.length <= maxLength) {
6869
+ return [normalized];
6870
+ }
6871
+ const parts = [];
6872
+ let index = 0;
6873
+ while (index < normalized.length) {
6874
+ const remaining = normalized.slice(index);
6875
+ if (remaining.length <= maxLength) {
6876
+ parts.push(remaining);
6877
+ break;
6878
+ }
6879
+ let cut = maxLength;
6880
+ const newlineCut = remaining.lastIndexOf("\n", maxLength);
6881
+ if (newlineCut >= Math.floor(maxLength * 0.5)) {
6882
+ cut = newlineCut;
6883
+ }
6884
+ const chunk = remaining.slice(0, cut).trim();
6885
+ parts.push(chunk || remaining.slice(0, maxLength));
6886
+ index += cut;
6887
+ }
6888
+ return parts;
6889
+ }
6890
+ function mergeStreamDeltaBuffer(current, incoming) {
6891
+ if (!incoming) {
6892
+ return current;
6893
+ }
6894
+ if (!current) {
6895
+ return incoming;
6896
+ }
6897
+ if (current.endsWith(incoming)) {
6898
+ return current;
6899
+ }
6900
+ if (incoming.startsWith(current)) {
6901
+ return incoming;
6902
+ }
6903
+ const maxProbe = Math.min(current.length, incoming.length, 2048);
6904
+ for (let size = maxProbe; size > 0; size -= 1) {
6905
+ if (current.slice(-size) === incoming.slice(0, size)) {
6906
+ return current + incoming.slice(size);
6907
+ }
6908
+ }
6909
+ return current + incoming;
6910
+ }
6911
+ function nowMs() {
6912
+ return Date.now();
6913
+ }
6914
+ var TelegramBridge = class {
6915
+ constructor(options) {
6916
+ this.settings = {
6917
+ enabled: false,
6918
+ token: null
6919
+ };
6920
+ this.status = {
6921
+ state: "stopped",
6922
+ running: false,
6923
+ botUsername: null,
6924
+ allowedChats: 0,
6925
+ activeChats: 0,
6926
+ streamMode: "message",
6927
+ lastError: null,
6928
+ startedAtMs: null
6929
+ };
6930
+ this.running = false;
6931
+ this.pollAbortController = null;
6932
+ this.pollLoopPromise = null;
6933
+ this.updateOffset = null;
6934
+ this.draftStreamingEnabled = false;
6935
+ this.messageStreamingEnabled = true;
6936
+ this.sessionsByChatId = /* @__PURE__ */ new Map();
6937
+ this.chatIdByThreadId = /* @__PURE__ */ new Map();
6938
+ this.pendingApprovalById = /* @__PURE__ */ new Map();
6939
+ this.pendingUserInputByRequestId = /* @__PURE__ */ new Map();
6940
+ this.appServerListenersBound = false;
6941
+ this.recentDeltaBySignature = /* @__PURE__ */ new Map();
6942
+ this.lastSendAtByChatId = /* @__PURE__ */ new Map();
6943
+ this.runtimeLockPath = null;
6944
+ this.handleCodexNotification = (payload) => {
6945
+ this.handleNotification(payload).catch((error) => {
6946
+ logWarn("[telegram-bridge] notification handling failed", error);
6947
+ });
6948
+ };
6949
+ this.handleCodexEvent = (payload) => {
6950
+ this.handleEvent(payload).catch((error) => {
6951
+ logWarn("[telegram-bridge] event handling failed", error);
6952
+ });
6953
+ };
6954
+ this.handleExecApprovalRequest = (payload) => {
6955
+ this.handleApprovalRequest("exec", payload.requestToken, payload.params).catch((error) => {
6956
+ logWarn("[telegram-bridge] exec approval handling failed", error);
6957
+ });
6958
+ };
6959
+ this.handlePatchApprovalRequest = (payload) => {
6960
+ this.handleApprovalRequest("patch", payload.requestToken, payload.params).catch((error) => {
6961
+ logWarn("[telegram-bridge] patch approval handling failed", error);
6962
+ });
6963
+ };
6964
+ this.handleServerRequest = (payload) => {
6965
+ this.handleUserInputRequest(payload).catch((error) => {
6966
+ logWarn("[telegram-bridge] user-input request handling failed", error);
6967
+ });
6968
+ };
6969
+ this.options = options;
6970
+ }
6971
+ getCurrentStreamMode() {
6972
+ if (this.messageStreamingEnabled) {
6973
+ return "message";
6974
+ }
6975
+ if (this.draftStreamingEnabled) {
6976
+ return "draft";
6977
+ }
6978
+ return "none";
6979
+ }
6980
+ getStatus() {
6981
+ return {
6982
+ ...this.status,
6983
+ allowedChats: 0,
6984
+ activeChats: this.sessionsByChatId.size,
6985
+ streamMode: this.getCurrentStreamMode()
6986
+ };
6987
+ }
6988
+ async testToken(tokenInput) {
6989
+ const token = asTrimmedString(tokenInput) || this.settings.token || "";
6990
+ if (!token) {
6991
+ return {
6992
+ ok: false,
6993
+ username: null,
6994
+ error: "Telegram bot token is required."
6995
+ };
6996
+ }
6997
+ try {
6998
+ const response = await this.callTelegramApi(
6999
+ token,
7000
+ "getMe",
7001
+ {},
7002
+ { retry429: false }
7003
+ );
7004
+ const username = asTrimmedString(asRecord2(response).username);
7005
+ return {
7006
+ ok: true,
7007
+ username: username || null,
7008
+ error: null
7009
+ };
7010
+ } catch (error) {
7011
+ return {
7012
+ ok: false,
7013
+ username: null,
7014
+ error: error instanceof Error ? error.message : String(error)
7015
+ };
7016
+ }
7017
+ }
7018
+ async applyRuntimeSettings(settings) {
7019
+ const next = this.extractSettings(settings);
7020
+ const tokenChanged = next.token !== this.settings.token;
7021
+ const enabledChanged = next.enabled !== this.settings.enabled;
7022
+ this.settings = next;
7023
+ this.status.allowedChats = 0;
7024
+ if (tokenChanged) {
7025
+ this.status.botUsername = null;
7026
+ this.draftStreamingEnabled = false;
7027
+ this.messageStreamingEnabled = true;
7028
+ this.status.streamMode = this.getCurrentStreamMode();
7029
+ }
7030
+ const shouldRestart = tokenChanged || enabledChanged;
7031
+ await this.syncRuntimeState({ shouldRestart });
7032
+ }
7033
+ async dispose() {
7034
+ await this.stop("Bridge disposed.");
7035
+ }
7036
+ async stop(reason) {
7037
+ if (!this.running && !this.pollLoopPromise) {
7038
+ await this.releaseRuntimeLock();
7039
+ this.status.state = "stopped";
7040
+ this.status.running = false;
7041
+ this.status.startedAtMs = null;
7042
+ if (reason) {
7043
+ this.status.lastError = null;
7044
+ }
7045
+ return;
7046
+ }
7047
+ this.running = false;
7048
+ this.status.running = false;
7049
+ this.status.state = "stopped";
7050
+ this.status.startedAtMs = null;
7051
+ if (this.pollAbortController) {
7052
+ this.pollAbortController.abort();
7053
+ this.pollAbortController = null;
7054
+ }
7055
+ if (this.pollLoopPromise) {
7056
+ try {
7057
+ await this.pollLoopPromise;
7058
+ } catch {
7059
+ }
7060
+ this.pollLoopPromise = null;
7061
+ }
7062
+ this.detachAppServerListeners();
7063
+ await this.detachAllConversationListeners();
7064
+ this.clearSessionState();
7065
+ await this.releaseRuntimeLock();
7066
+ if (reason) {
7067
+ logInfo("[telegram-bridge] stopped", reason);
7068
+ }
7069
+ }
7070
+ extractSettings(raw) {
7071
+ return {
7072
+ enabled: raw.telegramBridgeEnabled === true,
7073
+ token: (() => {
7074
+ const token = asTrimmedString(raw.telegramBotToken);
7075
+ return token || null;
7076
+ })()
7077
+ };
7078
+ }
7079
+ async syncRuntimeState(options) {
7080
+ const shouldRun = this.settings.enabled && Boolean(this.settings.token);
7081
+ if (!shouldRun) {
7082
+ await this.stop("Disabled or incomplete Telegram settings.");
7083
+ this.status.state = "stopped";
7084
+ this.status.lastError = null;
7085
+ return;
7086
+ }
7087
+ const isPro = await this.options.isProEnabled().catch((error) => {
7088
+ logWarn("[telegram-bridge] failed to verify Pro status", error);
7089
+ return false;
7090
+ });
7091
+ if (!isPro) {
7092
+ await this.stop("Telegram bridge requires Pro.");
7093
+ this.status.state = "error";
7094
+ this.status.lastError = "Telegram mobile access requires Pro.";
7095
+ return;
7096
+ }
7097
+ if (this.running && options.shouldRestart) {
7098
+ await this.stop("Restarting Telegram bridge after settings change.");
7099
+ }
7100
+ if (this.running) {
7101
+ return;
7102
+ }
7103
+ await this.start();
7104
+ }
7105
+ async start() {
7106
+ const token = this.settings.token;
7107
+ if (!token) {
7108
+ this.status.state = "error";
7109
+ this.status.lastError = "Telegram bot token is missing.";
7110
+ return;
7111
+ }
7112
+ const me = await this.testToken(token);
7113
+ if (!me.ok) {
7114
+ this.status.state = "error";
7115
+ this.status.lastError = me.error;
7116
+ this.status.botUsername = null;
7117
+ return;
7118
+ }
7119
+ try {
7120
+ await this.acquireRuntimeLock(token);
7121
+ } catch (error) {
7122
+ this.status.state = "error";
7123
+ this.status.lastError = error instanceof Error ? error.message : String(error);
7124
+ return;
7125
+ }
7126
+ this.status.botUsername = me.username;
7127
+ this.status.lastError = null;
7128
+ this.status.state = "running";
7129
+ this.status.running = true;
7130
+ this.status.startedAtMs = nowMs();
7131
+ this.running = true;
7132
+ this.attachAppServerListeners();
7133
+ this.pollAbortController = new AbortController();
7134
+ this.pollLoopPromise = this.runPolling(this.pollAbortController.signal).finally(() => {
7135
+ this.pollLoopPromise = null;
7136
+ });
7137
+ logInfo("[telegram-bridge] started", {
7138
+ allowedChats: "all-private-chats",
7139
+ botUsername: this.status.botUsername
7140
+ });
7141
+ }
7142
+ async runPolling(signal) {
7143
+ const token = this.settings.token;
7144
+ if (!token) {
7145
+ return;
7146
+ }
7147
+ await this.callTelegramApi(token, "deleteWebhook", { drop_pending_updates: false }).catch(
7148
+ (error) => {
7149
+ logWarn("[telegram-bridge] deleteWebhook failed", error);
7150
+ }
7151
+ );
7152
+ let backoffMs = 1e3;
7153
+ while (!signal.aborted && this.running) {
7154
+ try {
7155
+ this.prunePendingActions();
7156
+ const updates = await this.callTelegramApi(
7157
+ token,
7158
+ "getUpdates",
7159
+ {
7160
+ offset: this.updateOffset,
7161
+ timeout: TELEGRAM_POLL_TIMEOUT_SECONDS,
7162
+ allowed_updates: ["message", "callback_query", "my_chat_member"]
7163
+ },
7164
+ {
7165
+ signal,
7166
+ retry429: true
7167
+ }
7168
+ );
7169
+ const list = Array.isArray(updates) ? updates : [];
7170
+ for (const update of list) {
7171
+ const updateId = asNumber(update.update_id);
7172
+ if (updateId !== null) {
7173
+ this.updateOffset = updateId + 1;
7174
+ }
7175
+ await this.handleUpdate(update);
7176
+ }
7177
+ backoffMs = 1e3;
7178
+ } catch (error) {
7179
+ if (signal.aborted || !this.running) {
7180
+ break;
7181
+ }
7182
+ const message = error instanceof Error ? error.message : String(error);
7183
+ this.status.state = "error";
7184
+ this.status.lastError = message;
7185
+ logWarn("[telegram-bridge] polling error", message);
7186
+ await this.sleep(Math.min(backoffMs, TELEGRAM_RETRY_LIMIT_MS));
7187
+ backoffMs = Math.min(backoffMs * 2, TELEGRAM_RETRY_LIMIT_MS);
7188
+ }
7189
+ }
7190
+ if (!signal.aborted && this.running) {
7191
+ this.status.state = "error";
7192
+ this.status.lastError = "Telegram polling stopped unexpectedly.";
7193
+ }
7194
+ }
7195
+ async handleUpdate(update) {
7196
+ const record = asRecord2(update);
7197
+ if (record.callback_query) {
7198
+ await this.handleCallbackQuery(asRecord2(record.callback_query));
7199
+ return;
7200
+ }
7201
+ if (record.message) {
7202
+ await this.handleMessage(asRecord2(record.message));
7203
+ return;
7204
+ }
7205
+ if (record.my_chat_member) {
7206
+ this.handleMembershipUpdate(asRecord2(record.my_chat_member));
7207
+ return;
7208
+ }
7209
+ }
7210
+ handleMembershipUpdate(payload) {
7211
+ const chat = asRecord2(payload.chat);
7212
+ const chatId = normalizeChatId(chat.id);
7213
+ if (!chatId) {
7214
+ return;
7215
+ }
7216
+ const newMember = asRecord2(payload.new_chat_member);
7217
+ const status = asTrimmedString(newMember.status).toLowerCase();
7218
+ if (status === "kicked" || status === "left") {
7219
+ void this.resetSession(chatId);
7220
+ }
7221
+ }
7222
+ async handleMessage(message) {
7223
+ const chat = asRecord2(message.chat);
7224
+ const chatId = normalizeChatId(chat.id);
7225
+ if (!chatId) {
7226
+ return;
7227
+ }
7228
+ const session = this.getOrCreateSession(chatId);
7229
+ const messageId = asNumber(message.message_id ?? message.messageId);
7230
+ if (messageId !== null && this.isDuplicateIncomingMessage(session, messageId)) {
7231
+ return;
7232
+ }
7233
+ const chatType = asTrimmedString(chat.type).toLowerCase();
7234
+ if (chatType && chatType !== "private") {
7235
+ await this.sendSimpleMessage(chatId, "Private chats only. Use me in a direct chat.");
7236
+ return;
7237
+ }
7238
+ const text = asTrimmedString(message.text);
7239
+ if (!text) {
7240
+ return;
7241
+ }
7242
+ const parsedCommand = this.parseCommand(text);
7243
+ if (parsedCommand) {
7244
+ const handled = await this.handleCommand(chatId, parsedCommand.command, parsedCommand.args);
7245
+ if (handled) {
7246
+ return;
7247
+ }
7248
+ }
7249
+ await this.handlePromptText(chatId, text);
7250
+ }
7251
+ async handleCallbackQuery(callbackQuery) {
7252
+ const callbackId = asTrimmedString(callbackQuery.id);
7253
+ const message = asRecord2(callbackQuery.message);
7254
+ const chat = asRecord2(message.chat);
7255
+ const chatId = normalizeChatId(chat.id);
7256
+ const data = asTrimmedString(callbackQuery.data);
7257
+ if (!callbackId || !chatId || !data) {
7258
+ return;
7259
+ }
7260
+ const approvalMatch = /^cu:ap:([a-z0-9]+):(approved|approved_for_session|denied|abort)$/i.exec(
7261
+ data
7262
+ );
7263
+ if (approvalMatch) {
7264
+ const actionId = approvalMatch[1];
7265
+ const decision = approvalMatch[2];
7266
+ const action = this.pendingApprovalById.get(actionId);
7267
+ if (!action || action.chatId !== chatId) {
7268
+ await this.answerCallbackQuery(callbackId, "Action expired.");
7269
+ return;
7270
+ }
7271
+ const appServer = this.options.getAppServer();
7272
+ if (!appServer) {
7273
+ await this.answerCallbackQuery(callbackId, "Backend unavailable.");
7274
+ return;
7275
+ }
7276
+ try {
7277
+ if (action.kind === "exec") {
7278
+ await appServer.respondExecCommandRequest(action.requestToken, decision);
7279
+ } else {
7280
+ await appServer.respondApplyPatchRequest(action.requestToken, decision);
7281
+ }
7282
+ this.pendingApprovalById.delete(actionId);
7283
+ await this.answerCallbackQuery(callbackId, `Recorded: ${decision}`);
7284
+ await this.sendSimpleMessage(chatId, `Approval decision sent: ${decision}.`);
7285
+ } catch (error) {
7286
+ await this.answerCallbackQuery(callbackId, "Failed to submit decision.");
7287
+ await this.sendSimpleMessage(
7288
+ chatId,
7289
+ `Failed to submit approval decision: ${error instanceof Error ? error.message : String(error)}`
7290
+ );
7291
+ }
7292
+ return;
7293
+ }
7294
+ await this.answerCallbackQuery(callbackId, "Unknown action.");
7295
+ }
7296
+ parseCommand(text) {
7297
+ const trimmed = text.trim();
7298
+ if (!trimmed.startsWith("/")) {
7299
+ return null;
7300
+ }
7301
+ const [head, ...rest] = trimmed.split(/\s+/g);
7302
+ const commandWithSlash = head.slice(1);
7303
+ const command = commandWithSlash.split("@")[0]?.trim().toLowerCase();
7304
+ if (!command) {
7305
+ return null;
7306
+ }
7307
+ return {
7308
+ command,
7309
+ args: rest.join(" ").trim()
7310
+ };
7311
+ }
7312
+ async handleCommand(chatId, command, args) {
7313
+ switch (command) {
7314
+ case "start":
7315
+ case "help": {
7316
+ await this.sendHelp(chatId);
7317
+ return true;
7318
+ }
7319
+ case "status": {
7320
+ await this.sendStatus(chatId);
7321
+ return true;
7322
+ }
7323
+ case "projects":
7324
+ case "workspaces": {
7325
+ await this.sendProjects(chatId);
7326
+ return true;
7327
+ }
7328
+ case "project":
7329
+ case "workspace": {
7330
+ await this.selectProject(chatId, args);
7331
+ return true;
7332
+ }
7333
+ case "new": {
7334
+ await this.createNewThread(chatId);
7335
+ return true;
7336
+ }
7337
+ case "threads": {
7338
+ await this.listThreads(chatId);
7339
+ return true;
7340
+ }
7341
+ case "thread": {
7342
+ await this.selectThread(chatId, args);
7343
+ return true;
7344
+ }
7345
+ case "interrupt": {
7346
+ await this.interruptTurn(chatId);
7347
+ return true;
7348
+ }
7349
+ case "input": {
7350
+ await this.submitUserInput(chatId, args);
7351
+ return true;
7352
+ }
7353
+ default:
7354
+ return false;
7355
+ }
7356
+ }
7357
+ async sendHelp(chatId) {
7358
+ const lines = [
7359
+ "CodexUse Telegram bridge is connected.",
7360
+ "",
7361
+ "Commands:",
7362
+ "/status - Show current status",
7363
+ "/projects - List available projects",
7364
+ "/project <index|id> - Select active project",
7365
+ "/new - Start a new thread in active project",
7366
+ "/threads - List recent threads in active project",
7367
+ "/thread <index|id> - Switch active thread",
7368
+ "/interrupt - Interrupt active turn",
7369
+ "/input <request-id> qid=answer;... - Reply to request-user-input",
7370
+ "",
7371
+ "Send any normal text message to run it in the active thread."
7372
+ ];
7373
+ await this.sendSimpleMessage(chatId, lines.join("\n"));
7374
+ }
7375
+ async sendStatus(chatId) {
7376
+ const session = this.getOrCreateSession(chatId);
7377
+ const status = this.getStatus();
7378
+ const lines = [
7379
+ `Bridge: ${status.state}`,
7380
+ `Streaming mode: ${status.streamMode}`,
7381
+ "Allowed chats: all private chats",
7382
+ `Bot: ${status.botUsername ? `@${status.botUsername}` : "(unknown)"}`,
7383
+ `Active project: ${session.projectId ?? "(not selected)"}`,
7384
+ `Active thread: ${session.threadId ?? "(none)"}`,
7385
+ `Processing: ${session.processing ? "yes" : "no"}`
7386
+ ];
7387
+ if (status.lastError) {
7388
+ lines.push(`Last error: ${status.lastError}`);
7389
+ }
7390
+ await this.sendSimpleMessage(chatId, lines.join("\n"));
7391
+ }
7392
+ async sendProjects(chatId) {
7393
+ const projects = await this.options.listProjects();
7394
+ const session = this.getOrCreateSession(chatId);
7395
+ if (projects.length === 0) {
7396
+ await this.sendSimpleMessage(chatId, "No projects found in this desktop app.");
7397
+ return;
7398
+ }
7399
+ session.lastProjectOrder = projects.map((entry) => entry.id);
7400
+ const lines = ["Projects:"];
7401
+ projects.forEach((project, index) => {
7402
+ const isActive = project.id === session.projectId;
7403
+ lines.push(
7404
+ `${index + 1}. ${project.name}${isActive ? " (active)" : ""} [${project.id}]`
7405
+ );
7406
+ });
7407
+ await this.sendSimpleMessage(chatId, lines.join("\n"));
7408
+ }
7409
+ async selectProject(chatId, args) {
7410
+ const session = this.getOrCreateSession(chatId);
7411
+ const projects = await this.options.listProjects();
7412
+ if (projects.length === 0) {
7413
+ await this.sendSimpleMessage(chatId, "No projects available.");
7414
+ return;
7415
+ }
7416
+ const selected = this.resolveProjectSelection(args, projects, session.lastProjectOrder);
7417
+ if (!selected) {
7418
+ await this.sendSimpleMessage(
7419
+ chatId,
7420
+ "Project not found. Use /projects, then /project <index|id>."
7421
+ );
7422
+ return;
7423
+ }
7424
+ if (session.projectId !== selected.id) {
7425
+ await this.detachConversationListener(session);
7426
+ session.projectId = selected.id;
7427
+ session.threadId = null;
7428
+ session.activeTurnId = null;
7429
+ session.processing = false;
7430
+ session.completedTextCandidate = null;
7431
+ this.resetStream(session);
7432
+ }
7433
+ await this.sendSimpleMessage(chatId, `Active project set to: ${selected.name}.`);
7434
+ }
7435
+ resolveProjectSelection(args, projects, order) {
7436
+ const input = args.trim();
7437
+ if (!input) {
7438
+ return null;
7439
+ }
7440
+ const numeric = Number(input);
7441
+ if (Number.isInteger(numeric) && numeric >= 1) {
7442
+ if (order.length > 0 && numeric <= order.length) {
7443
+ const id = order[numeric - 1];
7444
+ return projects.find((entry) => entry.id === id) ?? null;
7445
+ }
7446
+ if (numeric <= projects.length) {
7447
+ return projects[numeric - 1] ?? null;
7448
+ }
7449
+ }
7450
+ return projects.find((entry) => entry.id === input) ?? null;
7451
+ }
7452
+ async createNewThread(chatId) {
7453
+ const session = this.getOrCreateSession(chatId);
7454
+ const project = await this.ensureSessionProject(session, chatId);
7455
+ if (!project) {
7456
+ return;
7457
+ }
7458
+ const appServer = this.options.getAppServer();
7459
+ if (!appServer) {
7460
+ await this.sendSimpleMessage(chatId, "Backend unavailable.");
7461
+ return;
7462
+ }
7463
+ try {
7464
+ const response = await appServer.threadStart({
7465
+ workspaceId: project.id,
7466
+ workspace_id: project.id,
7467
+ cwd: project.path
7468
+ });
7469
+ const threadId = this.extractThreadIdFromResponse(response);
7470
+ if (!threadId) {
7471
+ await this.sendSimpleMessage(chatId, "Thread started, but thread id was missing.");
7472
+ return;
7473
+ }
7474
+ session.threadId = threadId;
7475
+ session.activeTurnId = null;
7476
+ session.processing = false;
7477
+ session.completedTextCandidate = null;
7478
+ this.resetStream(session);
7479
+ await this.attachConversationListener(session, project.id, threadId);
7480
+ await this.sendSimpleMessage(chatId, `Started new thread: ${threadId}`);
7481
+ } catch (error) {
7482
+ await this.sendSimpleMessage(
7483
+ chatId,
7484
+ `Failed to start a new thread: ${error instanceof Error ? error.message : String(error)}`
7485
+ );
7486
+ }
7487
+ }
7488
+ async listThreads(chatId) {
7489
+ const session = this.getOrCreateSession(chatId);
7490
+ const project = await this.ensureSessionProject(session, chatId);
7491
+ if (!project) {
7492
+ return;
7493
+ }
7494
+ const appServer = this.options.getAppServer();
7495
+ if (!appServer) {
7496
+ await this.sendSimpleMessage(chatId, "Backend unavailable.");
7497
+ return;
7498
+ }
7499
+ try {
7500
+ const response = await appServer.listConversations({
7501
+ cwd: project.path,
7502
+ limit: 20,
7503
+ sortKey: "updated_at",
7504
+ archived: false
7505
+ });
7506
+ const threads = this.extractThreadList(response);
7507
+ if (threads.length === 0) {
7508
+ await this.sendSimpleMessage(chatId, "No threads found for this project.");
7509
+ return;
7510
+ }
7511
+ session.lastThreadOrder = threads.map((entry) => entry.id);
7512
+ const lines = ["Threads:"];
7513
+ threads.forEach((thread, index) => {
7514
+ const isActive = thread.id === session.threadId;
7515
+ const title = thread.title || "(untitled)";
7516
+ lines.push(`${index + 1}. ${title}${isActive ? " (active)" : ""} [${thread.id}]`);
7517
+ });
7518
+ await this.sendSimpleMessage(chatId, lines.join("\n"));
7519
+ } catch (error) {
7520
+ await this.sendSimpleMessage(
7521
+ chatId,
7522
+ `Failed to list threads: ${error instanceof Error ? error.message : String(error)}`
7523
+ );
7524
+ }
7525
+ }
7526
+ async selectThread(chatId, args) {
7527
+ const session = this.getOrCreateSession(chatId);
7528
+ const project = await this.ensureSessionProject(session, chatId);
7529
+ if (!project) {
7530
+ return;
7531
+ }
7532
+ const appServer = this.options.getAppServer();
7533
+ if (!appServer) {
7534
+ await this.sendSimpleMessage(chatId, "Backend unavailable.");
7535
+ return;
7536
+ }
7537
+ const input = args.trim();
7538
+ if (!input) {
7539
+ await this.sendSimpleMessage(chatId, "Usage: /thread <index|id>");
7540
+ return;
7541
+ }
7542
+ let threadId = "";
7543
+ const numeric = Number(input);
7544
+ if (Number.isInteger(numeric) && numeric >= 1) {
7545
+ if (session.lastThreadOrder.length > 0 && numeric <= session.lastThreadOrder.length) {
7546
+ threadId = session.lastThreadOrder[numeric - 1] ?? "";
7547
+ }
7548
+ }
7549
+ if (!threadId) {
7550
+ threadId = input;
7551
+ }
7552
+ try {
7553
+ await appServer.threadResume({
7554
+ workspaceId: project.id,
7555
+ workspace_id: project.id,
7556
+ threadId
7557
+ });
7558
+ session.threadId = threadId;
7559
+ session.activeTurnId = null;
7560
+ session.processing = false;
7561
+ session.completedTextCandidate = null;
7562
+ this.resetStream(session);
7563
+ await this.attachConversationListener(session, project.id, threadId);
7564
+ await this.sendSimpleMessage(chatId, `Active thread set to: ${threadId}`);
7565
+ } catch (error) {
7566
+ await this.sendSimpleMessage(
7567
+ chatId,
7568
+ `Failed to switch thread: ${error instanceof Error ? error.message : String(error)}`
7569
+ );
7570
+ }
7571
+ }
7572
+ async interruptTurn(chatId) {
7573
+ const session = this.getOrCreateSession(chatId);
7574
+ const project = await this.ensureSessionProject(session, chatId);
7575
+ if (!project || !session.threadId) {
7576
+ await this.sendSimpleMessage(chatId, "No active thread to interrupt.");
7577
+ return;
7578
+ }
7579
+ const appServer = this.options.getAppServer();
7580
+ if (!appServer) {
7581
+ await this.sendSimpleMessage(chatId, "Backend unavailable.");
7582
+ return;
7583
+ }
7584
+ const turnId = session.activeTurnId || "pending";
7585
+ try {
7586
+ await appServer.interruptTurn({
7587
+ workspaceId: project.id,
7588
+ workspace_id: project.id,
7589
+ threadId: session.threadId,
7590
+ turnId
7591
+ });
7592
+ session.processing = false;
7593
+ session.activeTurnId = null;
7594
+ this.resetStream(session);
7595
+ await this.sendSimpleMessage(chatId, "Interrupt requested.");
7596
+ } catch (error) {
7597
+ await this.sendSimpleMessage(
7598
+ chatId,
7599
+ `Failed to interrupt turn: ${error instanceof Error ? error.message : String(error)}`
7600
+ );
7601
+ }
7602
+ }
7603
+ async submitUserInput(chatId, args) {
7604
+ const appServer = this.options.getAppServer();
7605
+ if (!appServer) {
7606
+ await this.sendSimpleMessage(chatId, "Backend unavailable.");
7607
+ return;
7608
+ }
7609
+ const trimmed = args.trim();
7610
+ if (!trimmed) {
7611
+ await this.sendSimpleMessage(
7612
+ chatId,
7613
+ "Usage: /input <request-id> qid=answer; qid2=answer"
7614
+ );
7615
+ return;
7616
+ }
7617
+ const [requestTokenPart, ...restParts] = trimmed.split(/\s+/g);
7618
+ let requestIdToken = requestTokenPart.trim();
7619
+ let bodyText = restParts.join(" ").trim();
7620
+ if (!requestIdToken || requestIdToken.includes("=")) {
7621
+ const pendingForChat = Array.from(this.pendingUserInputByRequestId.values()).filter(
7622
+ (entry) => entry.chatId === chatId
7623
+ );
7624
+ if (pendingForChat.length !== 1) {
7625
+ await this.sendSimpleMessage(
7626
+ chatId,
7627
+ "Request id is required. Use /input <request-id> qid=answer;..."
7628
+ );
7629
+ return;
7630
+ }
7631
+ requestIdToken = String(pendingForChat[0].requestId);
7632
+ bodyText = trimmed;
7633
+ }
7634
+ const pending = this.pendingUserInputByRequestId.get(requestIdToken);
7635
+ if (!pending || pending.chatId !== chatId) {
7636
+ await this.sendSimpleMessage(chatId, "Unknown or expired request id.");
7637
+ return;
7638
+ }
7639
+ const answers = this.parseUserInputAnswers(bodyText, pending.params);
7640
+ if (!answers) {
7641
+ await this.sendSimpleMessage(
7642
+ chatId,
7643
+ "Could not parse answers. Format: /input <request-id> qid=answer; qid2=answer"
7644
+ );
7645
+ return;
7646
+ }
7647
+ try {
7648
+ await appServer.respondToServerRequest(pending.requestId, {
7649
+ answers
7650
+ });
7651
+ this.pendingUserInputByRequestId.delete(requestIdToken);
7652
+ await this.sendSimpleMessage(chatId, "Input submitted.");
7653
+ } catch (error) {
7654
+ await this.sendSimpleMessage(
7655
+ chatId,
7656
+ `Failed to submit input: ${error instanceof Error ? error.message : String(error)}`
7657
+ );
7658
+ }
7659
+ }
7660
+ parseUserInputAnswers(bodyText, params) {
7661
+ const questionsRaw = Array.isArray(params.questions) ? params.questions : [];
7662
+ const questions = questionsRaw.map((entry) => asRecord2(entry)).filter((entry) => asTrimmedString(entry.id));
7663
+ const questionIds = new Set(questions.map((entry) => asTrimmedString(entry.id)));
7664
+ const answers = {};
7665
+ const segments = bodyText.split(";").map((segment) => segment.trim()).filter(Boolean);
7666
+ if (segments.length === 0) {
7667
+ if (questions.length === 1 && bodyText.trim()) {
7668
+ const id = asTrimmedString(questions[0].id);
7669
+ answers[id] = { answers: [bodyText.trim()] };
7670
+ return answers;
7671
+ }
7672
+ return null;
7673
+ }
7674
+ for (const segment of segments) {
7675
+ const eqIndex = segment.indexOf("=");
7676
+ if (eqIndex <= 0) {
7677
+ if (questions.length === 1) {
7678
+ const id = asTrimmedString(questions[0].id);
7679
+ answers[id] = { answers: [segment.trim()] };
7680
+ continue;
7681
+ }
7682
+ return null;
7683
+ }
7684
+ const key = segment.slice(0, eqIndex).trim();
7685
+ const value = segment.slice(eqIndex + 1).trim();
7686
+ if (!key || !value || !questionIds.has(key)) {
7687
+ return null;
7688
+ }
7689
+ const splitValues = value.split(",").map((entry) => entry.trim()).filter(Boolean);
7690
+ answers[key] = {
7691
+ answers: splitValues.length > 0 ? splitValues : [value]
7692
+ };
7693
+ }
7694
+ return Object.keys(answers).length > 0 ? answers : null;
7695
+ }
7696
+ async handlePromptText(chatId, text) {
7697
+ const session = this.getOrCreateSession(chatId);
7698
+ if (session.processing) {
7699
+ await this.sendSimpleMessage(
7700
+ chatId,
7701
+ "A response is already in progress. Wait for completion or use /interrupt."
7702
+ );
7703
+ return;
7704
+ }
7705
+ const project = await this.ensureSessionProject(session, chatId);
7706
+ if (!project) {
7707
+ return;
7708
+ }
7709
+ const threadId = session.threadId || await this.startThreadForSession(chatId, session, project);
7710
+ if (!threadId) {
7711
+ return;
7712
+ }
7713
+ const appServer = this.options.getAppServer();
7714
+ if (!appServer) {
7715
+ await this.sendSimpleMessage(chatId, "Backend unavailable.");
7716
+ return;
7717
+ }
7718
+ try {
7719
+ session.completedTextCandidate = null;
7720
+ this.resetStream(session);
7721
+ const response = await appServer.sendUserMessage({
7722
+ workspaceId: project.id,
7723
+ workspace_id: project.id,
7724
+ threadId,
7725
+ thread_id: threadId,
7726
+ conversationId: threadId,
7727
+ conversation_id: threadId,
7728
+ text
7729
+ });
7730
+ const turnId = this.extractTurnIdFromResponse(response);
7731
+ session.processing = true;
7732
+ session.activeTurnId = turnId || null;
7733
+ this.ensureTypingIndicator(session);
7734
+ } catch (error) {
7735
+ await this.sendSimpleMessage(
7736
+ chatId,
7737
+ `Failed to send message: ${error instanceof Error ? error.message : String(error)}`
7738
+ );
7739
+ }
7740
+ }
7741
+ async startThreadForSession(chatId, session, project) {
7742
+ const appServer = this.options.getAppServer();
7743
+ if (!appServer) {
7744
+ await this.sendSimpleMessage(chatId, "Backend unavailable.");
7745
+ return null;
7746
+ }
7747
+ try {
7748
+ const response = await appServer.threadStart({
7749
+ workspaceId: project.id,
7750
+ workspace_id: project.id,
7751
+ cwd: project.path
7752
+ });
7753
+ const threadId = this.extractThreadIdFromResponse(response);
7754
+ if (!threadId) {
7755
+ await this.sendSimpleMessage(chatId, "Could not create thread (missing thread id).");
7756
+ return null;
7757
+ }
7758
+ session.threadId = threadId;
7759
+ await this.attachConversationListener(session, project.id, threadId);
7760
+ return threadId;
7761
+ } catch (error) {
7762
+ await this.sendSimpleMessage(
7763
+ chatId,
7764
+ `Failed to create thread: ${error instanceof Error ? error.message : String(error)}`
7765
+ );
7766
+ return null;
7767
+ }
7768
+ }
7769
+ async ensureSessionProject(session, chatId) {
7770
+ if (session.projectId) {
7771
+ const existing = await this.options.getProjectById(session.projectId);
7772
+ if (existing) {
7773
+ return existing;
7774
+ }
7775
+ session.projectId = null;
7776
+ session.threadId = null;
7777
+ }
7778
+ const defaultProjectId = await this.resolveDefaultProjectId();
7779
+ if (defaultProjectId) {
7780
+ const preferred = await this.options.getProjectById(defaultProjectId).catch(() => null);
7781
+ if (preferred) {
7782
+ session.projectId = preferred.id;
7783
+ return preferred;
7784
+ }
7785
+ }
7786
+ const projects = await this.options.listProjects();
7787
+ if (projects.length === 0) {
7788
+ await this.sendSimpleMessage(chatId, "No projects found. Add a project in desktop settings first.");
7789
+ return null;
7790
+ }
7791
+ const connected = projects.find((entry) => entry.connected);
7792
+ const selected = connected ?? projects[0];
7793
+ session.projectId = selected.id;
7794
+ session.lastProjectOrder = projects.map((entry) => entry.id);
7795
+ return selected;
7796
+ }
7797
+ async resolveDefaultProjectId() {
7798
+ const resolver = this.options.getDefaultProjectId;
7799
+ if (!resolver) {
7800
+ return null;
7801
+ }
7802
+ try {
7803
+ const value = await resolver();
7804
+ const normalized = asTrimmedString(value);
7805
+ return normalized || null;
7806
+ } catch {
7807
+ return null;
7808
+ }
7809
+ }
7810
+ extractThreadIdFromResponse(value) {
7811
+ const record = asRecord2(value);
7812
+ const result = asRecord2(record.result);
7813
+ const thread = asRecord2(result.thread ?? record.thread);
7814
+ return asTrimmedString(
7815
+ thread.id ?? thread.threadId ?? thread.thread_id ?? result.threadId ?? result.thread_id ?? record.threadId ?? record.thread_id ?? result.conversationId ?? result.conversation_id ?? ""
7816
+ );
7817
+ }
7818
+ extractTurnIdFromResponse(value) {
7819
+ const record = asRecord2(value);
7820
+ const result = asRecord2(record.result);
7821
+ const turn = asRecord2(result.turn ?? record.turn);
7822
+ return asTrimmedString(
7823
+ turn.id ?? turn.turnId ?? turn.turn_id ?? result.turnId ?? result.turn_id ?? record.turnId ?? record.turn_id ?? ""
7824
+ );
7825
+ }
7826
+ extractThreadList(value) {
7827
+ const root = asRecord2(value);
7828
+ const primary = Array.isArray(value) ? value : Array.isArray(root.data) ? root.data : Array.isArray(root.threads) ? root.threads : Array.isArray(root.items) ? root.items : [];
7829
+ const result = [];
7830
+ for (const entry of primary) {
7831
+ const thread = asRecord2(entry);
7832
+ const id = asTrimmedString(
7833
+ thread.id ?? thread.threadId ?? thread.thread_id ?? thread.conversationId ?? thread.conversation_id ?? ""
7834
+ );
7835
+ if (!id) {
7836
+ continue;
7837
+ }
7838
+ const title = asTrimmedString(
7839
+ thread.name ?? thread.title ?? thread.preview ?? thread.summary ?? thread.path ?? ""
7840
+ ) || "(untitled)";
7841
+ result.push({ id, title });
7842
+ }
7843
+ return result;
7844
+ }
7845
+ getOrCreateSession(chatId) {
7846
+ const existing = this.sessionsByChatId.get(chatId);
7847
+ if (existing) {
7848
+ return existing;
7849
+ }
7850
+ const session = {
7851
+ chatId,
7852
+ projectId: null,
7853
+ threadId: null,
7854
+ listenerSubscriptionId: null,
7855
+ activeTurnId: null,
7856
+ processing: false,
7857
+ finalizeInFlight: false,
7858
+ lastProjectOrder: [],
7859
+ lastThreadOrder: [],
7860
+ completedTextCandidate: null,
7861
+ recentFinalizedTurnIds: [],
7862
+ recentFinalSignatures: [],
7863
+ recentIncomingMessageIds: [],
7864
+ streamFlushPromise: null,
7865
+ streamFlushPending: false,
7866
+ lastDeliveredFinalText: null,
7867
+ lastDeliveredFinalAtMs: 0,
7868
+ typingTimer: null,
7869
+ stream: null
7870
+ };
7871
+ this.sessionsByChatId.set(chatId, session);
7872
+ this.status.activeChats = this.sessionsByChatId.size;
7873
+ return session;
7874
+ }
7875
+ async resetSession(chatId) {
7876
+ const session = this.sessionsByChatId.get(chatId);
7877
+ if (!session) {
7878
+ return;
7879
+ }
7880
+ await this.detachConversationListener(session);
7881
+ this.stopTypingIndicator(session);
7882
+ if (session.threadId) {
7883
+ this.chatIdByThreadId.delete(session.threadId);
7884
+ }
7885
+ this.sessionsByChatId.delete(chatId);
7886
+ this.status.activeChats = this.sessionsByChatId.size;
7887
+ }
7888
+ async attachConversationListener(session, projectId, threadId) {
7889
+ const appServer = this.options.getAppServer();
7890
+ if (!appServer) {
7891
+ return;
7892
+ }
7893
+ await this.detachConversationListener(session);
7894
+ try {
7895
+ const response = await appServer.addConversationListener({
7896
+ conversationId: threadId,
7897
+ workspaceId: projectId,
7898
+ workspace_id: projectId
7899
+ });
7900
+ const responseRecord = asRecord2(response);
7901
+ const subscriptionId = asTrimmedString(responseRecord.subscriptionId);
7902
+ session.listenerSubscriptionId = subscriptionId || null;
7903
+ session.threadId = threadId;
7904
+ this.chatIdByThreadId.set(threadId, session.chatId);
7905
+ } catch (error) {
7906
+ logWarn("[telegram-bridge] failed to attach conversation listener", error);
7907
+ }
7908
+ }
7909
+ async detachConversationListener(session) {
7910
+ const appServer = this.options.getAppServer();
7911
+ if (!appServer) {
7912
+ session.listenerSubscriptionId = null;
7913
+ return;
7914
+ }
7915
+ const subscriptionId = session.listenerSubscriptionId;
7916
+ session.listenerSubscriptionId = null;
7917
+ if (!subscriptionId) {
7918
+ return;
7919
+ }
7920
+ try {
7921
+ await appServer.removeConversationListener({
7922
+ subscriptionId
7923
+ });
7924
+ } catch (error) {
7925
+ logWarn("[telegram-bridge] failed to remove conversation listener", error);
7926
+ }
7927
+ }
7928
+ async detachAllConversationListeners() {
7929
+ const sessions = Array.from(this.sessionsByChatId.values());
7930
+ await Promise.all(sessions.map((session) => this.detachConversationListener(session)));
7931
+ }
7932
+ clearSessionState() {
7933
+ for (const session of this.sessionsByChatId.values()) {
7934
+ this.resetStream(session);
7935
+ }
7936
+ this.sessionsByChatId.clear();
7937
+ this.chatIdByThreadId.clear();
7938
+ this.pendingApprovalById.clear();
7939
+ this.pendingUserInputByRequestId.clear();
7940
+ this.recentDeltaBySignature.clear();
7941
+ this.lastSendAtByChatId.clear();
7942
+ this.status.activeChats = 0;
7943
+ }
7944
+ attachAppServerListeners() {
7945
+ if (this.appServerListenersBound) {
7946
+ return;
7947
+ }
7948
+ const appServer = this.options.getAppServer();
7949
+ if (!appServer) {
7950
+ this.status.state = "error";
7951
+ this.status.lastError = "Codex app-server is unavailable.";
7952
+ return;
7953
+ }
7954
+ appServer.on("codex:notification", this.handleCodexNotification);
7955
+ appServer.on("codex:event", this.handleCodexEvent);
7956
+ appServer.on("codex:exec-command-request", this.handleExecApprovalRequest);
7957
+ appServer.on("codex:apply-patch-request", this.handlePatchApprovalRequest);
7958
+ appServer.on("codex:server-request", this.handleServerRequest);
7959
+ this.appServerListenersBound = true;
7960
+ }
7961
+ detachAppServerListeners() {
7962
+ if (!this.appServerListenersBound) {
7963
+ return;
7964
+ }
7965
+ const appServer = this.options.getAppServer();
7966
+ if (!appServer) {
7967
+ this.appServerListenersBound = false;
7968
+ return;
7969
+ }
7970
+ appServer.off("codex:notification", this.handleCodexNotification);
7971
+ appServer.off("codex:event", this.handleCodexEvent);
7972
+ appServer.off("codex:exec-command-request", this.handleExecApprovalRequest);
7973
+ appServer.off("codex:apply-patch-request", this.handlePatchApprovalRequest);
7974
+ appServer.off("codex:server-request", this.handleServerRequest);
7975
+ this.appServerListenersBound = false;
7976
+ }
7977
+ async handleNotification(payload) {
7978
+ if (!this.running) {
7979
+ return;
7980
+ }
7981
+ const method = asTrimmedString(payload.method);
7982
+ if (!method) {
7983
+ return;
7984
+ }
7985
+ const params = asRecord2(payload.params);
7986
+ const threadId = extractThreadId(params);
7987
+ if (!threadId) {
7988
+ return;
7989
+ }
7990
+ const session = this.resolveSessionByThreadId(threadId);
7991
+ if (!session) {
7992
+ return;
7993
+ }
7994
+ if (method === "turn/started") {
7995
+ const turnId = extractTurnId(params);
7996
+ session.processing = true;
7997
+ if (turnId) {
7998
+ session.activeTurnId = turnId;
7999
+ }
8000
+ this.ensureTypingIndicator(session);
8001
+ return;
8002
+ }
8003
+ if (method === "item/agentMessage/delta") {
8004
+ const delta = extractDeltaFromNotification(params);
8005
+ if (!delta) {
8006
+ return;
8007
+ }
8008
+ if (this.isDuplicateDelta(`${threadId}|${delta}`)) {
8009
+ return;
8010
+ }
8011
+ this.appendAssistantDelta(session, delta);
8012
+ return;
8013
+ }
8014
+ if (method === "item/completed") {
8015
+ const text = extractAssistantTextFromItemCompleted(params);
8016
+ if (text.trim()) {
8017
+ session.completedTextCandidate = text;
8018
+ }
8019
+ return;
8020
+ }
8021
+ if (method === "turn/completed") {
8022
+ await this.finalizeTurn(session);
8023
+ return;
8024
+ }
8025
+ if (method === "error") {
8026
+ if (!isRetryableTurnErrorPayload(params)) {
8027
+ const errorMessage = asTrimmedString(asRecord2(params.error).message) || asTrimmedString(params.message) || "Turn failed.";
8028
+ await this.sendSimpleMessage(session.chatId, `Turn error: ${errorMessage}`);
8029
+ }
8030
+ await this.finalizeTurn(session);
8031
+ return;
8032
+ }
8033
+ }
8034
+ async handleEvent(payload) {
8035
+ if (!this.running) {
8036
+ return;
8037
+ }
8038
+ const method = asTrimmedString(payload.method);
8039
+ if (!method.startsWith("codex/event/")) {
8040
+ return;
8041
+ }
8042
+ const params = asRecord2(payload.params);
8043
+ const threadId = extractThreadId(params);
8044
+ if (!threadId) {
8045
+ return;
8046
+ }
8047
+ const session = this.resolveSessionByThreadId(threadId);
8048
+ if (!session) {
8049
+ return;
8050
+ }
8051
+ const msg = asRecord2(params.msg);
8052
+ const msgType = asTrimmedString(msg.type).toLowerCase();
8053
+ if (msgType === "agent_message_delta" || msgType === "agent_message_content_delta") {
8054
+ const delta = extractText(msg.delta ?? msg.message ?? msg.content ?? msg.value);
8055
+ if (!delta) {
8056
+ return;
8057
+ }
8058
+ if (this.isDuplicateDelta(`${threadId}|${delta}`)) {
8059
+ return;
8060
+ }
8061
+ this.appendAssistantDelta(session, delta);
8062
+ return;
8063
+ }
8064
+ if (msgType === "agent_message") {
8065
+ const text = extractText(msg.message ?? msg.content ?? msg.text ?? msg.value);
8066
+ if (text.trim()) {
8067
+ session.completedTextCandidate = text;
8068
+ }
8069
+ return;
8070
+ }
8071
+ if (msgType === "task_started") {
8072
+ session.processing = true;
8073
+ const turnId = extractTurnId(params);
8074
+ if (turnId) {
8075
+ session.activeTurnId = turnId;
8076
+ }
8077
+ this.ensureTypingIndicator(session);
8078
+ return;
8079
+ }
8080
+ if (msgType === "task_complete" || msgType === "turn_aborted") {
8081
+ await this.finalizeTurn(session);
8082
+ return;
8083
+ }
8084
+ if (msgType === "error" || msgType === "stream_error") {
8085
+ if (!isRetryableTurnErrorPayload(msg)) {
8086
+ const errorMessage = asTrimmedString(msg.message) || asTrimmedString(msg.error) || "Turn failed.";
8087
+ await this.sendSimpleMessage(session.chatId, `Turn error: ${errorMessage}`);
8088
+ }
8089
+ await this.finalizeTurn(session);
8090
+ }
8091
+ }
8092
+ resolveSessionByThreadId(threadId) {
8093
+ const chatId = this.chatIdByThreadId.get(threadId);
8094
+ if (!chatId) {
8095
+ return null;
8096
+ }
8097
+ const session = this.sessionsByChatId.get(chatId);
8098
+ if (!session || session.threadId !== threadId) {
8099
+ return null;
8100
+ }
8101
+ return session;
8102
+ }
8103
+ appendAssistantDelta(session, delta) {
8104
+ if (!session.stream) {
8105
+ session.stream = {
8106
+ draftId: (0, import_node_crypto4.randomUUID)().slice(0, 12),
8107
+ buffer: "",
8108
+ lastSent: "",
8109
+ flushTimer: null,
8110
+ messageId: null
8111
+ };
8112
+ }
8113
+ const nextBuffer = mergeStreamDeltaBuffer(session.stream.buffer, delta);
8114
+ if (nextBuffer === session.stream.buffer) {
8115
+ return;
8116
+ }
8117
+ session.stream.buffer = nextBuffer;
8118
+ session.processing = true;
8119
+ this.ensureTypingIndicator(session);
8120
+ this.scheduleDraftFlush(session, false);
8121
+ }
8122
+ scheduleDraftFlush(session, force) {
8123
+ if (!session.stream) {
8124
+ return;
8125
+ }
8126
+ if (force) {
8127
+ if (session.stream.flushTimer) {
8128
+ clearTimeout(session.stream.flushTimer);
8129
+ session.stream.flushTimer = null;
8130
+ }
8131
+ if (session.streamFlushPromise) {
8132
+ session.streamFlushPending = true;
8133
+ return;
8134
+ }
8135
+ void this.flushDraft(session, true);
8136
+ return;
8137
+ }
8138
+ if (session.streamFlushPromise) {
8139
+ session.streamFlushPending = true;
8140
+ return;
8141
+ }
8142
+ if (session.stream.flushTimer) {
8143
+ return;
8144
+ }
8145
+ session.stream.flushTimer = setTimeout(() => {
8146
+ if (session.stream) {
8147
+ session.stream.flushTimer = null;
8148
+ }
8149
+ void this.flushDraft(session, false);
8150
+ }, TELEGRAM_STREAM_FLUSH_MS);
8151
+ }
8152
+ async flushDraft(session, force) {
8153
+ if (!this.running) {
8154
+ return;
8155
+ }
8156
+ if (session.streamFlushPromise) {
8157
+ if (force) {
8158
+ session.streamFlushPending = true;
8159
+ }
8160
+ return;
8161
+ }
8162
+ const currentStream = session.stream;
8163
+ if (!currentStream) {
8164
+ return;
8165
+ }
8166
+ const draftId = currentStream.draftId;
8167
+ const rawText = currentStream.buffer;
8168
+ if (!rawText.trim()) {
8169
+ return;
8170
+ }
8171
+ const draftText = rawText.length <= TELEGRAM_MAX_MESSAGE_LENGTH ? rawText : rawText.slice(rawText.length - TELEGRAM_MAX_MESSAGE_LENGTH);
8172
+ if (!force && draftText === currentStream.lastSent) {
8173
+ return;
8174
+ }
8175
+ const runFlush = async () => {
8176
+ if (this.draftStreamingEnabled) {
8177
+ try {
8178
+ await this.sendDraftMessage(session.chatId, draftText, draftId);
8179
+ const activeStream = this.getStreamByDraftId(session, draftId);
8180
+ if (activeStream) {
8181
+ activeStream.lastSent = draftText;
8182
+ }
8183
+ this.status.streamMode = this.getCurrentStreamMode();
8184
+ return;
8185
+ } catch (error) {
8186
+ const message = error instanceof Error ? error.message : String(error);
8187
+ if (this.isDraftStreamingUnsupportedError(message)) {
8188
+ this.draftStreamingEnabled = false;
8189
+ this.status.streamMode = this.getCurrentStreamMode();
8190
+ logWarn(
8191
+ "[telegram-bridge] sendMessageDraft unavailable, falling back to message edit streaming",
8192
+ message
8193
+ );
8194
+ } else {
8195
+ logWarn("[telegram-bridge] failed to flush draft", message);
8196
+ return;
8197
+ }
8198
+ }
8199
+ }
8200
+ if (!this.messageStreamingEnabled) {
8201
+ return;
8202
+ }
8203
+ try {
8204
+ await this.sendOrEditStreamMessage(session, draftText, draftId);
8205
+ const activeStream = this.getStreamByDraftId(session, draftId);
8206
+ if (activeStream) {
8207
+ activeStream.lastSent = draftText;
8208
+ }
8209
+ this.status.streamMode = this.getCurrentStreamMode();
8210
+ } catch (error) {
8211
+ const message = error instanceof Error ? error.message : String(error);
8212
+ if (this.isMessageStreamingUnsupportedError(message)) {
8213
+ this.messageStreamingEnabled = false;
8214
+ this.status.streamMode = this.getCurrentStreamMode();
8215
+ logWarn("[telegram-bridge] edit streaming unavailable, disabling streaming", message);
8216
+ return;
8217
+ }
8218
+ logWarn("[telegram-bridge] failed to flush message-edit stream", message);
8219
+ }
8220
+ };
8221
+ const flushPromise = runFlush();
8222
+ session.streamFlushPromise = flushPromise;
8223
+ try {
8224
+ await flushPromise;
8225
+ } finally {
8226
+ if (session.streamFlushPromise === flushPromise) {
8227
+ session.streamFlushPromise = null;
8228
+ }
8229
+ if (session.streamFlushPending && this.running && session.stream) {
8230
+ session.streamFlushPending = false;
8231
+ void this.flushDraft(session, false);
8232
+ }
8233
+ }
8234
+ }
8235
+ async finalizeTurn(session) {
8236
+ if (session.finalizeInFlight) {
8237
+ return;
8238
+ }
8239
+ if (!session.processing && !session.stream?.buffer && !session.completedTextCandidate) {
8240
+ return;
8241
+ }
8242
+ session.finalizeInFlight = true;
8243
+ try {
8244
+ await this.flushDraft(session, true);
8245
+ if (session.streamFlushPromise) {
8246
+ await session.streamFlushPromise.catch(() => void 0);
8247
+ }
8248
+ if (session.streamFlushPending && session.stream) {
8249
+ session.streamFlushPending = false;
8250
+ await this.flushDraft(session, true);
8251
+ if (session.streamFlushPromise) {
8252
+ await session.streamFlushPromise.catch(() => void 0);
8253
+ }
8254
+ }
8255
+ const finalText = (session.completedTextCandidate ?? session.stream?.buffer ?? "").trim();
8256
+ const turnId = session.activeTurnId;
8257
+ const alreadyFinalizedTurn = turnId ? session.recentFinalizedTurnIds.includes(turnId) : false;
8258
+ const now = nowMs();
8259
+ const duplicateByRecentText = finalText.length > 0 && session.lastDeliveredFinalText === finalText && now - session.lastDeliveredFinalAtMs <= TELEGRAM_FINAL_TEXT_DEDUPE_MS;
8260
+ const duplicateByFinalSignature = this.isDuplicateFinalSignature(session, finalText, now);
8261
+ if (finalText && !alreadyFinalizedTurn && !duplicateByRecentText && !duplicateByFinalSignature) {
8262
+ const chunks = splitMessage(finalText, TELEGRAM_MAX_MESSAGE_LENGTH);
8263
+ let chunkStartIndex = 0;
8264
+ const streamMessageId = session.stream?.messageId ?? null;
8265
+ if (streamMessageId !== null && chunks.length > 0) {
8266
+ const reused = await this.tryFinalizeStreamMessage(session.chatId, streamMessageId, chunks[0]);
8267
+ if (!reused) {
8268
+ await this.sendSimpleMessage(session.chatId, chunks[0]);
8269
+ }
8270
+ chunkStartIndex = 1;
8271
+ }
8272
+ for (let index = chunkStartIndex; index < chunks.length; index += 1) {
8273
+ await this.sendSimpleMessage(session.chatId, chunks[index]);
8274
+ }
8275
+ session.lastDeliveredFinalText = finalText;
8276
+ session.lastDeliveredFinalAtMs = now;
8277
+ this.recordFinalSignature(session, finalText, now);
8278
+ if (turnId) {
8279
+ session.recentFinalizedTurnIds.push(turnId);
8280
+ if (session.recentFinalizedTurnIds.length > TELEGRAM_RECENT_FINALIZED_TURNS_MAX) {
8281
+ session.recentFinalizedTurnIds.splice(
8282
+ 0,
8283
+ session.recentFinalizedTurnIds.length - TELEGRAM_RECENT_FINALIZED_TURNS_MAX
8284
+ );
8285
+ }
8286
+ }
8287
+ }
8288
+ } finally {
8289
+ session.processing = false;
8290
+ session.activeTurnId = null;
8291
+ session.completedTextCandidate = null;
8292
+ this.resetStream(session);
8293
+ session.finalizeInFlight = false;
8294
+ }
8295
+ }
8296
+ resetStream(session) {
8297
+ this.stopTypingIndicator(session);
8298
+ if (session.stream?.flushTimer) {
8299
+ clearTimeout(session.stream.flushTimer);
8300
+ }
8301
+ session.streamFlushPending = false;
8302
+ session.streamFlushPromise = null;
8303
+ session.stream = null;
8304
+ }
8305
+ ensureTypingIndicator(session) {
8306
+ if (!this.running || !session.processing) {
8307
+ return;
8308
+ }
8309
+ if (session.typingTimer) {
8310
+ return;
8311
+ }
8312
+ void this.sendTypingAction(session.chatId);
8313
+ session.typingTimer = setInterval(() => {
8314
+ if (!this.running || !session.processing) {
8315
+ this.stopTypingIndicator(session);
8316
+ return;
8317
+ }
8318
+ void this.sendTypingAction(session.chatId);
8319
+ }, TELEGRAM_TYPING_HEARTBEAT_MS);
8320
+ }
8321
+ stopTypingIndicator(session) {
8322
+ if (session.typingTimer) {
8323
+ clearInterval(session.typingTimer);
8324
+ session.typingTimer = null;
8325
+ }
8326
+ }
8327
+ isDraftStreamingUnsupportedError(message) {
8328
+ const lower = message.toLowerCase();
8329
+ return lower.includes("sendmessagedraft") || lower.includes("method not found") || lower.includes("there is no method") || lower.includes("text must be non-empty") || lower.includes("random_id_invalid") || lower.includes("random id invalid") || lower.includes("not found");
8330
+ }
8331
+ isMessageStreamingUnsupportedError(message) {
8332
+ const lower = message.toLowerCase();
8333
+ return lower.includes("editmessagetext") && (lower.includes("method not found") || lower.includes("there is no method"));
8334
+ }
8335
+ async sendOrEditStreamMessage(session, text, draftId) {
8336
+ const stream = this.getStreamByDraftId(session, draftId);
8337
+ if (!stream) {
8338
+ return;
8339
+ }
8340
+ const existingMessageId = stream.messageId;
8341
+ if (existingMessageId !== null) {
8342
+ const edited = await this.tryEditMessageText(session.chatId, existingMessageId, text);
8343
+ if (edited) {
8344
+ return;
8345
+ }
8346
+ const activeStream = this.getStreamByDraftId(session, draftId);
8347
+ if (activeStream) {
8348
+ activeStream.messageId = null;
8349
+ }
8350
+ }
8351
+ const sent = await this.sendSimpleMessage(session.chatId, text);
8352
+ const messageId = this.extractTelegramMessageId(sent);
8353
+ if (messageId !== null) {
8354
+ const activeStream = this.getStreamByDraftId(session, draftId);
8355
+ if (activeStream) {
8356
+ activeStream.messageId = messageId;
8357
+ }
8358
+ }
8359
+ }
8360
+ getStreamByDraftId(session, draftId) {
8361
+ const stream = session.stream;
8362
+ if (!stream || stream.draftId !== draftId) {
8363
+ return null;
8364
+ }
8365
+ return stream;
8366
+ }
8367
+ async tryFinalizeStreamMessage(chatId, messageId, text) {
8368
+ return this.tryEditMessageText(chatId, messageId, text);
8369
+ }
8370
+ async tryEditMessageText(chatId, messageId, text) {
8371
+ try {
8372
+ await this.editMessageText(chatId, messageId, text);
8373
+ return true;
8374
+ } catch (error) {
8375
+ const message = error instanceof Error ? error.message : String(error);
8376
+ const lower = message.toLowerCase();
8377
+ if (lower.includes("message is not modified")) {
8378
+ return true;
8379
+ }
8380
+ if (lower.includes("message to edit not found") || lower.includes("message can't be edited") || lower.includes("message cant be edited")) {
8381
+ return false;
8382
+ }
8383
+ throw error;
8384
+ }
8385
+ }
8386
+ isDuplicateIncomingMessage(session, messageId) {
8387
+ if (session.recentIncomingMessageIds.includes(messageId)) {
8388
+ return true;
8389
+ }
8390
+ session.recentIncomingMessageIds.push(messageId);
8391
+ if (session.recentIncomingMessageIds.length > TELEGRAM_RECENT_INCOMING_MESSAGES_MAX) {
8392
+ session.recentIncomingMessageIds.splice(
8393
+ 0,
8394
+ session.recentIncomingMessageIds.length - TELEGRAM_RECENT_INCOMING_MESSAGES_MAX
8395
+ );
8396
+ }
8397
+ return false;
8398
+ }
8399
+ buildFinalSignature(session, finalText) {
8400
+ const normalized = finalText.trim();
8401
+ if (!normalized) {
8402
+ return "";
8403
+ }
8404
+ const threadKey = session.threadId ?? "no-thread";
8405
+ const digest = (0, import_node_crypto4.createHash)("sha1").update(normalized).digest("hex");
8406
+ return `${threadKey}|${digest}`;
8407
+ }
8408
+ isDuplicateFinalSignature(session, finalText, now) {
8409
+ const signature = this.buildFinalSignature(session, finalText);
8410
+ if (!signature) {
8411
+ return false;
8412
+ }
8413
+ let duplicated = false;
8414
+ const kept = [];
8415
+ for (const entry of session.recentFinalSignatures) {
8416
+ if (now - entry.at > TELEGRAM_FINAL_SIGNATURE_DEDUPE_MS) {
8417
+ continue;
8418
+ }
8419
+ if (entry.signature === signature) {
8420
+ duplicated = true;
8421
+ }
8422
+ kept.push(entry);
8423
+ }
8424
+ session.recentFinalSignatures = kept;
8425
+ return duplicated;
8426
+ }
8427
+ recordFinalSignature(session, finalText, now) {
8428
+ const signature = this.buildFinalSignature(session, finalText);
8429
+ if (!signature) {
8430
+ return;
8431
+ }
8432
+ const kept = session.recentFinalSignatures.filter(
8433
+ (entry) => entry.signature !== signature && now - entry.at <= TELEGRAM_FINAL_SIGNATURE_DEDUPE_MS
8434
+ );
8435
+ kept.push({ signature, at: now });
8436
+ if (kept.length > TELEGRAM_RECENT_FINAL_SIGNATURES_MAX) {
8437
+ kept.splice(0, kept.length - TELEGRAM_RECENT_FINAL_SIGNATURES_MAX);
8438
+ }
8439
+ session.recentFinalSignatures = kept;
8440
+ }
8441
+ isDuplicateDelta(signature) {
8442
+ const now = nowMs();
8443
+ const previous = this.recentDeltaBySignature.get(signature) ?? 0;
8444
+ this.recentDeltaBySignature.set(signature, now);
8445
+ if (this.recentDeltaBySignature.size > 2048) {
8446
+ for (const [key, at] of this.recentDeltaBySignature) {
8447
+ if (now - at > 5e3) {
8448
+ this.recentDeltaBySignature.delete(key);
8449
+ }
8450
+ }
8451
+ }
8452
+ return previous > 0 && now - previous <= 500;
8453
+ }
8454
+ async handleApprovalRequest(kind, requestToken, params) {
8455
+ if (!this.running) {
8456
+ return;
8457
+ }
8458
+ const chatId = this.resolveChatIdForRequest(params);
8459
+ if (!chatId) {
8460
+ return;
8461
+ }
8462
+ const actionId = (0, import_node_crypto4.randomUUID)().replace(/-/g, "").slice(0, 10);
8463
+ this.pendingApprovalById.set(actionId, {
8464
+ id: actionId,
8465
+ kind,
8466
+ chatId,
8467
+ requestToken,
8468
+ createdAtMs: nowMs()
8469
+ });
8470
+ const threadId = extractThreadId(params);
8471
+ const title = kind === "exec" ? "Command approval required" : "Patch approval required";
8472
+ const detail = kind === "exec" ? extractCommandText(params) : summarizePatchRequest(params);
8473
+ const lines = [title];
8474
+ if (threadId) {
8475
+ lines.push(`Thread: ${threadId}`);
8476
+ }
8477
+ if (detail) {
8478
+ lines.push(`Detail: ${detail}`);
8479
+ }
8480
+ await this.sendSimpleMessage(chatId, lines.join("\n"), {
8481
+ reply_markup: {
8482
+ inline_keyboard: [
8483
+ [
8484
+ { text: "Approve", callback_data: `cu:ap:${actionId}:approved` },
8485
+ { text: "Approve session", callback_data: `cu:ap:${actionId}:approved_for_session` }
8486
+ ],
8487
+ [
8488
+ { text: "Deny", callback_data: `cu:ap:${actionId}:denied` },
8489
+ { text: "Abort", callback_data: `cu:ap:${actionId}:abort` }
8490
+ ]
8491
+ ]
8492
+ }
8493
+ });
8494
+ }
8495
+ async handleUserInputRequest(payload) {
8496
+ if (!this.running) {
8497
+ return;
8498
+ }
8499
+ const method = asTrimmedString(payload.method).toLowerCase();
8500
+ if (!method.includes("requestuserinput")) {
8501
+ return;
8502
+ }
8503
+ const params = asRecord2(payload.params);
8504
+ const chatId = this.resolveChatIdForRequest(params);
8505
+ if (!chatId) {
8506
+ return;
8507
+ }
8508
+ const token = String(payload.requestId);
8509
+ this.pendingUserInputByRequestId.set(token, {
8510
+ chatId,
8511
+ requestId: payload.requestId,
8512
+ method: payload.method,
8513
+ params,
8514
+ createdAtMs: nowMs()
8515
+ });
8516
+ const questions = Array.isArray(params.questions) ? params.questions.map((entry) => asRecord2(entry)).filter((entry) => asTrimmedString(entry.id)) : [];
8517
+ const lines = [
8518
+ "Input required.",
8519
+ `Request ID: ${token}`,
8520
+ "Reply with: /input <request-id> qid=answer; qid2=answer"
8521
+ ];
8522
+ if (questions.length > 0) {
8523
+ lines.push("");
8524
+ for (const question of questions) {
8525
+ const qid = asTrimmedString(question.id);
8526
+ const qtext = asTrimmedString(question.question) || asTrimmedString(question.header) || "Question";
8527
+ lines.push(`${qid}: ${qtext}`);
8528
+ const options = Array.isArray(question.options) ? question.options.map((entry) => asRecord2(entry)).map((entry) => asTrimmedString(entry.label)).filter(Boolean) : [];
8529
+ if (options.length > 0) {
8530
+ lines.push(`Options: ${options.join(", ")}`);
8531
+ }
8532
+ }
8533
+ }
8534
+ await this.sendSimpleMessage(chatId, lines.join("\n"));
8535
+ }
8536
+ resolveChatIdForRequest(params) {
8537
+ const threadId = extractThreadId(params);
8538
+ if (threadId) {
8539
+ const mapped = this.chatIdByThreadId.get(threadId);
8540
+ if (mapped) {
8541
+ return mapped;
8542
+ }
8543
+ }
8544
+ const sessions = Array.from(this.sessionsByChatId.values());
8545
+ if (sessions.length === 1) {
8546
+ return sessions[0].chatId;
8547
+ }
8548
+ return null;
8549
+ }
8550
+ prunePendingActions() {
8551
+ const threshold = nowMs() - TELEGRAM_PENDING_TTL_MS;
8552
+ for (const [id, entry] of this.pendingApprovalById) {
8553
+ if (entry.createdAtMs < threshold) {
8554
+ this.pendingApprovalById.delete(id);
8555
+ }
8556
+ }
8557
+ for (const [requestId, entry] of this.pendingUserInputByRequestId) {
8558
+ if (entry.createdAtMs < threshold) {
8559
+ this.pendingUserInputByRequestId.delete(requestId);
8560
+ }
8561
+ }
8562
+ }
8563
+ async answerCallbackQuery(callbackQueryId, text) {
8564
+ const token = this.settings.token;
8565
+ if (!token) {
8566
+ return;
8567
+ }
8568
+ await this.callTelegramApi(
8569
+ token,
8570
+ "answerCallbackQuery",
8571
+ {
8572
+ callback_query_id: callbackQueryId,
8573
+ text: text || void 0
8574
+ },
8575
+ {
8576
+ retry429: false
8577
+ }
8578
+ ).catch(() => void 0);
8579
+ }
8580
+ async sendTypingAction(chatId) {
8581
+ const token = this.settings.token;
8582
+ if (!token) {
8583
+ return;
8584
+ }
8585
+ await this.callTelegramApi(
8586
+ token,
8587
+ "sendChatAction",
8588
+ {
8589
+ chat_id: chatId,
8590
+ action: "typing"
8591
+ },
8592
+ {
8593
+ retry429: false
8594
+ }
8595
+ ).catch(() => void 0);
8596
+ }
8597
+ async sendDraftMessage(chatId, text, draftId) {
8598
+ const token = this.settings.token;
8599
+ if (!token) {
8600
+ return;
8601
+ }
8602
+ await this.sendWithRateLimit(chatId);
8603
+ await this.callTelegramApi(
8604
+ token,
8605
+ "sendMessageDraft",
8606
+ {
8607
+ chat_id: chatId,
8608
+ text,
8609
+ message_text: text,
8610
+ draft_id: draftId
8611
+ },
8612
+ {
8613
+ retry429: true
8614
+ }
8615
+ );
8616
+ }
8617
+ async sendSimpleMessage(chatId, text, extra) {
8618
+ const token = this.settings.token;
8619
+ if (!token) {
8620
+ return null;
8621
+ }
8622
+ const payload = {
8623
+ chat_id: chatId,
8624
+ text,
8625
+ ...extra
8626
+ };
8627
+ await this.sendWithRateLimit(chatId);
8628
+ const result = await this.callTelegramApi(token, "sendMessage", payload, {
8629
+ retry429: true
8630
+ });
8631
+ return asRecord2(result);
8632
+ }
8633
+ async editMessageText(chatId, messageId, text) {
8634
+ const token = this.settings.token;
8635
+ if (!token) {
8636
+ return;
8637
+ }
8638
+ await this.sendWithRateLimit(chatId);
8639
+ await this.callTelegramApi(
8640
+ token,
8641
+ "editMessageText",
8642
+ {
8643
+ chat_id: chatId,
8644
+ message_id: messageId,
8645
+ text
8646
+ },
8647
+ {
8648
+ retry429: true
8649
+ }
8650
+ );
8651
+ }
8652
+ extractTelegramMessageId(value) {
8653
+ const record = asRecord2(value);
8654
+ return asNumber(record.message_id ?? record.messageId);
8655
+ }
8656
+ async sendWithRateLimit(chatId) {
8657
+ const lastSend = this.lastSendAtByChatId.get(chatId) ?? 0;
8658
+ const now = nowMs();
8659
+ const waitMs = TELEGRAM_MIN_SEND_INTERVAL_MS - (now - lastSend);
8660
+ if (waitMs > 0) {
8661
+ await this.sleep(waitMs);
8662
+ }
8663
+ this.lastSendAtByChatId.set(chatId, nowMs());
8664
+ }
8665
+ async callTelegramApi(token, method, payload, options) {
8666
+ const url = `${TELEGRAM_API_BASE}/bot${token}/${method}`;
8667
+ const execute = async () => {
8668
+ const controller = new AbortController();
8669
+ const timeoutId = setTimeout(() => {
8670
+ controller.abort();
8671
+ }, TELEGRAM_API_TIMEOUT_MS);
8672
+ const signal = options?.signal;
8673
+ let externalAbortListener = null;
8674
+ if (signal) {
8675
+ if (signal.aborted) {
8676
+ clearTimeout(timeoutId);
8677
+ controller.abort();
8678
+ } else {
8679
+ externalAbortListener = () => controller.abort();
8680
+ signal.addEventListener("abort", externalAbortListener, { once: true });
8681
+ }
8682
+ }
8683
+ try {
8684
+ const response2 = await fetch(url, {
8685
+ method: "POST",
8686
+ headers: {
8687
+ "Content-Type": "application/json"
8688
+ },
8689
+ body: JSON.stringify(payload),
8690
+ signal: controller.signal
8691
+ });
8692
+ const json = await response2.json();
8693
+ return json;
8694
+ } finally {
8695
+ clearTimeout(timeoutId);
8696
+ if (signal && externalAbortListener) {
8697
+ signal.removeEventListener("abort", externalAbortListener);
8698
+ }
8699
+ }
8700
+ };
8701
+ const response = await execute();
8702
+ if (response.ok) {
8703
+ return response.result;
8704
+ }
8705
+ const retryAfterSeconds = response.parameters?.retry_after;
8706
+ if (options?.retry429 && response.error_code === 429 && typeof retryAfterSeconds === "number" && retryAfterSeconds > 0) {
8707
+ await this.sleep(retryAfterSeconds * 1e3);
8708
+ const retried = await execute();
8709
+ if (retried.ok) {
8710
+ return retried.result;
8711
+ }
8712
+ throw new Error(retried.description || `${method} failed (code ${retried.error_code ?? "unknown"})`);
8713
+ }
8714
+ throw new Error(response.description || `${method} failed (code ${response.error_code ?? "unknown"})`);
8715
+ }
8716
+ sleep(ms) {
8717
+ return new Promise((resolve) => {
8718
+ setTimeout(resolve, Math.max(0, Math.floor(ms)));
8719
+ });
8720
+ }
8721
+ buildRuntimeLockPath(token) {
8722
+ const tokenHash = (0, import_node_crypto4.createHash)("sha1").update(token).digest("hex").slice(0, 16);
8723
+ return import_node_path13.default.join(getUserDataDir(), `${TELEGRAM_BRIDGE_LOCK_FILE_PREFIX}-${tokenHash}.lock`);
8724
+ }
8725
+ async acquireRuntimeLock(token) {
8726
+ if (this.runtimeLockPath) {
8727
+ return;
8728
+ }
8729
+ const lockPath = this.buildRuntimeLockPath(token);
8730
+ await import_promises4.default.mkdir(import_node_path13.default.dirname(lockPath), { recursive: true });
8731
+ const payload = JSON.stringify(
8732
+ {
8733
+ pid: process.pid,
8734
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
8735
+ },
8736
+ null,
8737
+ 2
8738
+ );
8739
+ try {
8740
+ await import_promises4.default.writeFile(lockPath, payload, { encoding: "utf8", flag: "wx" });
8741
+ this.runtimeLockPath = lockPath;
8742
+ return;
8743
+ } catch (error) {
8744
+ const code = error.code;
8745
+ if (code !== "EEXIST") {
8746
+ throw error;
8747
+ }
8748
+ }
8749
+ const existingPid = await this.readLockPid(lockPath);
8750
+ if (existingPid && existingPid !== process.pid && this.isProcessAlive(existingPid)) {
8751
+ throw new Error(
8752
+ `Telegram bridge already running in another process (pid ${existingPid}). Stop the other instance to avoid duplicate replies.`
8753
+ );
8754
+ }
8755
+ await import_promises4.default.unlink(lockPath).catch(() => void 0);
8756
+ await import_promises4.default.writeFile(lockPath, payload, { encoding: "utf8", flag: "wx" });
8757
+ this.runtimeLockPath = lockPath;
8758
+ }
8759
+ async releaseRuntimeLock() {
8760
+ const lockPath = this.runtimeLockPath;
8761
+ this.runtimeLockPath = null;
8762
+ if (!lockPath) {
8763
+ return;
8764
+ }
8765
+ await import_promises4.default.unlink(lockPath).catch(() => void 0);
8766
+ }
8767
+ async readLockPid(lockPath) {
8768
+ try {
8769
+ const raw = await import_promises4.default.readFile(lockPath, "utf8");
8770
+ const parsed = JSON.parse(raw);
8771
+ const pid = asNumber(parsed.pid);
8772
+ if (pid === null || pid <= 0) {
8773
+ return null;
8774
+ }
8775
+ return Math.trunc(pid);
8776
+ } catch {
8777
+ return null;
8778
+ }
8779
+ }
8780
+ isProcessAlive(pid) {
8781
+ try {
8782
+ process.kill(pid, 0);
8783
+ return true;
8784
+ } catch (error) {
8785
+ const code = error.code;
8786
+ return code === "EPERM";
8787
+ }
8788
+ }
8789
+ };
8790
+ function createTelegramBridge(options) {
8791
+ return new TelegramBridge(options);
8792
+ }
8793
+
8794
+ // src/daemon.ts
8795
+ function hasFlag(args, flag) {
8796
+ return args.includes(flag);
8797
+ }
8798
+ function stripFlags(args) {
8799
+ return args.filter((arg) => !arg.startsWith("-"));
8800
+ }
8801
+ function parseStringFlag(flags, name) {
8802
+ const explicit = flags.find((flag) => flag.startsWith(`${name}=`));
8803
+ if (!explicit) {
8804
+ return null;
8805
+ }
8806
+ const value = explicit.slice(`${name}=`.length).trim();
8807
+ return value.length > 0 ? value : "";
8808
+ }
8809
+ function printDaemonHelp(version) {
8810
+ console.log(`CodexUse CLI v${version}
8811
+
8812
+ Usage:
8813
+ codexuse daemon start --telegram-bot-token=<token> [--project-path=/abs/path]
8814
+
8815
+ Environment:
8816
+ CODEXUSE_TELEGRAM_BOT_TOKEN Telegram bot token fallback
8817
+
8818
+ Notes:
8819
+ - Runs Codex app-server + Telegram bridge in headless mode (no desktop app).
8820
+ - --project-path auto-registers that path and makes it the default project for new chats.
8821
+ - Use on VPS/Linux with a process manager (systemd, pm2, supervisord).
8822
+ - Stop with Ctrl+C or SIGTERM.
8823
+ `);
8824
+ }
8825
+ async function ensureProjectRegistered(projectPath) {
8826
+ const resolvedPath = import_node_path14.default.resolve(projectPath);
8827
+ const projects = await listParityWorkspaces();
8828
+ const existing = projects.find((entry) => import_node_path14.default.resolve(entry.path) === resolvedPath) ?? null;
8829
+ if (!existing) {
8830
+ await assertProjectCreationAllowed(projects);
8831
+ const added = await addParityWorkspace(resolvedPath);
8832
+ console.log(`Added project: ${added.name} (${added.path})`);
8833
+ return added;
8834
+ }
8835
+ if (!existing.connected) {
8836
+ const connected = await connectParityWorkspace(existing.id);
8837
+ console.log(`Connected project: ${connected.name} (${connected.path})`);
8838
+ return connected;
8839
+ }
8840
+ return existing;
8841
+ }
8842
+ async function resolveProEnabled() {
8843
+ const cached = await licenseService.getCachedStatus().catch(() => null);
8844
+ if (cached?.isPro) {
8845
+ return true;
8846
+ }
8847
+ const refreshed = await licenseService.getStatus().catch(() => cached);
8848
+ return Boolean(refreshed?.isPro);
8849
+ }
8850
+ function parseProcessExitPayload(payload) {
8851
+ if (!payload || typeof payload !== "object") {
8852
+ return "";
8853
+ }
8854
+ const record = payload;
8855
+ const code = typeof record.code === "number" ? record.code : null;
8856
+ const signal = typeof record.signal === "string" ? record.signal : null;
8857
+ if (code !== null && signal) {
8858
+ return `code=${code}, signal=${signal}`;
8859
+ }
8860
+ if (code !== null) {
8861
+ return `code=${code}`;
8862
+ }
8863
+ if (signal) {
8864
+ return `signal=${signal}`;
8865
+ }
8866
+ return "";
8867
+ }
8868
+ async function runDaemonStart(options) {
8869
+ if (!options.telegramBotToken) {
8870
+ throw new Error(
8871
+ "Telegram bot token is required. Pass --telegram-bot-token=<token> or set CODEXUSE_TELEGRAM_BOT_TOKEN."
8872
+ );
8873
+ }
8874
+ let preferredProject = null;
8875
+ if (options.projectPath) {
8876
+ preferredProject = await ensureProjectRegistered(options.projectPath);
8877
+ }
8878
+ const appServer = new CodexAppServer();
8879
+ await appServer.initialize({
8880
+ name: "codexuse-cli-daemon",
8881
+ title: "CodexUse CLI Daemon",
8882
+ version: options.version
8883
+ });
8884
+ const bridge = createTelegramBridge({
8885
+ getAppServer: () => appServer,
8886
+ listProjects: () => listParityWorkspaces(),
8887
+ getProjectById: (id) => getParityWorkspaceById(id),
8888
+ isProEnabled: () => resolveProEnabled(),
8889
+ getDefaultProjectId: () => preferredProject?.id ?? null
8890
+ });
8891
+ await bridge.applyRuntimeSettings({
8892
+ telegramBridgeEnabled: true,
8893
+ telegramBotToken: options.telegramBotToken
8894
+ });
8895
+ const status = bridge.getStatus();
8896
+ if (!status.running) {
8897
+ await bridge.dispose().catch(() => void 0);
8898
+ await appServer.stop().catch(() => void 0);
8899
+ throw new Error(status.lastError || "Telegram bridge failed to start.");
8900
+ }
8901
+ const botLabel = status.botUsername ? `@${status.botUsername}` : "(unknown)";
8902
+ const projects = await listParityWorkspaces().catch(() => []);
8903
+ console.log(`Daemon started. Telegram bot: ${botLabel}`);
8904
+ console.log(`Streaming mode: ${status.streamMode}`);
8905
+ if (preferredProject) {
8906
+ console.log(`Default project: ${preferredProject.name} [${preferredProject.id}]`);
8907
+ }
8908
+ console.log(`Projects available: ${projects.length}`);
8909
+ console.log("Running until SIGINT/SIGTERM...");
8910
+ let stopping = false;
8911
+ let resolveWait = null;
8912
+ const stop = async (reason, exitCode) => {
8913
+ if (stopping) {
8914
+ return;
8915
+ }
8916
+ stopping = true;
8917
+ process.exitCode = exitCode;
8918
+ console.log(`Stopping daemon (${reason})...`);
8919
+ await bridge.dispose().catch((error) => {
8920
+ const message = error instanceof Error ? error.message : String(error);
8921
+ console.error(`Failed to stop Telegram bridge cleanly: ${message}`);
8922
+ });
8923
+ await appServer.stop().catch((error) => {
8924
+ const message = error instanceof Error ? error.message : String(error);
8925
+ console.error(`Failed to stop app-server cleanly: ${message}`);
8926
+ });
8927
+ resolveWait?.();
8928
+ };
8929
+ const onSigInt = () => {
8930
+ void stop("SIGINT", 0);
8931
+ };
8932
+ const onSigTerm = () => {
8933
+ void stop("SIGTERM", 0);
8934
+ };
8935
+ const onProcessExited = (payload) => {
8936
+ const suffix = parseProcessExitPayload(payload);
8937
+ const detail = suffix ? ` (${suffix})` : "";
8938
+ console.error(`Codex app-server exited unexpectedly${detail}.`);
8939
+ void stop("app-server-exited", 1);
8940
+ };
8941
+ process.once("SIGINT", onSigInt);
8942
+ process.once("SIGTERM", onSigTerm);
8943
+ appServer.once("codex:process-exited", onProcessExited);
8944
+ try {
8945
+ await new Promise((resolve) => {
8946
+ resolveWait = resolve;
8947
+ });
8948
+ } finally {
8949
+ process.off("SIGINT", onSigInt);
8950
+ process.off("SIGTERM", onSigTerm);
8951
+ appServer.off("codex:process-exited", onProcessExited);
8952
+ }
8953
+ }
8954
+ async function handleDaemonCommand(args, version) {
8955
+ const flags = args.filter((arg) => arg.startsWith("-"));
8956
+ const params = stripFlags(args);
8957
+ const sub = params[0];
8958
+ if (!sub || hasFlag(flags, "--help") || hasFlag(flags, "-h")) {
8959
+ printDaemonHelp(version);
8960
+ return;
8961
+ }
8962
+ switch (sub) {
8963
+ case "start": {
8964
+ const tokenFlag = parseStringFlag(flags, "--telegram-bot-token") ?? parseStringFlag(flags, "--bot-token");
8965
+ const tokenEnv = process.env.CODEXUSE_TELEGRAM_BOT_TOKEN?.trim() || null;
8966
+ const telegramBotToken = tokenFlag && tokenFlag.trim() || tokenEnv || "";
8967
+ const projectPath = parseStringFlag(flags, "--project-path") ?? parseStringFlag(flags, "--project") ?? null;
8968
+ await runDaemonStart({
8969
+ telegramBotToken,
8970
+ projectPath,
8971
+ version
8972
+ });
8973
+ return;
8974
+ }
8975
+ default:
8976
+ printDaemonHelp(version);
8977
+ return;
8978
+ }
8979
+ }
8980
+
8981
+ // src/index.ts
8982
+ var VERSION = true ? "2.5.8" : "0.0.0";
8983
+ var cliStorageReadyPromise = null;
8984
+ async function ensureCliStorageReady() {
8985
+ if (cliStorageReadyPromise) {
8986
+ return cliStorageReadyPromise;
8987
+ }
8988
+ cliStorageReadyPromise = (async () => {
8989
+ await initializeAppState(getUserDataDir());
8990
+ const migrated = await runStorageMigrationV1();
8991
+ if (migrated.migration.status === "pending_local_storage") {
8992
+ await importLegacyLocalStorageOnce({});
8993
+ }
8994
+ const state = await getAppState();
8995
+ if (state.migration.status !== "complete") {
8996
+ throw new Error(
8997
+ `Storage migration is not complete (status: ${state.migration.status}). CLI cannot continue until migration succeeds.`
8998
+ );
8999
+ }
9000
+ })().catch((error) => {
9001
+ cliStorageReadyPromise = null;
9002
+ throw error;
9003
+ });
9004
+ return cliStorageReadyPromise;
9005
+ }
9006
+ function printHelp() {
9007
+ console.log(`CodexUse CLI v${VERSION}
9008
+
9009
+ Usage:
9010
+ codexuse profile list [--no-usage] [--compact]
9011
+ codexuse profile current
9012
+ codexuse profile add <name> [--skip-login] [--device-auth] [--login=browser|device]
9013
+ codexuse profile refresh <name> [--skip-login] [--device-auth] [--login=browser|device]
9014
+ codexuse profile switch <name>
9015
+ codexuse profile autoroll [--threshold=50-100] [--dry-run] [--watch] [--interval=seconds]
9016
+ codexuse profile delete <name>
9017
+ codexuse profile rename <old> <new>
9018
+
9019
+ codexuse license status [--refresh]
9020
+ codexuse license activate <license-key>
9021
+
9022
+ codexuse sync status
9023
+ codexuse sync pull
9024
+ codexuse sync push
9025
+
9026
+ codexuse daemon start --telegram-bot-token=<token> [--project-path=/abs/path]
9027
+
9028
+ Flags:
9029
+ -h, --help Show help
9030
+ -v, --version Show version
9031
+ --no-usage Skip rate-limit usage fetch
9032
+ --compact Names only
9033
+ --device-auth Use device auth for Codex login
9034
+ --login=MODE Login mode: browser | device
9035
+ --threshold=NN Auto-roll switch threshold percent (50-100)
9036
+ --watch Keep checking and auto-switch when threshold is reached
9037
+ --interval=SEC Watch interval in seconds (default: 30)
9038
+ --dry-run Print planned switch without changing active profile
9039
+ --telegram-bot-token=TOKEN Telegram bot token for daemon mode
9040
+ --project-path=PATH Optional path to auto-register and set as default project in daemon mode
9041
+ `);
9042
+ }
9043
+ function hasFlag2(args, flag) {
9044
+ return args.includes(flag);
9045
+ }
9046
+ function stripFlags2(args) {
9047
+ return args.filter((arg) => !arg.startsWith("-"));
9048
+ }
9049
+ function parseLoginMode(flags) {
9050
+ if (flags.includes("--device-auth")) return "device";
9051
+ const explicit = flags.find((flag) => flag.startsWith("--login="));
9052
+ if (!explicit) return null;
9053
+ const value = explicit.split("=")[1];
9054
+ if (value === "browser" || value === "device") {
9055
+ return value;
9056
+ }
9057
+ return null;
9058
+ }
9059
+ var DEFAULT_AUTOROLL_INTERVAL_SECONDS = 30;
9060
+ var DEFAULT_AUTOROLL_THRESHOLD = 95;
9061
+ function parseNumericFlag(flags, name) {
9062
+ const explicit = flags.find((flag) => flag.startsWith(`${name}=`));
9063
+ if (!explicit) {
9064
+ return null;
9065
+ }
9066
+ const raw = explicit.slice(`${name}=`.length);
9067
+ const value = Number.parseFloat(raw);
9068
+ return Number.isFinite(value) ? value : Number.NaN;
9069
+ }
9070
+ function parseIntegerFlag(flags, name) {
9071
+ const explicit = flags.find((flag) => flag.startsWith(`${name}=`));
9072
+ if (!explicit) {
9073
+ return null;
9074
+ }
9075
+ const raw = explicit.slice(`${name}=`.length);
9076
+ const value = Number.parseInt(raw, 10);
9077
+ return Number.isFinite(value) ? value : Number.NaN;
9078
+ }
9079
+ function formatProfileLabel(name, displayName) {
9080
+ if (displayName && displayName.trim() && displayName !== name) {
9081
+ return `${displayName} (${name})`;
5135
9082
  }
5136
9083
  return name;
5137
9084
  }
@@ -5602,9 +9549,9 @@ function delay(ms) {
5602
9549
  }
5603
9550
  async function handleProfile(args) {
5604
9551
  const flags = args.filter((arg) => arg.startsWith("-"));
5605
- const params = stripFlags(args);
9552
+ const params = stripFlags2(args);
5606
9553
  const sub = params[0];
5607
- if (!sub || hasFlag(flags, "--help") || hasFlag(flags, "-h")) {
9554
+ if (!sub || hasFlag2(flags, "--help") || hasFlag2(flags, "-h")) {
5608
9555
  printHelp();
5609
9556
  return;
5610
9557
  }
@@ -5617,8 +9564,8 @@ async function handleProfile(args) {
5617
9564
  console.log("No profiles found.");
5618
9565
  return;
5619
9566
  }
5620
- const compact = hasFlag(flags, "--compact");
5621
- const withUsage = !hasFlag(flags, "--no-usage");
9567
+ const compact = hasFlag2(flags, "--compact");
9568
+ const withUsage = !hasFlag2(flags, "--no-usage");
5622
9569
  let usageMap = null;
5623
9570
  if (withUsage) {
5624
9571
  const codexPath = resolveCodexBinary();
@@ -5664,7 +9611,7 @@ async function handleProfile(args) {
5664
9611
  throw new Error("Profile name is required.");
5665
9612
  }
5666
9613
  await assertProfileCreationAllowed(manager);
5667
- if (!hasFlag(flags, "--skip-login")) {
9614
+ if (!hasFlag2(flags, "--skip-login")) {
5668
9615
  const loginMode = resolveLoginMode(parseLoginMode(flags));
5669
9616
  await runCodexLogin(loginMode);
5670
9617
  }
@@ -5678,7 +9625,7 @@ async function handleProfile(args) {
5678
9625
  if (!name) {
5679
9626
  throw new Error("Profile name is required.");
5680
9627
  }
5681
- if (!hasFlag(flags, "--skip-login")) {
9628
+ if (!hasFlag2(flags, "--skip-login")) {
5682
9629
  const loginMode = resolveLoginMode(parseLoginMode(flags));
5683
9630
  await runCodexLogin(loginMode);
5684
9631
  }
@@ -5698,8 +9645,8 @@ async function handleProfile(args) {
5698
9645
  }
5699
9646
  case "autoroll":
5700
9647
  case "auto-roll": {
5701
- const watch = hasFlag(flags, "--watch");
5702
- const dryRun = hasFlag(flags, "--dry-run");
9648
+ const watch = hasFlag2(flags, "--watch");
9649
+ const dryRun = hasFlag2(flags, "--dry-run");
5703
9650
  const settings = await getStoredAutoRollSettings().catch(() => null);
5704
9651
  const fallbackThreshold = typeof settings?.switchThreshold === "number" && Number.isFinite(settings.switchThreshold) ? Math.round(settings.switchThreshold) : DEFAULT_AUTOROLL_THRESHOLD;
5705
9652
  const threshold = parseAutoRollThreshold(flags, fallbackThreshold);
@@ -5754,15 +9701,15 @@ async function handleProfile(args) {
5754
9701
  }
5755
9702
  async function handleLicense(args) {
5756
9703
  const flags = args.filter((arg) => arg.startsWith("-"));
5757
- const params = stripFlags(args);
9704
+ const params = stripFlags2(args);
5758
9705
  const sub = params[0];
5759
- if (!sub || hasFlag(flags, "--help") || hasFlag(flags, "-h")) {
9706
+ if (!sub || hasFlag2(flags, "--help") || hasFlag2(flags, "-h")) {
5760
9707
  printHelp();
5761
9708
  return;
5762
9709
  }
5763
9710
  switch (sub) {
5764
9711
  case "status": {
5765
- const forceRefresh = hasFlag(flags, "--refresh");
9712
+ const forceRefresh = hasFlag2(flags, "--refresh");
5766
9713
  const status = await licenseService.getStatus({ forceRefresh });
5767
9714
  console.log(`Tier: ${status.tier}`);
5768
9715
  console.log(`State: ${status.state}`);
@@ -5811,9 +9758,9 @@ function printSyncResult(result) {
5811
9758
  }
5812
9759
  async function handleSync(args) {
5813
9760
  const flags = args.filter((arg) => arg.startsWith("-"));
5814
- const params = stripFlags(args);
9761
+ const params = stripFlags2(args);
5815
9762
  const sub = params[0];
5816
- if (!sub || hasFlag(flags, "--help") || hasFlag(flags, "-h")) {
9763
+ if (!sub || hasFlag2(flags, "--help") || hasFlag2(flags, "-h")) {
5817
9764
  printHelp();
5818
9765
  return;
5819
9766
  }
@@ -5866,11 +9813,15 @@ async function handleSync(args) {
5866
9813
  }
5867
9814
  async function main() {
5868
9815
  const args = process.argv.slice(2);
5869
- if (args.length === 0 || hasFlag(args, "--help") || hasFlag(args, "-h")) {
9816
+ if (args.length === 0) {
9817
+ printHelp();
9818
+ return;
9819
+ }
9820
+ if (args[0]?.startsWith("-") && (hasFlag2(args, "--help") || hasFlag2(args, "-h"))) {
5870
9821
  printHelp();
5871
9822
  return;
5872
9823
  }
5873
- if (hasFlag(args, "--version") || hasFlag(args, "-v")) {
9824
+ if (args[0]?.startsWith("-") && (hasFlag2(args, "--version") || hasFlag2(args, "-v"))) {
5874
9825
  console.log(VERSION);
5875
9826
  return;
5876
9827
  }
@@ -5889,6 +9840,10 @@ async function main() {
5889
9840
  await ensureCliStorageReady();
5890
9841
  await handleSync(rest);
5891
9842
  return;
9843
+ case "daemon":
9844
+ await ensureCliStorageReady();
9845
+ await handleDaemonCommand(rest, VERSION);
9846
+ return;
5892
9847
  default:
5893
9848
  printHelp();
5894
9849
  return;