codexapp 0.1.53 → 0.1.56
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 +51 -0
- package/dist/assets/index-BwkNEgMe.css +1 -0
- package/dist/assets/index-C0kJJe0e.js +1429 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +1449 -199
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/index-B_hsCD9g.js +0 -1425
- package/dist/assets/index-Dpf9bMnE.css +0 -1
package/dist-cli/index.js
CHANGED
|
@@ -2,22 +2,164 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
|
-
import { chmodSync, createWriteStream, existsSync as
|
|
5
|
+
import { chmodSync, createWriteStream, existsSync as existsSync5, mkdirSync } from "fs";
|
|
6
6
|
import { readFile as readFile4, stat as stat5, writeFile as writeFile4 } from "fs/promises";
|
|
7
|
-
import { homedir as
|
|
8
|
-
import { isAbsolute as isAbsolute3, join as
|
|
9
|
-
import { spawn as spawn3
|
|
10
|
-
import { createInterface } from "readline/promises";
|
|
7
|
+
import { homedir as homedir4, networkInterfaces } from "os";
|
|
8
|
+
import { isAbsolute as isAbsolute3, join as join6, resolve as resolve2 } from "path";
|
|
9
|
+
import { spawn as spawn3 } from "child_process";
|
|
10
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
|
-
import { dirname as
|
|
12
|
+
import { dirname as dirname4 } from "path";
|
|
13
13
|
import { get as httpsGet } from "https";
|
|
14
14
|
import { Command } from "commander";
|
|
15
15
|
import qrcode from "qrcode-terminal";
|
|
16
16
|
|
|
17
|
+
// src/commandResolution.ts
|
|
18
|
+
import { spawnSync } from "child_process";
|
|
19
|
+
import { existsSync } from "fs";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
import { delimiter, join } from "path";
|
|
22
|
+
function uniqueStrings(values) {
|
|
23
|
+
const unique = [];
|
|
24
|
+
for (const value of values) {
|
|
25
|
+
const normalized = value?.trim();
|
|
26
|
+
if (!normalized || unique.includes(normalized)) continue;
|
|
27
|
+
unique.push(normalized);
|
|
28
|
+
}
|
|
29
|
+
return unique;
|
|
30
|
+
}
|
|
31
|
+
function isPathLike(command) {
|
|
32
|
+
return command.includes("/") || command.includes("\\") || /^[a-zA-Z]:/.test(command);
|
|
33
|
+
}
|
|
34
|
+
function isRunnableCommand(command, args = []) {
|
|
35
|
+
if (isPathLike(command) && !existsSync(command)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return canRunCommand(command, args);
|
|
39
|
+
}
|
|
40
|
+
function getWindowsAppDataNpmPrefix() {
|
|
41
|
+
const appData = process.env.APPDATA?.trim();
|
|
42
|
+
return appData ? join(appData, "npm") : null;
|
|
43
|
+
}
|
|
44
|
+
function getPotentialNpmPrefixes() {
|
|
45
|
+
return uniqueStrings([
|
|
46
|
+
process.env.npm_config_prefix,
|
|
47
|
+
process.env.PREFIX,
|
|
48
|
+
getUserNpmPrefix(),
|
|
49
|
+
process.platform === "win32" ? getWindowsAppDataNpmPrefix() : null
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
function getPotentialCodexPackageDirs(prefix) {
|
|
53
|
+
const dirs = [join(prefix, "node_modules", "@openai", "codex")];
|
|
54
|
+
if (process.platform !== "win32") {
|
|
55
|
+
dirs.push(join(prefix, "lib", "node_modules", "@openai", "codex"));
|
|
56
|
+
}
|
|
57
|
+
return dirs;
|
|
58
|
+
}
|
|
59
|
+
function getPotentialCodexExecutables(prefix) {
|
|
60
|
+
return getPotentialCodexPackageDirs(prefix).map((packageDir) => process.platform === "win32" ? join(
|
|
61
|
+
packageDir,
|
|
62
|
+
"node_modules",
|
|
63
|
+
"@openai",
|
|
64
|
+
"codex-win32-x64",
|
|
65
|
+
"vendor",
|
|
66
|
+
"x86_64-pc-windows-msvc",
|
|
67
|
+
"codex",
|
|
68
|
+
"codex.exe"
|
|
69
|
+
) : join(packageDir, "bin", "codex"));
|
|
70
|
+
}
|
|
71
|
+
function getPotentialRipgrepExecutables(prefix) {
|
|
72
|
+
return getPotentialCodexPackageDirs(prefix).map((packageDir) => process.platform === "win32" ? join(
|
|
73
|
+
packageDir,
|
|
74
|
+
"node_modules",
|
|
75
|
+
"@openai",
|
|
76
|
+
"codex-win32-x64",
|
|
77
|
+
"vendor",
|
|
78
|
+
"x86_64-pc-windows-msvc",
|
|
79
|
+
"path",
|
|
80
|
+
"rg.exe"
|
|
81
|
+
) : join(packageDir, "bin", "rg"));
|
|
82
|
+
}
|
|
83
|
+
function canRunCommand(command, args = []) {
|
|
84
|
+
const result = spawnSync(command, args, {
|
|
85
|
+
stdio: "ignore",
|
|
86
|
+
windowsHide: true
|
|
87
|
+
});
|
|
88
|
+
return !result.error && result.status === 0;
|
|
89
|
+
}
|
|
90
|
+
function getUserNpmPrefix() {
|
|
91
|
+
return join(homedir(), ".npm-global");
|
|
92
|
+
}
|
|
93
|
+
function getNpmGlobalBinDir(prefix) {
|
|
94
|
+
return process.platform === "win32" ? prefix : join(prefix, "bin");
|
|
95
|
+
}
|
|
96
|
+
function prependPathEntry(existingPath, entry) {
|
|
97
|
+
const normalizedEntry = entry.trim();
|
|
98
|
+
if (!normalizedEntry) return existingPath;
|
|
99
|
+
const parts = existingPath.split(delimiter).map((value) => value.trim()).filter(Boolean);
|
|
100
|
+
if (parts.includes(normalizedEntry)) {
|
|
101
|
+
return existingPath;
|
|
102
|
+
}
|
|
103
|
+
return existingPath ? `${normalizedEntry}${delimiter}${existingPath}` : normalizedEntry;
|
|
104
|
+
}
|
|
105
|
+
function resolveCodexCommand() {
|
|
106
|
+
const explicit = process.env.CODEXUI_CODEX_COMMAND?.trim();
|
|
107
|
+
const packageCandidates = getPotentialNpmPrefixes().flatMap(getPotentialCodexExecutables);
|
|
108
|
+
const fallbackCandidates = process.platform === "win32" ? [...packageCandidates, "codex"] : ["codex", ...packageCandidates];
|
|
109
|
+
for (const candidate of uniqueStrings([explicit, ...fallbackCandidates])) {
|
|
110
|
+
if (isRunnableCommand(candidate, ["--version"])) {
|
|
111
|
+
return candidate;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function resolveRipgrepCommand() {
|
|
117
|
+
const explicit = process.env.CODEXUI_RG_COMMAND?.trim();
|
|
118
|
+
const packageCandidates = getPotentialNpmPrefixes().flatMap(getPotentialRipgrepExecutables);
|
|
119
|
+
const fallbackCandidates = process.platform === "win32" ? [...packageCandidates, "rg"] : ["rg", ...packageCandidates];
|
|
120
|
+
for (const candidate of uniqueStrings([explicit, ...fallbackCandidates])) {
|
|
121
|
+
if (isRunnableCommand(candidate, ["--version"])) {
|
|
122
|
+
return candidate;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function resolvePythonCommand() {
|
|
128
|
+
const candidates = process.platform === "win32" ? [
|
|
129
|
+
{ command: "python", args: [] },
|
|
130
|
+
{ command: "py", args: ["-3"] },
|
|
131
|
+
{ command: "python3", args: [] }
|
|
132
|
+
] : [
|
|
133
|
+
{ command: "python3", args: [] },
|
|
134
|
+
{ command: "python", args: [] }
|
|
135
|
+
];
|
|
136
|
+
for (const candidate of candidates) {
|
|
137
|
+
if (isRunnableCommand(candidate.command, [...candidate.args, "--version"])) {
|
|
138
|
+
return candidate;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
function resolveSkillInstallerScriptPath(codexHome) {
|
|
144
|
+
const normalizedCodexHome = codexHome?.trim();
|
|
145
|
+
const candidates = uniqueStrings([
|
|
146
|
+
normalizedCodexHome ? join(normalizedCodexHome, "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py") : null,
|
|
147
|
+
process.env.CODEX_HOME?.trim() ? join(process.env.CODEX_HOME.trim(), "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py") : null,
|
|
148
|
+
join(homedir(), ".codex", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py"),
|
|
149
|
+
join(homedir(), ".cursor", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py")
|
|
150
|
+
]);
|
|
151
|
+
for (const candidate of candidates) {
|
|
152
|
+
if (existsSync(candidate)) {
|
|
153
|
+
return candidate;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
17
159
|
// src/server/httpServer.ts
|
|
18
160
|
import { fileURLToPath } from "url";
|
|
19
|
-
import { dirname as
|
|
20
|
-
import { existsSync as
|
|
161
|
+
import { dirname as dirname3, extname as extname3, isAbsolute as isAbsolute2, join as join5 } from "path";
|
|
162
|
+
import { existsSync as existsSync4 } from "fs";
|
|
21
163
|
import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
|
|
22
164
|
import express from "express";
|
|
23
165
|
|
|
@@ -25,18 +167,21 @@ import express from "express";
|
|
|
25
167
|
import { spawn as spawn2 } from "child_process";
|
|
26
168
|
import { randomBytes } from "crypto";
|
|
27
169
|
import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
|
|
170
|
+
import { createReadStream } from "fs";
|
|
171
|
+
import { request as httpRequest } from "http";
|
|
28
172
|
import { request as httpsRequest } from "https";
|
|
29
|
-
import { homedir as
|
|
173
|
+
import { homedir as homedir3 } from "os";
|
|
30
174
|
import { tmpdir as tmpdir2 } from "os";
|
|
31
|
-
import { basename, isAbsolute, join as
|
|
175
|
+
import { basename as basename3, dirname, isAbsolute, join as join3, resolve } from "path";
|
|
176
|
+
import { createInterface } from "readline";
|
|
32
177
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
33
178
|
|
|
34
179
|
// src/server/skillsRoutes.ts
|
|
35
180
|
import { spawn } from "child_process";
|
|
36
181
|
import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
|
|
37
|
-
import { existsSync } from "fs";
|
|
38
|
-
import { homedir, tmpdir } from "os";
|
|
39
|
-
import { join } from "path";
|
|
182
|
+
import { existsSync as existsSync2 } from "fs";
|
|
183
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
184
|
+
import { join as join2 } from "path";
|
|
40
185
|
import { writeFile } from "fs/promises";
|
|
41
186
|
function asRecord(value) {
|
|
42
187
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -62,28 +207,45 @@ function setJson(res, statusCode, payload) {
|
|
|
62
207
|
}
|
|
63
208
|
function getCodexHomeDir() {
|
|
64
209
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
65
|
-
return codexHome && codexHome.length > 0 ? codexHome :
|
|
210
|
+
return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
|
|
66
211
|
}
|
|
67
212
|
function getSkillsInstallDir() {
|
|
68
|
-
return
|
|
213
|
+
return join2(getCodexHomeDir(), "skills");
|
|
69
214
|
}
|
|
215
|
+
var DEFAULT_COMMAND_TIMEOUT_MS = 12e4;
|
|
70
216
|
async function runCommand(command, args, options = {}) {
|
|
217
|
+
const timeout = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
71
218
|
await new Promise((resolve3, reject) => {
|
|
72
219
|
const proc = spawn(command, args, {
|
|
73
220
|
cwd: options.cwd,
|
|
74
221
|
env: process.env,
|
|
75
222
|
stdio: ["ignore", "pipe", "pipe"]
|
|
76
223
|
});
|
|
224
|
+
let settled = false;
|
|
77
225
|
let stdout = "";
|
|
78
226
|
let stderr = "";
|
|
227
|
+
const timer = setTimeout(() => {
|
|
228
|
+
if (settled) return;
|
|
229
|
+
settled = true;
|
|
230
|
+
proc.kill("SIGKILL");
|
|
231
|
+
reject(new Error(`Command timed out after ${timeout}ms (${command} ${args.join(" ")})`));
|
|
232
|
+
}, timeout);
|
|
79
233
|
proc.stdout.on("data", (chunk) => {
|
|
80
234
|
stdout += chunk.toString();
|
|
81
235
|
});
|
|
82
236
|
proc.stderr.on("data", (chunk) => {
|
|
83
237
|
stderr += chunk.toString();
|
|
84
238
|
});
|
|
85
|
-
proc.on("error",
|
|
239
|
+
proc.on("error", (err) => {
|
|
240
|
+
if (settled) return;
|
|
241
|
+
settled = true;
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
reject(err);
|
|
244
|
+
});
|
|
86
245
|
proc.on("close", (code) => {
|
|
246
|
+
if (settled) return;
|
|
247
|
+
settled = true;
|
|
248
|
+
clearTimeout(timer);
|
|
87
249
|
if (code === 0) {
|
|
88
250
|
resolve3();
|
|
89
251
|
return;
|
|
@@ -95,22 +257,38 @@ async function runCommand(command, args, options = {}) {
|
|
|
95
257
|
});
|
|
96
258
|
}
|
|
97
259
|
async function runCommandWithOutput(command, args, options = {}) {
|
|
260
|
+
const timeout = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
98
261
|
return await new Promise((resolve3, reject) => {
|
|
99
262
|
const proc = spawn(command, args, {
|
|
100
263
|
cwd: options.cwd,
|
|
101
264
|
env: process.env,
|
|
102
265
|
stdio: ["ignore", "pipe", "pipe"]
|
|
103
266
|
});
|
|
267
|
+
let settled = false;
|
|
104
268
|
let stdout = "";
|
|
105
269
|
let stderr = "";
|
|
270
|
+
const timer = setTimeout(() => {
|
|
271
|
+
if (settled) return;
|
|
272
|
+
settled = true;
|
|
273
|
+
proc.kill("SIGKILL");
|
|
274
|
+
reject(new Error(`Command timed out after ${timeout}ms (${command} ${args.join(" ")})`));
|
|
275
|
+
}, timeout);
|
|
106
276
|
proc.stdout.on("data", (chunk) => {
|
|
107
277
|
stdout += chunk.toString();
|
|
108
278
|
});
|
|
109
279
|
proc.stderr.on("data", (chunk) => {
|
|
110
280
|
stderr += chunk.toString();
|
|
111
281
|
});
|
|
112
|
-
proc.on("error",
|
|
282
|
+
proc.on("error", (err) => {
|
|
283
|
+
if (settled) return;
|
|
284
|
+
settled = true;
|
|
285
|
+
clearTimeout(timer);
|
|
286
|
+
reject(err);
|
|
287
|
+
});
|
|
113
288
|
proc.on("close", (code) => {
|
|
289
|
+
if (settled) return;
|
|
290
|
+
settled = true;
|
|
291
|
+
clearTimeout(timer);
|
|
114
292
|
if (code === 0) {
|
|
115
293
|
resolve3(stdout.trim());
|
|
116
294
|
return;
|
|
@@ -121,6 +299,21 @@ async function runCommandWithOutput(command, args, options = {}) {
|
|
|
121
299
|
});
|
|
122
300
|
});
|
|
123
301
|
}
|
|
302
|
+
function withTimeout(promise, ms, label) {
|
|
303
|
+
return new Promise((resolve3, reject) => {
|
|
304
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
305
|
+
promise.then(
|
|
306
|
+
(val) => {
|
|
307
|
+
clearTimeout(timer);
|
|
308
|
+
resolve3(val);
|
|
309
|
+
},
|
|
310
|
+
(err) => {
|
|
311
|
+
clearTimeout(timer);
|
|
312
|
+
reject(err);
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
124
317
|
async function detectUserSkillsDir(appServer) {
|
|
125
318
|
try {
|
|
126
319
|
const result = await appServer.rpc("skills/list", {});
|
|
@@ -236,13 +429,14 @@ var DEFAULT_SKILLS_SYNC_REPO_NAME = "codexskills";
|
|
|
236
429
|
var SKILLS_SYNC_MANIFEST_PATH = "installed-skills.json";
|
|
237
430
|
var SYNC_UPSTREAM_SKILLS_OWNER = "OpenClawAndroid";
|
|
238
431
|
var SYNC_UPSTREAM_SKILLS_REPO = "skills";
|
|
432
|
+
var PRIVATE_SYNC_BRANCH = "main";
|
|
239
433
|
var HUB_SKILLS_OWNER = "openclaw";
|
|
240
434
|
var HUB_SKILLS_REPO = "skills";
|
|
241
435
|
var startupSkillsSyncInitialized = false;
|
|
242
436
|
var startupSyncStatus = {
|
|
243
437
|
inProgress: false,
|
|
244
438
|
mode: "idle",
|
|
245
|
-
branch:
|
|
439
|
+
branch: PRIVATE_SYNC_BRANCH,
|
|
246
440
|
lastAction: "not-started",
|
|
247
441
|
lastRunAtIso: "",
|
|
248
442
|
lastSuccessAtIso: "",
|
|
@@ -255,7 +449,7 @@ async function scanInstalledSkillsFromDisk() {
|
|
|
255
449
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
256
450
|
for (const entry of entries) {
|
|
257
451
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
258
|
-
const skillMd =
|
|
452
|
+
const skillMd = join2(skillsDir, entry.name, "SKILL.md");
|
|
259
453
|
try {
|
|
260
454
|
await stat(skillMd);
|
|
261
455
|
map.set(entry.name, { name: entry.name, path: skillMd, enabled: true });
|
|
@@ -266,8 +460,25 @@ async function scanInstalledSkillsFromDisk() {
|
|
|
266
460
|
}
|
|
267
461
|
return map;
|
|
268
462
|
}
|
|
463
|
+
function extractSkillDescriptionFromMarkdown(markdown) {
|
|
464
|
+
const lines = markdown.split(/\r?\n/);
|
|
465
|
+
let inCodeFence = false;
|
|
466
|
+
for (const rawLine of lines) {
|
|
467
|
+
const line = rawLine.trim();
|
|
468
|
+
if (line.startsWith("```")) {
|
|
469
|
+
inCodeFence = !inCodeFence;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (inCodeFence || line.length === 0) continue;
|
|
473
|
+
if (line.startsWith("#")) continue;
|
|
474
|
+
if (line.startsWith(">")) continue;
|
|
475
|
+
if (line.startsWith("- ") || line.startsWith("* ")) continue;
|
|
476
|
+
return line;
|
|
477
|
+
}
|
|
478
|
+
return "";
|
|
479
|
+
}
|
|
269
480
|
function getSkillsSyncStatePath() {
|
|
270
|
-
return
|
|
481
|
+
return join2(getCodexHomeDir(), "skills-sync.json");
|
|
271
482
|
}
|
|
272
483
|
async function readSkillsSyncState() {
|
|
273
484
|
try {
|
|
@@ -340,14 +551,14 @@ async function completeGithubDeviceLogin(deviceCode) {
|
|
|
340
551
|
}
|
|
341
552
|
function isAndroidLikeRuntime() {
|
|
342
553
|
if (process.platform === "android") return true;
|
|
343
|
-
if (
|
|
554
|
+
if (existsSync2("/data/data/com.termux")) return true;
|
|
344
555
|
if (process.env.TERMUX_VERSION) return true;
|
|
345
556
|
const prefix = process.env.PREFIX?.toLowerCase() ?? "";
|
|
346
557
|
if (prefix.includes("/com.termux/")) return true;
|
|
347
558
|
const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
|
|
348
559
|
return proot.length > 0;
|
|
349
560
|
}
|
|
350
|
-
function
|
|
561
|
+
function getPreferredPublicUpstreamBranch() {
|
|
351
562
|
return isAndroidLikeRuntime() ? "android" : "main";
|
|
352
563
|
}
|
|
353
564
|
function isUpstreamSkillsRepo(repoOwner, repoName) {
|
|
@@ -402,10 +613,10 @@ async function ensurePrivateForkFromUpstream(token, username, repoName) {
|
|
|
402
613
|
}
|
|
403
614
|
if (!ready) throw new Error("Private mirror repo was created but is not available yet");
|
|
404
615
|
if (!created) return;
|
|
405
|
-
const tmp = await mkdtemp(
|
|
616
|
+
const tmp = await mkdtemp(join2(tmpdir(), "codex-skills-seed-"));
|
|
406
617
|
try {
|
|
407
618
|
const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
408
|
-
const branch =
|
|
619
|
+
const branch = PRIVATE_SYNC_BRANCH;
|
|
409
620
|
try {
|
|
410
621
|
await runCommand("git", ["clone", "--depth", "1", "--single-branch", "--branch", branch, upstreamUrl, tmp]);
|
|
411
622
|
} catch {
|
|
@@ -476,7 +687,7 @@ function toGitHubTokenRemote(repoOwner, repoName, token) {
|
|
|
476
687
|
async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
|
|
477
688
|
const localDir = getSkillsInstallDir();
|
|
478
689
|
await mkdir(localDir, { recursive: true });
|
|
479
|
-
const gitDir =
|
|
690
|
+
const gitDir = join2(localDir, ".git");
|
|
480
691
|
let hasGitDir = false;
|
|
481
692
|
try {
|
|
482
693
|
hasGitDir = (await stat(gitDir)).isDirectory();
|
|
@@ -522,7 +733,7 @@ async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
|
|
|
522
733
|
}
|
|
523
734
|
let pulledMtimes = /* @__PURE__ */ new Map();
|
|
524
735
|
try {
|
|
525
|
-
await runCommand("git", ["pull", "--no-rebase", "origin", branch], { cwd: localDir });
|
|
736
|
+
await runCommand("git", ["pull", "--no-rebase", "--no-ff", "origin", branch], { cwd: localDir });
|
|
526
737
|
pulledMtimes = await snapshotFileMtimes(localDir);
|
|
527
738
|
} catch {
|
|
528
739
|
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
@@ -591,7 +802,7 @@ async function walkFileMtimes(rootDir, currentDir, out) {
|
|
|
591
802
|
for (const entry of entries) {
|
|
592
803
|
const entryName = String(entry.name);
|
|
593
804
|
if (entryName === ".git") continue;
|
|
594
|
-
const absolutePath =
|
|
805
|
+
const absolutePath = join2(currentDir, entryName);
|
|
595
806
|
const relativePath = absolutePath.slice(rootDir.length + 1);
|
|
596
807
|
if (entry.isDirectory()) {
|
|
597
808
|
await walkFileMtimes(rootDir, absolutePath, out);
|
|
@@ -606,8 +817,37 @@ async function walkFileMtimes(rootDir, currentDir, out) {
|
|
|
606
817
|
}
|
|
607
818
|
}
|
|
608
819
|
async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _installedMap) {
|
|
820
|
+
function isNonFastForwardPushError(error) {
|
|
821
|
+
const text = getErrorMessage(error, "").toLowerCase();
|
|
822
|
+
return text.includes("non-fast-forward") || text.includes("fetch first") || text.includes("rejected") && text.includes("push");
|
|
823
|
+
}
|
|
824
|
+
async function pushWithNonFastForwardRetry(repoDir2, branch2) {
|
|
825
|
+
const maxAttempts = 3;
|
|
826
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
827
|
+
try {
|
|
828
|
+
await runCommand("git", ["push", "origin", `HEAD:${branch2}`], { cwd: repoDir2 });
|
|
829
|
+
return;
|
|
830
|
+
} catch (error) {
|
|
831
|
+
if (!isNonFastForwardPushError(error) || attempt >= maxAttempts) {
|
|
832
|
+
throw error;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
await runCommand("git", ["fetch", "origin"], { cwd: repoDir2 });
|
|
836
|
+
try {
|
|
837
|
+
await runCommand("git", ["pull", "--no-rebase", "--no-ff", "origin", branch2], { cwd: repoDir2 });
|
|
838
|
+
} catch {
|
|
839
|
+
await resolveMergeConflictsByNewerCommit(repoDir2, branch2);
|
|
840
|
+
}
|
|
841
|
+
await runCommand("git", ["add", "."], { cwd: repoDir2 });
|
|
842
|
+
const statusAfterReconcile = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir2 })).trim();
|
|
843
|
+
if (statusAfterReconcile) {
|
|
844
|
+
await runCommand("git", ["commit", "-m", "Reconcile skills sync before push retry"], { cwd: repoDir2 });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
throw new Error("Failed to push after non-fast-forward retries");
|
|
848
|
+
}
|
|
609
849
|
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
610
|
-
const branch =
|
|
850
|
+
const branch = PRIVATE_SYNC_BRANCH;
|
|
611
851
|
const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
612
852
|
void _installedMap;
|
|
613
853
|
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
|
|
@@ -616,16 +856,16 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
|
|
|
616
856
|
const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
|
|
617
857
|
if (!status) return;
|
|
618
858
|
await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
|
|
619
|
-
await
|
|
859
|
+
await pushWithNonFastForwardRetry(repoDir, branch);
|
|
620
860
|
}
|
|
621
861
|
async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName) {
|
|
622
862
|
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
623
|
-
const branch =
|
|
863
|
+
const branch = PRIVATE_SYNC_BRANCH;
|
|
624
864
|
await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
625
865
|
}
|
|
626
866
|
async function bootstrapSkillsFromUpstreamIntoLocal() {
|
|
627
867
|
const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
628
|
-
const branch =
|
|
868
|
+
const branch = getPreferredPublicUpstreamBranch();
|
|
629
869
|
await ensureSkillsWorkingTreeRepo(repoUrl, branch);
|
|
630
870
|
}
|
|
631
871
|
async function collectLocalSyncedSkills(appServer) {
|
|
@@ -685,9 +925,9 @@ async function autoPushSyncedSkills(appServer) {
|
|
|
685
925
|
}
|
|
686
926
|
async function ensureCodexAgentsSymlinkToSkillsAgents() {
|
|
687
927
|
const codexHomeDir = getCodexHomeDir();
|
|
688
|
-
const skillsAgentsPath =
|
|
689
|
-
const codexAgentsPath =
|
|
690
|
-
await mkdir(
|
|
928
|
+
const skillsAgentsPath = join2(codexHomeDir, "skills", "AGENTS.md");
|
|
929
|
+
const codexAgentsPath = join2(codexHomeDir, "AGENTS.md");
|
|
930
|
+
await mkdir(join2(codexHomeDir, "skills"), { recursive: true });
|
|
691
931
|
let copiedFromCodex = false;
|
|
692
932
|
try {
|
|
693
933
|
const codexAgentsStat = await lstat(codexAgentsPath);
|
|
@@ -711,7 +951,7 @@ async function ensureCodexAgentsSymlinkToSkillsAgents() {
|
|
|
711
951
|
await writeFile(skillsAgentsPath, "", "utf8");
|
|
712
952
|
}
|
|
713
953
|
}
|
|
714
|
-
const relativeTarget =
|
|
954
|
+
const relativeTarget = join2("skills", "AGENTS.md");
|
|
715
955
|
try {
|
|
716
956
|
const current = await lstat(codexAgentsPath);
|
|
717
957
|
if (current.isSymbolicLink()) {
|
|
@@ -729,7 +969,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
|
|
|
729
969
|
startupSyncStatus.inProgress = true;
|
|
730
970
|
startupSyncStatus.lastRunAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
731
971
|
startupSyncStatus.lastError = "";
|
|
732
|
-
startupSyncStatus.branch =
|
|
972
|
+
startupSyncStatus.branch = PRIVATE_SYNC_BRANCH;
|
|
733
973
|
try {
|
|
734
974
|
const state = await readSkillsSyncState();
|
|
735
975
|
if (!state.githubToken) {
|
|
@@ -741,6 +981,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
|
|
|
741
981
|
return;
|
|
742
982
|
}
|
|
743
983
|
startupSyncStatus.mode = "unauthenticated-bootstrap";
|
|
984
|
+
startupSyncStatus.branch = getPreferredPublicUpstreamBranch();
|
|
744
985
|
startupSyncStatus.lastAction = "pull-upstream";
|
|
745
986
|
await bootstrapSkillsFromUpstreamIntoLocal();
|
|
746
987
|
try {
|
|
@@ -752,6 +993,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
|
|
|
752
993
|
return;
|
|
753
994
|
}
|
|
754
995
|
startupSyncStatus.mode = "authenticated-fork-sync";
|
|
996
|
+
startupSyncStatus.branch = PRIVATE_SYNC_BRANCH;
|
|
755
997
|
startupSyncStatus.lastAction = "ensure-private-fork";
|
|
756
998
|
const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
|
|
757
999
|
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
@@ -991,13 +1233,21 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
991
1233
|
}
|
|
992
1234
|
const localDir = await detectUserSkillsDir(appServer);
|
|
993
1235
|
await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
|
|
994
|
-
const installerScript =
|
|
1236
|
+
const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir());
|
|
1237
|
+
if (!installerScript) {
|
|
1238
|
+
throw new Error("Skill installer script not found");
|
|
1239
|
+
}
|
|
1240
|
+
const pythonCommand = resolvePythonCommand();
|
|
1241
|
+
if (!pythonCommand) {
|
|
1242
|
+
throw new Error("Python 3 is required to install skills");
|
|
1243
|
+
}
|
|
995
1244
|
const localSkills = await scanInstalledSkillsFromDisk();
|
|
996
1245
|
for (const skill of remote) {
|
|
997
1246
|
const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
|
|
998
1247
|
if (!owner) continue;
|
|
999
1248
|
if (!localSkills.has(skill.name)) {
|
|
1000
|
-
await runCommand(
|
|
1249
|
+
await runCommand(pythonCommand.command, [
|
|
1250
|
+
...pythonCommand.args,
|
|
1001
1251
|
installerScript,
|
|
1002
1252
|
"--repo",
|
|
1003
1253
|
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
@@ -1009,7 +1259,7 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1009
1259
|
"git"
|
|
1010
1260
|
]);
|
|
1011
1261
|
}
|
|
1012
|
-
const skillPath =
|
|
1262
|
+
const skillPath = join2(localDir, skill.name);
|
|
1013
1263
|
await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
|
|
1014
1264
|
}
|
|
1015
1265
|
const remoteNames = new Set(remote.map((row) => row.name));
|
|
@@ -1038,15 +1288,29 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1038
1288
|
try {
|
|
1039
1289
|
const owner = url.searchParams.get("owner") || "";
|
|
1040
1290
|
const name = url.searchParams.get("name") || "";
|
|
1291
|
+
const installed = url.searchParams.get("installed") === "true";
|
|
1292
|
+
const skillPath = url.searchParams.get("path") || "";
|
|
1041
1293
|
if (!owner || !name) {
|
|
1042
1294
|
setJson(res, 400, { error: "Missing owner or name" });
|
|
1043
1295
|
return true;
|
|
1044
1296
|
}
|
|
1297
|
+
if (installed) {
|
|
1298
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
1299
|
+
const installedInfo = installedMap.get(name);
|
|
1300
|
+
const localSkillPath = installedInfo?.path || (skillPath ? skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md` : "");
|
|
1301
|
+
if (localSkillPath) {
|
|
1302
|
+
const content2 = await readFile(localSkillPath, "utf8");
|
|
1303
|
+
const description2 = extractSkillDescriptionFromMarkdown(content2);
|
|
1304
|
+
setJson(res, 200, { content: content2, description: description2, source: "local" });
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1045
1308
|
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
1046
1309
|
const resp = await fetch(rawUrl);
|
|
1047
1310
|
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1048
1311
|
const content = await resp.text();
|
|
1049
|
-
|
|
1312
|
+
const description = extractSkillDescriptionFromMarkdown(content);
|
|
1313
|
+
setJson(res, 200, { content, description, source: "remote" });
|
|
1050
1314
|
} catch (error) {
|
|
1051
1315
|
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
|
|
1052
1316
|
}
|
|
@@ -1061,9 +1325,25 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1061
1325
|
setJson(res, 400, { error: "Missing owner or name" });
|
|
1062
1326
|
return true;
|
|
1063
1327
|
}
|
|
1064
|
-
const installerScript =
|
|
1065
|
-
|
|
1066
|
-
|
|
1328
|
+
const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir());
|
|
1329
|
+
if (!installerScript) {
|
|
1330
|
+
throw new Error("Skill installer script not found");
|
|
1331
|
+
}
|
|
1332
|
+
const pythonCommand = resolvePythonCommand();
|
|
1333
|
+
if (!pythonCommand) {
|
|
1334
|
+
throw new Error("Python 3 is required to install skills");
|
|
1335
|
+
}
|
|
1336
|
+
const installDest = await withTimeout(
|
|
1337
|
+
detectUserSkillsDir(appServer),
|
|
1338
|
+
1e4,
|
|
1339
|
+
"detectUserSkillsDir"
|
|
1340
|
+
).catch(() => getSkillsInstallDir());
|
|
1341
|
+
const skillDir = join2(installDest, name);
|
|
1342
|
+
if (existsSync2(skillDir)) {
|
|
1343
|
+
await rm(skillDir, { recursive: true, force: true });
|
|
1344
|
+
}
|
|
1345
|
+
await runCommand(pythonCommand.command, [
|
|
1346
|
+
...pythonCommand.args,
|
|
1067
1347
|
installerScript,
|
|
1068
1348
|
"--repo",
|
|
1069
1349
|
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
@@ -1073,13 +1353,16 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1073
1353
|
installDest,
|
|
1074
1354
|
"--method",
|
|
1075
1355
|
"git"
|
|
1076
|
-
]);
|
|
1077
|
-
|
|
1078
|
-
|
|
1356
|
+
], { timeoutMs: 9e4 });
|
|
1357
|
+
try {
|
|
1358
|
+
await withTimeout(ensureInstalledSkillIsValid(appServer, skillDir), 1e4, "ensureInstalledSkillIsValid");
|
|
1359
|
+
} catch {
|
|
1360
|
+
}
|
|
1079
1361
|
const syncState = await readSkillsSyncState();
|
|
1080
1362
|
const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
|
|
1081
1363
|
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1082
|
-
|
|
1364
|
+
autoPushSyncedSkills(appServer).catch(() => {
|
|
1365
|
+
});
|
|
1083
1366
|
setJson(res, 200, { ok: true, path: skillDir });
|
|
1084
1367
|
} catch (error) {
|
|
1085
1368
|
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
@@ -1091,7 +1374,7 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1091
1374
|
const payload = asRecord(await readJsonBody2(req));
|
|
1092
1375
|
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1093
1376
|
const path = typeof payload?.path === "string" ? payload.path : "";
|
|
1094
|
-
const target = path || (name ?
|
|
1377
|
+
const target = path || (name ? join2(getSkillsInstallDir(), name) : "");
|
|
1095
1378
|
if (!target) {
|
|
1096
1379
|
setJson(res, 400, { error: "Missing name or path" });
|
|
1097
1380
|
return true;
|
|
@@ -1103,9 +1386,10 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1103
1386
|
delete nextOwners[name];
|
|
1104
1387
|
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1105
1388
|
}
|
|
1106
|
-
|
|
1389
|
+
autoPushSyncedSkills(appServer).catch(() => {
|
|
1390
|
+
});
|
|
1107
1391
|
try {
|
|
1108
|
-
await appServer.rpc("skills/list", { forceReload: true });
|
|
1392
|
+
await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
|
|
1109
1393
|
} catch {
|
|
1110
1394
|
}
|
|
1111
1395
|
setJson(res, 200, { ok: true, deletedPath: target });
|
|
@@ -1117,7 +1401,8 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1117
1401
|
return false;
|
|
1118
1402
|
}
|
|
1119
1403
|
|
|
1120
|
-
// src/server/
|
|
1404
|
+
// src/server/telegramThreadBridge.ts
|
|
1405
|
+
import { basename } from "path";
|
|
1121
1406
|
function asRecord2(value) {
|
|
1122
1407
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1123
1408
|
}
|
|
@@ -1135,21 +1420,425 @@ function getErrorMessage2(payload, fallback) {
|
|
|
1135
1420
|
}
|
|
1136
1421
|
return fallback;
|
|
1137
1422
|
}
|
|
1423
|
+
var TelegramThreadBridge = class {
|
|
1424
|
+
constructor(appServer) {
|
|
1425
|
+
this.threadIdByChatId = /* @__PURE__ */ new Map();
|
|
1426
|
+
this.chatIdsByThreadId = /* @__PURE__ */ new Map();
|
|
1427
|
+
this.lastForwardedTurnByThreadId = /* @__PURE__ */ new Map();
|
|
1428
|
+
this.active = false;
|
|
1429
|
+
this.pollingTask = null;
|
|
1430
|
+
this.nextUpdateOffset = 0;
|
|
1431
|
+
this.lastError = "";
|
|
1432
|
+
this.appServer = appServer;
|
|
1433
|
+
this.token = process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
|
|
1434
|
+
this.defaultCwd = process.env.TELEGRAM_DEFAULT_CWD?.trim() ?? process.cwd();
|
|
1435
|
+
}
|
|
1436
|
+
start() {
|
|
1437
|
+
if (!this.token || this.active) return;
|
|
1438
|
+
this.active = true;
|
|
1439
|
+
void this.notifyOnlineForKnownChats().catch(() => {
|
|
1440
|
+
});
|
|
1441
|
+
this.pollingTask = this.pollLoop();
|
|
1442
|
+
this.appServer.onNotification((notification) => {
|
|
1443
|
+
void this.handleNotification(notification).catch(() => {
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
stop() {
|
|
1448
|
+
this.active = false;
|
|
1449
|
+
}
|
|
1450
|
+
async pollLoop() {
|
|
1451
|
+
while (this.active) {
|
|
1452
|
+
try {
|
|
1453
|
+
const updates = await this.getUpdates();
|
|
1454
|
+
this.lastError = "";
|
|
1455
|
+
for (const update of updates) {
|
|
1456
|
+
const updateId = typeof update.update_id === "number" ? update.update_id : -1;
|
|
1457
|
+
if (updateId >= 0) {
|
|
1458
|
+
this.nextUpdateOffset = Math.max(this.nextUpdateOffset, updateId + 1);
|
|
1459
|
+
}
|
|
1460
|
+
await this.handleIncomingUpdate(update);
|
|
1461
|
+
}
|
|
1462
|
+
} catch (error) {
|
|
1463
|
+
this.lastError = getErrorMessage2(error, "Telegram polling failed");
|
|
1464
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1500));
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
async getUpdates() {
|
|
1469
|
+
if (!this.token) {
|
|
1470
|
+
throw new Error("Telegram bot token is not configured");
|
|
1471
|
+
}
|
|
1472
|
+
const response = await fetch(this.apiUrl("getUpdates"), {
|
|
1473
|
+
method: "POST",
|
|
1474
|
+
headers: { "Content-Type": "application/json" },
|
|
1475
|
+
body: JSON.stringify({
|
|
1476
|
+
timeout: 45,
|
|
1477
|
+
offset: this.nextUpdateOffset,
|
|
1478
|
+
allowed_updates: ["message", "callback_query"]
|
|
1479
|
+
})
|
|
1480
|
+
});
|
|
1481
|
+
const payload = asRecord2(await response.json());
|
|
1482
|
+
const result = Array.isArray(payload?.result) ? payload.result : [];
|
|
1483
|
+
return result;
|
|
1484
|
+
}
|
|
1485
|
+
apiUrl(method) {
|
|
1486
|
+
return `https://api.telegram.org/bot${this.token}/${method}`;
|
|
1487
|
+
}
|
|
1488
|
+
configureToken(token) {
|
|
1489
|
+
const normalizedToken = token.trim();
|
|
1490
|
+
if (!normalizedToken) {
|
|
1491
|
+
throw new Error("Telegram bot token is required");
|
|
1492
|
+
}
|
|
1493
|
+
this.token = normalizedToken;
|
|
1494
|
+
}
|
|
1495
|
+
getStatus() {
|
|
1496
|
+
return {
|
|
1497
|
+
configured: this.token.length > 0,
|
|
1498
|
+
active: this.active,
|
|
1499
|
+
mappedChats: this.threadIdByChatId.size,
|
|
1500
|
+
mappedThreads: this.chatIdsByThreadId.size,
|
|
1501
|
+
lastError: this.lastError
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
connectThread(threadId, chatId, token) {
|
|
1505
|
+
const normalizedThreadId = threadId.trim();
|
|
1506
|
+
if (!normalizedThreadId) {
|
|
1507
|
+
throw new Error("threadId is required");
|
|
1508
|
+
}
|
|
1509
|
+
if (!Number.isFinite(chatId)) {
|
|
1510
|
+
throw new Error("chatId must be a number");
|
|
1511
|
+
}
|
|
1512
|
+
if (typeof token === "string" && token.trim().length > 0) {
|
|
1513
|
+
this.configureToken(token);
|
|
1514
|
+
}
|
|
1515
|
+
if (!this.token) {
|
|
1516
|
+
throw new Error("Telegram bot token is not configured");
|
|
1517
|
+
}
|
|
1518
|
+
this.bindChatToThread(chatId, normalizedThreadId);
|
|
1519
|
+
this.start();
|
|
1520
|
+
void this.sendOnlineMessage(chatId).catch(() => {
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
async sendTelegramMessage(chatId, text, options = {}) {
|
|
1524
|
+
const message = text.trim();
|
|
1525
|
+
if (!message) return;
|
|
1526
|
+
const payload = { chat_id: chatId, text: message };
|
|
1527
|
+
if (options.replyMarkup) {
|
|
1528
|
+
payload.reply_markup = options.replyMarkup;
|
|
1529
|
+
}
|
|
1530
|
+
await fetch(this.apiUrl("sendMessage"), {
|
|
1531
|
+
method: "POST",
|
|
1532
|
+
headers: { "Content-Type": "application/json" },
|
|
1533
|
+
body: JSON.stringify(payload)
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
async sendOnlineMessage(chatId) {
|
|
1537
|
+
await this.sendTelegramMessage(chatId, "Codex thread bridge went online.");
|
|
1538
|
+
}
|
|
1539
|
+
async notifyOnlineForKnownChats() {
|
|
1540
|
+
const knownChatIds = Array.from(this.threadIdByChatId.keys());
|
|
1541
|
+
for (const chatId of knownChatIds) {
|
|
1542
|
+
await this.sendOnlineMessage(chatId);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
async handleIncomingUpdate(update) {
|
|
1546
|
+
if (update.callback_query) {
|
|
1547
|
+
await this.handleCallbackQuery(update.callback_query);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const message = update.message;
|
|
1551
|
+
const chatId = message?.chat?.id;
|
|
1552
|
+
const text = message?.text?.trim();
|
|
1553
|
+
if (typeof chatId !== "number" || !text) return;
|
|
1554
|
+
if (text === "/start") {
|
|
1555
|
+
await this.sendThreadPicker(chatId);
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
if (text === "/newthread") {
|
|
1559
|
+
const threadId2 = await this.createThreadForChat(chatId);
|
|
1560
|
+
await this.sendTelegramMessage(chatId, `Mapped to new thread: ${threadId2}`);
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const threadCommand = text.match(/^\/thread\s+(\S+)$/);
|
|
1564
|
+
if (threadCommand) {
|
|
1565
|
+
const threadId2 = threadCommand[1];
|
|
1566
|
+
this.bindChatToThread(chatId, threadId2);
|
|
1567
|
+
await this.sendTelegramMessage(chatId, `Mapped to thread: ${threadId2}`);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const threadId = await this.ensureThreadForChat(chatId);
|
|
1571
|
+
try {
|
|
1572
|
+
await this.appServer.rpc("turn/start", {
|
|
1573
|
+
threadId,
|
|
1574
|
+
input: [{ type: "text", text }]
|
|
1575
|
+
});
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
const message2 = getErrorMessage2(error, "Failed to forward message to thread");
|
|
1578
|
+
await this.sendTelegramMessage(chatId, `Forward failed: ${message2}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async handleCallbackQuery(callbackQuery) {
|
|
1582
|
+
const callbackId = typeof callbackQuery.id === "string" ? callbackQuery.id : "";
|
|
1583
|
+
const data = typeof callbackQuery.data === "string" ? callbackQuery.data : "";
|
|
1584
|
+
const chatId = callbackQuery.message?.chat?.id;
|
|
1585
|
+
if (!callbackId) return;
|
|
1586
|
+
if (!data.startsWith("thread:") || typeof chatId !== "number") {
|
|
1587
|
+
await this.answerCallbackQuery(callbackId, "Invalid selection");
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
const threadId = data.slice("thread:".length).trim();
|
|
1591
|
+
if (!threadId) {
|
|
1592
|
+
await this.answerCallbackQuery(callbackId, "Invalid thread id");
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
this.bindChatToThread(chatId, threadId);
|
|
1596
|
+
await this.answerCallbackQuery(callbackId, "Thread connected");
|
|
1597
|
+
await this.sendTelegramMessage(chatId, `Connected to thread: ${threadId}`);
|
|
1598
|
+
const history = await this.readThreadHistorySummary(threadId);
|
|
1599
|
+
if (history) {
|
|
1600
|
+
await this.sendTelegramMessage(chatId, history);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
async answerCallbackQuery(callbackQueryId, text) {
|
|
1604
|
+
await fetch(this.apiUrl("answerCallbackQuery"), {
|
|
1605
|
+
method: "POST",
|
|
1606
|
+
headers: { "Content-Type": "application/json" },
|
|
1607
|
+
body: JSON.stringify({
|
|
1608
|
+
callback_query_id: callbackQueryId,
|
|
1609
|
+
text
|
|
1610
|
+
})
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
async sendThreadPicker(chatId) {
|
|
1614
|
+
const threads = await this.listRecentThreads();
|
|
1615
|
+
if (threads.length === 0) {
|
|
1616
|
+
await this.sendTelegramMessage(chatId, "No threads found. Send /newthread to create one.");
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const inlineKeyboard = threads.map((thread) => [
|
|
1620
|
+
{
|
|
1621
|
+
text: thread.title,
|
|
1622
|
+
callback_data: `thread:${thread.id}`
|
|
1623
|
+
}
|
|
1624
|
+
]);
|
|
1625
|
+
await this.sendTelegramMessage(chatId, "Select a thread to connect:", {
|
|
1626
|
+
replyMarkup: { inline_keyboard: inlineKeyboard }
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
async listRecentThreads() {
|
|
1630
|
+
const payload = asRecord2(await this.appServer.rpc("thread/list", {
|
|
1631
|
+
archived: false,
|
|
1632
|
+
limit: 20,
|
|
1633
|
+
sortKey: "updated_at"
|
|
1634
|
+
}));
|
|
1635
|
+
const rows = Array.isArray(payload?.data) ? payload.data : [];
|
|
1636
|
+
const threads = [];
|
|
1637
|
+
for (const row of rows) {
|
|
1638
|
+
const record = asRecord2(row);
|
|
1639
|
+
const id = typeof record?.id === "string" ? record.id.trim() : "";
|
|
1640
|
+
if (!id) continue;
|
|
1641
|
+
const name = typeof record?.name === "string" ? record.name.trim() : "";
|
|
1642
|
+
const preview = typeof record?.preview === "string" ? record.preview.trim() : "";
|
|
1643
|
+
const cwd = typeof record?.cwd === "string" ? record.cwd.trim() : "";
|
|
1644
|
+
const projectName = cwd ? basename(cwd) : "project";
|
|
1645
|
+
const threadTitle = (name || preview || id).replace(/\s+/g, " ").trim();
|
|
1646
|
+
const title = `${projectName}/${threadTitle}`.slice(0, 64);
|
|
1647
|
+
threads.push({ id, title });
|
|
1648
|
+
}
|
|
1649
|
+
return threads;
|
|
1650
|
+
}
|
|
1651
|
+
async createThreadForChat(chatId) {
|
|
1652
|
+
const response = asRecord2(await this.appServer.rpc("thread/start", { cwd: this.defaultCwd }));
|
|
1653
|
+
const thread = asRecord2(response?.thread);
|
|
1654
|
+
const threadId = typeof thread?.id === "string" ? thread.id : "";
|
|
1655
|
+
if (!threadId) {
|
|
1656
|
+
throw new Error("thread/start did not return thread id");
|
|
1657
|
+
}
|
|
1658
|
+
this.bindChatToThread(chatId, threadId);
|
|
1659
|
+
return threadId;
|
|
1660
|
+
}
|
|
1661
|
+
async ensureThreadForChat(chatId) {
|
|
1662
|
+
const existing = this.threadIdByChatId.get(chatId);
|
|
1663
|
+
if (existing) return existing;
|
|
1664
|
+
return this.createThreadForChat(chatId);
|
|
1665
|
+
}
|
|
1666
|
+
bindChatToThread(chatId, threadId) {
|
|
1667
|
+
const previousThreadId = this.threadIdByChatId.get(chatId);
|
|
1668
|
+
if (previousThreadId && previousThreadId !== threadId) {
|
|
1669
|
+
const previousSet = this.chatIdsByThreadId.get(previousThreadId);
|
|
1670
|
+
previousSet?.delete(chatId);
|
|
1671
|
+
if (previousSet && previousSet.size === 0) {
|
|
1672
|
+
this.chatIdsByThreadId.delete(previousThreadId);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
this.threadIdByChatId.set(chatId, threadId);
|
|
1676
|
+
const chatIds = this.chatIdsByThreadId.get(threadId) ?? /* @__PURE__ */ new Set();
|
|
1677
|
+
chatIds.add(chatId);
|
|
1678
|
+
this.chatIdsByThreadId.set(threadId, chatIds);
|
|
1679
|
+
}
|
|
1680
|
+
extractThreadId(notification) {
|
|
1681
|
+
const params = asRecord2(notification.params);
|
|
1682
|
+
if (!params) return "";
|
|
1683
|
+
const directThreadId = typeof params.threadId === "string" ? params.threadId : "";
|
|
1684
|
+
if (directThreadId) return directThreadId;
|
|
1685
|
+
const turn = asRecord2(params.turn);
|
|
1686
|
+
const turnThreadId = typeof turn?.threadId === "string" ? turn.threadId : "";
|
|
1687
|
+
return turnThreadId;
|
|
1688
|
+
}
|
|
1689
|
+
extractTurnId(notification) {
|
|
1690
|
+
const params = asRecord2(notification.params);
|
|
1691
|
+
if (!params) return "";
|
|
1692
|
+
const directTurnId = typeof params.turnId === "string" ? params.turnId : "";
|
|
1693
|
+
if (directTurnId) return directTurnId;
|
|
1694
|
+
const turn = asRecord2(params.turn);
|
|
1695
|
+
const turnId = typeof turn?.id === "string" ? turn.id : "";
|
|
1696
|
+
return turnId;
|
|
1697
|
+
}
|
|
1698
|
+
async handleNotification(notification) {
|
|
1699
|
+
if (notification.method !== "turn/completed") return;
|
|
1700
|
+
const threadId = this.extractThreadId(notification);
|
|
1701
|
+
if (!threadId) return;
|
|
1702
|
+
const chatIds = this.chatIdsByThreadId.get(threadId);
|
|
1703
|
+
if (!chatIds || chatIds.size === 0) return;
|
|
1704
|
+
const turnId = this.extractTurnId(notification);
|
|
1705
|
+
const lastForwardedTurnId = this.lastForwardedTurnByThreadId.get(threadId);
|
|
1706
|
+
if (turnId && lastForwardedTurnId === turnId) return;
|
|
1707
|
+
const assistantReply = await this.readLatestAssistantMessage(threadId);
|
|
1708
|
+
if (!assistantReply) return;
|
|
1709
|
+
for (const chatId of chatIds) {
|
|
1710
|
+
await this.sendTelegramMessage(chatId, assistantReply);
|
|
1711
|
+
}
|
|
1712
|
+
if (turnId) {
|
|
1713
|
+
this.lastForwardedTurnByThreadId.set(threadId, turnId);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
async readLatestAssistantMessage(threadId) {
|
|
1717
|
+
const response = asRecord2(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
|
|
1718
|
+
const thread = asRecord2(response?.thread);
|
|
1719
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1720
|
+
for (let turnIndex = turns.length - 1; turnIndex >= 0; turnIndex -= 1) {
|
|
1721
|
+
const turn = asRecord2(turns[turnIndex]);
|
|
1722
|
+
const items = Array.isArray(turn?.items) ? turn.items : [];
|
|
1723
|
+
for (let itemIndex = items.length - 1; itemIndex >= 0; itemIndex -= 1) {
|
|
1724
|
+
const item = asRecord2(items[itemIndex]);
|
|
1725
|
+
if (item?.type === "agentMessage") {
|
|
1726
|
+
const text = typeof item.text === "string" ? item.text.trim() : "";
|
|
1727
|
+
if (text) return text;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
return "";
|
|
1732
|
+
}
|
|
1733
|
+
async readThreadHistorySummary(threadId) {
|
|
1734
|
+
const response = asRecord2(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
|
|
1735
|
+
const thread = asRecord2(response?.thread);
|
|
1736
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1737
|
+
const historyRows = [];
|
|
1738
|
+
for (const turn of turns) {
|
|
1739
|
+
const turnRecord = asRecord2(turn);
|
|
1740
|
+
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
1741
|
+
for (const item of items) {
|
|
1742
|
+
const itemRecord = asRecord2(item);
|
|
1743
|
+
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
1744
|
+
if (type === "userMessage") {
|
|
1745
|
+
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
1746
|
+
for (const block of content) {
|
|
1747
|
+
const blockRecord = asRecord2(block);
|
|
1748
|
+
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim()) {
|
|
1749
|
+
historyRows.push(`User: ${blockRecord.text.trim()}`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim()) {
|
|
1754
|
+
historyRows.push(`Assistant: ${itemRecord.text.trim()}`);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (historyRows.length === 0) {
|
|
1759
|
+
return "Thread has no message history yet.";
|
|
1760
|
+
}
|
|
1761
|
+
const tail = historyRows.slice(-12).join("\n\n");
|
|
1762
|
+
const maxLen = 3800;
|
|
1763
|
+
const summary = tail.length > maxLen ? tail.slice(tail.length - maxLen) : tail;
|
|
1764
|
+
return `Recent history:
|
|
1765
|
+
|
|
1766
|
+
${summary}`;
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
// src/utils/commandInvocation.ts
|
|
1771
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1772
|
+
import { basename as basename2, extname } from "path";
|
|
1773
|
+
var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
|
|
1774
|
+
function quoteCmdExeArg(value) {
|
|
1775
|
+
const normalized = value.replace(/"/g, '""');
|
|
1776
|
+
if (!/[\s"]/u.test(normalized)) {
|
|
1777
|
+
return normalized;
|
|
1778
|
+
}
|
|
1779
|
+
return `"${normalized}"`;
|
|
1780
|
+
}
|
|
1781
|
+
function needsCmdExeWrapper(command) {
|
|
1782
|
+
if (process.platform !== "win32") {
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
const lowerCommand = command.toLowerCase();
|
|
1786
|
+
const baseName = basename2(lowerCommand);
|
|
1787
|
+
if (/\.(cmd|bat)$/i.test(baseName)) {
|
|
1788
|
+
return true;
|
|
1789
|
+
}
|
|
1790
|
+
if (extname(baseName)) {
|
|
1791
|
+
return false;
|
|
1792
|
+
}
|
|
1793
|
+
return WINDOWS_CMD_NAMES.has(baseName);
|
|
1794
|
+
}
|
|
1795
|
+
function getSpawnInvocation(command, args = []) {
|
|
1796
|
+
if (needsCmdExeWrapper(command)) {
|
|
1797
|
+
return {
|
|
1798
|
+
command: "cmd.exe",
|
|
1799
|
+
args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
return { command, args };
|
|
1803
|
+
}
|
|
1804
|
+
function spawnSyncCommand(command, args = [], options = {}) {
|
|
1805
|
+
const invocation = getSpawnInvocation(command, args);
|
|
1806
|
+
return spawnSync2(invocation.command, invocation.args, options);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/server/codexAppServerBridge.ts
|
|
1810
|
+
function asRecord3(value) {
|
|
1811
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1812
|
+
}
|
|
1813
|
+
function getErrorMessage3(payload, fallback) {
|
|
1814
|
+
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
1815
|
+
return payload.message;
|
|
1816
|
+
}
|
|
1817
|
+
const record = asRecord3(payload);
|
|
1818
|
+
if (!record) return fallback;
|
|
1819
|
+
const error = record.error;
|
|
1820
|
+
if (typeof error === "string" && error.length > 0) return error;
|
|
1821
|
+
const nestedError = asRecord3(error);
|
|
1822
|
+
if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
|
|
1823
|
+
return nestedError.message;
|
|
1824
|
+
}
|
|
1825
|
+
return fallback;
|
|
1826
|
+
}
|
|
1138
1827
|
function setJson2(res, statusCode, payload) {
|
|
1139
1828
|
res.statusCode = statusCode;
|
|
1140
1829
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1141
1830
|
res.end(JSON.stringify(payload));
|
|
1142
1831
|
}
|
|
1143
1832
|
function extractThreadMessageText(threadReadPayload) {
|
|
1144
|
-
const payload =
|
|
1145
|
-
const thread =
|
|
1833
|
+
const payload = asRecord3(threadReadPayload);
|
|
1834
|
+
const thread = asRecord3(payload?.thread);
|
|
1146
1835
|
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1147
1836
|
const parts = [];
|
|
1148
1837
|
for (const turn of turns) {
|
|
1149
|
-
const turnRecord =
|
|
1838
|
+
const turnRecord = asRecord3(turn);
|
|
1150
1839
|
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
1151
1840
|
for (const item of items) {
|
|
1152
|
-
const itemRecord =
|
|
1841
|
+
const itemRecord = asRecord3(item);
|
|
1153
1842
|
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
1154
1843
|
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
1155
1844
|
parts.push(itemRecord.text.trim());
|
|
@@ -1158,7 +1847,7 @@ function extractThreadMessageText(threadReadPayload) {
|
|
|
1158
1847
|
if (type === "userMessage") {
|
|
1159
1848
|
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
1160
1849
|
for (const block of content) {
|
|
1161
|
-
const blockRecord =
|
|
1850
|
+
const blockRecord = asRecord3(block);
|
|
1162
1851
|
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
1163
1852
|
parts.push(blockRecord.text.trim());
|
|
1164
1853
|
}
|
|
@@ -1192,9 +1881,62 @@ function scoreFileCandidate(path, query) {
|
|
|
1192
1881
|
if (lowerPath.includes(lowerQuery)) return 4;
|
|
1193
1882
|
return 10;
|
|
1194
1883
|
}
|
|
1884
|
+
function decodeHtmlEntities(value) {
|
|
1885
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(///gi, "/");
|
|
1886
|
+
}
|
|
1887
|
+
function stripHtml(value) {
|
|
1888
|
+
return decodeHtmlEntities(value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
|
|
1889
|
+
}
|
|
1890
|
+
function parseGithubTrendingHtml(html, limit) {
|
|
1891
|
+
const rows = html.match(/<article[\s\S]*?<\/article>/g) ?? [];
|
|
1892
|
+
const items = [];
|
|
1893
|
+
let seq = Date.now();
|
|
1894
|
+
for (const row of rows) {
|
|
1895
|
+
const repoBlockMatch = row.match(/<h2[\s\S]*?<\/h2>/);
|
|
1896
|
+
const hrefMatch = repoBlockMatch?.[0]?.match(/href="\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)"/);
|
|
1897
|
+
if (!hrefMatch) continue;
|
|
1898
|
+
const fullName = hrefMatch[1] ?? "";
|
|
1899
|
+
if (!fullName || items.some((item) => item.fullName === fullName)) continue;
|
|
1900
|
+
const descriptionMatch = row.match(/<p[^>]*class="[^"]*col-9[^"]*"[^>]*>([\s\S]*?)<\/p>/) ?? row.match(/<p[^>]*class="[^"]*color-fg-muted[^"]*"[^>]*>([\s\S]*?)<\/p>/) ?? row.match(/<p[^>]*>([\s\S]*?)<\/p>/);
|
|
1901
|
+
const languageMatch = row.match(/programmingLanguage[^>]*>\s*([\s\S]*?)\s*<\/span>/);
|
|
1902
|
+
const starsMatch = row.match(/href="\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/stargazers"[\s\S]*?>([\s\S]*?)<\/a>/);
|
|
1903
|
+
const starsText = stripHtml(starsMatch?.[1] ?? "").replace(/,/g, "");
|
|
1904
|
+
const stars = Number.parseInt(starsText, 10);
|
|
1905
|
+
items.push({
|
|
1906
|
+
id: seq,
|
|
1907
|
+
fullName,
|
|
1908
|
+
url: `https://github.com/${fullName}`,
|
|
1909
|
+
description: stripHtml(descriptionMatch?.[1] ?? ""),
|
|
1910
|
+
language: stripHtml(languageMatch?.[1] ?? ""),
|
|
1911
|
+
stars: Number.isFinite(stars) ? stars : 0
|
|
1912
|
+
});
|
|
1913
|
+
seq += 1;
|
|
1914
|
+
if (items.length >= limit) break;
|
|
1915
|
+
}
|
|
1916
|
+
return items;
|
|
1917
|
+
}
|
|
1918
|
+
async function fetchGithubTrending(since, limit) {
|
|
1919
|
+
const endpoint = `https://github.com/trending?since=${since}`;
|
|
1920
|
+
const response = await fetch(endpoint, {
|
|
1921
|
+
headers: {
|
|
1922
|
+
"User-Agent": "codex-web-local",
|
|
1923
|
+
Accept: "text/html"
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
if (!response.ok) {
|
|
1927
|
+
throw new Error(`GitHub trending fetch failed (${response.status})`);
|
|
1928
|
+
}
|
|
1929
|
+
const html = await response.text();
|
|
1930
|
+
return parseGithubTrendingHtml(html, limit);
|
|
1931
|
+
}
|
|
1195
1932
|
async function listFilesWithRipgrep(cwd) {
|
|
1196
1933
|
return await new Promise((resolve3, reject) => {
|
|
1197
|
-
const
|
|
1934
|
+
const ripgrepCommand = resolveRipgrepCommand();
|
|
1935
|
+
if (!ripgrepCommand) {
|
|
1936
|
+
reject(new Error("ripgrep (rg) is not available"));
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
const proc = spawn2(ripgrepCommand, ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
1198
1940
|
cwd,
|
|
1199
1941
|
env: process.env,
|
|
1200
1942
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1221,7 +1963,7 @@ async function listFilesWithRipgrep(cwd) {
|
|
|
1221
1963
|
}
|
|
1222
1964
|
function getCodexHomeDir2() {
|
|
1223
1965
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
1224
|
-
return codexHome && codexHome.length > 0 ? codexHome :
|
|
1966
|
+
return codexHome && codexHome.length > 0 ? codexHome : join3(homedir3(), ".codex");
|
|
1225
1967
|
}
|
|
1226
1968
|
async function runCommand2(command, args, options = {}) {
|
|
1227
1969
|
await new Promise((resolve3, reject) => {
|
|
@@ -1251,15 +1993,15 @@ async function runCommand2(command, args, options = {}) {
|
|
|
1251
1993
|
});
|
|
1252
1994
|
}
|
|
1253
1995
|
function isMissingHeadError(error) {
|
|
1254
|
-
const message =
|
|
1996
|
+
const message = getErrorMessage3(error, "").toLowerCase();
|
|
1255
1997
|
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
1256
1998
|
}
|
|
1257
1999
|
function isNotGitRepositoryError(error) {
|
|
1258
|
-
const message =
|
|
2000
|
+
const message = getErrorMessage3(error, "").toLowerCase();
|
|
1259
2001
|
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
1260
2002
|
}
|
|
1261
2003
|
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
1262
|
-
const agentsPath =
|
|
2004
|
+
const agentsPath = join3(repoRoot, "AGENTS.md");
|
|
1263
2005
|
try {
|
|
1264
2006
|
await stat2(agentsPath);
|
|
1265
2007
|
} catch {
|
|
@@ -1299,6 +2041,33 @@ async function runCommandCapture(command, args, options = {}) {
|
|
|
1299
2041
|
});
|
|
1300
2042
|
});
|
|
1301
2043
|
}
|
|
2044
|
+
async function runCommandWithOutput2(command, args, options = {}) {
|
|
2045
|
+
return await new Promise((resolve3, reject) => {
|
|
2046
|
+
const proc = spawn2(command, args, {
|
|
2047
|
+
cwd: options.cwd,
|
|
2048
|
+
env: process.env,
|
|
2049
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2050
|
+
});
|
|
2051
|
+
let stdout = "";
|
|
2052
|
+
let stderr = "";
|
|
2053
|
+
proc.stdout.on("data", (chunk) => {
|
|
2054
|
+
stdout += chunk.toString();
|
|
2055
|
+
});
|
|
2056
|
+
proc.stderr.on("data", (chunk) => {
|
|
2057
|
+
stderr += chunk.toString();
|
|
2058
|
+
});
|
|
2059
|
+
proc.on("error", reject);
|
|
2060
|
+
proc.on("close", (code) => {
|
|
2061
|
+
if (code === 0) {
|
|
2062
|
+
resolve3(stdout.trim());
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
2066
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
2067
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
2068
|
+
});
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
1302
2071
|
function normalizeStringArray(value) {
|
|
1303
2072
|
if (!Array.isArray(value)) return [];
|
|
1304
2073
|
const normalized = [];
|
|
@@ -1319,8 +2088,88 @@ function normalizeStringRecord(value) {
|
|
|
1319
2088
|
}
|
|
1320
2089
|
return next;
|
|
1321
2090
|
}
|
|
2091
|
+
function normalizeCommitMessage(value) {
|
|
2092
|
+
if (typeof value !== "string") return "";
|
|
2093
|
+
const normalized = value.replace(/\r\n?/gu, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n").trim();
|
|
2094
|
+
return normalized.slice(0, 2e3);
|
|
2095
|
+
}
|
|
2096
|
+
function getRollbackGitDirForCwd(cwd) {
|
|
2097
|
+
return join3(cwd, ".codex", "rollbacks", ".git");
|
|
2098
|
+
}
|
|
2099
|
+
async function ensureLocalCodexGitignoreHasRollbacks(cwd) {
|
|
2100
|
+
const localCodexDir = join3(cwd, ".codex");
|
|
2101
|
+
const gitignorePath = join3(localCodexDir, ".gitignore");
|
|
2102
|
+
await mkdir2(localCodexDir, { recursive: true });
|
|
2103
|
+
let current = "";
|
|
2104
|
+
try {
|
|
2105
|
+
current = await readFile2(gitignorePath, "utf8");
|
|
2106
|
+
} catch {
|
|
2107
|
+
current = "";
|
|
2108
|
+
}
|
|
2109
|
+
const rows = current.split(/\r?\n/).map((line) => line.trim());
|
|
2110
|
+
if (rows.includes("rollbacks/")) return;
|
|
2111
|
+
const prefix = current.length > 0 && !current.endsWith("\n") ? `${current}
|
|
2112
|
+
` : current;
|
|
2113
|
+
await writeFile2(gitignorePath, `${prefix}rollbacks/
|
|
2114
|
+
`, "utf8");
|
|
2115
|
+
}
|
|
2116
|
+
async function ensureRollbackGitRepo(cwd) {
|
|
2117
|
+
const gitDir = getRollbackGitDirForCwd(cwd);
|
|
2118
|
+
try {
|
|
2119
|
+
const headInfo = await stat2(join3(gitDir, "HEAD"));
|
|
2120
|
+
if (!headInfo.isFile()) {
|
|
2121
|
+
throw new Error("Invalid rollback git repository");
|
|
2122
|
+
}
|
|
2123
|
+
} catch {
|
|
2124
|
+
await mkdir2(dirname(gitDir), { recursive: true });
|
|
2125
|
+
await runCommand2("git", ["--git-dir", gitDir, "--work-tree", cwd, "init"]);
|
|
2126
|
+
}
|
|
2127
|
+
await runCommand2("git", ["--git-dir", gitDir, "config", "user.email", "codex@local"]);
|
|
2128
|
+
await runCommand2("git", ["--git-dir", gitDir, "config", "user.name", "Codex Rollback"]);
|
|
2129
|
+
try {
|
|
2130
|
+
await runCommandCapture("git", ["--git-dir", gitDir, "--work-tree", cwd, "rev-parse", "--verify", "HEAD"]);
|
|
2131
|
+
} catch {
|
|
2132
|
+
await runCommand2(
|
|
2133
|
+
"git",
|
|
2134
|
+
["--git-dir", gitDir, "--work-tree", cwd, "commit", "--allow-empty", "-m", "Initialize rollback history"]
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
await ensureLocalCodexGitignoreHasRollbacks(cwd);
|
|
2138
|
+
return gitDir;
|
|
2139
|
+
}
|
|
2140
|
+
async function runRollbackGit(cwd, args) {
|
|
2141
|
+
const gitDir = await ensureRollbackGitRepo(cwd);
|
|
2142
|
+
await runCommand2("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
|
|
2143
|
+
}
|
|
2144
|
+
async function runRollbackGitCapture(cwd, args) {
|
|
2145
|
+
const gitDir = await ensureRollbackGitRepo(cwd);
|
|
2146
|
+
return await runCommandCapture("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
|
|
2147
|
+
}
|
|
2148
|
+
async function runRollbackGitWithOutput(cwd, args) {
|
|
2149
|
+
const gitDir = await ensureRollbackGitRepo(cwd);
|
|
2150
|
+
return await runCommandWithOutput2("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
|
|
2151
|
+
}
|
|
2152
|
+
async function hasRollbackGitWorkingTreeChanges(cwd) {
|
|
2153
|
+
const status = await runRollbackGitWithOutput(cwd, ["status", "--porcelain"]);
|
|
2154
|
+
return status.trim().length > 0;
|
|
2155
|
+
}
|
|
2156
|
+
async function findRollbackCommitByExactMessage(cwd, message) {
|
|
2157
|
+
const normalizedTarget = normalizeCommitMessage(message);
|
|
2158
|
+
if (!normalizedTarget) return "";
|
|
2159
|
+
const raw = await runRollbackGitWithOutput(cwd, ["log", "--format=%H%x1f%B%x1e"]);
|
|
2160
|
+
const entries = raw.split("");
|
|
2161
|
+
for (const entry of entries) {
|
|
2162
|
+
if (!entry.trim()) continue;
|
|
2163
|
+
const [shaRaw, bodyRaw] = entry.split("");
|
|
2164
|
+
const sha = (shaRaw ?? "").trim();
|
|
2165
|
+
const body = normalizeCommitMessage(bodyRaw ?? "");
|
|
2166
|
+
if (!sha) continue;
|
|
2167
|
+
if (body === normalizedTarget) return sha;
|
|
2168
|
+
}
|
|
2169
|
+
return "";
|
|
2170
|
+
}
|
|
1322
2171
|
function getCodexAuthPath() {
|
|
1323
|
-
return
|
|
2172
|
+
return join3(getCodexHomeDir2(), "auth.json");
|
|
1324
2173
|
}
|
|
1325
2174
|
async function readCodexAuth() {
|
|
1326
2175
|
try {
|
|
@@ -1334,13 +2183,21 @@ async function readCodexAuth() {
|
|
|
1334
2183
|
}
|
|
1335
2184
|
}
|
|
1336
2185
|
function getCodexGlobalStatePath() {
|
|
1337
|
-
return
|
|
2186
|
+
return join3(getCodexHomeDir2(), ".codex-global-state.json");
|
|
2187
|
+
}
|
|
2188
|
+
function getCodexSessionIndexPath() {
|
|
2189
|
+
return join3(getCodexHomeDir2(), "session_index.jsonl");
|
|
1338
2190
|
}
|
|
1339
2191
|
var MAX_THREAD_TITLES = 500;
|
|
2192
|
+
var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
|
|
2193
|
+
var sessionIndexThreadTitleCacheState = {
|
|
2194
|
+
fileSignature: null,
|
|
2195
|
+
cache: EMPTY_THREAD_TITLE_CACHE
|
|
2196
|
+
};
|
|
1340
2197
|
function normalizeThreadTitleCache(value) {
|
|
1341
|
-
const record =
|
|
1342
|
-
if (!record) return
|
|
1343
|
-
const rawTitles =
|
|
2198
|
+
const record = asRecord3(value);
|
|
2199
|
+
if (!record) return EMPTY_THREAD_TITLE_CACHE;
|
|
2200
|
+
const rawTitles = asRecord3(record.titles);
|
|
1344
2201
|
const titles = {};
|
|
1345
2202
|
if (rawTitles) {
|
|
1346
2203
|
for (const [k, v] of Object.entries(rawTitles)) {
|
|
@@ -1363,14 +2220,55 @@ function removeFromThreadTitleCache(cache, id) {
|
|
|
1363
2220
|
const { [id]: _, ...titles } = cache.titles;
|
|
1364
2221
|
return { titles, order: cache.order.filter((o) => o !== id) };
|
|
1365
2222
|
}
|
|
2223
|
+
function normalizeSessionIndexThreadTitle(value) {
|
|
2224
|
+
const record = asRecord3(value);
|
|
2225
|
+
if (!record) return null;
|
|
2226
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
2227
|
+
const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
|
|
2228
|
+
const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
|
|
2229
|
+
const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
|
|
2230
|
+
if (!id || !title) return null;
|
|
2231
|
+
return {
|
|
2232
|
+
id,
|
|
2233
|
+
title,
|
|
2234
|
+
updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
function trimThreadTitleCache(cache) {
|
|
2238
|
+
const titles = { ...cache.titles };
|
|
2239
|
+
const order = cache.order.filter((id) => {
|
|
2240
|
+
if (!titles[id]) return false;
|
|
2241
|
+
return true;
|
|
2242
|
+
}).slice(0, MAX_THREAD_TITLES);
|
|
2243
|
+
for (const id of Object.keys(titles)) {
|
|
2244
|
+
if (!order.includes(id)) {
|
|
2245
|
+
delete titles[id];
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
return { titles, order };
|
|
2249
|
+
}
|
|
2250
|
+
function mergeThreadTitleCaches(base, overlay) {
|
|
2251
|
+
const titles = { ...base.titles, ...overlay.titles };
|
|
2252
|
+
const order = [];
|
|
2253
|
+
for (const id of [...overlay.order, ...base.order]) {
|
|
2254
|
+
if (!titles[id] || order.includes(id)) continue;
|
|
2255
|
+
order.push(id);
|
|
2256
|
+
}
|
|
2257
|
+
for (const id of Object.keys(titles)) {
|
|
2258
|
+
if (!order.includes(id)) {
|
|
2259
|
+
order.push(id);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
return trimThreadTitleCache({ titles, order });
|
|
2263
|
+
}
|
|
1366
2264
|
async function readThreadTitleCache() {
|
|
1367
2265
|
const statePath = getCodexGlobalStatePath();
|
|
1368
2266
|
try {
|
|
1369
2267
|
const raw = await readFile2(statePath, "utf8");
|
|
1370
|
-
const payload =
|
|
2268
|
+
const payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
1371
2269
|
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
1372
2270
|
} catch {
|
|
1373
|
-
return
|
|
2271
|
+
return EMPTY_THREAD_TITLE_CACHE;
|
|
1374
2272
|
}
|
|
1375
2273
|
}
|
|
1376
2274
|
async function writeThreadTitleCache(cache) {
|
|
@@ -1378,20 +2276,83 @@ async function writeThreadTitleCache(cache) {
|
|
|
1378
2276
|
let payload = {};
|
|
1379
2277
|
try {
|
|
1380
2278
|
const raw = await readFile2(statePath, "utf8");
|
|
1381
|
-
payload =
|
|
2279
|
+
payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
1382
2280
|
} catch {
|
|
1383
2281
|
payload = {};
|
|
1384
2282
|
}
|
|
1385
2283
|
payload["thread-titles"] = cache;
|
|
1386
2284
|
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1387
2285
|
}
|
|
2286
|
+
function getSessionIndexFileSignature(stats) {
|
|
2287
|
+
return `${String(stats.mtimeMs)}:${String(stats.size)}`;
|
|
2288
|
+
}
|
|
2289
|
+
async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
|
|
2290
|
+
const latestById = /* @__PURE__ */ new Map();
|
|
2291
|
+
const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
|
|
2292
|
+
const lines = createInterface({
|
|
2293
|
+
input,
|
|
2294
|
+
crlfDelay: Infinity
|
|
2295
|
+
});
|
|
2296
|
+
try {
|
|
2297
|
+
for await (const line of lines) {
|
|
2298
|
+
const trimmed = line.trim();
|
|
2299
|
+
if (!trimmed) continue;
|
|
2300
|
+
try {
|
|
2301
|
+
const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
|
|
2302
|
+
if (!entry) continue;
|
|
2303
|
+
const previous = latestById.get(entry.id);
|
|
2304
|
+
if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
|
|
2305
|
+
latestById.set(entry.id, entry);
|
|
2306
|
+
}
|
|
2307
|
+
} catch {
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
} finally {
|
|
2311
|
+
lines.close();
|
|
2312
|
+
input.close();
|
|
2313
|
+
}
|
|
2314
|
+
const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
|
|
2315
|
+
const titles = {};
|
|
2316
|
+
const order = [];
|
|
2317
|
+
for (const entry of entries) {
|
|
2318
|
+
titles[entry.id] = entry.title;
|
|
2319
|
+
order.push(entry.id);
|
|
2320
|
+
}
|
|
2321
|
+
return trimThreadTitleCache({ titles, order });
|
|
2322
|
+
}
|
|
2323
|
+
async function readThreadTitlesFromSessionIndex() {
|
|
2324
|
+
const sessionIndexPath = getCodexSessionIndexPath();
|
|
2325
|
+
try {
|
|
2326
|
+
const stats = await stat2(sessionIndexPath);
|
|
2327
|
+
const fileSignature = getSessionIndexFileSignature(stats);
|
|
2328
|
+
if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
|
|
2329
|
+
return sessionIndexThreadTitleCacheState.cache;
|
|
2330
|
+
}
|
|
2331
|
+
const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
|
|
2332
|
+
sessionIndexThreadTitleCacheState = { fileSignature, cache };
|
|
2333
|
+
return cache;
|
|
2334
|
+
} catch {
|
|
2335
|
+
sessionIndexThreadTitleCacheState = {
|
|
2336
|
+
fileSignature: "missing",
|
|
2337
|
+
cache: EMPTY_THREAD_TITLE_CACHE
|
|
2338
|
+
};
|
|
2339
|
+
return sessionIndexThreadTitleCacheState.cache;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
async function readMergedThreadTitleCache() {
|
|
2343
|
+
const [sessionIndexCache, persistedCache] = await Promise.all([
|
|
2344
|
+
readThreadTitlesFromSessionIndex(),
|
|
2345
|
+
readThreadTitleCache()
|
|
2346
|
+
]);
|
|
2347
|
+
return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
|
|
2348
|
+
}
|
|
1388
2349
|
async function readWorkspaceRootsState() {
|
|
1389
2350
|
const statePath = getCodexGlobalStatePath();
|
|
1390
2351
|
let payload = {};
|
|
1391
2352
|
try {
|
|
1392
2353
|
const raw = await readFile2(statePath, "utf8");
|
|
1393
2354
|
const parsed = JSON.parse(raw);
|
|
1394
|
-
payload =
|
|
2355
|
+
payload = asRecord3(parsed) ?? {};
|
|
1395
2356
|
} catch {
|
|
1396
2357
|
payload = {};
|
|
1397
2358
|
}
|
|
@@ -1406,7 +2367,7 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
1406
2367
|
let payload = {};
|
|
1407
2368
|
try {
|
|
1408
2369
|
const raw = await readFile2(statePath, "utf8");
|
|
1409
|
-
payload =
|
|
2370
|
+
payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
1410
2371
|
} catch {
|
|
1411
2372
|
payload = {};
|
|
1412
2373
|
}
|
|
@@ -1415,6 +2376,36 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
1415
2376
|
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
1416
2377
|
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1417
2378
|
}
|
|
2379
|
+
function normalizeTelegramBridgeConfig(value) {
|
|
2380
|
+
const record = asRecord3(value);
|
|
2381
|
+
if (!record) return { botToken: "" };
|
|
2382
|
+
const botToken = typeof record.botToken === "string" ? record.botToken.trim() : "";
|
|
2383
|
+
return { botToken };
|
|
2384
|
+
}
|
|
2385
|
+
async function readTelegramBridgeConfig() {
|
|
2386
|
+
const statePath = getCodexGlobalStatePath();
|
|
2387
|
+
try {
|
|
2388
|
+
const raw = await readFile2(statePath, "utf8");
|
|
2389
|
+
const payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
2390
|
+
return normalizeTelegramBridgeConfig(payload["telegram-bridge"]);
|
|
2391
|
+
} catch {
|
|
2392
|
+
return { botToken: "" };
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
async function writeTelegramBridgeConfig(nextState) {
|
|
2396
|
+
const statePath = getCodexGlobalStatePath();
|
|
2397
|
+
let payload = {};
|
|
2398
|
+
try {
|
|
2399
|
+
const raw = await readFile2(statePath, "utf8");
|
|
2400
|
+
payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
2401
|
+
} catch {
|
|
2402
|
+
payload = {};
|
|
2403
|
+
}
|
|
2404
|
+
payload["telegram-bridge"] = {
|
|
2405
|
+
botToken: nextState.botToken.trim()
|
|
2406
|
+
};
|
|
2407
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
2408
|
+
}
|
|
1418
2409
|
async function readJsonBody(req) {
|
|
1419
2410
|
const raw = await readRawBody(req);
|
|
1420
2411
|
if (raw.length === 0) return null;
|
|
@@ -1484,46 +2475,93 @@ function handleFileUpload(req, res) {
|
|
|
1484
2475
|
setJson2(res, 400, { error: "No file in request" });
|
|
1485
2476
|
return;
|
|
1486
2477
|
}
|
|
1487
|
-
const uploadDir =
|
|
2478
|
+
const uploadDir = join3(tmpdir2(), "codex-web-uploads");
|
|
1488
2479
|
await mkdir2(uploadDir, { recursive: true });
|
|
1489
|
-
const destDir = await mkdtemp2(
|
|
1490
|
-
const destPath =
|
|
2480
|
+
const destDir = await mkdtemp2(join3(uploadDir, "f-"));
|
|
2481
|
+
const destPath = join3(destDir, fileName);
|
|
1491
2482
|
await writeFile2(destPath, fileData);
|
|
1492
2483
|
setJson2(res, 200, { path: destPath });
|
|
1493
2484
|
} catch (err) {
|
|
1494
|
-
setJson2(res, 500, { error:
|
|
2485
|
+
setJson2(res, 500, { error: getErrorMessage3(err, "Upload failed") });
|
|
1495
2486
|
}
|
|
1496
2487
|
});
|
|
1497
2488
|
req.on("error", (err) => {
|
|
1498
|
-
setJson2(res, 500, { error:
|
|
2489
|
+
setJson2(res, 500, { error: getErrorMessage3(err, "Upload stream error") });
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
function httpPost(url, headers, body) {
|
|
2493
|
+
const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
|
|
2494
|
+
return new Promise((resolve3, reject) => {
|
|
2495
|
+
const req = doRequest(url, { method: "POST", headers }, (res) => {
|
|
2496
|
+
const chunks = [];
|
|
2497
|
+
res.on("data", (c) => chunks.push(c));
|
|
2498
|
+
res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
|
|
2499
|
+
res.on("error", reject);
|
|
2500
|
+
});
|
|
2501
|
+
req.on("error", reject);
|
|
2502
|
+
req.write(body);
|
|
2503
|
+
req.end();
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
var curlImpersonateAvailable = null;
|
|
2507
|
+
function curlImpersonatePost(url, headers, body) {
|
|
2508
|
+
return new Promise((resolve3, reject) => {
|
|
2509
|
+
const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
|
|
2510
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
2511
|
+
if (k.toLowerCase() === "content-length") continue;
|
|
2512
|
+
args.push("-H", `${k}: ${String(v)}`);
|
|
2513
|
+
}
|
|
2514
|
+
args.push("--data-binary", "@-");
|
|
2515
|
+
const proc = spawn2("curl-impersonate-chrome", args, {
|
|
2516
|
+
env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
|
|
2517
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2518
|
+
});
|
|
2519
|
+
const chunks = [];
|
|
2520
|
+
proc.stdout.on("data", (c) => chunks.push(c));
|
|
2521
|
+
proc.on("error", (e) => {
|
|
2522
|
+
curlImpersonateAvailable = false;
|
|
2523
|
+
reject(e);
|
|
2524
|
+
});
|
|
2525
|
+
proc.on("close", (code) => {
|
|
2526
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
2527
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2528
|
+
const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
|
|
2529
|
+
const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
|
|
2530
|
+
const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
|
|
2531
|
+
curlImpersonateAvailable = true;
|
|
2532
|
+
resolve3({ status, body: responseBody });
|
|
2533
|
+
});
|
|
2534
|
+
proc.stdin.write(body);
|
|
2535
|
+
proc.stdin.end();
|
|
1499
2536
|
});
|
|
1500
2537
|
}
|
|
1501
2538
|
async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
1502
|
-
const
|
|
2539
|
+
const chatgptHeaders = {
|
|
1503
2540
|
"Content-Type": contentType,
|
|
1504
2541
|
"Content-Length": body.length,
|
|
1505
2542
|
Authorization: `Bearer ${authToken}`,
|
|
1506
2543
|
originator: "Codex Desktop",
|
|
1507
2544
|
"User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
|
|
1508
2545
|
};
|
|
1509
|
-
if (accountId)
|
|
1510
|
-
|
|
2546
|
+
if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
|
|
2547
|
+
const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
|
|
2548
|
+
let result;
|
|
2549
|
+
try {
|
|
2550
|
+
result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
2551
|
+
} catch {
|
|
2552
|
+
result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1511
2553
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
res.on("data", (c) => chunks.push(c));
|
|
1519
|
-
res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
|
|
1520
|
-
res.on("error", reject);
|
|
2554
|
+
if (result.status === 403 && result.body.includes("cf_chl")) {
|
|
2555
|
+
if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
|
|
2556
|
+
try {
|
|
2557
|
+
const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
2558
|
+
if (ciResult.status !== 403) return ciResult;
|
|
2559
|
+
} catch {
|
|
1521
2560
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
});
|
|
2561
|
+
}
|
|
2562
|
+
return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
|
|
2563
|
+
}
|
|
2564
|
+
return result;
|
|
1527
2565
|
}
|
|
1528
2566
|
var AppServerProcess = class {
|
|
1529
2567
|
constructor() {
|
|
@@ -1544,10 +2582,18 @@ var AppServerProcess = class {
|
|
|
1544
2582
|
'sandbox_mode="danger-full-access"'
|
|
1545
2583
|
];
|
|
1546
2584
|
}
|
|
2585
|
+
getCodexCommand() {
|
|
2586
|
+
const codexCommand = resolveCodexCommand();
|
|
2587
|
+
if (!codexCommand) {
|
|
2588
|
+
throw new Error("Codex CLI is not available. Install @openai/codex or set CODEXUI_CODEX_COMMAND.");
|
|
2589
|
+
}
|
|
2590
|
+
return codexCommand;
|
|
2591
|
+
}
|
|
1547
2592
|
start() {
|
|
1548
2593
|
if (this.process) return;
|
|
1549
2594
|
this.stopping = false;
|
|
1550
|
-
const
|
|
2595
|
+
const invocation = getSpawnInvocation(this.getCodexCommand(), this.appServerArgs);
|
|
2596
|
+
const proc = spawn2(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1551
2597
|
this.process = proc;
|
|
1552
2598
|
proc.stdout.setEncoding("utf8");
|
|
1553
2599
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -1641,7 +2687,7 @@ var AppServerProcess = class {
|
|
|
1641
2687
|
}
|
|
1642
2688
|
this.pendingServerRequests.delete(requestId);
|
|
1643
2689
|
this.sendServerRequestReply(requestId, reply);
|
|
1644
|
-
const requestParams =
|
|
2690
|
+
const requestParams = asRecord3(pendingRequest.params);
|
|
1645
2691
|
const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
|
|
1646
2692
|
this.emitNotification({
|
|
1647
2693
|
method: "server/request/resolved",
|
|
@@ -1710,7 +2756,7 @@ var AppServerProcess = class {
|
|
|
1710
2756
|
}
|
|
1711
2757
|
async respondToServerRequest(payload) {
|
|
1712
2758
|
await this.ensureInitialized();
|
|
1713
|
-
const body =
|
|
2759
|
+
const body = asRecord3(payload);
|
|
1714
2760
|
if (!body) {
|
|
1715
2761
|
throw new Error("Invalid response payload: expected object");
|
|
1716
2762
|
}
|
|
@@ -1718,7 +2764,7 @@ var AppServerProcess = class {
|
|
|
1718
2764
|
if (typeof id !== "number" || !Number.isInteger(id)) {
|
|
1719
2765
|
throw new Error('Invalid response payload: "id" must be an integer');
|
|
1720
2766
|
}
|
|
1721
|
-
const rawError =
|
|
2767
|
+
const rawError = asRecord3(body.error);
|
|
1722
2768
|
if (rawError) {
|
|
1723
2769
|
const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
|
|
1724
2770
|
const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
|
|
@@ -1773,7 +2819,13 @@ var MethodCatalog = class {
|
|
|
1773
2819
|
}
|
|
1774
2820
|
async runGenerateSchemaCommand(outDir) {
|
|
1775
2821
|
await new Promise((resolve3, reject) => {
|
|
1776
|
-
const
|
|
2822
|
+
const codexCommand = resolveCodexCommand();
|
|
2823
|
+
if (!codexCommand) {
|
|
2824
|
+
reject(new Error("Codex CLI is not available. Install @openai/codex or set CODEXUI_CODEX_COMMAND."));
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
const invocation = getSpawnInvocation(codexCommand, ["app-server", "generate-json-schema", "--out", outDir]);
|
|
2828
|
+
const process2 = spawn2(invocation.command, invocation.args, {
|
|
1777
2829
|
stdio: ["ignore", "ignore", "pipe"]
|
|
1778
2830
|
});
|
|
1779
2831
|
let stderr = "";
|
|
@@ -1792,13 +2844,13 @@ var MethodCatalog = class {
|
|
|
1792
2844
|
});
|
|
1793
2845
|
}
|
|
1794
2846
|
extractMethodsFromClientRequest(payload) {
|
|
1795
|
-
const root =
|
|
2847
|
+
const root = asRecord3(payload);
|
|
1796
2848
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
1797
2849
|
const methods = /* @__PURE__ */ new Set();
|
|
1798
2850
|
for (const entry of oneOf) {
|
|
1799
|
-
const row =
|
|
1800
|
-
const properties =
|
|
1801
|
-
const methodDef =
|
|
2851
|
+
const row = asRecord3(entry);
|
|
2852
|
+
const properties = asRecord3(row?.properties);
|
|
2853
|
+
const methodDef = asRecord3(properties?.method);
|
|
1802
2854
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
1803
2855
|
for (const item of methodEnum) {
|
|
1804
2856
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -1809,13 +2861,13 @@ var MethodCatalog = class {
|
|
|
1809
2861
|
return Array.from(methods).sort((a, b) => a.localeCompare(b));
|
|
1810
2862
|
}
|
|
1811
2863
|
extractMethodsFromServerNotification(payload) {
|
|
1812
|
-
const root =
|
|
2864
|
+
const root = asRecord3(payload);
|
|
1813
2865
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
1814
2866
|
const methods = /* @__PURE__ */ new Set();
|
|
1815
2867
|
for (const entry of oneOf) {
|
|
1816
|
-
const row =
|
|
1817
|
-
const properties =
|
|
1818
|
-
const methodDef =
|
|
2868
|
+
const row = asRecord3(entry);
|
|
2869
|
+
const properties = asRecord3(row?.properties);
|
|
2870
|
+
const methodDef = asRecord3(properties?.method);
|
|
1819
2871
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
1820
2872
|
for (const item of methodEnum) {
|
|
1821
2873
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -1829,9 +2881,9 @@ var MethodCatalog = class {
|
|
|
1829
2881
|
if (this.methodCache) {
|
|
1830
2882
|
return this.methodCache;
|
|
1831
2883
|
}
|
|
1832
|
-
const outDir = await mkdtemp2(
|
|
2884
|
+
const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
|
|
1833
2885
|
await this.runGenerateSchemaCommand(outDir);
|
|
1834
|
-
const clientRequestPath =
|
|
2886
|
+
const clientRequestPath = join3(outDir, "ClientRequest.json");
|
|
1835
2887
|
const raw = await readFile2(clientRequestPath, "utf8");
|
|
1836
2888
|
const parsed = JSON.parse(raw);
|
|
1837
2889
|
const methods = this.extractMethodsFromClientRequest(parsed);
|
|
@@ -1842,9 +2894,9 @@ var MethodCatalog = class {
|
|
|
1842
2894
|
if (this.notificationCache) {
|
|
1843
2895
|
return this.notificationCache;
|
|
1844
2896
|
}
|
|
1845
|
-
const outDir = await mkdtemp2(
|
|
2897
|
+
const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
|
|
1846
2898
|
await this.runGenerateSchemaCommand(outDir);
|
|
1847
|
-
const serverNotificationPath =
|
|
2899
|
+
const serverNotificationPath = join3(outDir, "ServerNotification.json");
|
|
1848
2900
|
const raw = await readFile2(serverNotificationPath, "utf8");
|
|
1849
2901
|
const parsed = JSON.parse(raw);
|
|
1850
2902
|
const methods = this.extractMethodsFromServerNotification(parsed);
|
|
@@ -1857,9 +2909,11 @@ function getSharedBridgeState() {
|
|
|
1857
2909
|
const globalScope = globalThis;
|
|
1858
2910
|
const existing = globalScope[SHARED_BRIDGE_KEY];
|
|
1859
2911
|
if (existing) return existing;
|
|
2912
|
+
const appServer = new AppServerProcess();
|
|
1860
2913
|
const created = {
|
|
1861
|
-
appServer
|
|
1862
|
-
methodCatalog: new MethodCatalog()
|
|
2914
|
+
appServer,
|
|
2915
|
+
methodCatalog: new MethodCatalog(),
|
|
2916
|
+
telegramBridge: new TelegramThreadBridge(appServer)
|
|
1863
2917
|
};
|
|
1864
2918
|
globalScope[SHARED_BRIDGE_KEY] = created;
|
|
1865
2919
|
return created;
|
|
@@ -1868,7 +2922,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
1868
2922
|
const threads = [];
|
|
1869
2923
|
let cursor = null;
|
|
1870
2924
|
do {
|
|
1871
|
-
const response =
|
|
2925
|
+
const response = asRecord3(await appServer.rpc("thread/list", {
|
|
1872
2926
|
archived: false,
|
|
1873
2927
|
limit: 100,
|
|
1874
2928
|
sortKey: "updated_at",
|
|
@@ -1876,7 +2930,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
1876
2930
|
}));
|
|
1877
2931
|
const data = Array.isArray(response?.data) ? response.data : [];
|
|
1878
2932
|
for (const row of data) {
|
|
1879
|
-
const record =
|
|
2933
|
+
const record = asRecord3(row);
|
|
1880
2934
|
const id = typeof record?.id === "string" ? record.id : "";
|
|
1881
2935
|
if (!id) continue;
|
|
1882
2936
|
const title = typeof record?.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof record?.preview === "string" && record.preview.trim().length > 0 ? record.preview.trim() : "Untitled thread";
|
|
@@ -1925,7 +2979,7 @@ async function buildThreadSearchIndex(appServer) {
|
|
|
1925
2979
|
return { docsById };
|
|
1926
2980
|
}
|
|
1927
2981
|
function createCodexBridgeMiddleware() {
|
|
1928
|
-
const { appServer, methodCatalog } = getSharedBridgeState();
|
|
2982
|
+
const { appServer, methodCatalog, telegramBridge } = getSharedBridgeState();
|
|
1929
2983
|
let threadSearchIndex = null;
|
|
1930
2984
|
let threadSearchIndexPromise = null;
|
|
1931
2985
|
async function getThreadSearchIndex() {
|
|
@@ -1941,6 +2995,12 @@ function createCodexBridgeMiddleware() {
|
|
|
1941
2995
|
return threadSearchIndexPromise;
|
|
1942
2996
|
}
|
|
1943
2997
|
void initializeSkillsSyncOnStartup(appServer);
|
|
2998
|
+
void readTelegramBridgeConfig().then((config) => {
|
|
2999
|
+
if (!config.botToken) return;
|
|
3000
|
+
telegramBridge.configureToken(config.botToken);
|
|
3001
|
+
telegramBridge.start();
|
|
3002
|
+
}).catch(() => {
|
|
3003
|
+
});
|
|
1944
3004
|
const middleware = async (req, res, next) => {
|
|
1945
3005
|
try {
|
|
1946
3006
|
if (!req.url) {
|
|
@@ -1957,7 +3017,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1957
3017
|
}
|
|
1958
3018
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
1959
3019
|
const payload = await readJsonBody(req);
|
|
1960
|
-
const body =
|
|
3020
|
+
const body = asRecord3(payload);
|
|
1961
3021
|
if (!body || typeof body.method !== "string" || body.method.length === 0) {
|
|
1962
3022
|
setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
1963
3023
|
return;
|
|
@@ -2006,11 +3066,24 @@ function createCodexBridgeMiddleware() {
|
|
|
2006
3066
|
return;
|
|
2007
3067
|
}
|
|
2008
3068
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
2009
|
-
setJson2(res, 200, { data: { path:
|
|
3069
|
+
setJson2(res, 200, { data: { path: homedir3() } });
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
3072
|
+
if (req.method === "GET" && url.pathname === "/codex-api/github-trending") {
|
|
3073
|
+
const sinceRaw = (url.searchParams.get("since") ?? "").trim().toLowerCase();
|
|
3074
|
+
const since = sinceRaw === "weekly" ? "weekly" : sinceRaw === "monthly" ? "monthly" : "daily";
|
|
3075
|
+
const limitRaw = Number.parseInt((url.searchParams.get("limit") ?? "6").trim(), 10);
|
|
3076
|
+
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(10, limitRaw)) : 6;
|
|
3077
|
+
try {
|
|
3078
|
+
const data = await fetchGithubTrending(since, limit);
|
|
3079
|
+
setJson2(res, 200, { data });
|
|
3080
|
+
} catch (error) {
|
|
3081
|
+
setJson2(res, 502, { error: getErrorMessage3(error, "Failed to fetch GitHub trending") });
|
|
3082
|
+
}
|
|
2010
3083
|
return;
|
|
2011
3084
|
}
|
|
2012
3085
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
2013
|
-
const payload =
|
|
3086
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2014
3087
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
2015
3088
|
if (!rawSourceCwd) {
|
|
2016
3089
|
setJson2(res, 400, { error: "Missing sourceCwd" });
|
|
@@ -2036,22 +3109,22 @@ function createCodexBridgeMiddleware() {
|
|
|
2036
3109
|
await runCommand2("git", ["init"], { cwd: sourceCwd });
|
|
2037
3110
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
2038
3111
|
}
|
|
2039
|
-
const repoName =
|
|
2040
|
-
const worktreesRoot =
|
|
3112
|
+
const repoName = basename3(gitRoot) || "repo";
|
|
3113
|
+
const worktreesRoot = join3(getCodexHomeDir2(), "worktrees");
|
|
2041
3114
|
await mkdir2(worktreesRoot, { recursive: true });
|
|
2042
3115
|
let worktreeId = "";
|
|
2043
3116
|
let worktreeParent = "";
|
|
2044
3117
|
let worktreeCwd = "";
|
|
2045
3118
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
2046
3119
|
const candidate = randomBytes(2).toString("hex");
|
|
2047
|
-
const parent =
|
|
3120
|
+
const parent = join3(worktreesRoot, candidate);
|
|
2048
3121
|
try {
|
|
2049
3122
|
await stat2(parent);
|
|
2050
3123
|
continue;
|
|
2051
3124
|
} catch {
|
|
2052
3125
|
worktreeId = candidate;
|
|
2053
3126
|
worktreeParent = parent;
|
|
2054
|
-
worktreeCwd =
|
|
3127
|
+
worktreeCwd = join3(parent, repoName);
|
|
2055
3128
|
break;
|
|
2056
3129
|
}
|
|
2057
3130
|
}
|
|
@@ -2075,13 +3148,106 @@ function createCodexBridgeMiddleware() {
|
|
|
2075
3148
|
}
|
|
2076
3149
|
});
|
|
2077
3150
|
} catch (error) {
|
|
2078
|
-
setJson2(res, 500, { error:
|
|
3151
|
+
setJson2(res, 500, { error: getErrorMessage3(error, "Failed to create worktree") });
|
|
3152
|
+
}
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
if (req.method === "POST" && url.pathname === "/codex-api/worktree/auto-commit") {
|
|
3156
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
3157
|
+
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
3158
|
+
const commitMessage = normalizeCommitMessage(payload?.message);
|
|
3159
|
+
if (!rawCwd) {
|
|
3160
|
+
setJson2(res, 400, { error: "Missing cwd" });
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
if (!commitMessage) {
|
|
3164
|
+
setJson2(res, 400, { error: "Missing message" });
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
3168
|
+
try {
|
|
3169
|
+
const cwdInfo = await stat2(cwd);
|
|
3170
|
+
if (!cwdInfo.isDirectory()) {
|
|
3171
|
+
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
} catch {
|
|
3175
|
+
setJson2(res, 404, { error: "cwd does not exist" });
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
try {
|
|
3179
|
+
await ensureRollbackGitRepo(cwd);
|
|
3180
|
+
const beforeStatus = await runRollbackGitWithOutput(cwd, ["status", "--porcelain"]);
|
|
3181
|
+
if (!beforeStatus.trim()) {
|
|
3182
|
+
setJson2(res, 200, { data: { committed: false } });
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
await runRollbackGit(cwd, ["add", "-A"]);
|
|
3186
|
+
const stagedStatus = await runRollbackGitWithOutput(cwd, ["diff", "--cached", "--name-only"]);
|
|
3187
|
+
if (!stagedStatus.trim()) {
|
|
3188
|
+
setJson2(res, 200, { data: { committed: false } });
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
await runRollbackGit(cwd, ["commit", "-m", commitMessage]);
|
|
3192
|
+
setJson2(res, 200, { data: { committed: true } });
|
|
3193
|
+
} catch (error) {
|
|
3194
|
+
setJson2(res, 500, { error: getErrorMessage3(error, "Failed to auto-commit rollback changes") });
|
|
3195
|
+
}
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
if (req.method === "POST" && url.pathname === "/codex-api/worktree/rollback-to-message") {
|
|
3199
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
3200
|
+
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
3201
|
+
const commitMessage = normalizeCommitMessage(payload?.message);
|
|
3202
|
+
if (!rawCwd) {
|
|
3203
|
+
setJson2(res, 400, { error: "Missing cwd" });
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
if (!commitMessage) {
|
|
3207
|
+
setJson2(res, 400, { error: "Missing message" });
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
3211
|
+
try {
|
|
3212
|
+
const cwdInfo = await stat2(cwd);
|
|
3213
|
+
if (!cwdInfo.isDirectory()) {
|
|
3214
|
+
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
} catch {
|
|
3218
|
+
setJson2(res, 404, { error: "cwd does not exist" });
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
try {
|
|
3222
|
+
await ensureRollbackGitRepo(cwd);
|
|
3223
|
+
const commitSha = await findRollbackCommitByExactMessage(cwd, commitMessage);
|
|
3224
|
+
if (!commitSha) {
|
|
3225
|
+
setJson2(res, 404, { error: "No matching commit found for this user message" });
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
let resetTargetSha = "";
|
|
3229
|
+
try {
|
|
3230
|
+
resetTargetSha = await runRollbackGitCapture(cwd, ["rev-parse", `${commitSha}^`]);
|
|
3231
|
+
} catch {
|
|
3232
|
+
setJson2(res, 409, { error: "Cannot rollback: matched commit has no parent commit" });
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
let stashed = false;
|
|
3236
|
+
if (await hasRollbackGitWorkingTreeChanges(cwd)) {
|
|
3237
|
+
const stashMessage = `codex-auto-stash-before-rollback-${Date.now()}`;
|
|
3238
|
+
await runRollbackGit(cwd, ["stash", "push", "-u", "-m", stashMessage]);
|
|
3239
|
+
stashed = true;
|
|
3240
|
+
}
|
|
3241
|
+
await runRollbackGit(cwd, ["reset", "--hard", resetTargetSha]);
|
|
3242
|
+
setJson2(res, 200, { data: { reset: true, commitSha, resetTargetSha, stashed } });
|
|
3243
|
+
} catch (error) {
|
|
3244
|
+
setJson2(res, 500, { error: getErrorMessage3(error, "Failed to rollback project to user message commit") });
|
|
2079
3245
|
}
|
|
2080
3246
|
return;
|
|
2081
3247
|
}
|
|
2082
3248
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
2083
3249
|
const payload = await readJsonBody(req);
|
|
2084
|
-
const record =
|
|
3250
|
+
const record = asRecord3(payload);
|
|
2085
3251
|
if (!record) {
|
|
2086
3252
|
setJson2(res, 400, { error: "Invalid body: expected object" });
|
|
2087
3253
|
return;
|
|
@@ -2096,7 +3262,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2096
3262
|
return;
|
|
2097
3263
|
}
|
|
2098
3264
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
2099
|
-
const payload =
|
|
3265
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2100
3266
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
2101
3267
|
const createIfMissing = payload?.createIfMissing === true;
|
|
2102
3268
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
@@ -2156,7 +3322,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2156
3322
|
let index = 1;
|
|
2157
3323
|
while (index < 1e5) {
|
|
2158
3324
|
const candidateName = `New Project (${String(index)})`;
|
|
2159
|
-
const candidatePath =
|
|
3325
|
+
const candidatePath = join3(normalizedBasePath, candidateName);
|
|
2160
3326
|
try {
|
|
2161
3327
|
await stat2(candidatePath);
|
|
2162
3328
|
index += 1;
|
|
@@ -2170,7 +3336,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2170
3336
|
return;
|
|
2171
3337
|
}
|
|
2172
3338
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
2173
|
-
const payload =
|
|
3339
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2174
3340
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
2175
3341
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
2176
3342
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
@@ -2195,17 +3361,17 @@ function createCodexBridgeMiddleware() {
|
|
|
2195
3361
|
const scored = files.map((path) => ({ path, score: scoreFileCandidate(path, query) })).filter((row) => query.length === 0 || row.score < 10).sort((a, b) => a.score - b.score || a.path.localeCompare(b.path)).slice(0, limit).map((row) => ({ path: row.path }));
|
|
2196
3362
|
setJson2(res, 200, { data: scored });
|
|
2197
3363
|
} catch (error) {
|
|
2198
|
-
setJson2(res, 500, { error:
|
|
3364
|
+
setJson2(res, 500, { error: getErrorMessage3(error, "Failed to search files") });
|
|
2199
3365
|
}
|
|
2200
3366
|
return;
|
|
2201
3367
|
}
|
|
2202
3368
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
2203
|
-
const cache = await
|
|
3369
|
+
const cache = await readMergedThreadTitleCache();
|
|
2204
3370
|
setJson2(res, 200, { data: cache });
|
|
2205
3371
|
return;
|
|
2206
3372
|
}
|
|
2207
3373
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
2208
|
-
const payload =
|
|
3374
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2209
3375
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
2210
3376
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
2211
3377
|
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
@@ -2219,7 +3385,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2219
3385
|
return;
|
|
2220
3386
|
}
|
|
2221
3387
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
2222
|
-
const payload =
|
|
3388
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2223
3389
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
2224
3390
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
2225
3391
|
if (!id) {
|
|
@@ -2232,6 +3398,23 @@ function createCodexBridgeMiddleware() {
|
|
|
2232
3398
|
setJson2(res, 200, { ok: true });
|
|
2233
3399
|
return;
|
|
2234
3400
|
}
|
|
3401
|
+
if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
|
|
3402
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
3403
|
+
const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
|
|
3404
|
+
if (!botToken) {
|
|
3405
|
+
setJson2(res, 400, { error: "Missing botToken" });
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
telegramBridge.configureToken(botToken);
|
|
3409
|
+
telegramBridge.start();
|
|
3410
|
+
await writeTelegramBridgeConfig({ botToken });
|
|
3411
|
+
setJson2(res, 200, { ok: true });
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
if (req.method === "GET" && url.pathname === "/codex-api/telegram/status") {
|
|
3415
|
+
setJson2(res, 200, { data: telegramBridge.getStatus() });
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
2235
3418
|
if (req.method === "GET" && url.pathname === "/codex-api/events") {
|
|
2236
3419
|
res.statusCode = 200;
|
|
2237
3420
|
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
@@ -2264,12 +3447,13 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
2264
3447
|
}
|
|
2265
3448
|
next();
|
|
2266
3449
|
} catch (error) {
|
|
2267
|
-
const message =
|
|
3450
|
+
const message = getErrorMessage3(error, "Unknown bridge error");
|
|
2268
3451
|
setJson2(res, 502, { error: message });
|
|
2269
3452
|
}
|
|
2270
3453
|
};
|
|
2271
3454
|
middleware.dispose = () => {
|
|
2272
3455
|
threadSearchIndex = null;
|
|
3456
|
+
telegramBridge.stop();
|
|
2273
3457
|
appServer.dispose();
|
|
2274
3458
|
};
|
|
2275
3459
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -2402,7 +3586,7 @@ function createAuthSession(password) {
|
|
|
2402
3586
|
}
|
|
2403
3587
|
|
|
2404
3588
|
// src/server/localBrowseUi.ts
|
|
2405
|
-
import { dirname, extname, join as
|
|
3589
|
+
import { dirname as dirname2, extname as extname2, join as join4 } from "path";
|
|
2406
3590
|
import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
2407
3591
|
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2408
3592
|
".txt",
|
|
@@ -2433,7 +3617,7 @@ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2433
3617
|
".ps1"
|
|
2434
3618
|
]);
|
|
2435
3619
|
function languageForPath(pathValue) {
|
|
2436
|
-
const extension =
|
|
3620
|
+
const extension = extname2(pathValue).toLowerCase();
|
|
2437
3621
|
switch (extension) {
|
|
2438
3622
|
case ".js":
|
|
2439
3623
|
return "javascript";
|
|
@@ -2494,7 +3678,7 @@ function decodeBrowsePath(rawPath) {
|
|
|
2494
3678
|
}
|
|
2495
3679
|
}
|
|
2496
3680
|
function isTextEditablePath(pathValue) {
|
|
2497
|
-
return TEXT_EDITABLE_EXTENSIONS.has(
|
|
3681
|
+
return TEXT_EDITABLE_EXTENSIONS.has(extname2(pathValue).toLowerCase());
|
|
2498
3682
|
}
|
|
2499
3683
|
function looksLikeTextBuffer(buffer) {
|
|
2500
3684
|
if (buffer.length === 0) return true;
|
|
@@ -2540,7 +3724,7 @@ function escapeForInlineScriptString(value) {
|
|
|
2540
3724
|
async function getDirectoryItems(localPath) {
|
|
2541
3725
|
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
2542
3726
|
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
2543
|
-
const entryPath =
|
|
3727
|
+
const entryPath = join4(localPath, entry.name);
|
|
2544
3728
|
const entryStat = await stat3(entryPath);
|
|
2545
3729
|
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
2546
3730
|
return {
|
|
@@ -2560,13 +3744,13 @@ async function getDirectoryItems(localPath) {
|
|
|
2560
3744
|
}
|
|
2561
3745
|
async function createDirectoryListingHtml(localPath) {
|
|
2562
3746
|
const items = await getDirectoryItems(localPath);
|
|
2563
|
-
const parentPath =
|
|
3747
|
+
const parentPath = dirname2(localPath);
|
|
2564
3748
|
const rows = items.map((item) => {
|
|
2565
3749
|
const suffix = item.isDirectory ? "/" : "";
|
|
2566
3750
|
const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
|
|
2567
|
-
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
|
|
3751
|
+
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
|
|
2568
3752
|
}).join("\n");
|
|
2569
|
-
const parentLink = localPath !== parentPath ? `<
|
|
3753
|
+
const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
|
|
2570
3754
|
return `<!doctype html>
|
|
2571
3755
|
<html lang="en">
|
|
2572
3756
|
<head>
|
|
@@ -2580,8 +3764,27 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
2580
3764
|
ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
2581
3765
|
.file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
|
|
2582
3766
|
.file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
|
|
2583
|
-
.
|
|
3767
|
+
.header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
|
|
3768
|
+
.header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
|
|
3769
|
+
.header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
|
|
3770
|
+
.header-open-btn {
|
|
3771
|
+
height: 42px;
|
|
3772
|
+
padding: 0 14px;
|
|
3773
|
+
border: 1px solid #4f8de0;
|
|
3774
|
+
border-radius: 10px;
|
|
3775
|
+
background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
|
|
3776
|
+
color: #eef6ff;
|
|
3777
|
+
font-weight: 700;
|
|
3778
|
+
letter-spacing: 0.01em;
|
|
3779
|
+
cursor: pointer;
|
|
3780
|
+
box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
|
|
3781
|
+
}
|
|
3782
|
+
.header-open-btn:hover { filter: brightness(1.08); }
|
|
3783
|
+
.header-open-btn:disabled { opacity: 0.6; cursor: default; }
|
|
3784
|
+
.row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
|
|
3785
|
+
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; color: #dbe6ff; text-decoration: none; cursor: pointer; }
|
|
2584
3786
|
.icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
|
|
3787
|
+
.status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
|
|
2585
3788
|
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
2586
3789
|
@media (max-width: 640px) {
|
|
2587
3790
|
body { margin: 12px; }
|
|
@@ -2593,14 +3796,52 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
2593
3796
|
</head>
|
|
2594
3797
|
<body>
|
|
2595
3798
|
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
2596
|
-
|
|
3799
|
+
<div class="header-actions">
|
|
3800
|
+
${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
|
|
3801
|
+
<button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}">Open folder in Codex</button>
|
|
3802
|
+
</div>
|
|
3803
|
+
<p id="status" class="status"></p>
|
|
2597
3804
|
<ul>${rows}</ul>
|
|
3805
|
+
<script>
|
|
3806
|
+
const status = document.getElementById('status');
|
|
3807
|
+
document.addEventListener('click', async (event) => {
|
|
3808
|
+
const target = event.target;
|
|
3809
|
+
if (!(target instanceof Element)) return;
|
|
3810
|
+
const button = target.closest('.open-folder-btn');
|
|
3811
|
+
if (!(button instanceof HTMLButtonElement)) return;
|
|
3812
|
+
|
|
3813
|
+
const path = button.getAttribute('data-path') || '';
|
|
3814
|
+
if (!path) return;
|
|
3815
|
+
button.disabled = true;
|
|
3816
|
+
status.textContent = 'Opening folder in Codex...';
|
|
3817
|
+
try {
|
|
3818
|
+
const response = await fetch('/codex-api/project-root', {
|
|
3819
|
+
method: 'POST',
|
|
3820
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3821
|
+
body: JSON.stringify({
|
|
3822
|
+
path,
|
|
3823
|
+
createIfMissing: false,
|
|
3824
|
+
label: '',
|
|
3825
|
+
}),
|
|
3826
|
+
});
|
|
3827
|
+
if (!response.ok) {
|
|
3828
|
+
status.textContent = 'Failed to open folder.';
|
|
3829
|
+
button.disabled = false;
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
window.location.assign('/#/');
|
|
3833
|
+
} catch {
|
|
3834
|
+
status.textContent = 'Failed to open folder.';
|
|
3835
|
+
button.disabled = false;
|
|
3836
|
+
}
|
|
3837
|
+
});
|
|
3838
|
+
</script>
|
|
2598
3839
|
</body>
|
|
2599
3840
|
</html>`;
|
|
2600
3841
|
}
|
|
2601
3842
|
async function createTextEditorHtml(localPath) {
|
|
2602
3843
|
const content = await readFile3(localPath, "utf8");
|
|
2603
|
-
const parentPath =
|
|
3844
|
+
const parentPath = dirname2(localPath);
|
|
2604
3845
|
const language = languageForPath(localPath);
|
|
2605
3846
|
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
2606
3847
|
return `<!doctype html>
|
|
@@ -2669,9 +3910,9 @@ async function createTextEditorHtml(localPath) {
|
|
|
2669
3910
|
|
|
2670
3911
|
// src/server/httpServer.ts
|
|
2671
3912
|
import { WebSocketServer } from "ws";
|
|
2672
|
-
var __dirname =
|
|
2673
|
-
var distDir =
|
|
2674
|
-
var spaEntryFile =
|
|
3913
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
3914
|
+
var distDir = join5(__dirname, "..", "dist");
|
|
3915
|
+
var spaEntryFile = join5(distDir, "index.html");
|
|
2675
3916
|
var IMAGE_CONTENT_TYPES = {
|
|
2676
3917
|
".avif": "image/avif",
|
|
2677
3918
|
".bmp": "image/bmp",
|
|
@@ -2728,7 +3969,7 @@ function createServer(options = {}) {
|
|
|
2728
3969
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2729
3970
|
return;
|
|
2730
3971
|
}
|
|
2731
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
3972
|
+
const contentType = IMAGE_CONTENT_TYPES[extname3(localPath).toLowerCase()];
|
|
2732
3973
|
if (!contentType) {
|
|
2733
3974
|
res.status(415).json({ error: "Unsupported image type." });
|
|
2734
3975
|
return;
|
|
@@ -2815,7 +4056,7 @@ function createServer(options = {}) {
|
|
|
2815
4056
|
res.status(404).json({ error: "File not found." });
|
|
2816
4057
|
}
|
|
2817
4058
|
});
|
|
2818
|
-
const hasFrontendAssets =
|
|
4059
|
+
const hasFrontendAssets = existsSync4(spaEntryFile);
|
|
2819
4060
|
if (hasFrontendAssets) {
|
|
2820
4061
|
app.use(express.static(distDir));
|
|
2821
4062
|
}
|
|
@@ -2885,10 +4126,26 @@ function generatePassword() {
|
|
|
2885
4126
|
|
|
2886
4127
|
// src/cli/index.ts
|
|
2887
4128
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
2888
|
-
var __dirname2 =
|
|
4129
|
+
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
4130
|
+
var hasPromptedCloudflaredInstall = false;
|
|
4131
|
+
function getCodexHomePath() {
|
|
4132
|
+
return process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
|
|
4133
|
+
}
|
|
4134
|
+
function getCloudflaredPromptMarkerPath() {
|
|
4135
|
+
return join6(getCodexHomePath(), ".cloudflared-install-prompted");
|
|
4136
|
+
}
|
|
4137
|
+
function hasPromptedCloudflaredInstallPersisted() {
|
|
4138
|
+
return existsSync5(getCloudflaredPromptMarkerPath());
|
|
4139
|
+
}
|
|
4140
|
+
async function persistCloudflaredInstallPrompted() {
|
|
4141
|
+
const codexHome = getCodexHomePath();
|
|
4142
|
+
mkdirSync(codexHome, { recursive: true });
|
|
4143
|
+
await writeFile4(getCloudflaredPromptMarkerPath(), `${Date.now()}
|
|
4144
|
+
`, "utf8");
|
|
4145
|
+
}
|
|
2889
4146
|
async function readCliVersion() {
|
|
2890
4147
|
try {
|
|
2891
|
-
const packageJsonPath =
|
|
4148
|
+
const packageJsonPath = join6(__dirname2, "..", "package.json");
|
|
2892
4149
|
const raw = await readFile4(packageJsonPath, "utf8");
|
|
2893
4150
|
const parsed = JSON.parse(raw);
|
|
2894
4151
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
@@ -2899,47 +4156,22 @@ async function readCliVersion() {
|
|
|
2899
4156
|
function isTermuxRuntime() {
|
|
2900
4157
|
return Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("/com.termux/"));
|
|
2901
4158
|
}
|
|
2902
|
-
function canRun(command, args = []) {
|
|
2903
|
-
const result = spawnSync(command, args, { stdio: "ignore" });
|
|
2904
|
-
return result.status === 0;
|
|
2905
|
-
}
|
|
2906
4159
|
function runOrFail(command, args, label) {
|
|
2907
|
-
const result =
|
|
4160
|
+
const result = spawnSyncCommand(command, args, { stdio: "inherit" });
|
|
2908
4161
|
if (result.status !== 0) {
|
|
2909
4162
|
throw new Error(`${label} failed with exit code ${String(result.status ?? -1)}`);
|
|
2910
4163
|
}
|
|
2911
4164
|
}
|
|
2912
4165
|
function runWithStatus(command, args) {
|
|
2913
|
-
const result =
|
|
4166
|
+
const result = spawnSyncCommand(command, args, { stdio: "inherit" });
|
|
2914
4167
|
return result.status ?? -1;
|
|
2915
4168
|
}
|
|
2916
|
-
function getUserNpmPrefix() {
|
|
2917
|
-
return join5(homedir3(), ".npm-global");
|
|
2918
|
-
}
|
|
2919
|
-
function resolveCodexCommand() {
|
|
2920
|
-
if (canRun("codex", ["--version"])) {
|
|
2921
|
-
return "codex";
|
|
2922
|
-
}
|
|
2923
|
-
const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
|
|
2924
|
-
if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2925
|
-
return userCandidate;
|
|
2926
|
-
}
|
|
2927
|
-
const prefix = process.env.PREFIX?.trim();
|
|
2928
|
-
if (!prefix) {
|
|
2929
|
-
return null;
|
|
2930
|
-
}
|
|
2931
|
-
const candidate = join5(prefix, "bin", "codex");
|
|
2932
|
-
if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
|
|
2933
|
-
return candidate;
|
|
2934
|
-
}
|
|
2935
|
-
return null;
|
|
2936
|
-
}
|
|
2937
4169
|
function resolveCloudflaredCommand() {
|
|
2938
|
-
if (
|
|
4170
|
+
if (canRunCommand("cloudflared", ["--version"])) {
|
|
2939
4171
|
return "cloudflared";
|
|
2940
4172
|
}
|
|
2941
|
-
const localCandidate =
|
|
2942
|
-
if (
|
|
4173
|
+
const localCandidate = join6(homedir4(), ".local", "bin", "cloudflared");
|
|
4174
|
+
if (existsSync5(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
|
|
2943
4175
|
return localCandidate;
|
|
2944
4176
|
}
|
|
2945
4177
|
return null;
|
|
@@ -2992,14 +4224,14 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
2992
4224
|
if (!mappedArch) {
|
|
2993
4225
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
2994
4226
|
}
|
|
2995
|
-
const userBinDir =
|
|
4227
|
+
const userBinDir = join6(homedir4(), ".local", "bin");
|
|
2996
4228
|
mkdirSync(userBinDir, { recursive: true });
|
|
2997
|
-
const destination =
|
|
4229
|
+
const destination = join6(userBinDir, "cloudflared");
|
|
2998
4230
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
2999
4231
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
3000
4232
|
await downloadFile(downloadUrl, destination);
|
|
3001
4233
|
chmodSync(destination, 493);
|
|
3002
|
-
process.env.PATH =
|
|
4234
|
+
process.env.PATH = prependPathEntry(process.env.PATH ?? "", userBinDir);
|
|
3003
4235
|
const installed = resolveCloudflaredCommand();
|
|
3004
4236
|
if (!installed) {
|
|
3005
4237
|
throw new Error("cloudflared download completed but executable is still not available");
|
|
@@ -3008,11 +4240,19 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
3008
4240
|
return installed;
|
|
3009
4241
|
}
|
|
3010
4242
|
async function shouldInstallCloudflaredInteractively() {
|
|
4243
|
+
if (hasPromptedCloudflaredInstall || hasPromptedCloudflaredInstallPersisted()) {
|
|
4244
|
+
return false;
|
|
4245
|
+
}
|
|
4246
|
+
hasPromptedCloudflaredInstall = true;
|
|
4247
|
+
await persistCloudflaredInstallPrompted();
|
|
4248
|
+
if (process.platform === "win32") {
|
|
4249
|
+
return false;
|
|
4250
|
+
}
|
|
3011
4251
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3012
4252
|
console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
|
|
3013
4253
|
return false;
|
|
3014
4254
|
}
|
|
3015
|
-
const prompt =
|
|
4255
|
+
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
3016
4256
|
try {
|
|
3017
4257
|
const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
|
|
3018
4258
|
const normalized = answer.trim().toLowerCase();
|
|
@@ -3026,6 +4266,9 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3026
4266
|
if (current) {
|
|
3027
4267
|
return current;
|
|
3028
4268
|
}
|
|
4269
|
+
if (process.platform === "win32") {
|
|
4270
|
+
return null;
|
|
4271
|
+
}
|
|
3029
4272
|
const installApproved = await shouldInstallCloudflaredInteractively();
|
|
3030
4273
|
if (!installApproved) {
|
|
3031
4274
|
return null;
|
|
@@ -3033,8 +4276,8 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3033
4276
|
return ensureCloudflaredInstalledLinux();
|
|
3034
4277
|
}
|
|
3035
4278
|
function hasCodexAuth() {
|
|
3036
|
-
const codexHome =
|
|
3037
|
-
return
|
|
4279
|
+
const codexHome = getCodexHomePath();
|
|
4280
|
+
return existsSync5(join6(codexHome, "auth.json"));
|
|
3038
4281
|
}
|
|
3039
4282
|
function ensureCodexInstalled() {
|
|
3040
4283
|
let codexCommand = resolveCodexCommand();
|
|
@@ -3052,7 +4295,7 @@ function ensureCodexInstalled() {
|
|
|
3052
4295
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
3053
4296
|
`);
|
|
3054
4297
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
3055
|
-
process.env.PATH =
|
|
4298
|
+
process.env.PATH = prependPathEntry(process.env.PATH ?? "", getNpmGlobalBinDir(userPrefix));
|
|
3056
4299
|
};
|
|
3057
4300
|
if (isTermuxRuntime()) {
|
|
3058
4301
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -3115,19 +4358,22 @@ function parseCloudflaredUrl(chunk) {
|
|
|
3115
4358
|
}
|
|
3116
4359
|
function getAccessibleUrls(port) {
|
|
3117
4360
|
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
}
|
|
3123
|
-
for (const entry of entries) {
|
|
3124
|
-
if (entry.internal) {
|
|
4361
|
+
try {
|
|
4362
|
+
const interfaces = networkInterfaces();
|
|
4363
|
+
for (const entries of Object.values(interfaces)) {
|
|
4364
|
+
if (!entries) {
|
|
3125
4365
|
continue;
|
|
3126
4366
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
4367
|
+
for (const entry of entries) {
|
|
4368
|
+
if (entry.internal) {
|
|
4369
|
+
continue;
|
|
4370
|
+
}
|
|
4371
|
+
if (entry.family === "IPv4") {
|
|
4372
|
+
urls.add(`http://${entry.address}:${String(port)}`);
|
|
4373
|
+
}
|
|
3129
4374
|
}
|
|
3130
4375
|
}
|
|
4376
|
+
} catch {
|
|
3131
4377
|
}
|
|
3132
4378
|
return Array.from(urls);
|
|
3133
4379
|
}
|
|
@@ -3190,8 +4436,8 @@ function listenWithFallback(server, startPort) {
|
|
|
3190
4436
|
});
|
|
3191
4437
|
}
|
|
3192
4438
|
function getCodexGlobalStatePath2() {
|
|
3193
|
-
const codexHome =
|
|
3194
|
-
return
|
|
4439
|
+
const codexHome = getCodexHomePath();
|
|
4440
|
+
return join6(codexHome, ".codex-global-state.json");
|
|
3195
4441
|
}
|
|
3196
4442
|
function normalizeUniqueStrings(value) {
|
|
3197
4443
|
if (!Array.isArray(value)) return [];
|
|
@@ -3256,6 +4502,9 @@ async function startServer(options) {
|
|
|
3256
4502
|
}
|
|
3257
4503
|
}
|
|
3258
4504
|
const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
|
|
4505
|
+
if (codexCommand) {
|
|
4506
|
+
process.env.CODEXUI_CODEX_COMMAND = codexCommand;
|
|
4507
|
+
}
|
|
3259
4508
|
if (!hasCodexAuth() && codexCommand) {
|
|
3260
4509
|
console.log("\nCodex is not logged in. Starting `codex login`...\n");
|
|
3261
4510
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
@@ -3315,7 +4564,7 @@ async function startServer(options) {
|
|
|
3315
4564
|
qrcode.generate(tunnelUrl, { small: true });
|
|
3316
4565
|
console.log("");
|
|
3317
4566
|
}
|
|
3318
|
-
openBrowser(`http://localhost:${String(port)}`);
|
|
4567
|
+
if (options.open) openBrowser(`http://localhost:${String(port)}`);
|
|
3319
4568
|
function shutdown() {
|
|
3320
4569
|
console.log("\nShutting down...");
|
|
3321
4570
|
if (tunnelChild && !tunnelChild.killed) {
|
|
@@ -3335,10 +4584,11 @@ async function startServer(options) {
|
|
|
3335
4584
|
}
|
|
3336
4585
|
async function runLogin() {
|
|
3337
4586
|
const codexCommand = ensureCodexInstalled() ?? "codex";
|
|
4587
|
+
process.env.CODEXUI_CODEX_COMMAND = codexCommand;
|
|
3338
4588
|
console.log("\nStarting `codex login`...\n");
|
|
3339
4589
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
3340
4590
|
}
|
|
3341
|
-
program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (projectPath, opts) => {
|
|
4591
|
+
program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").option("--open", "open browser on startup", true).option("--no-open", "do not open browser on startup").action(async (projectPath, opts) => {
|
|
3342
4592
|
const rawArgv = process.argv.slice(2);
|
|
3343
4593
|
const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
|
|
3344
4594
|
let openProjectOnly = (opts.openProject ?? "").trim();
|