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