codexapp 0.1.53 → 0.1.57
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-CeCihVgJ.js +1429 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +1441 -209
- 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,25 +1233,14 @@ 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 = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
995
1236
|
const localSkills = await scanInstalledSkillsFromDisk();
|
|
996
1237
|
for (const skill of remote) {
|
|
997
1238
|
const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
|
|
998
1239
|
if (!owner) continue;
|
|
999
1240
|
if (!localSkills.has(skill.name)) {
|
|
1000
|
-
|
|
1001
|
-
installerScript,
|
|
1002
|
-
"--repo",
|
|
1003
|
-
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1004
|
-
"--path",
|
|
1005
|
-
`skills/${owner}/${skill.name}`,
|
|
1006
|
-
"--dest",
|
|
1007
|
-
localDir,
|
|
1008
|
-
"--method",
|
|
1009
|
-
"git"
|
|
1010
|
-
]);
|
|
1241
|
+
continue;
|
|
1011
1242
|
}
|
|
1012
|
-
const skillPath =
|
|
1243
|
+
const skillPath = join2(localDir, skill.name);
|
|
1013
1244
|
await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
|
|
1014
1245
|
}
|
|
1015
1246
|
const remoteNames = new Set(remote.map((row) => row.name));
|
|
@@ -1038,15 +1269,29 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1038
1269
|
try {
|
|
1039
1270
|
const owner = url.searchParams.get("owner") || "";
|
|
1040
1271
|
const name = url.searchParams.get("name") || "";
|
|
1272
|
+
const installed = url.searchParams.get("installed") === "true";
|
|
1273
|
+
const skillPath = url.searchParams.get("path") || "";
|
|
1041
1274
|
if (!owner || !name) {
|
|
1042
1275
|
setJson(res, 400, { error: "Missing owner or name" });
|
|
1043
1276
|
return true;
|
|
1044
1277
|
}
|
|
1278
|
+
if (installed) {
|
|
1279
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
1280
|
+
const installedInfo = installedMap.get(name);
|
|
1281
|
+
const localSkillPath = installedInfo?.path || (skillPath ? skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md` : "");
|
|
1282
|
+
if (localSkillPath) {
|
|
1283
|
+
const content2 = await readFile(localSkillPath, "utf8");
|
|
1284
|
+
const description2 = extractSkillDescriptionFromMarkdown(content2);
|
|
1285
|
+
setJson(res, 200, { content: content2, description: description2, source: "local" });
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1045
1289
|
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
1046
1290
|
const resp = await fetch(rawUrl);
|
|
1047
1291
|
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1048
1292
|
const content = await resp.text();
|
|
1049
|
-
|
|
1293
|
+
const description = extractSkillDescriptionFromMarkdown(content);
|
|
1294
|
+
setJson(res, 200, { content, description, source: "remote" });
|
|
1050
1295
|
} catch (error) {
|
|
1051
1296
|
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
|
|
1052
1297
|
}
|
|
@@ -1061,9 +1306,25 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1061
1306
|
setJson(res, 400, { error: "Missing owner or name" });
|
|
1062
1307
|
return true;
|
|
1063
1308
|
}
|
|
1064
|
-
const installerScript =
|
|
1065
|
-
|
|
1066
|
-
|
|
1309
|
+
const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir());
|
|
1310
|
+
if (!installerScript) {
|
|
1311
|
+
throw new Error("Skill installer script not found");
|
|
1312
|
+
}
|
|
1313
|
+
const pythonCommand = resolvePythonCommand();
|
|
1314
|
+
if (!pythonCommand) {
|
|
1315
|
+
throw new Error("Python 3 is required to install skills");
|
|
1316
|
+
}
|
|
1317
|
+
const installDest = await withTimeout(
|
|
1318
|
+
detectUserSkillsDir(appServer),
|
|
1319
|
+
1e4,
|
|
1320
|
+
"detectUserSkillsDir"
|
|
1321
|
+
).catch(() => getSkillsInstallDir());
|
|
1322
|
+
const skillDir = join2(installDest, name);
|
|
1323
|
+
if (existsSync2(skillDir)) {
|
|
1324
|
+
await rm(skillDir, { recursive: true, force: true });
|
|
1325
|
+
}
|
|
1326
|
+
await runCommand(pythonCommand.command, [
|
|
1327
|
+
...pythonCommand.args,
|
|
1067
1328
|
installerScript,
|
|
1068
1329
|
"--repo",
|
|
1069
1330
|
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
@@ -1073,13 +1334,16 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1073
1334
|
installDest,
|
|
1074
1335
|
"--method",
|
|
1075
1336
|
"git"
|
|
1076
|
-
]);
|
|
1077
|
-
|
|
1078
|
-
|
|
1337
|
+
], { timeoutMs: 9e4 });
|
|
1338
|
+
try {
|
|
1339
|
+
await withTimeout(ensureInstalledSkillIsValid(appServer, skillDir), 1e4, "ensureInstalledSkillIsValid");
|
|
1340
|
+
} catch {
|
|
1341
|
+
}
|
|
1079
1342
|
const syncState = await readSkillsSyncState();
|
|
1080
1343
|
const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
|
|
1081
1344
|
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1082
|
-
|
|
1345
|
+
autoPushSyncedSkills(appServer).catch(() => {
|
|
1346
|
+
});
|
|
1083
1347
|
setJson(res, 200, { ok: true, path: skillDir });
|
|
1084
1348
|
} catch (error) {
|
|
1085
1349
|
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
@@ -1091,7 +1355,8 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1091
1355
|
const payload = asRecord(await readJsonBody2(req));
|
|
1092
1356
|
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1093
1357
|
const path = typeof payload?.path === "string" ? payload.path : "";
|
|
1094
|
-
const
|
|
1358
|
+
const normalizedPath = path.endsWith("/SKILL.md") ? path.slice(0, -"/SKILL.md".length) : path;
|
|
1359
|
+
const target = normalizedPath || (name ? join2(getSkillsInstallDir(), name) : "");
|
|
1095
1360
|
if (!target) {
|
|
1096
1361
|
setJson(res, 400, { error: "Missing name or path" });
|
|
1097
1362
|
return true;
|
|
@@ -1103,9 +1368,10 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1103
1368
|
delete nextOwners[name];
|
|
1104
1369
|
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1105
1370
|
}
|
|
1106
|
-
|
|
1371
|
+
autoPushSyncedSkills(appServer).catch(() => {
|
|
1372
|
+
});
|
|
1107
1373
|
try {
|
|
1108
|
-
await appServer.rpc("skills/list", { forceReload: true });
|
|
1374
|
+
await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
|
|
1109
1375
|
} catch {
|
|
1110
1376
|
}
|
|
1111
1377
|
setJson(res, 200, { ok: true, deletedPath: target });
|
|
@@ -1117,7 +1383,8 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1117
1383
|
return false;
|
|
1118
1384
|
}
|
|
1119
1385
|
|
|
1120
|
-
// src/server/
|
|
1386
|
+
// src/server/telegramThreadBridge.ts
|
|
1387
|
+
import { basename } from "path";
|
|
1121
1388
|
function asRecord2(value) {
|
|
1122
1389
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1123
1390
|
}
|
|
@@ -1135,21 +1402,425 @@ function getErrorMessage2(payload, fallback) {
|
|
|
1135
1402
|
}
|
|
1136
1403
|
return fallback;
|
|
1137
1404
|
}
|
|
1405
|
+
var TelegramThreadBridge = class {
|
|
1406
|
+
constructor(appServer) {
|
|
1407
|
+
this.threadIdByChatId = /* @__PURE__ */ new Map();
|
|
1408
|
+
this.chatIdsByThreadId = /* @__PURE__ */ new Map();
|
|
1409
|
+
this.lastForwardedTurnByThreadId = /* @__PURE__ */ new Map();
|
|
1410
|
+
this.active = false;
|
|
1411
|
+
this.pollingTask = null;
|
|
1412
|
+
this.nextUpdateOffset = 0;
|
|
1413
|
+
this.lastError = "";
|
|
1414
|
+
this.appServer = appServer;
|
|
1415
|
+
this.token = process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
|
|
1416
|
+
this.defaultCwd = process.env.TELEGRAM_DEFAULT_CWD?.trim() ?? process.cwd();
|
|
1417
|
+
}
|
|
1418
|
+
start() {
|
|
1419
|
+
if (!this.token || this.active) return;
|
|
1420
|
+
this.active = true;
|
|
1421
|
+
void this.notifyOnlineForKnownChats().catch(() => {
|
|
1422
|
+
});
|
|
1423
|
+
this.pollingTask = this.pollLoop();
|
|
1424
|
+
this.appServer.onNotification((notification) => {
|
|
1425
|
+
void this.handleNotification(notification).catch(() => {
|
|
1426
|
+
});
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
stop() {
|
|
1430
|
+
this.active = false;
|
|
1431
|
+
}
|
|
1432
|
+
async pollLoop() {
|
|
1433
|
+
while (this.active) {
|
|
1434
|
+
try {
|
|
1435
|
+
const updates = await this.getUpdates();
|
|
1436
|
+
this.lastError = "";
|
|
1437
|
+
for (const update of updates) {
|
|
1438
|
+
const updateId = typeof update.update_id === "number" ? update.update_id : -1;
|
|
1439
|
+
if (updateId >= 0) {
|
|
1440
|
+
this.nextUpdateOffset = Math.max(this.nextUpdateOffset, updateId + 1);
|
|
1441
|
+
}
|
|
1442
|
+
await this.handleIncomingUpdate(update);
|
|
1443
|
+
}
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
this.lastError = getErrorMessage2(error, "Telegram polling failed");
|
|
1446
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1500));
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async getUpdates() {
|
|
1451
|
+
if (!this.token) {
|
|
1452
|
+
throw new Error("Telegram bot token is not configured");
|
|
1453
|
+
}
|
|
1454
|
+
const response = await fetch(this.apiUrl("getUpdates"), {
|
|
1455
|
+
method: "POST",
|
|
1456
|
+
headers: { "Content-Type": "application/json" },
|
|
1457
|
+
body: JSON.stringify({
|
|
1458
|
+
timeout: 45,
|
|
1459
|
+
offset: this.nextUpdateOffset,
|
|
1460
|
+
allowed_updates: ["message", "callback_query"]
|
|
1461
|
+
})
|
|
1462
|
+
});
|
|
1463
|
+
const payload = asRecord2(await response.json());
|
|
1464
|
+
const result = Array.isArray(payload?.result) ? payload.result : [];
|
|
1465
|
+
return result;
|
|
1466
|
+
}
|
|
1467
|
+
apiUrl(method) {
|
|
1468
|
+
return `https://api.telegram.org/bot${this.token}/${method}`;
|
|
1469
|
+
}
|
|
1470
|
+
configureToken(token) {
|
|
1471
|
+
const normalizedToken = token.trim();
|
|
1472
|
+
if (!normalizedToken) {
|
|
1473
|
+
throw new Error("Telegram bot token is required");
|
|
1474
|
+
}
|
|
1475
|
+
this.token = normalizedToken;
|
|
1476
|
+
}
|
|
1477
|
+
getStatus() {
|
|
1478
|
+
return {
|
|
1479
|
+
configured: this.token.length > 0,
|
|
1480
|
+
active: this.active,
|
|
1481
|
+
mappedChats: this.threadIdByChatId.size,
|
|
1482
|
+
mappedThreads: this.chatIdsByThreadId.size,
|
|
1483
|
+
lastError: this.lastError
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
connectThread(threadId, chatId, token) {
|
|
1487
|
+
const normalizedThreadId = threadId.trim();
|
|
1488
|
+
if (!normalizedThreadId) {
|
|
1489
|
+
throw new Error("threadId is required");
|
|
1490
|
+
}
|
|
1491
|
+
if (!Number.isFinite(chatId)) {
|
|
1492
|
+
throw new Error("chatId must be a number");
|
|
1493
|
+
}
|
|
1494
|
+
if (typeof token === "string" && token.trim().length > 0) {
|
|
1495
|
+
this.configureToken(token);
|
|
1496
|
+
}
|
|
1497
|
+
if (!this.token) {
|
|
1498
|
+
throw new Error("Telegram bot token is not configured");
|
|
1499
|
+
}
|
|
1500
|
+
this.bindChatToThread(chatId, normalizedThreadId);
|
|
1501
|
+
this.start();
|
|
1502
|
+
void this.sendOnlineMessage(chatId).catch(() => {
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
async sendTelegramMessage(chatId, text, options = {}) {
|
|
1506
|
+
const message = text.trim();
|
|
1507
|
+
if (!message) return;
|
|
1508
|
+
const payload = { chat_id: chatId, text: message };
|
|
1509
|
+
if (options.replyMarkup) {
|
|
1510
|
+
payload.reply_markup = options.replyMarkup;
|
|
1511
|
+
}
|
|
1512
|
+
await fetch(this.apiUrl("sendMessage"), {
|
|
1513
|
+
method: "POST",
|
|
1514
|
+
headers: { "Content-Type": "application/json" },
|
|
1515
|
+
body: JSON.stringify(payload)
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
async sendOnlineMessage(chatId) {
|
|
1519
|
+
await this.sendTelegramMessage(chatId, "Codex thread bridge went online.");
|
|
1520
|
+
}
|
|
1521
|
+
async notifyOnlineForKnownChats() {
|
|
1522
|
+
const knownChatIds = Array.from(this.threadIdByChatId.keys());
|
|
1523
|
+
for (const chatId of knownChatIds) {
|
|
1524
|
+
await this.sendOnlineMessage(chatId);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async handleIncomingUpdate(update) {
|
|
1528
|
+
if (update.callback_query) {
|
|
1529
|
+
await this.handleCallbackQuery(update.callback_query);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
const message = update.message;
|
|
1533
|
+
const chatId = message?.chat?.id;
|
|
1534
|
+
const text = message?.text?.trim();
|
|
1535
|
+
if (typeof chatId !== "number" || !text) return;
|
|
1536
|
+
if (text === "/start") {
|
|
1537
|
+
await this.sendThreadPicker(chatId);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
if (text === "/newthread") {
|
|
1541
|
+
const threadId2 = await this.createThreadForChat(chatId);
|
|
1542
|
+
await this.sendTelegramMessage(chatId, `Mapped to new thread: ${threadId2}`);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
const threadCommand = text.match(/^\/thread\s+(\S+)$/);
|
|
1546
|
+
if (threadCommand) {
|
|
1547
|
+
const threadId2 = threadCommand[1];
|
|
1548
|
+
this.bindChatToThread(chatId, threadId2);
|
|
1549
|
+
await this.sendTelegramMessage(chatId, `Mapped to thread: ${threadId2}`);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const threadId = await this.ensureThreadForChat(chatId);
|
|
1553
|
+
try {
|
|
1554
|
+
await this.appServer.rpc("turn/start", {
|
|
1555
|
+
threadId,
|
|
1556
|
+
input: [{ type: "text", text }]
|
|
1557
|
+
});
|
|
1558
|
+
} catch (error) {
|
|
1559
|
+
const message2 = getErrorMessage2(error, "Failed to forward message to thread");
|
|
1560
|
+
await this.sendTelegramMessage(chatId, `Forward failed: ${message2}`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async handleCallbackQuery(callbackQuery) {
|
|
1564
|
+
const callbackId = typeof callbackQuery.id === "string" ? callbackQuery.id : "";
|
|
1565
|
+
const data = typeof callbackQuery.data === "string" ? callbackQuery.data : "";
|
|
1566
|
+
const chatId = callbackQuery.message?.chat?.id;
|
|
1567
|
+
if (!callbackId) return;
|
|
1568
|
+
if (!data.startsWith("thread:") || typeof chatId !== "number") {
|
|
1569
|
+
await this.answerCallbackQuery(callbackId, "Invalid selection");
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
const threadId = data.slice("thread:".length).trim();
|
|
1573
|
+
if (!threadId) {
|
|
1574
|
+
await this.answerCallbackQuery(callbackId, "Invalid thread id");
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
this.bindChatToThread(chatId, threadId);
|
|
1578
|
+
await this.answerCallbackQuery(callbackId, "Thread connected");
|
|
1579
|
+
await this.sendTelegramMessage(chatId, `Connected to thread: ${threadId}`);
|
|
1580
|
+
const history = await this.readThreadHistorySummary(threadId);
|
|
1581
|
+
if (history) {
|
|
1582
|
+
await this.sendTelegramMessage(chatId, history);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
async answerCallbackQuery(callbackQueryId, text) {
|
|
1586
|
+
await fetch(this.apiUrl("answerCallbackQuery"), {
|
|
1587
|
+
method: "POST",
|
|
1588
|
+
headers: { "Content-Type": "application/json" },
|
|
1589
|
+
body: JSON.stringify({
|
|
1590
|
+
callback_query_id: callbackQueryId,
|
|
1591
|
+
text
|
|
1592
|
+
})
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
async sendThreadPicker(chatId) {
|
|
1596
|
+
const threads = await this.listRecentThreads();
|
|
1597
|
+
if (threads.length === 0) {
|
|
1598
|
+
await this.sendTelegramMessage(chatId, "No threads found. Send /newthread to create one.");
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const inlineKeyboard = threads.map((thread) => [
|
|
1602
|
+
{
|
|
1603
|
+
text: thread.title,
|
|
1604
|
+
callback_data: `thread:${thread.id}`
|
|
1605
|
+
}
|
|
1606
|
+
]);
|
|
1607
|
+
await this.sendTelegramMessage(chatId, "Select a thread to connect:", {
|
|
1608
|
+
replyMarkup: { inline_keyboard: inlineKeyboard }
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
async listRecentThreads() {
|
|
1612
|
+
const payload = asRecord2(await this.appServer.rpc("thread/list", {
|
|
1613
|
+
archived: false,
|
|
1614
|
+
limit: 20,
|
|
1615
|
+
sortKey: "updated_at"
|
|
1616
|
+
}));
|
|
1617
|
+
const rows = Array.isArray(payload?.data) ? payload.data : [];
|
|
1618
|
+
const threads = [];
|
|
1619
|
+
for (const row of rows) {
|
|
1620
|
+
const record = asRecord2(row);
|
|
1621
|
+
const id = typeof record?.id === "string" ? record.id.trim() : "";
|
|
1622
|
+
if (!id) continue;
|
|
1623
|
+
const name = typeof record?.name === "string" ? record.name.trim() : "";
|
|
1624
|
+
const preview = typeof record?.preview === "string" ? record.preview.trim() : "";
|
|
1625
|
+
const cwd = typeof record?.cwd === "string" ? record.cwd.trim() : "";
|
|
1626
|
+
const projectName = cwd ? basename(cwd) : "project";
|
|
1627
|
+
const threadTitle = (name || preview || id).replace(/\s+/g, " ").trim();
|
|
1628
|
+
const title = `${projectName}/${threadTitle}`.slice(0, 64);
|
|
1629
|
+
threads.push({ id, title });
|
|
1630
|
+
}
|
|
1631
|
+
return threads;
|
|
1632
|
+
}
|
|
1633
|
+
async createThreadForChat(chatId) {
|
|
1634
|
+
const response = asRecord2(await this.appServer.rpc("thread/start", { cwd: this.defaultCwd }));
|
|
1635
|
+
const thread = asRecord2(response?.thread);
|
|
1636
|
+
const threadId = typeof thread?.id === "string" ? thread.id : "";
|
|
1637
|
+
if (!threadId) {
|
|
1638
|
+
throw new Error("thread/start did not return thread id");
|
|
1639
|
+
}
|
|
1640
|
+
this.bindChatToThread(chatId, threadId);
|
|
1641
|
+
return threadId;
|
|
1642
|
+
}
|
|
1643
|
+
async ensureThreadForChat(chatId) {
|
|
1644
|
+
const existing = this.threadIdByChatId.get(chatId);
|
|
1645
|
+
if (existing) return existing;
|
|
1646
|
+
return this.createThreadForChat(chatId);
|
|
1647
|
+
}
|
|
1648
|
+
bindChatToThread(chatId, threadId) {
|
|
1649
|
+
const previousThreadId = this.threadIdByChatId.get(chatId);
|
|
1650
|
+
if (previousThreadId && previousThreadId !== threadId) {
|
|
1651
|
+
const previousSet = this.chatIdsByThreadId.get(previousThreadId);
|
|
1652
|
+
previousSet?.delete(chatId);
|
|
1653
|
+
if (previousSet && previousSet.size === 0) {
|
|
1654
|
+
this.chatIdsByThreadId.delete(previousThreadId);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
this.threadIdByChatId.set(chatId, threadId);
|
|
1658
|
+
const chatIds = this.chatIdsByThreadId.get(threadId) ?? /* @__PURE__ */ new Set();
|
|
1659
|
+
chatIds.add(chatId);
|
|
1660
|
+
this.chatIdsByThreadId.set(threadId, chatIds);
|
|
1661
|
+
}
|
|
1662
|
+
extractThreadId(notification) {
|
|
1663
|
+
const params = asRecord2(notification.params);
|
|
1664
|
+
if (!params) return "";
|
|
1665
|
+
const directThreadId = typeof params.threadId === "string" ? params.threadId : "";
|
|
1666
|
+
if (directThreadId) return directThreadId;
|
|
1667
|
+
const turn = asRecord2(params.turn);
|
|
1668
|
+
const turnThreadId = typeof turn?.threadId === "string" ? turn.threadId : "";
|
|
1669
|
+
return turnThreadId;
|
|
1670
|
+
}
|
|
1671
|
+
extractTurnId(notification) {
|
|
1672
|
+
const params = asRecord2(notification.params);
|
|
1673
|
+
if (!params) return "";
|
|
1674
|
+
const directTurnId = typeof params.turnId === "string" ? params.turnId : "";
|
|
1675
|
+
if (directTurnId) return directTurnId;
|
|
1676
|
+
const turn = asRecord2(params.turn);
|
|
1677
|
+
const turnId = typeof turn?.id === "string" ? turn.id : "";
|
|
1678
|
+
return turnId;
|
|
1679
|
+
}
|
|
1680
|
+
async handleNotification(notification) {
|
|
1681
|
+
if (notification.method !== "turn/completed") return;
|
|
1682
|
+
const threadId = this.extractThreadId(notification);
|
|
1683
|
+
if (!threadId) return;
|
|
1684
|
+
const chatIds = this.chatIdsByThreadId.get(threadId);
|
|
1685
|
+
if (!chatIds || chatIds.size === 0) return;
|
|
1686
|
+
const turnId = this.extractTurnId(notification);
|
|
1687
|
+
const lastForwardedTurnId = this.lastForwardedTurnByThreadId.get(threadId);
|
|
1688
|
+
if (turnId && lastForwardedTurnId === turnId) return;
|
|
1689
|
+
const assistantReply = await this.readLatestAssistantMessage(threadId);
|
|
1690
|
+
if (!assistantReply) return;
|
|
1691
|
+
for (const chatId of chatIds) {
|
|
1692
|
+
await this.sendTelegramMessage(chatId, assistantReply);
|
|
1693
|
+
}
|
|
1694
|
+
if (turnId) {
|
|
1695
|
+
this.lastForwardedTurnByThreadId.set(threadId, turnId);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
async readLatestAssistantMessage(threadId) {
|
|
1699
|
+
const response = asRecord2(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
|
|
1700
|
+
const thread = asRecord2(response?.thread);
|
|
1701
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1702
|
+
for (let turnIndex = turns.length - 1; turnIndex >= 0; turnIndex -= 1) {
|
|
1703
|
+
const turn = asRecord2(turns[turnIndex]);
|
|
1704
|
+
const items = Array.isArray(turn?.items) ? turn.items : [];
|
|
1705
|
+
for (let itemIndex = items.length - 1; itemIndex >= 0; itemIndex -= 1) {
|
|
1706
|
+
const item = asRecord2(items[itemIndex]);
|
|
1707
|
+
if (item?.type === "agentMessage") {
|
|
1708
|
+
const text = typeof item.text === "string" ? item.text.trim() : "";
|
|
1709
|
+
if (text) return text;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return "";
|
|
1714
|
+
}
|
|
1715
|
+
async readThreadHistorySummary(threadId) {
|
|
1716
|
+
const response = asRecord2(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
|
|
1717
|
+
const thread = asRecord2(response?.thread);
|
|
1718
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1719
|
+
const historyRows = [];
|
|
1720
|
+
for (const turn of turns) {
|
|
1721
|
+
const turnRecord = asRecord2(turn);
|
|
1722
|
+
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
1723
|
+
for (const item of items) {
|
|
1724
|
+
const itemRecord = asRecord2(item);
|
|
1725
|
+
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
1726
|
+
if (type === "userMessage") {
|
|
1727
|
+
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
1728
|
+
for (const block of content) {
|
|
1729
|
+
const blockRecord = asRecord2(block);
|
|
1730
|
+
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim()) {
|
|
1731
|
+
historyRows.push(`User: ${blockRecord.text.trim()}`);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim()) {
|
|
1736
|
+
historyRows.push(`Assistant: ${itemRecord.text.trim()}`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (historyRows.length === 0) {
|
|
1741
|
+
return "Thread has no message history yet.";
|
|
1742
|
+
}
|
|
1743
|
+
const tail = historyRows.slice(-12).join("\n\n");
|
|
1744
|
+
const maxLen = 3800;
|
|
1745
|
+
const summary = tail.length > maxLen ? tail.slice(tail.length - maxLen) : tail;
|
|
1746
|
+
return `Recent history:
|
|
1747
|
+
|
|
1748
|
+
${summary}`;
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
// src/utils/commandInvocation.ts
|
|
1753
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1754
|
+
import { basename as basename2, extname } from "path";
|
|
1755
|
+
var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
|
|
1756
|
+
function quoteCmdExeArg(value) {
|
|
1757
|
+
const normalized = value.replace(/"/g, '""');
|
|
1758
|
+
if (!/[\s"]/u.test(normalized)) {
|
|
1759
|
+
return normalized;
|
|
1760
|
+
}
|
|
1761
|
+
return `"${normalized}"`;
|
|
1762
|
+
}
|
|
1763
|
+
function needsCmdExeWrapper(command) {
|
|
1764
|
+
if (process.platform !== "win32") {
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
const lowerCommand = command.toLowerCase();
|
|
1768
|
+
const baseName = basename2(lowerCommand);
|
|
1769
|
+
if (/\.(cmd|bat)$/i.test(baseName)) {
|
|
1770
|
+
return true;
|
|
1771
|
+
}
|
|
1772
|
+
if (extname(baseName)) {
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
return WINDOWS_CMD_NAMES.has(baseName);
|
|
1776
|
+
}
|
|
1777
|
+
function getSpawnInvocation(command, args = []) {
|
|
1778
|
+
if (needsCmdExeWrapper(command)) {
|
|
1779
|
+
return {
|
|
1780
|
+
command: "cmd.exe",
|
|
1781
|
+
args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
return { command, args };
|
|
1785
|
+
}
|
|
1786
|
+
function spawnSyncCommand(command, args = [], options = {}) {
|
|
1787
|
+
const invocation = getSpawnInvocation(command, args);
|
|
1788
|
+
return spawnSync2(invocation.command, invocation.args, options);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// src/server/codexAppServerBridge.ts
|
|
1792
|
+
function asRecord3(value) {
|
|
1793
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1794
|
+
}
|
|
1795
|
+
function getErrorMessage3(payload, fallback) {
|
|
1796
|
+
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
1797
|
+
return payload.message;
|
|
1798
|
+
}
|
|
1799
|
+
const record = asRecord3(payload);
|
|
1800
|
+
if (!record) return fallback;
|
|
1801
|
+
const error = record.error;
|
|
1802
|
+
if (typeof error === "string" && error.length > 0) return error;
|
|
1803
|
+
const nestedError = asRecord3(error);
|
|
1804
|
+
if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
|
|
1805
|
+
return nestedError.message;
|
|
1806
|
+
}
|
|
1807
|
+
return fallback;
|
|
1808
|
+
}
|
|
1138
1809
|
function setJson2(res, statusCode, payload) {
|
|
1139
1810
|
res.statusCode = statusCode;
|
|
1140
1811
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1141
1812
|
res.end(JSON.stringify(payload));
|
|
1142
1813
|
}
|
|
1143
1814
|
function extractThreadMessageText(threadReadPayload) {
|
|
1144
|
-
const payload =
|
|
1145
|
-
const thread =
|
|
1815
|
+
const payload = asRecord3(threadReadPayload);
|
|
1816
|
+
const thread = asRecord3(payload?.thread);
|
|
1146
1817
|
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1147
1818
|
const parts = [];
|
|
1148
1819
|
for (const turn of turns) {
|
|
1149
|
-
const turnRecord =
|
|
1820
|
+
const turnRecord = asRecord3(turn);
|
|
1150
1821
|
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
1151
1822
|
for (const item of items) {
|
|
1152
|
-
const itemRecord =
|
|
1823
|
+
const itemRecord = asRecord3(item);
|
|
1153
1824
|
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
1154
1825
|
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
1155
1826
|
parts.push(itemRecord.text.trim());
|
|
@@ -1158,7 +1829,7 @@ function extractThreadMessageText(threadReadPayload) {
|
|
|
1158
1829
|
if (type === "userMessage") {
|
|
1159
1830
|
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
1160
1831
|
for (const block of content) {
|
|
1161
|
-
const blockRecord =
|
|
1832
|
+
const blockRecord = asRecord3(block);
|
|
1162
1833
|
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
1163
1834
|
parts.push(blockRecord.text.trim());
|
|
1164
1835
|
}
|
|
@@ -1192,9 +1863,62 @@ function scoreFileCandidate(path, query) {
|
|
|
1192
1863
|
if (lowerPath.includes(lowerQuery)) return 4;
|
|
1193
1864
|
return 10;
|
|
1194
1865
|
}
|
|
1866
|
+
function decodeHtmlEntities(value) {
|
|
1867
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(///gi, "/");
|
|
1868
|
+
}
|
|
1869
|
+
function stripHtml(value) {
|
|
1870
|
+
return decodeHtmlEntities(value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
|
|
1871
|
+
}
|
|
1872
|
+
function parseGithubTrendingHtml(html, limit) {
|
|
1873
|
+
const rows = html.match(/<article[\s\S]*?<\/article>/g) ?? [];
|
|
1874
|
+
const items = [];
|
|
1875
|
+
let seq = Date.now();
|
|
1876
|
+
for (const row of rows) {
|
|
1877
|
+
const repoBlockMatch = row.match(/<h2[\s\S]*?<\/h2>/);
|
|
1878
|
+
const hrefMatch = repoBlockMatch?.[0]?.match(/href="\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)"/);
|
|
1879
|
+
if (!hrefMatch) continue;
|
|
1880
|
+
const fullName = hrefMatch[1] ?? "";
|
|
1881
|
+
if (!fullName || items.some((item) => item.fullName === fullName)) continue;
|
|
1882
|
+
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>/);
|
|
1883
|
+
const languageMatch = row.match(/programmingLanguage[^>]*>\s*([\s\S]*?)\s*<\/span>/);
|
|
1884
|
+
const starsMatch = row.match(/href="\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/stargazers"[\s\S]*?>([\s\S]*?)<\/a>/);
|
|
1885
|
+
const starsText = stripHtml(starsMatch?.[1] ?? "").replace(/,/g, "");
|
|
1886
|
+
const stars = Number.parseInt(starsText, 10);
|
|
1887
|
+
items.push({
|
|
1888
|
+
id: seq,
|
|
1889
|
+
fullName,
|
|
1890
|
+
url: `https://github.com/${fullName}`,
|
|
1891
|
+
description: stripHtml(descriptionMatch?.[1] ?? ""),
|
|
1892
|
+
language: stripHtml(languageMatch?.[1] ?? ""),
|
|
1893
|
+
stars: Number.isFinite(stars) ? stars : 0
|
|
1894
|
+
});
|
|
1895
|
+
seq += 1;
|
|
1896
|
+
if (items.length >= limit) break;
|
|
1897
|
+
}
|
|
1898
|
+
return items;
|
|
1899
|
+
}
|
|
1900
|
+
async function fetchGithubTrending(since, limit) {
|
|
1901
|
+
const endpoint = `https://github.com/trending?since=${since}`;
|
|
1902
|
+
const response = await fetch(endpoint, {
|
|
1903
|
+
headers: {
|
|
1904
|
+
"User-Agent": "codex-web-local",
|
|
1905
|
+
Accept: "text/html"
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
if (!response.ok) {
|
|
1909
|
+
throw new Error(`GitHub trending fetch failed (${response.status})`);
|
|
1910
|
+
}
|
|
1911
|
+
const html = await response.text();
|
|
1912
|
+
return parseGithubTrendingHtml(html, limit);
|
|
1913
|
+
}
|
|
1195
1914
|
async function listFilesWithRipgrep(cwd) {
|
|
1196
1915
|
return await new Promise((resolve3, reject) => {
|
|
1197
|
-
const
|
|
1916
|
+
const ripgrepCommand = resolveRipgrepCommand();
|
|
1917
|
+
if (!ripgrepCommand) {
|
|
1918
|
+
reject(new Error("ripgrep (rg) is not available"));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const proc = spawn2(ripgrepCommand, ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
1198
1922
|
cwd,
|
|
1199
1923
|
env: process.env,
|
|
1200
1924
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1221,7 +1945,7 @@ async function listFilesWithRipgrep(cwd) {
|
|
|
1221
1945
|
}
|
|
1222
1946
|
function getCodexHomeDir2() {
|
|
1223
1947
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
1224
|
-
return codexHome && codexHome.length > 0 ? codexHome :
|
|
1948
|
+
return codexHome && codexHome.length > 0 ? codexHome : join3(homedir3(), ".codex");
|
|
1225
1949
|
}
|
|
1226
1950
|
async function runCommand2(command, args, options = {}) {
|
|
1227
1951
|
await new Promise((resolve3, reject) => {
|
|
@@ -1251,15 +1975,15 @@ async function runCommand2(command, args, options = {}) {
|
|
|
1251
1975
|
});
|
|
1252
1976
|
}
|
|
1253
1977
|
function isMissingHeadError(error) {
|
|
1254
|
-
const message =
|
|
1978
|
+
const message = getErrorMessage3(error, "").toLowerCase();
|
|
1255
1979
|
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
1256
1980
|
}
|
|
1257
1981
|
function isNotGitRepositoryError(error) {
|
|
1258
|
-
const message =
|
|
1982
|
+
const message = getErrorMessage3(error, "").toLowerCase();
|
|
1259
1983
|
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
1260
1984
|
}
|
|
1261
1985
|
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
1262
|
-
const agentsPath =
|
|
1986
|
+
const agentsPath = join3(repoRoot, "AGENTS.md");
|
|
1263
1987
|
try {
|
|
1264
1988
|
await stat2(agentsPath);
|
|
1265
1989
|
} catch {
|
|
@@ -1299,6 +2023,33 @@ async function runCommandCapture(command, args, options = {}) {
|
|
|
1299
2023
|
});
|
|
1300
2024
|
});
|
|
1301
2025
|
}
|
|
2026
|
+
async function runCommandWithOutput2(command, args, options = {}) {
|
|
2027
|
+
return await new Promise((resolve3, reject) => {
|
|
2028
|
+
const proc = spawn2(command, args, {
|
|
2029
|
+
cwd: options.cwd,
|
|
2030
|
+
env: process.env,
|
|
2031
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2032
|
+
});
|
|
2033
|
+
let stdout = "";
|
|
2034
|
+
let stderr = "";
|
|
2035
|
+
proc.stdout.on("data", (chunk) => {
|
|
2036
|
+
stdout += chunk.toString();
|
|
2037
|
+
});
|
|
2038
|
+
proc.stderr.on("data", (chunk) => {
|
|
2039
|
+
stderr += chunk.toString();
|
|
2040
|
+
});
|
|
2041
|
+
proc.on("error", reject);
|
|
2042
|
+
proc.on("close", (code) => {
|
|
2043
|
+
if (code === 0) {
|
|
2044
|
+
resolve3(stdout.trim());
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
2048
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
2049
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
2050
|
+
});
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
1302
2053
|
function normalizeStringArray(value) {
|
|
1303
2054
|
if (!Array.isArray(value)) return [];
|
|
1304
2055
|
const normalized = [];
|
|
@@ -1319,8 +2070,88 @@ function normalizeStringRecord(value) {
|
|
|
1319
2070
|
}
|
|
1320
2071
|
return next;
|
|
1321
2072
|
}
|
|
2073
|
+
function normalizeCommitMessage(value) {
|
|
2074
|
+
if (typeof value !== "string") return "";
|
|
2075
|
+
const normalized = value.replace(/\r\n?/gu, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n").trim();
|
|
2076
|
+
return normalized.slice(0, 2e3);
|
|
2077
|
+
}
|
|
2078
|
+
function getRollbackGitDirForCwd(cwd) {
|
|
2079
|
+
return join3(cwd, ".codex", "rollbacks", ".git");
|
|
2080
|
+
}
|
|
2081
|
+
async function ensureLocalCodexGitignoreHasRollbacks(cwd) {
|
|
2082
|
+
const localCodexDir = join3(cwd, ".codex");
|
|
2083
|
+
const gitignorePath = join3(localCodexDir, ".gitignore");
|
|
2084
|
+
await mkdir2(localCodexDir, { recursive: true });
|
|
2085
|
+
let current = "";
|
|
2086
|
+
try {
|
|
2087
|
+
current = await readFile2(gitignorePath, "utf8");
|
|
2088
|
+
} catch {
|
|
2089
|
+
current = "";
|
|
2090
|
+
}
|
|
2091
|
+
const rows = current.split(/\r?\n/).map((line) => line.trim());
|
|
2092
|
+
if (rows.includes("rollbacks/")) return;
|
|
2093
|
+
const prefix = current.length > 0 && !current.endsWith("\n") ? `${current}
|
|
2094
|
+
` : current;
|
|
2095
|
+
await writeFile2(gitignorePath, `${prefix}rollbacks/
|
|
2096
|
+
`, "utf8");
|
|
2097
|
+
}
|
|
2098
|
+
async function ensureRollbackGitRepo(cwd) {
|
|
2099
|
+
const gitDir = getRollbackGitDirForCwd(cwd);
|
|
2100
|
+
try {
|
|
2101
|
+
const headInfo = await stat2(join3(gitDir, "HEAD"));
|
|
2102
|
+
if (!headInfo.isFile()) {
|
|
2103
|
+
throw new Error("Invalid rollback git repository");
|
|
2104
|
+
}
|
|
2105
|
+
} catch {
|
|
2106
|
+
await mkdir2(dirname(gitDir), { recursive: true });
|
|
2107
|
+
await runCommand2("git", ["--git-dir", gitDir, "--work-tree", cwd, "init"]);
|
|
2108
|
+
}
|
|
2109
|
+
await runCommand2("git", ["--git-dir", gitDir, "config", "user.email", "codex@local"]);
|
|
2110
|
+
await runCommand2("git", ["--git-dir", gitDir, "config", "user.name", "Codex Rollback"]);
|
|
2111
|
+
try {
|
|
2112
|
+
await runCommandCapture("git", ["--git-dir", gitDir, "--work-tree", cwd, "rev-parse", "--verify", "HEAD"]);
|
|
2113
|
+
} catch {
|
|
2114
|
+
await runCommand2(
|
|
2115
|
+
"git",
|
|
2116
|
+
["--git-dir", gitDir, "--work-tree", cwd, "commit", "--allow-empty", "-m", "Initialize rollback history"]
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
await ensureLocalCodexGitignoreHasRollbacks(cwd);
|
|
2120
|
+
return gitDir;
|
|
2121
|
+
}
|
|
2122
|
+
async function runRollbackGit(cwd, args) {
|
|
2123
|
+
const gitDir = await ensureRollbackGitRepo(cwd);
|
|
2124
|
+
await runCommand2("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
|
|
2125
|
+
}
|
|
2126
|
+
async function runRollbackGitCapture(cwd, args) {
|
|
2127
|
+
const gitDir = await ensureRollbackGitRepo(cwd);
|
|
2128
|
+
return await runCommandCapture("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
|
|
2129
|
+
}
|
|
2130
|
+
async function runRollbackGitWithOutput(cwd, args) {
|
|
2131
|
+
const gitDir = await ensureRollbackGitRepo(cwd);
|
|
2132
|
+
return await runCommandWithOutput2("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
|
|
2133
|
+
}
|
|
2134
|
+
async function hasRollbackGitWorkingTreeChanges(cwd) {
|
|
2135
|
+
const status = await runRollbackGitWithOutput(cwd, ["status", "--porcelain"]);
|
|
2136
|
+
return status.trim().length > 0;
|
|
2137
|
+
}
|
|
2138
|
+
async function findRollbackCommitByExactMessage(cwd, message) {
|
|
2139
|
+
const normalizedTarget = normalizeCommitMessage(message);
|
|
2140
|
+
if (!normalizedTarget) return "";
|
|
2141
|
+
const raw = await runRollbackGitWithOutput(cwd, ["log", "--format=%H%x1f%B%x1e"]);
|
|
2142
|
+
const entries = raw.split("");
|
|
2143
|
+
for (const entry of entries) {
|
|
2144
|
+
if (!entry.trim()) continue;
|
|
2145
|
+
const [shaRaw, bodyRaw] = entry.split("");
|
|
2146
|
+
const sha = (shaRaw ?? "").trim();
|
|
2147
|
+
const body = normalizeCommitMessage(bodyRaw ?? "");
|
|
2148
|
+
if (!sha) continue;
|
|
2149
|
+
if (body === normalizedTarget) return sha;
|
|
2150
|
+
}
|
|
2151
|
+
return "";
|
|
2152
|
+
}
|
|
1322
2153
|
function getCodexAuthPath() {
|
|
1323
|
-
return
|
|
2154
|
+
return join3(getCodexHomeDir2(), "auth.json");
|
|
1324
2155
|
}
|
|
1325
2156
|
async function readCodexAuth() {
|
|
1326
2157
|
try {
|
|
@@ -1334,13 +2165,21 @@ async function readCodexAuth() {
|
|
|
1334
2165
|
}
|
|
1335
2166
|
}
|
|
1336
2167
|
function getCodexGlobalStatePath() {
|
|
1337
|
-
return
|
|
2168
|
+
return join3(getCodexHomeDir2(), ".codex-global-state.json");
|
|
2169
|
+
}
|
|
2170
|
+
function getCodexSessionIndexPath() {
|
|
2171
|
+
return join3(getCodexHomeDir2(), "session_index.jsonl");
|
|
1338
2172
|
}
|
|
1339
2173
|
var MAX_THREAD_TITLES = 500;
|
|
2174
|
+
var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
|
|
2175
|
+
var sessionIndexThreadTitleCacheState = {
|
|
2176
|
+
fileSignature: null,
|
|
2177
|
+
cache: EMPTY_THREAD_TITLE_CACHE
|
|
2178
|
+
};
|
|
1340
2179
|
function normalizeThreadTitleCache(value) {
|
|
1341
|
-
const record =
|
|
1342
|
-
if (!record) return
|
|
1343
|
-
const rawTitles =
|
|
2180
|
+
const record = asRecord3(value);
|
|
2181
|
+
if (!record) return EMPTY_THREAD_TITLE_CACHE;
|
|
2182
|
+
const rawTitles = asRecord3(record.titles);
|
|
1344
2183
|
const titles = {};
|
|
1345
2184
|
if (rawTitles) {
|
|
1346
2185
|
for (const [k, v] of Object.entries(rawTitles)) {
|
|
@@ -1363,14 +2202,55 @@ function removeFromThreadTitleCache(cache, id) {
|
|
|
1363
2202
|
const { [id]: _, ...titles } = cache.titles;
|
|
1364
2203
|
return { titles, order: cache.order.filter((o) => o !== id) };
|
|
1365
2204
|
}
|
|
2205
|
+
function normalizeSessionIndexThreadTitle(value) {
|
|
2206
|
+
const record = asRecord3(value);
|
|
2207
|
+
if (!record) return null;
|
|
2208
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
2209
|
+
const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
|
|
2210
|
+
const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
|
|
2211
|
+
const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
|
|
2212
|
+
if (!id || !title) return null;
|
|
2213
|
+
return {
|
|
2214
|
+
id,
|
|
2215
|
+
title,
|
|
2216
|
+
updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
function trimThreadTitleCache(cache) {
|
|
2220
|
+
const titles = { ...cache.titles };
|
|
2221
|
+
const order = cache.order.filter((id) => {
|
|
2222
|
+
if (!titles[id]) return false;
|
|
2223
|
+
return true;
|
|
2224
|
+
}).slice(0, MAX_THREAD_TITLES);
|
|
2225
|
+
for (const id of Object.keys(titles)) {
|
|
2226
|
+
if (!order.includes(id)) {
|
|
2227
|
+
delete titles[id];
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
return { titles, order };
|
|
2231
|
+
}
|
|
2232
|
+
function mergeThreadTitleCaches(base, overlay) {
|
|
2233
|
+
const titles = { ...base.titles, ...overlay.titles };
|
|
2234
|
+
const order = [];
|
|
2235
|
+
for (const id of [...overlay.order, ...base.order]) {
|
|
2236
|
+
if (!titles[id] || order.includes(id)) continue;
|
|
2237
|
+
order.push(id);
|
|
2238
|
+
}
|
|
2239
|
+
for (const id of Object.keys(titles)) {
|
|
2240
|
+
if (!order.includes(id)) {
|
|
2241
|
+
order.push(id);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return trimThreadTitleCache({ titles, order });
|
|
2245
|
+
}
|
|
1366
2246
|
async function readThreadTitleCache() {
|
|
1367
2247
|
const statePath = getCodexGlobalStatePath();
|
|
1368
2248
|
try {
|
|
1369
2249
|
const raw = await readFile2(statePath, "utf8");
|
|
1370
|
-
const payload =
|
|
2250
|
+
const payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
1371
2251
|
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
1372
2252
|
} catch {
|
|
1373
|
-
return
|
|
2253
|
+
return EMPTY_THREAD_TITLE_CACHE;
|
|
1374
2254
|
}
|
|
1375
2255
|
}
|
|
1376
2256
|
async function writeThreadTitleCache(cache) {
|
|
@@ -1378,20 +2258,83 @@ async function writeThreadTitleCache(cache) {
|
|
|
1378
2258
|
let payload = {};
|
|
1379
2259
|
try {
|
|
1380
2260
|
const raw = await readFile2(statePath, "utf8");
|
|
1381
|
-
payload =
|
|
2261
|
+
payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
1382
2262
|
} catch {
|
|
1383
2263
|
payload = {};
|
|
1384
2264
|
}
|
|
1385
2265
|
payload["thread-titles"] = cache;
|
|
1386
2266
|
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1387
2267
|
}
|
|
2268
|
+
function getSessionIndexFileSignature(stats) {
|
|
2269
|
+
return `${String(stats.mtimeMs)}:${String(stats.size)}`;
|
|
2270
|
+
}
|
|
2271
|
+
async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
|
|
2272
|
+
const latestById = /* @__PURE__ */ new Map();
|
|
2273
|
+
const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
|
|
2274
|
+
const lines = createInterface({
|
|
2275
|
+
input,
|
|
2276
|
+
crlfDelay: Infinity
|
|
2277
|
+
});
|
|
2278
|
+
try {
|
|
2279
|
+
for await (const line of lines) {
|
|
2280
|
+
const trimmed = line.trim();
|
|
2281
|
+
if (!trimmed) continue;
|
|
2282
|
+
try {
|
|
2283
|
+
const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
|
|
2284
|
+
if (!entry) continue;
|
|
2285
|
+
const previous = latestById.get(entry.id);
|
|
2286
|
+
if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
|
|
2287
|
+
latestById.set(entry.id, entry);
|
|
2288
|
+
}
|
|
2289
|
+
} catch {
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
} finally {
|
|
2293
|
+
lines.close();
|
|
2294
|
+
input.close();
|
|
2295
|
+
}
|
|
2296
|
+
const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
|
|
2297
|
+
const titles = {};
|
|
2298
|
+
const order = [];
|
|
2299
|
+
for (const entry of entries) {
|
|
2300
|
+
titles[entry.id] = entry.title;
|
|
2301
|
+
order.push(entry.id);
|
|
2302
|
+
}
|
|
2303
|
+
return trimThreadTitleCache({ titles, order });
|
|
2304
|
+
}
|
|
2305
|
+
async function readThreadTitlesFromSessionIndex() {
|
|
2306
|
+
const sessionIndexPath = getCodexSessionIndexPath();
|
|
2307
|
+
try {
|
|
2308
|
+
const stats = await stat2(sessionIndexPath);
|
|
2309
|
+
const fileSignature = getSessionIndexFileSignature(stats);
|
|
2310
|
+
if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
|
|
2311
|
+
return sessionIndexThreadTitleCacheState.cache;
|
|
2312
|
+
}
|
|
2313
|
+
const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
|
|
2314
|
+
sessionIndexThreadTitleCacheState = { fileSignature, cache };
|
|
2315
|
+
return cache;
|
|
2316
|
+
} catch {
|
|
2317
|
+
sessionIndexThreadTitleCacheState = {
|
|
2318
|
+
fileSignature: "missing",
|
|
2319
|
+
cache: EMPTY_THREAD_TITLE_CACHE
|
|
2320
|
+
};
|
|
2321
|
+
return sessionIndexThreadTitleCacheState.cache;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
async function readMergedThreadTitleCache() {
|
|
2325
|
+
const [sessionIndexCache, persistedCache] = await Promise.all([
|
|
2326
|
+
readThreadTitlesFromSessionIndex(),
|
|
2327
|
+
readThreadTitleCache()
|
|
2328
|
+
]);
|
|
2329
|
+
return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
|
|
2330
|
+
}
|
|
1388
2331
|
async function readWorkspaceRootsState() {
|
|
1389
2332
|
const statePath = getCodexGlobalStatePath();
|
|
1390
2333
|
let payload = {};
|
|
1391
2334
|
try {
|
|
1392
2335
|
const raw = await readFile2(statePath, "utf8");
|
|
1393
2336
|
const parsed = JSON.parse(raw);
|
|
1394
|
-
payload =
|
|
2337
|
+
payload = asRecord3(parsed) ?? {};
|
|
1395
2338
|
} catch {
|
|
1396
2339
|
payload = {};
|
|
1397
2340
|
}
|
|
@@ -1406,7 +2349,7 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
1406
2349
|
let payload = {};
|
|
1407
2350
|
try {
|
|
1408
2351
|
const raw = await readFile2(statePath, "utf8");
|
|
1409
|
-
payload =
|
|
2352
|
+
payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
1410
2353
|
} catch {
|
|
1411
2354
|
payload = {};
|
|
1412
2355
|
}
|
|
@@ -1415,6 +2358,36 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
1415
2358
|
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
1416
2359
|
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1417
2360
|
}
|
|
2361
|
+
function normalizeTelegramBridgeConfig(value) {
|
|
2362
|
+
const record = asRecord3(value);
|
|
2363
|
+
if (!record) return { botToken: "" };
|
|
2364
|
+
const botToken = typeof record.botToken === "string" ? record.botToken.trim() : "";
|
|
2365
|
+
return { botToken };
|
|
2366
|
+
}
|
|
2367
|
+
async function readTelegramBridgeConfig() {
|
|
2368
|
+
const statePath = getCodexGlobalStatePath();
|
|
2369
|
+
try {
|
|
2370
|
+
const raw = await readFile2(statePath, "utf8");
|
|
2371
|
+
const payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
2372
|
+
return normalizeTelegramBridgeConfig(payload["telegram-bridge"]);
|
|
2373
|
+
} catch {
|
|
2374
|
+
return { botToken: "" };
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
async function writeTelegramBridgeConfig(nextState) {
|
|
2378
|
+
const statePath = getCodexGlobalStatePath();
|
|
2379
|
+
let payload = {};
|
|
2380
|
+
try {
|
|
2381
|
+
const raw = await readFile2(statePath, "utf8");
|
|
2382
|
+
payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
2383
|
+
} catch {
|
|
2384
|
+
payload = {};
|
|
2385
|
+
}
|
|
2386
|
+
payload["telegram-bridge"] = {
|
|
2387
|
+
botToken: nextState.botToken.trim()
|
|
2388
|
+
};
|
|
2389
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
2390
|
+
}
|
|
1418
2391
|
async function readJsonBody(req) {
|
|
1419
2392
|
const raw = await readRawBody(req);
|
|
1420
2393
|
if (raw.length === 0) return null;
|
|
@@ -1484,46 +2457,93 @@ function handleFileUpload(req, res) {
|
|
|
1484
2457
|
setJson2(res, 400, { error: "No file in request" });
|
|
1485
2458
|
return;
|
|
1486
2459
|
}
|
|
1487
|
-
const uploadDir =
|
|
2460
|
+
const uploadDir = join3(tmpdir2(), "codex-web-uploads");
|
|
1488
2461
|
await mkdir2(uploadDir, { recursive: true });
|
|
1489
|
-
const destDir = await mkdtemp2(
|
|
1490
|
-
const destPath =
|
|
2462
|
+
const destDir = await mkdtemp2(join3(uploadDir, "f-"));
|
|
2463
|
+
const destPath = join3(destDir, fileName);
|
|
1491
2464
|
await writeFile2(destPath, fileData);
|
|
1492
2465
|
setJson2(res, 200, { path: destPath });
|
|
1493
2466
|
} catch (err) {
|
|
1494
|
-
setJson2(res, 500, { error:
|
|
2467
|
+
setJson2(res, 500, { error: getErrorMessage3(err, "Upload failed") });
|
|
1495
2468
|
}
|
|
1496
2469
|
});
|
|
1497
2470
|
req.on("error", (err) => {
|
|
1498
|
-
setJson2(res, 500, { error:
|
|
2471
|
+
setJson2(res, 500, { error: getErrorMessage3(err, "Upload stream error") });
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
function httpPost(url, headers, body) {
|
|
2475
|
+
const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
|
|
2476
|
+
return new Promise((resolve3, reject) => {
|
|
2477
|
+
const req = doRequest(url, { method: "POST", headers }, (res) => {
|
|
2478
|
+
const chunks = [];
|
|
2479
|
+
res.on("data", (c) => chunks.push(c));
|
|
2480
|
+
res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
|
|
2481
|
+
res.on("error", reject);
|
|
2482
|
+
});
|
|
2483
|
+
req.on("error", reject);
|
|
2484
|
+
req.write(body);
|
|
2485
|
+
req.end();
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
var curlImpersonateAvailable = null;
|
|
2489
|
+
function curlImpersonatePost(url, headers, body) {
|
|
2490
|
+
return new Promise((resolve3, reject) => {
|
|
2491
|
+
const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
|
|
2492
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
2493
|
+
if (k.toLowerCase() === "content-length") continue;
|
|
2494
|
+
args.push("-H", `${k}: ${String(v)}`);
|
|
2495
|
+
}
|
|
2496
|
+
args.push("--data-binary", "@-");
|
|
2497
|
+
const proc = spawn2("curl-impersonate-chrome", args, {
|
|
2498
|
+
env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
|
|
2499
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2500
|
+
});
|
|
2501
|
+
const chunks = [];
|
|
2502
|
+
proc.stdout.on("data", (c) => chunks.push(c));
|
|
2503
|
+
proc.on("error", (e) => {
|
|
2504
|
+
curlImpersonateAvailable = false;
|
|
2505
|
+
reject(e);
|
|
2506
|
+
});
|
|
2507
|
+
proc.on("close", (code) => {
|
|
2508
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
2509
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2510
|
+
const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
|
|
2511
|
+
const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
|
|
2512
|
+
const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
|
|
2513
|
+
curlImpersonateAvailable = true;
|
|
2514
|
+
resolve3({ status, body: responseBody });
|
|
2515
|
+
});
|
|
2516
|
+
proc.stdin.write(body);
|
|
2517
|
+
proc.stdin.end();
|
|
1499
2518
|
});
|
|
1500
2519
|
}
|
|
1501
2520
|
async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
1502
|
-
const
|
|
2521
|
+
const chatgptHeaders = {
|
|
1503
2522
|
"Content-Type": contentType,
|
|
1504
2523
|
"Content-Length": body.length,
|
|
1505
2524
|
Authorization: `Bearer ${authToken}`,
|
|
1506
2525
|
originator: "Codex Desktop",
|
|
1507
2526
|
"User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
|
|
1508
2527
|
};
|
|
1509
|
-
if (accountId)
|
|
1510
|
-
|
|
2528
|
+
if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
|
|
2529
|
+
const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
|
|
2530
|
+
let result;
|
|
2531
|
+
try {
|
|
2532
|
+
result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
2533
|
+
} catch {
|
|
2534
|
+
result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1511
2535
|
}
|
|
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);
|
|
2536
|
+
if (result.status === 403 && result.body.includes("cf_chl")) {
|
|
2537
|
+
if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
|
|
2538
|
+
try {
|
|
2539
|
+
const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
2540
|
+
if (ciResult.status !== 403) return ciResult;
|
|
2541
|
+
} catch {
|
|
1521
2542
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
});
|
|
2543
|
+
}
|
|
2544
|
+
return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
|
|
2545
|
+
}
|
|
2546
|
+
return result;
|
|
1527
2547
|
}
|
|
1528
2548
|
var AppServerProcess = class {
|
|
1529
2549
|
constructor() {
|
|
@@ -1544,10 +2564,18 @@ var AppServerProcess = class {
|
|
|
1544
2564
|
'sandbox_mode="danger-full-access"'
|
|
1545
2565
|
];
|
|
1546
2566
|
}
|
|
2567
|
+
getCodexCommand() {
|
|
2568
|
+
const codexCommand = resolveCodexCommand();
|
|
2569
|
+
if (!codexCommand) {
|
|
2570
|
+
throw new Error("Codex CLI is not available. Install @openai/codex or set CODEXUI_CODEX_COMMAND.");
|
|
2571
|
+
}
|
|
2572
|
+
return codexCommand;
|
|
2573
|
+
}
|
|
1547
2574
|
start() {
|
|
1548
2575
|
if (this.process) return;
|
|
1549
2576
|
this.stopping = false;
|
|
1550
|
-
const
|
|
2577
|
+
const invocation = getSpawnInvocation(this.getCodexCommand(), this.appServerArgs);
|
|
2578
|
+
const proc = spawn2(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1551
2579
|
this.process = proc;
|
|
1552
2580
|
proc.stdout.setEncoding("utf8");
|
|
1553
2581
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -1641,7 +2669,7 @@ var AppServerProcess = class {
|
|
|
1641
2669
|
}
|
|
1642
2670
|
this.pendingServerRequests.delete(requestId);
|
|
1643
2671
|
this.sendServerRequestReply(requestId, reply);
|
|
1644
|
-
const requestParams =
|
|
2672
|
+
const requestParams = asRecord3(pendingRequest.params);
|
|
1645
2673
|
const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
|
|
1646
2674
|
this.emitNotification({
|
|
1647
2675
|
method: "server/request/resolved",
|
|
@@ -1710,7 +2738,7 @@ var AppServerProcess = class {
|
|
|
1710
2738
|
}
|
|
1711
2739
|
async respondToServerRequest(payload) {
|
|
1712
2740
|
await this.ensureInitialized();
|
|
1713
|
-
const body =
|
|
2741
|
+
const body = asRecord3(payload);
|
|
1714
2742
|
if (!body) {
|
|
1715
2743
|
throw new Error("Invalid response payload: expected object");
|
|
1716
2744
|
}
|
|
@@ -1718,7 +2746,7 @@ var AppServerProcess = class {
|
|
|
1718
2746
|
if (typeof id !== "number" || !Number.isInteger(id)) {
|
|
1719
2747
|
throw new Error('Invalid response payload: "id" must be an integer');
|
|
1720
2748
|
}
|
|
1721
|
-
const rawError =
|
|
2749
|
+
const rawError = asRecord3(body.error);
|
|
1722
2750
|
if (rawError) {
|
|
1723
2751
|
const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
|
|
1724
2752
|
const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
|
|
@@ -1773,7 +2801,13 @@ var MethodCatalog = class {
|
|
|
1773
2801
|
}
|
|
1774
2802
|
async runGenerateSchemaCommand(outDir) {
|
|
1775
2803
|
await new Promise((resolve3, reject) => {
|
|
1776
|
-
const
|
|
2804
|
+
const codexCommand = resolveCodexCommand();
|
|
2805
|
+
if (!codexCommand) {
|
|
2806
|
+
reject(new Error("Codex CLI is not available. Install @openai/codex or set CODEXUI_CODEX_COMMAND."));
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
const invocation = getSpawnInvocation(codexCommand, ["app-server", "generate-json-schema", "--out", outDir]);
|
|
2810
|
+
const process2 = spawn2(invocation.command, invocation.args, {
|
|
1777
2811
|
stdio: ["ignore", "ignore", "pipe"]
|
|
1778
2812
|
});
|
|
1779
2813
|
let stderr = "";
|
|
@@ -1792,13 +2826,13 @@ var MethodCatalog = class {
|
|
|
1792
2826
|
});
|
|
1793
2827
|
}
|
|
1794
2828
|
extractMethodsFromClientRequest(payload) {
|
|
1795
|
-
const root =
|
|
2829
|
+
const root = asRecord3(payload);
|
|
1796
2830
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
1797
2831
|
const methods = /* @__PURE__ */ new Set();
|
|
1798
2832
|
for (const entry of oneOf) {
|
|
1799
|
-
const row =
|
|
1800
|
-
const properties =
|
|
1801
|
-
const methodDef =
|
|
2833
|
+
const row = asRecord3(entry);
|
|
2834
|
+
const properties = asRecord3(row?.properties);
|
|
2835
|
+
const methodDef = asRecord3(properties?.method);
|
|
1802
2836
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
1803
2837
|
for (const item of methodEnum) {
|
|
1804
2838
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -1809,13 +2843,13 @@ var MethodCatalog = class {
|
|
|
1809
2843
|
return Array.from(methods).sort((a, b) => a.localeCompare(b));
|
|
1810
2844
|
}
|
|
1811
2845
|
extractMethodsFromServerNotification(payload) {
|
|
1812
|
-
const root =
|
|
2846
|
+
const root = asRecord3(payload);
|
|
1813
2847
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
1814
2848
|
const methods = /* @__PURE__ */ new Set();
|
|
1815
2849
|
for (const entry of oneOf) {
|
|
1816
|
-
const row =
|
|
1817
|
-
const properties =
|
|
1818
|
-
const methodDef =
|
|
2850
|
+
const row = asRecord3(entry);
|
|
2851
|
+
const properties = asRecord3(row?.properties);
|
|
2852
|
+
const methodDef = asRecord3(properties?.method);
|
|
1819
2853
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
1820
2854
|
for (const item of methodEnum) {
|
|
1821
2855
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -1829,9 +2863,9 @@ var MethodCatalog = class {
|
|
|
1829
2863
|
if (this.methodCache) {
|
|
1830
2864
|
return this.methodCache;
|
|
1831
2865
|
}
|
|
1832
|
-
const outDir = await mkdtemp2(
|
|
2866
|
+
const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
|
|
1833
2867
|
await this.runGenerateSchemaCommand(outDir);
|
|
1834
|
-
const clientRequestPath =
|
|
2868
|
+
const clientRequestPath = join3(outDir, "ClientRequest.json");
|
|
1835
2869
|
const raw = await readFile2(clientRequestPath, "utf8");
|
|
1836
2870
|
const parsed = JSON.parse(raw);
|
|
1837
2871
|
const methods = this.extractMethodsFromClientRequest(parsed);
|
|
@@ -1842,9 +2876,9 @@ var MethodCatalog = class {
|
|
|
1842
2876
|
if (this.notificationCache) {
|
|
1843
2877
|
return this.notificationCache;
|
|
1844
2878
|
}
|
|
1845
|
-
const outDir = await mkdtemp2(
|
|
2879
|
+
const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
|
|
1846
2880
|
await this.runGenerateSchemaCommand(outDir);
|
|
1847
|
-
const serverNotificationPath =
|
|
2881
|
+
const serverNotificationPath = join3(outDir, "ServerNotification.json");
|
|
1848
2882
|
const raw = await readFile2(serverNotificationPath, "utf8");
|
|
1849
2883
|
const parsed = JSON.parse(raw);
|
|
1850
2884
|
const methods = this.extractMethodsFromServerNotification(parsed);
|
|
@@ -1857,9 +2891,11 @@ function getSharedBridgeState() {
|
|
|
1857
2891
|
const globalScope = globalThis;
|
|
1858
2892
|
const existing = globalScope[SHARED_BRIDGE_KEY];
|
|
1859
2893
|
if (existing) return existing;
|
|
2894
|
+
const appServer = new AppServerProcess();
|
|
1860
2895
|
const created = {
|
|
1861
|
-
appServer
|
|
1862
|
-
methodCatalog: new MethodCatalog()
|
|
2896
|
+
appServer,
|
|
2897
|
+
methodCatalog: new MethodCatalog(),
|
|
2898
|
+
telegramBridge: new TelegramThreadBridge(appServer)
|
|
1863
2899
|
};
|
|
1864
2900
|
globalScope[SHARED_BRIDGE_KEY] = created;
|
|
1865
2901
|
return created;
|
|
@@ -1868,7 +2904,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
1868
2904
|
const threads = [];
|
|
1869
2905
|
let cursor = null;
|
|
1870
2906
|
do {
|
|
1871
|
-
const response =
|
|
2907
|
+
const response = asRecord3(await appServer.rpc("thread/list", {
|
|
1872
2908
|
archived: false,
|
|
1873
2909
|
limit: 100,
|
|
1874
2910
|
sortKey: "updated_at",
|
|
@@ -1876,7 +2912,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
1876
2912
|
}));
|
|
1877
2913
|
const data = Array.isArray(response?.data) ? response.data : [];
|
|
1878
2914
|
for (const row of data) {
|
|
1879
|
-
const record =
|
|
2915
|
+
const record = asRecord3(row);
|
|
1880
2916
|
const id = typeof record?.id === "string" ? record.id : "";
|
|
1881
2917
|
if (!id) continue;
|
|
1882
2918
|
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 +2961,7 @@ async function buildThreadSearchIndex(appServer) {
|
|
|
1925
2961
|
return { docsById };
|
|
1926
2962
|
}
|
|
1927
2963
|
function createCodexBridgeMiddleware() {
|
|
1928
|
-
const { appServer, methodCatalog } = getSharedBridgeState();
|
|
2964
|
+
const { appServer, methodCatalog, telegramBridge } = getSharedBridgeState();
|
|
1929
2965
|
let threadSearchIndex = null;
|
|
1930
2966
|
let threadSearchIndexPromise = null;
|
|
1931
2967
|
async function getThreadSearchIndex() {
|
|
@@ -1941,6 +2977,12 @@ function createCodexBridgeMiddleware() {
|
|
|
1941
2977
|
return threadSearchIndexPromise;
|
|
1942
2978
|
}
|
|
1943
2979
|
void initializeSkillsSyncOnStartup(appServer);
|
|
2980
|
+
void readTelegramBridgeConfig().then((config) => {
|
|
2981
|
+
if (!config.botToken) return;
|
|
2982
|
+
telegramBridge.configureToken(config.botToken);
|
|
2983
|
+
telegramBridge.start();
|
|
2984
|
+
}).catch(() => {
|
|
2985
|
+
});
|
|
1944
2986
|
const middleware = async (req, res, next) => {
|
|
1945
2987
|
try {
|
|
1946
2988
|
if (!req.url) {
|
|
@@ -1957,7 +2999,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1957
2999
|
}
|
|
1958
3000
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
1959
3001
|
const payload = await readJsonBody(req);
|
|
1960
|
-
const body =
|
|
3002
|
+
const body = asRecord3(payload);
|
|
1961
3003
|
if (!body || typeof body.method !== "string" || body.method.length === 0) {
|
|
1962
3004
|
setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
1963
3005
|
return;
|
|
@@ -2006,11 +3048,24 @@ function createCodexBridgeMiddleware() {
|
|
|
2006
3048
|
return;
|
|
2007
3049
|
}
|
|
2008
3050
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
2009
|
-
setJson2(res, 200, { data: { path:
|
|
3051
|
+
setJson2(res, 200, { data: { path: homedir3() } });
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
if (req.method === "GET" && url.pathname === "/codex-api/github-trending") {
|
|
3055
|
+
const sinceRaw = (url.searchParams.get("since") ?? "").trim().toLowerCase();
|
|
3056
|
+
const since = sinceRaw === "weekly" ? "weekly" : sinceRaw === "monthly" ? "monthly" : "daily";
|
|
3057
|
+
const limitRaw = Number.parseInt((url.searchParams.get("limit") ?? "6").trim(), 10);
|
|
3058
|
+
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(10, limitRaw)) : 6;
|
|
3059
|
+
try {
|
|
3060
|
+
const data = await fetchGithubTrending(since, limit);
|
|
3061
|
+
setJson2(res, 200, { data });
|
|
3062
|
+
} catch (error) {
|
|
3063
|
+
setJson2(res, 502, { error: getErrorMessage3(error, "Failed to fetch GitHub trending") });
|
|
3064
|
+
}
|
|
2010
3065
|
return;
|
|
2011
3066
|
}
|
|
2012
3067
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
2013
|
-
const payload =
|
|
3068
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2014
3069
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
2015
3070
|
if (!rawSourceCwd) {
|
|
2016
3071
|
setJson2(res, 400, { error: "Missing sourceCwd" });
|
|
@@ -2036,22 +3091,22 @@ function createCodexBridgeMiddleware() {
|
|
|
2036
3091
|
await runCommand2("git", ["init"], { cwd: sourceCwd });
|
|
2037
3092
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
2038
3093
|
}
|
|
2039
|
-
const repoName =
|
|
2040
|
-
const worktreesRoot =
|
|
3094
|
+
const repoName = basename3(gitRoot) || "repo";
|
|
3095
|
+
const worktreesRoot = join3(getCodexHomeDir2(), "worktrees");
|
|
2041
3096
|
await mkdir2(worktreesRoot, { recursive: true });
|
|
2042
3097
|
let worktreeId = "";
|
|
2043
3098
|
let worktreeParent = "";
|
|
2044
3099
|
let worktreeCwd = "";
|
|
2045
3100
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
2046
3101
|
const candidate = randomBytes(2).toString("hex");
|
|
2047
|
-
const parent =
|
|
3102
|
+
const parent = join3(worktreesRoot, candidate);
|
|
2048
3103
|
try {
|
|
2049
3104
|
await stat2(parent);
|
|
2050
3105
|
continue;
|
|
2051
3106
|
} catch {
|
|
2052
3107
|
worktreeId = candidate;
|
|
2053
3108
|
worktreeParent = parent;
|
|
2054
|
-
worktreeCwd =
|
|
3109
|
+
worktreeCwd = join3(parent, repoName);
|
|
2055
3110
|
break;
|
|
2056
3111
|
}
|
|
2057
3112
|
}
|
|
@@ -2075,13 +3130,106 @@ function createCodexBridgeMiddleware() {
|
|
|
2075
3130
|
}
|
|
2076
3131
|
});
|
|
2077
3132
|
} catch (error) {
|
|
2078
|
-
setJson2(res, 500, { error:
|
|
3133
|
+
setJson2(res, 500, { error: getErrorMessage3(error, "Failed to create worktree") });
|
|
3134
|
+
}
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
if (req.method === "POST" && url.pathname === "/codex-api/worktree/auto-commit") {
|
|
3138
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
3139
|
+
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
3140
|
+
const commitMessage = normalizeCommitMessage(payload?.message);
|
|
3141
|
+
if (!rawCwd) {
|
|
3142
|
+
setJson2(res, 400, { error: "Missing cwd" });
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
if (!commitMessage) {
|
|
3146
|
+
setJson2(res, 400, { error: "Missing message" });
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
3150
|
+
try {
|
|
3151
|
+
const cwdInfo = await stat2(cwd);
|
|
3152
|
+
if (!cwdInfo.isDirectory()) {
|
|
3153
|
+
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
} catch {
|
|
3157
|
+
setJson2(res, 404, { error: "cwd does not exist" });
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
try {
|
|
3161
|
+
await ensureRollbackGitRepo(cwd);
|
|
3162
|
+
const beforeStatus = await runRollbackGitWithOutput(cwd, ["status", "--porcelain"]);
|
|
3163
|
+
if (!beforeStatus.trim()) {
|
|
3164
|
+
setJson2(res, 200, { data: { committed: false } });
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
await runRollbackGit(cwd, ["add", "-A"]);
|
|
3168
|
+
const stagedStatus = await runRollbackGitWithOutput(cwd, ["diff", "--cached", "--name-only"]);
|
|
3169
|
+
if (!stagedStatus.trim()) {
|
|
3170
|
+
setJson2(res, 200, { data: { committed: false } });
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
await runRollbackGit(cwd, ["commit", "-m", commitMessage]);
|
|
3174
|
+
setJson2(res, 200, { data: { committed: true } });
|
|
3175
|
+
} catch (error) {
|
|
3176
|
+
setJson2(res, 500, { error: getErrorMessage3(error, "Failed to auto-commit rollback changes") });
|
|
3177
|
+
}
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
if (req.method === "POST" && url.pathname === "/codex-api/worktree/rollback-to-message") {
|
|
3181
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
3182
|
+
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
3183
|
+
const commitMessage = normalizeCommitMessage(payload?.message);
|
|
3184
|
+
if (!rawCwd) {
|
|
3185
|
+
setJson2(res, 400, { error: "Missing cwd" });
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
if (!commitMessage) {
|
|
3189
|
+
setJson2(res, 400, { error: "Missing message" });
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
3193
|
+
try {
|
|
3194
|
+
const cwdInfo = await stat2(cwd);
|
|
3195
|
+
if (!cwdInfo.isDirectory()) {
|
|
3196
|
+
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
} catch {
|
|
3200
|
+
setJson2(res, 404, { error: "cwd does not exist" });
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
try {
|
|
3204
|
+
await ensureRollbackGitRepo(cwd);
|
|
3205
|
+
const commitSha = await findRollbackCommitByExactMessage(cwd, commitMessage);
|
|
3206
|
+
if (!commitSha) {
|
|
3207
|
+
setJson2(res, 404, { error: "No matching commit found for this user message" });
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
let resetTargetSha = "";
|
|
3211
|
+
try {
|
|
3212
|
+
resetTargetSha = await runRollbackGitCapture(cwd, ["rev-parse", `${commitSha}^`]);
|
|
3213
|
+
} catch {
|
|
3214
|
+
setJson2(res, 409, { error: "Cannot rollback: matched commit has no parent commit" });
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
let stashed = false;
|
|
3218
|
+
if (await hasRollbackGitWorkingTreeChanges(cwd)) {
|
|
3219
|
+
const stashMessage = `codex-auto-stash-before-rollback-${Date.now()}`;
|
|
3220
|
+
await runRollbackGit(cwd, ["stash", "push", "-u", "-m", stashMessage]);
|
|
3221
|
+
stashed = true;
|
|
3222
|
+
}
|
|
3223
|
+
await runRollbackGit(cwd, ["reset", "--hard", resetTargetSha]);
|
|
3224
|
+
setJson2(res, 200, { data: { reset: true, commitSha, resetTargetSha, stashed } });
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
setJson2(res, 500, { error: getErrorMessage3(error, "Failed to rollback project to user message commit") });
|
|
2079
3227
|
}
|
|
2080
3228
|
return;
|
|
2081
3229
|
}
|
|
2082
3230
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
2083
3231
|
const payload = await readJsonBody(req);
|
|
2084
|
-
const record =
|
|
3232
|
+
const record = asRecord3(payload);
|
|
2085
3233
|
if (!record) {
|
|
2086
3234
|
setJson2(res, 400, { error: "Invalid body: expected object" });
|
|
2087
3235
|
return;
|
|
@@ -2096,7 +3244,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2096
3244
|
return;
|
|
2097
3245
|
}
|
|
2098
3246
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
2099
|
-
const payload =
|
|
3247
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2100
3248
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
2101
3249
|
const createIfMissing = payload?.createIfMissing === true;
|
|
2102
3250
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
@@ -2156,7 +3304,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2156
3304
|
let index = 1;
|
|
2157
3305
|
while (index < 1e5) {
|
|
2158
3306
|
const candidateName = `New Project (${String(index)})`;
|
|
2159
|
-
const candidatePath =
|
|
3307
|
+
const candidatePath = join3(normalizedBasePath, candidateName);
|
|
2160
3308
|
try {
|
|
2161
3309
|
await stat2(candidatePath);
|
|
2162
3310
|
index += 1;
|
|
@@ -2170,7 +3318,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2170
3318
|
return;
|
|
2171
3319
|
}
|
|
2172
3320
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
2173
|
-
const payload =
|
|
3321
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2174
3322
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
2175
3323
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
2176
3324
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
@@ -2195,17 +3343,17 @@ function createCodexBridgeMiddleware() {
|
|
|
2195
3343
|
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
3344
|
setJson2(res, 200, { data: scored });
|
|
2197
3345
|
} catch (error) {
|
|
2198
|
-
setJson2(res, 500, { error:
|
|
3346
|
+
setJson2(res, 500, { error: getErrorMessage3(error, "Failed to search files") });
|
|
2199
3347
|
}
|
|
2200
3348
|
return;
|
|
2201
3349
|
}
|
|
2202
3350
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
2203
|
-
const cache = await
|
|
3351
|
+
const cache = await readMergedThreadTitleCache();
|
|
2204
3352
|
setJson2(res, 200, { data: cache });
|
|
2205
3353
|
return;
|
|
2206
3354
|
}
|
|
2207
3355
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
2208
|
-
const payload =
|
|
3356
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2209
3357
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
2210
3358
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
2211
3359
|
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
@@ -2219,7 +3367,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2219
3367
|
return;
|
|
2220
3368
|
}
|
|
2221
3369
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
2222
|
-
const payload =
|
|
3370
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2223
3371
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
2224
3372
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
2225
3373
|
if (!id) {
|
|
@@ -2232,6 +3380,23 @@ function createCodexBridgeMiddleware() {
|
|
|
2232
3380
|
setJson2(res, 200, { ok: true });
|
|
2233
3381
|
return;
|
|
2234
3382
|
}
|
|
3383
|
+
if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
|
|
3384
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
3385
|
+
const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
|
|
3386
|
+
if (!botToken) {
|
|
3387
|
+
setJson2(res, 400, { error: "Missing botToken" });
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
telegramBridge.configureToken(botToken);
|
|
3391
|
+
telegramBridge.start();
|
|
3392
|
+
await writeTelegramBridgeConfig({ botToken });
|
|
3393
|
+
setJson2(res, 200, { ok: true });
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
if (req.method === "GET" && url.pathname === "/codex-api/telegram/status") {
|
|
3397
|
+
setJson2(res, 200, { data: telegramBridge.getStatus() });
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
2235
3400
|
if (req.method === "GET" && url.pathname === "/codex-api/events") {
|
|
2236
3401
|
res.statusCode = 200;
|
|
2237
3402
|
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
@@ -2264,12 +3429,13 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
2264
3429
|
}
|
|
2265
3430
|
next();
|
|
2266
3431
|
} catch (error) {
|
|
2267
|
-
const message =
|
|
3432
|
+
const message = getErrorMessage3(error, "Unknown bridge error");
|
|
2268
3433
|
setJson2(res, 502, { error: message });
|
|
2269
3434
|
}
|
|
2270
3435
|
};
|
|
2271
3436
|
middleware.dispose = () => {
|
|
2272
3437
|
threadSearchIndex = null;
|
|
3438
|
+
telegramBridge.stop();
|
|
2273
3439
|
appServer.dispose();
|
|
2274
3440
|
};
|
|
2275
3441
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -2402,7 +3568,7 @@ function createAuthSession(password) {
|
|
|
2402
3568
|
}
|
|
2403
3569
|
|
|
2404
3570
|
// src/server/localBrowseUi.ts
|
|
2405
|
-
import { dirname, extname, join as
|
|
3571
|
+
import { dirname as dirname2, extname as extname2, join as join4 } from "path";
|
|
2406
3572
|
import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
2407
3573
|
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2408
3574
|
".txt",
|
|
@@ -2433,7 +3599,7 @@ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2433
3599
|
".ps1"
|
|
2434
3600
|
]);
|
|
2435
3601
|
function languageForPath(pathValue) {
|
|
2436
|
-
const extension =
|
|
3602
|
+
const extension = extname2(pathValue).toLowerCase();
|
|
2437
3603
|
switch (extension) {
|
|
2438
3604
|
case ".js":
|
|
2439
3605
|
return "javascript";
|
|
@@ -2494,7 +3660,7 @@ function decodeBrowsePath(rawPath) {
|
|
|
2494
3660
|
}
|
|
2495
3661
|
}
|
|
2496
3662
|
function isTextEditablePath(pathValue) {
|
|
2497
|
-
return TEXT_EDITABLE_EXTENSIONS.has(
|
|
3663
|
+
return TEXT_EDITABLE_EXTENSIONS.has(extname2(pathValue).toLowerCase());
|
|
2498
3664
|
}
|
|
2499
3665
|
function looksLikeTextBuffer(buffer) {
|
|
2500
3666
|
if (buffer.length === 0) return true;
|
|
@@ -2540,7 +3706,7 @@ function escapeForInlineScriptString(value) {
|
|
|
2540
3706
|
async function getDirectoryItems(localPath) {
|
|
2541
3707
|
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
2542
3708
|
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
2543
|
-
const entryPath =
|
|
3709
|
+
const entryPath = join4(localPath, entry.name);
|
|
2544
3710
|
const entryStat = await stat3(entryPath);
|
|
2545
3711
|
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
2546
3712
|
return {
|
|
@@ -2560,13 +3726,13 @@ async function getDirectoryItems(localPath) {
|
|
|
2560
3726
|
}
|
|
2561
3727
|
async function createDirectoryListingHtml(localPath) {
|
|
2562
3728
|
const items = await getDirectoryItems(localPath);
|
|
2563
|
-
const parentPath =
|
|
3729
|
+
const parentPath = dirname2(localPath);
|
|
2564
3730
|
const rows = items.map((item) => {
|
|
2565
3731
|
const suffix = item.isDirectory ? "/" : "";
|
|
2566
3732
|
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>`;
|
|
3733
|
+
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
3734
|
}).join("\n");
|
|
2569
|
-
const parentLink = localPath !== parentPath ? `<
|
|
3735
|
+
const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
|
|
2570
3736
|
return `<!doctype html>
|
|
2571
3737
|
<html lang="en">
|
|
2572
3738
|
<head>
|
|
@@ -2580,8 +3746,27 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
2580
3746
|
ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
2581
3747
|
.file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
|
|
2582
3748
|
.file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
|
|
2583
|
-
.
|
|
3749
|
+
.header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
|
|
3750
|
+
.header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
|
|
3751
|
+
.header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
|
|
3752
|
+
.header-open-btn {
|
|
3753
|
+
height: 42px;
|
|
3754
|
+
padding: 0 14px;
|
|
3755
|
+
border: 1px solid #4f8de0;
|
|
3756
|
+
border-radius: 10px;
|
|
3757
|
+
background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
|
|
3758
|
+
color: #eef6ff;
|
|
3759
|
+
font-weight: 700;
|
|
3760
|
+
letter-spacing: 0.01em;
|
|
3761
|
+
cursor: pointer;
|
|
3762
|
+
box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
|
|
3763
|
+
}
|
|
3764
|
+
.header-open-btn:hover { filter: brightness(1.08); }
|
|
3765
|
+
.header-open-btn:disabled { opacity: 0.6; cursor: default; }
|
|
3766
|
+
.row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
|
|
3767
|
+
.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
3768
|
.icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
|
|
3769
|
+
.status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
|
|
2585
3770
|
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
2586
3771
|
@media (max-width: 640px) {
|
|
2587
3772
|
body { margin: 12px; }
|
|
@@ -2593,14 +3778,52 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
2593
3778
|
</head>
|
|
2594
3779
|
<body>
|
|
2595
3780
|
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
2596
|
-
|
|
3781
|
+
<div class="header-actions">
|
|
3782
|
+
${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
|
|
3783
|
+
<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>
|
|
3784
|
+
</div>
|
|
3785
|
+
<p id="status" class="status"></p>
|
|
2597
3786
|
<ul>${rows}</ul>
|
|
3787
|
+
<script>
|
|
3788
|
+
const status = document.getElementById('status');
|
|
3789
|
+
document.addEventListener('click', async (event) => {
|
|
3790
|
+
const target = event.target;
|
|
3791
|
+
if (!(target instanceof Element)) return;
|
|
3792
|
+
const button = target.closest('.open-folder-btn');
|
|
3793
|
+
if (!(button instanceof HTMLButtonElement)) return;
|
|
3794
|
+
|
|
3795
|
+
const path = button.getAttribute('data-path') || '';
|
|
3796
|
+
if (!path) return;
|
|
3797
|
+
button.disabled = true;
|
|
3798
|
+
status.textContent = 'Opening folder in Codex...';
|
|
3799
|
+
try {
|
|
3800
|
+
const response = await fetch('/codex-api/project-root', {
|
|
3801
|
+
method: 'POST',
|
|
3802
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3803
|
+
body: JSON.stringify({
|
|
3804
|
+
path,
|
|
3805
|
+
createIfMissing: false,
|
|
3806
|
+
label: '',
|
|
3807
|
+
}),
|
|
3808
|
+
});
|
|
3809
|
+
if (!response.ok) {
|
|
3810
|
+
status.textContent = 'Failed to open folder.';
|
|
3811
|
+
button.disabled = false;
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3814
|
+
window.location.assign('/#/');
|
|
3815
|
+
} catch {
|
|
3816
|
+
status.textContent = 'Failed to open folder.';
|
|
3817
|
+
button.disabled = false;
|
|
3818
|
+
}
|
|
3819
|
+
});
|
|
3820
|
+
</script>
|
|
2598
3821
|
</body>
|
|
2599
3822
|
</html>`;
|
|
2600
3823
|
}
|
|
2601
3824
|
async function createTextEditorHtml(localPath) {
|
|
2602
3825
|
const content = await readFile3(localPath, "utf8");
|
|
2603
|
-
const parentPath =
|
|
3826
|
+
const parentPath = dirname2(localPath);
|
|
2604
3827
|
const language = languageForPath(localPath);
|
|
2605
3828
|
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
2606
3829
|
return `<!doctype html>
|
|
@@ -2669,9 +3892,9 @@ async function createTextEditorHtml(localPath) {
|
|
|
2669
3892
|
|
|
2670
3893
|
// src/server/httpServer.ts
|
|
2671
3894
|
import { WebSocketServer } from "ws";
|
|
2672
|
-
var __dirname =
|
|
2673
|
-
var distDir =
|
|
2674
|
-
var spaEntryFile =
|
|
3895
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
3896
|
+
var distDir = join5(__dirname, "..", "dist");
|
|
3897
|
+
var spaEntryFile = join5(distDir, "index.html");
|
|
2675
3898
|
var IMAGE_CONTENT_TYPES = {
|
|
2676
3899
|
".avif": "image/avif",
|
|
2677
3900
|
".bmp": "image/bmp",
|
|
@@ -2728,7 +3951,7 @@ function createServer(options = {}) {
|
|
|
2728
3951
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2729
3952
|
return;
|
|
2730
3953
|
}
|
|
2731
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
3954
|
+
const contentType = IMAGE_CONTENT_TYPES[extname3(localPath).toLowerCase()];
|
|
2732
3955
|
if (!contentType) {
|
|
2733
3956
|
res.status(415).json({ error: "Unsupported image type." });
|
|
2734
3957
|
return;
|
|
@@ -2815,7 +4038,7 @@ function createServer(options = {}) {
|
|
|
2815
4038
|
res.status(404).json({ error: "File not found." });
|
|
2816
4039
|
}
|
|
2817
4040
|
});
|
|
2818
|
-
const hasFrontendAssets =
|
|
4041
|
+
const hasFrontendAssets = existsSync4(spaEntryFile);
|
|
2819
4042
|
if (hasFrontendAssets) {
|
|
2820
4043
|
app.use(express.static(distDir));
|
|
2821
4044
|
}
|
|
@@ -2885,10 +4108,26 @@ function generatePassword() {
|
|
|
2885
4108
|
|
|
2886
4109
|
// src/cli/index.ts
|
|
2887
4110
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
2888
|
-
var __dirname2 =
|
|
4111
|
+
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
4112
|
+
var hasPromptedCloudflaredInstall = false;
|
|
4113
|
+
function getCodexHomePath() {
|
|
4114
|
+
return process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
|
|
4115
|
+
}
|
|
4116
|
+
function getCloudflaredPromptMarkerPath() {
|
|
4117
|
+
return join6(getCodexHomePath(), ".cloudflared-install-prompted");
|
|
4118
|
+
}
|
|
4119
|
+
function hasPromptedCloudflaredInstallPersisted() {
|
|
4120
|
+
return existsSync5(getCloudflaredPromptMarkerPath());
|
|
4121
|
+
}
|
|
4122
|
+
async function persistCloudflaredInstallPrompted() {
|
|
4123
|
+
const codexHome = getCodexHomePath();
|
|
4124
|
+
mkdirSync(codexHome, { recursive: true });
|
|
4125
|
+
await writeFile4(getCloudflaredPromptMarkerPath(), `${Date.now()}
|
|
4126
|
+
`, "utf8");
|
|
4127
|
+
}
|
|
2889
4128
|
async function readCliVersion() {
|
|
2890
4129
|
try {
|
|
2891
|
-
const packageJsonPath =
|
|
4130
|
+
const packageJsonPath = join6(__dirname2, "..", "package.json");
|
|
2892
4131
|
const raw = await readFile4(packageJsonPath, "utf8");
|
|
2893
4132
|
const parsed = JSON.parse(raw);
|
|
2894
4133
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
@@ -2899,47 +4138,22 @@ async function readCliVersion() {
|
|
|
2899
4138
|
function isTermuxRuntime() {
|
|
2900
4139
|
return Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("/com.termux/"));
|
|
2901
4140
|
}
|
|
2902
|
-
function canRun(command, args = []) {
|
|
2903
|
-
const result = spawnSync(command, args, { stdio: "ignore" });
|
|
2904
|
-
return result.status === 0;
|
|
2905
|
-
}
|
|
2906
4141
|
function runOrFail(command, args, label) {
|
|
2907
|
-
const result =
|
|
4142
|
+
const result = spawnSyncCommand(command, args, { stdio: "inherit" });
|
|
2908
4143
|
if (result.status !== 0) {
|
|
2909
4144
|
throw new Error(`${label} failed with exit code ${String(result.status ?? -1)}`);
|
|
2910
4145
|
}
|
|
2911
4146
|
}
|
|
2912
4147
|
function runWithStatus(command, args) {
|
|
2913
|
-
const result =
|
|
4148
|
+
const result = spawnSyncCommand(command, args, { stdio: "inherit" });
|
|
2914
4149
|
return result.status ?? -1;
|
|
2915
4150
|
}
|
|
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
4151
|
function resolveCloudflaredCommand() {
|
|
2938
|
-
if (
|
|
4152
|
+
if (canRunCommand("cloudflared", ["--version"])) {
|
|
2939
4153
|
return "cloudflared";
|
|
2940
4154
|
}
|
|
2941
|
-
const localCandidate =
|
|
2942
|
-
if (
|
|
4155
|
+
const localCandidate = join6(homedir4(), ".local", "bin", "cloudflared");
|
|
4156
|
+
if (existsSync5(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
|
|
2943
4157
|
return localCandidate;
|
|
2944
4158
|
}
|
|
2945
4159
|
return null;
|
|
@@ -2992,14 +4206,14 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
2992
4206
|
if (!mappedArch) {
|
|
2993
4207
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
2994
4208
|
}
|
|
2995
|
-
const userBinDir =
|
|
4209
|
+
const userBinDir = join6(homedir4(), ".local", "bin");
|
|
2996
4210
|
mkdirSync(userBinDir, { recursive: true });
|
|
2997
|
-
const destination =
|
|
4211
|
+
const destination = join6(userBinDir, "cloudflared");
|
|
2998
4212
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
2999
4213
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
3000
4214
|
await downloadFile(downloadUrl, destination);
|
|
3001
4215
|
chmodSync(destination, 493);
|
|
3002
|
-
process.env.PATH =
|
|
4216
|
+
process.env.PATH = prependPathEntry(process.env.PATH ?? "", userBinDir);
|
|
3003
4217
|
const installed = resolveCloudflaredCommand();
|
|
3004
4218
|
if (!installed) {
|
|
3005
4219
|
throw new Error("cloudflared download completed but executable is still not available");
|
|
@@ -3008,11 +4222,19 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
3008
4222
|
return installed;
|
|
3009
4223
|
}
|
|
3010
4224
|
async function shouldInstallCloudflaredInteractively() {
|
|
4225
|
+
if (hasPromptedCloudflaredInstall || hasPromptedCloudflaredInstallPersisted()) {
|
|
4226
|
+
return false;
|
|
4227
|
+
}
|
|
4228
|
+
hasPromptedCloudflaredInstall = true;
|
|
4229
|
+
await persistCloudflaredInstallPrompted();
|
|
4230
|
+
if (process.platform === "win32") {
|
|
4231
|
+
return false;
|
|
4232
|
+
}
|
|
3011
4233
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3012
4234
|
console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
|
|
3013
4235
|
return false;
|
|
3014
4236
|
}
|
|
3015
|
-
const prompt =
|
|
4237
|
+
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
3016
4238
|
try {
|
|
3017
4239
|
const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
|
|
3018
4240
|
const normalized = answer.trim().toLowerCase();
|
|
@@ -3026,6 +4248,9 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3026
4248
|
if (current) {
|
|
3027
4249
|
return current;
|
|
3028
4250
|
}
|
|
4251
|
+
if (process.platform === "win32") {
|
|
4252
|
+
return null;
|
|
4253
|
+
}
|
|
3029
4254
|
const installApproved = await shouldInstallCloudflaredInteractively();
|
|
3030
4255
|
if (!installApproved) {
|
|
3031
4256
|
return null;
|
|
@@ -3033,8 +4258,8 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3033
4258
|
return ensureCloudflaredInstalledLinux();
|
|
3034
4259
|
}
|
|
3035
4260
|
function hasCodexAuth() {
|
|
3036
|
-
const codexHome =
|
|
3037
|
-
return
|
|
4261
|
+
const codexHome = getCodexHomePath();
|
|
4262
|
+
return existsSync5(join6(codexHome, "auth.json"));
|
|
3038
4263
|
}
|
|
3039
4264
|
function ensureCodexInstalled() {
|
|
3040
4265
|
let codexCommand = resolveCodexCommand();
|
|
@@ -3052,7 +4277,7 @@ function ensureCodexInstalled() {
|
|
|
3052
4277
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
3053
4278
|
`);
|
|
3054
4279
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
3055
|
-
process.env.PATH =
|
|
4280
|
+
process.env.PATH = prependPathEntry(process.env.PATH ?? "", getNpmGlobalBinDir(userPrefix));
|
|
3056
4281
|
};
|
|
3057
4282
|
if (isTermuxRuntime()) {
|
|
3058
4283
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -3115,19 +4340,22 @@ function parseCloudflaredUrl(chunk) {
|
|
|
3115
4340
|
}
|
|
3116
4341
|
function getAccessibleUrls(port) {
|
|
3117
4342
|
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
}
|
|
3123
|
-
for (const entry of entries) {
|
|
3124
|
-
if (entry.internal) {
|
|
4343
|
+
try {
|
|
4344
|
+
const interfaces = networkInterfaces();
|
|
4345
|
+
for (const entries of Object.values(interfaces)) {
|
|
4346
|
+
if (!entries) {
|
|
3125
4347
|
continue;
|
|
3126
4348
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
4349
|
+
for (const entry of entries) {
|
|
4350
|
+
if (entry.internal) {
|
|
4351
|
+
continue;
|
|
4352
|
+
}
|
|
4353
|
+
if (entry.family === "IPv4") {
|
|
4354
|
+
urls.add(`http://${entry.address}:${String(port)}`);
|
|
4355
|
+
}
|
|
3129
4356
|
}
|
|
3130
4357
|
}
|
|
4358
|
+
} catch {
|
|
3131
4359
|
}
|
|
3132
4360
|
return Array.from(urls);
|
|
3133
4361
|
}
|
|
@@ -3190,8 +4418,8 @@ function listenWithFallback(server, startPort) {
|
|
|
3190
4418
|
});
|
|
3191
4419
|
}
|
|
3192
4420
|
function getCodexGlobalStatePath2() {
|
|
3193
|
-
const codexHome =
|
|
3194
|
-
return
|
|
4421
|
+
const codexHome = getCodexHomePath();
|
|
4422
|
+
return join6(codexHome, ".codex-global-state.json");
|
|
3195
4423
|
}
|
|
3196
4424
|
function normalizeUniqueStrings(value) {
|
|
3197
4425
|
if (!Array.isArray(value)) return [];
|
|
@@ -3256,6 +4484,9 @@ async function startServer(options) {
|
|
|
3256
4484
|
}
|
|
3257
4485
|
}
|
|
3258
4486
|
const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
|
|
4487
|
+
if (codexCommand) {
|
|
4488
|
+
process.env.CODEXUI_CODEX_COMMAND = codexCommand;
|
|
4489
|
+
}
|
|
3259
4490
|
if (!hasCodexAuth() && codexCommand) {
|
|
3260
4491
|
console.log("\nCodex is not logged in. Starting `codex login`...\n");
|
|
3261
4492
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
@@ -3315,7 +4546,7 @@ async function startServer(options) {
|
|
|
3315
4546
|
qrcode.generate(tunnelUrl, { small: true });
|
|
3316
4547
|
console.log("");
|
|
3317
4548
|
}
|
|
3318
|
-
openBrowser(`http://localhost:${String(port)}`);
|
|
4549
|
+
if (options.open) openBrowser(`http://localhost:${String(port)}`);
|
|
3319
4550
|
function shutdown() {
|
|
3320
4551
|
console.log("\nShutting down...");
|
|
3321
4552
|
if (tunnelChild && !tunnelChild.killed) {
|
|
@@ -3335,10 +4566,11 @@ async function startServer(options) {
|
|
|
3335
4566
|
}
|
|
3336
4567
|
async function runLogin() {
|
|
3337
4568
|
const codexCommand = ensureCodexInstalled() ?? "codex";
|
|
4569
|
+
process.env.CODEXUI_CODEX_COMMAND = codexCommand;
|
|
3338
4570
|
console.log("\nStarting `codex login`...\n");
|
|
3339
4571
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
3340
4572
|
}
|
|
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) => {
|
|
4573
|
+
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
4574
|
const rawArgv = process.argv.slice(2);
|
|
3343
4575
|
const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
|
|
3344
4576
|
let openProjectOnly = (opts.openProject ?? "").trim();
|