codexapp 0.1.4

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.
@@ -0,0 +1,1414 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { createServer as createServer2 } from "http";
5
+ import { existsSync } from "fs";
6
+ import { readFile as readFile2 } from "fs/promises";
7
+ import { homedir as homedir2 } from "os";
8
+ import { join as join3 } from "path";
9
+ import { spawn as spawn2, spawnSync } from "child_process";
10
+ import { fileURLToPath as fileURLToPath2 } from "url";
11
+ import { dirname as dirname2 } from "path";
12
+ import { Command } from "commander";
13
+
14
+ // src/server/httpServer.ts
15
+ import { fileURLToPath } from "url";
16
+ import { dirname, extname, isAbsolute as isAbsolute2, join as join2 } from "path";
17
+ import express from "express";
18
+
19
+ // src/server/codexAppServerBridge.ts
20
+ import { spawn } from "child_process";
21
+ import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
22
+ import { request as httpsRequest } from "https";
23
+ import { homedir } from "os";
24
+ import { tmpdir } from "os";
25
+ import { isAbsolute, join, resolve } from "path";
26
+ import { writeFile } from "fs/promises";
27
+ function asRecord(value) {
28
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
29
+ }
30
+ function getErrorMessage(payload, fallback) {
31
+ if (payload instanceof Error && payload.message.trim().length > 0) {
32
+ return payload.message;
33
+ }
34
+ const record = asRecord(payload);
35
+ if (!record) return fallback;
36
+ const error = record.error;
37
+ if (typeof error === "string" && error.length > 0) return error;
38
+ const nestedError = asRecord(error);
39
+ if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
40
+ return nestedError.message;
41
+ }
42
+ return fallback;
43
+ }
44
+ function setJson(res, statusCode, payload) {
45
+ res.statusCode = statusCode;
46
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
47
+ res.end(JSON.stringify(payload));
48
+ }
49
+ function getCodexHomeDir() {
50
+ const codexHome = process.env.CODEX_HOME?.trim();
51
+ return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
52
+ }
53
+ function getSkillsInstallDir() {
54
+ return join(getCodexHomeDir(), "skills");
55
+ }
56
+ async function runCommand(command, args, options = {}) {
57
+ await new Promise((resolve2, reject) => {
58
+ const proc = spawn(command, args, {
59
+ cwd: options.cwd,
60
+ env: process.env,
61
+ stdio: ["ignore", "pipe", "pipe"]
62
+ });
63
+ let stdout = "";
64
+ let stderr = "";
65
+ proc.stdout.on("data", (chunk) => {
66
+ stdout += chunk.toString();
67
+ });
68
+ proc.stderr.on("data", (chunk) => {
69
+ stderr += chunk.toString();
70
+ });
71
+ proc.on("error", reject);
72
+ proc.on("close", (code) => {
73
+ if (code === 0) {
74
+ resolve2();
75
+ return;
76
+ }
77
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
78
+ const suffix = details.length > 0 ? `: ${details}` : "";
79
+ reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
80
+ });
81
+ });
82
+ }
83
+ async function detectUserSkillsDir(appServer) {
84
+ try {
85
+ const result = await appServer.rpc("skills/list", {});
86
+ for (const entry of result.data ?? []) {
87
+ for (const skill of entry.skills ?? []) {
88
+ if (skill.scope !== "user" || !skill.path) continue;
89
+ const parts = skill.path.split("/").filter(Boolean);
90
+ if (parts.length < 2) continue;
91
+ return `/${parts.slice(0, -2).join("/")}`;
92
+ }
93
+ }
94
+ } catch {
95
+ }
96
+ return getSkillsInstallDir();
97
+ }
98
+ async function ensureInstalledSkillIsValid(appServer, skillPath) {
99
+ const result = await appServer.rpc("skills/list", { forceReload: true });
100
+ const normalized = skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md`;
101
+ for (const entry of result.data ?? []) {
102
+ for (const error of entry.errors ?? []) {
103
+ if (error.path === normalized) {
104
+ throw new Error(error.message || "Installed skill is invalid");
105
+ }
106
+ }
107
+ }
108
+ }
109
+ var TREE_CACHE_TTL_MS = 5 * 60 * 1e3;
110
+ var skillsTreeCache = null;
111
+ var metaCache = /* @__PURE__ */ new Map();
112
+ async function getGhToken() {
113
+ try {
114
+ const proc = spawn("gh", ["auth", "token"], { stdio: ["ignore", "pipe", "ignore"] });
115
+ let out = "";
116
+ proc.stdout.on("data", (d) => {
117
+ out += d.toString();
118
+ });
119
+ return new Promise((resolve2) => {
120
+ proc.on("close", (code) => resolve2(code === 0 ? out.trim() : null));
121
+ proc.on("error", () => resolve2(null));
122
+ });
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+ async function ghFetch(url) {
128
+ const token = await getGhToken();
129
+ const headers = {
130
+ Accept: "application/vnd.github+json",
131
+ "User-Agent": "codex-web-local"
132
+ };
133
+ if (token) headers.Authorization = `Bearer ${token}`;
134
+ return fetch(url, { headers });
135
+ }
136
+ async function fetchSkillsTree() {
137
+ if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
138
+ return skillsTreeCache.entries;
139
+ }
140
+ const resp = await ghFetch("https://api.github.com/repos/openclaw/skills/git/trees/main?recursive=1");
141
+ if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
142
+ const data = await resp.json();
143
+ const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
144
+ const seen = /* @__PURE__ */ new Set();
145
+ const entries = [];
146
+ for (const node of data.tree ?? []) {
147
+ const match = metaPattern.exec(node.path);
148
+ if (!match) continue;
149
+ const [, owner, skillName] = match;
150
+ const key = `${owner}/${skillName}`;
151
+ if (seen.has(key)) continue;
152
+ seen.add(key);
153
+ entries.push({
154
+ name: skillName,
155
+ owner,
156
+ url: `https://github.com/openclaw/skills/tree/main/skills/${owner}/${skillName}`
157
+ });
158
+ }
159
+ skillsTreeCache = { entries, fetchedAt: Date.now() };
160
+ return entries;
161
+ }
162
+ async function fetchMetaBatch(entries) {
163
+ const toFetch = entries.filter((e) => !metaCache.has(`${e.owner}/${e.name}`));
164
+ if (toFetch.length === 0) return;
165
+ const batch = toFetch.slice(0, 50);
166
+ const results = await Promise.allSettled(
167
+ batch.map(async (e) => {
168
+ const rawUrl = `https://raw.githubusercontent.com/openclaw/skills/main/skills/${e.owner}/${e.name}/_meta.json`;
169
+ const resp = await fetch(rawUrl);
170
+ if (!resp.ok) return;
171
+ const meta = await resp.json();
172
+ metaCache.set(`${e.owner}/${e.name}`, {
173
+ displayName: typeof meta.displayName === "string" ? meta.displayName : "",
174
+ description: typeof meta.displayName === "string" ? meta.displayName : "",
175
+ publishedAt: meta.latest?.publishedAt ?? 0
176
+ });
177
+ })
178
+ );
179
+ void results;
180
+ }
181
+ function buildHubEntry(e) {
182
+ const cached = metaCache.get(`${e.owner}/${e.name}`);
183
+ return {
184
+ name: e.name,
185
+ owner: e.owner,
186
+ description: cached?.description ?? "",
187
+ displayName: cached?.displayName ?? "",
188
+ publishedAt: cached?.publishedAt ?? 0,
189
+ avatarUrl: `https://github.com/${e.owner}.png?size=40`,
190
+ url: e.url,
191
+ installed: false
192
+ };
193
+ }
194
+ async function scanInstalledSkillsFromDisk() {
195
+ const map = /* @__PURE__ */ new Map();
196
+ const skillsDir = getSkillsInstallDir();
197
+ try {
198
+ const entries = await readdir(skillsDir, { withFileTypes: true });
199
+ for (const entry of entries) {
200
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
201
+ const skillMd = join(skillsDir, entry.name, "SKILL.md");
202
+ try {
203
+ await stat(skillMd);
204
+ map.set(entry.name, { name: entry.name, path: skillMd, enabled: true });
205
+ } catch {
206
+ }
207
+ }
208
+ } catch {
209
+ }
210
+ return map;
211
+ }
212
+ async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
213
+ const q = query.toLowerCase().trim();
214
+ let filtered = q ? allEntries.filter((s) => {
215
+ if (s.name.toLowerCase().includes(q) || s.owner.toLowerCase().includes(q)) return true;
216
+ const cached = metaCache.get(`${s.owner}/${s.name}`);
217
+ if (cached?.displayName?.toLowerCase().includes(q)) return true;
218
+ return false;
219
+ }) : allEntries;
220
+ const page = filtered.slice(0, Math.min(limit * 2, 200));
221
+ await fetchMetaBatch(page);
222
+ let results = page.map(buildHubEntry);
223
+ if (sort === "date") {
224
+ results.sort((a, b) => b.publishedAt - a.publishedAt);
225
+ } else if (q) {
226
+ results.sort((a, b) => {
227
+ const aExact = a.name.toLowerCase() === q ? 1 : 0;
228
+ const bExact = b.name.toLowerCase() === q ? 1 : 0;
229
+ if (aExact !== bExact) return bExact - aExact;
230
+ return b.publishedAt - a.publishedAt;
231
+ });
232
+ }
233
+ return results.slice(0, limit).map((s) => {
234
+ const local = installedMap.get(s.name);
235
+ return local ? { ...s, installed: true, path: local.path, enabled: local.enabled } : s;
236
+ });
237
+ }
238
+ function normalizeStringArray(value) {
239
+ if (!Array.isArray(value)) return [];
240
+ const normalized = [];
241
+ for (const item of value) {
242
+ if (typeof item === "string" && item.length > 0 && !normalized.includes(item)) {
243
+ normalized.push(item);
244
+ }
245
+ }
246
+ return normalized;
247
+ }
248
+ function normalizeStringRecord(value) {
249
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
250
+ const next = {};
251
+ for (const [key, item] of Object.entries(value)) {
252
+ if (typeof key === "string" && key.length > 0 && typeof item === "string") {
253
+ next[key] = item;
254
+ }
255
+ }
256
+ return next;
257
+ }
258
+ function getCodexAuthPath() {
259
+ return join(getCodexHomeDir(), "auth.json");
260
+ }
261
+ async function readCodexAuth() {
262
+ try {
263
+ const raw = await readFile(getCodexAuthPath(), "utf8");
264
+ const auth = JSON.parse(raw);
265
+ const token = auth.tokens?.access_token;
266
+ if (!token) return null;
267
+ return { accessToken: token, accountId: auth.tokens?.account_id ?? void 0 };
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+ function getCodexGlobalStatePath() {
273
+ return join(getCodexHomeDir(), ".codex-global-state.json");
274
+ }
275
+ var MAX_THREAD_TITLES = 500;
276
+ function normalizeThreadTitleCache(value) {
277
+ const record = asRecord(value);
278
+ if (!record) return { titles: {}, order: [] };
279
+ const rawTitles = asRecord(record.titles);
280
+ const titles = {};
281
+ if (rawTitles) {
282
+ for (const [k, v] of Object.entries(rawTitles)) {
283
+ if (typeof v === "string" && v.length > 0) titles[k] = v;
284
+ }
285
+ }
286
+ const order = normalizeStringArray(record.order);
287
+ return { titles, order };
288
+ }
289
+ function updateThreadTitleCache(cache, id, title) {
290
+ const titles = { ...cache.titles, [id]: title };
291
+ const order = [id, ...cache.order.filter((o) => o !== id)];
292
+ while (order.length > MAX_THREAD_TITLES) {
293
+ const removed = order.pop();
294
+ if (removed) delete titles[removed];
295
+ }
296
+ return { titles, order };
297
+ }
298
+ function removeFromThreadTitleCache(cache, id) {
299
+ const { [id]: _, ...titles } = cache.titles;
300
+ return { titles, order: cache.order.filter((o) => o !== id) };
301
+ }
302
+ async function readThreadTitleCache() {
303
+ const statePath = getCodexGlobalStatePath();
304
+ try {
305
+ const raw = await readFile(statePath, "utf8");
306
+ const payload = asRecord(JSON.parse(raw)) ?? {};
307
+ return normalizeThreadTitleCache(payload["thread-titles"]);
308
+ } catch {
309
+ return { titles: {}, order: [] };
310
+ }
311
+ }
312
+ async function writeThreadTitleCache(cache) {
313
+ const statePath = getCodexGlobalStatePath();
314
+ let payload = {};
315
+ try {
316
+ const raw = await readFile(statePath, "utf8");
317
+ payload = asRecord(JSON.parse(raw)) ?? {};
318
+ } catch {
319
+ payload = {};
320
+ }
321
+ payload["thread-titles"] = cache;
322
+ await writeFile(statePath, JSON.stringify(payload), "utf8");
323
+ }
324
+ async function readWorkspaceRootsState() {
325
+ const statePath = getCodexGlobalStatePath();
326
+ let payload = {};
327
+ try {
328
+ const raw = await readFile(statePath, "utf8");
329
+ const parsed = JSON.parse(raw);
330
+ payload = asRecord(parsed) ?? {};
331
+ } catch {
332
+ payload = {};
333
+ }
334
+ return {
335
+ order: normalizeStringArray(payload["electron-saved-workspace-roots"]),
336
+ labels: normalizeStringRecord(payload["electron-workspace-root-labels"]),
337
+ active: normalizeStringArray(payload["active-workspace-roots"])
338
+ };
339
+ }
340
+ async function writeWorkspaceRootsState(nextState) {
341
+ const statePath = getCodexGlobalStatePath();
342
+ let payload = {};
343
+ try {
344
+ const raw = await readFile(statePath, "utf8");
345
+ payload = asRecord(JSON.parse(raw)) ?? {};
346
+ } catch {
347
+ payload = {};
348
+ }
349
+ payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
350
+ payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
351
+ payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
352
+ await writeFile(statePath, JSON.stringify(payload), "utf8");
353
+ }
354
+ async function readJsonBody(req) {
355
+ const raw = await readRawBody(req);
356
+ if (raw.length === 0) return null;
357
+ const text = raw.toString("utf8").trim();
358
+ if (text.length === 0) return null;
359
+ return JSON.parse(text);
360
+ }
361
+ async function readRawBody(req) {
362
+ const chunks = [];
363
+ for await (const chunk of req) {
364
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
365
+ }
366
+ return Buffer.concat(chunks);
367
+ }
368
+ async function proxyTranscribe(body, contentType, authToken, accountId) {
369
+ const headers = {
370
+ "Content-Type": contentType,
371
+ "Content-Length": body.length,
372
+ Authorization: `Bearer ${authToken}`,
373
+ originator: "Codex Desktop",
374
+ "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
375
+ };
376
+ if (accountId) {
377
+ headers["ChatGPT-Account-Id"] = accountId;
378
+ }
379
+ return new Promise((resolve2, reject) => {
380
+ const req = httpsRequest(
381
+ "https://chatgpt.com/backend-api/transcribe",
382
+ { method: "POST", headers },
383
+ (res) => {
384
+ const chunks = [];
385
+ res.on("data", (c) => chunks.push(c));
386
+ res.on("end", () => resolve2({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
387
+ res.on("error", reject);
388
+ }
389
+ );
390
+ req.on("error", reject);
391
+ req.write(body);
392
+ req.end();
393
+ });
394
+ }
395
+ var AppServerProcess = class {
396
+ constructor() {
397
+ this.process = null;
398
+ this.initialized = false;
399
+ this.initializePromise = null;
400
+ this.readBuffer = "";
401
+ this.nextId = 1;
402
+ this.stopping = false;
403
+ this.pending = /* @__PURE__ */ new Map();
404
+ this.notificationListeners = /* @__PURE__ */ new Set();
405
+ this.pendingServerRequests = /* @__PURE__ */ new Map();
406
+ }
407
+ start() {
408
+ if (this.process) return;
409
+ this.stopping = false;
410
+ const proc = spawn("codex", ["app-server"], { stdio: ["pipe", "pipe", "pipe"] });
411
+ this.process = proc;
412
+ proc.stdout.setEncoding("utf8");
413
+ proc.stdout.on("data", (chunk) => {
414
+ this.readBuffer += chunk;
415
+ let lineEnd = this.readBuffer.indexOf("\n");
416
+ while (lineEnd !== -1) {
417
+ const line = this.readBuffer.slice(0, lineEnd).trim();
418
+ this.readBuffer = this.readBuffer.slice(lineEnd + 1);
419
+ if (line.length > 0) {
420
+ this.handleLine(line);
421
+ }
422
+ lineEnd = this.readBuffer.indexOf("\n");
423
+ }
424
+ });
425
+ proc.stderr.setEncoding("utf8");
426
+ proc.stderr.on("data", () => {
427
+ });
428
+ proc.on("exit", () => {
429
+ const failure = new Error(this.stopping ? "codex app-server stopped" : "codex app-server exited unexpectedly");
430
+ for (const request of this.pending.values()) {
431
+ request.reject(failure);
432
+ }
433
+ this.pending.clear();
434
+ this.pendingServerRequests.clear();
435
+ this.process = null;
436
+ this.initialized = false;
437
+ this.initializePromise = null;
438
+ this.readBuffer = "";
439
+ });
440
+ }
441
+ sendLine(payload) {
442
+ if (!this.process) {
443
+ throw new Error("codex app-server is not running");
444
+ }
445
+ this.process.stdin.write(`${JSON.stringify(payload)}
446
+ `);
447
+ }
448
+ handleLine(line) {
449
+ let message;
450
+ try {
451
+ message = JSON.parse(line);
452
+ } catch {
453
+ return;
454
+ }
455
+ if (typeof message.id === "number" && this.pending.has(message.id)) {
456
+ const pendingRequest = this.pending.get(message.id);
457
+ this.pending.delete(message.id);
458
+ if (!pendingRequest) return;
459
+ if (message.error) {
460
+ pendingRequest.reject(new Error(message.error.message));
461
+ } else {
462
+ pendingRequest.resolve(message.result);
463
+ }
464
+ return;
465
+ }
466
+ if (typeof message.method === "string" && typeof message.id !== "number") {
467
+ this.emitNotification({
468
+ method: message.method,
469
+ params: message.params ?? null
470
+ });
471
+ return;
472
+ }
473
+ if (typeof message.id === "number" && typeof message.method === "string") {
474
+ this.handleServerRequest(message.id, message.method, message.params ?? null);
475
+ }
476
+ }
477
+ emitNotification(notification) {
478
+ for (const listener of this.notificationListeners) {
479
+ listener(notification);
480
+ }
481
+ }
482
+ sendServerRequestReply(requestId, reply) {
483
+ if (reply.error) {
484
+ this.sendLine({
485
+ jsonrpc: "2.0",
486
+ id: requestId,
487
+ error: reply.error
488
+ });
489
+ return;
490
+ }
491
+ this.sendLine({
492
+ jsonrpc: "2.0",
493
+ id: requestId,
494
+ result: reply.result ?? {}
495
+ });
496
+ }
497
+ resolvePendingServerRequest(requestId, reply) {
498
+ const pendingRequest = this.pendingServerRequests.get(requestId);
499
+ if (!pendingRequest) {
500
+ throw new Error(`No pending server request found for id ${String(requestId)}`);
501
+ }
502
+ this.pendingServerRequests.delete(requestId);
503
+ this.sendServerRequestReply(requestId, reply);
504
+ const requestParams = asRecord(pendingRequest.params);
505
+ const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
506
+ this.emitNotification({
507
+ method: "server/request/resolved",
508
+ params: {
509
+ id: requestId,
510
+ method: pendingRequest.method,
511
+ threadId,
512
+ mode: "manual",
513
+ resolvedAtIso: (/* @__PURE__ */ new Date()).toISOString()
514
+ }
515
+ });
516
+ }
517
+ handleServerRequest(requestId, method, params) {
518
+ const pendingRequest = {
519
+ id: requestId,
520
+ method,
521
+ params,
522
+ receivedAtIso: (/* @__PURE__ */ new Date()).toISOString()
523
+ };
524
+ this.pendingServerRequests.set(requestId, pendingRequest);
525
+ this.emitNotification({
526
+ method: "server/request",
527
+ params: pendingRequest
528
+ });
529
+ }
530
+ async call(method, params) {
531
+ this.start();
532
+ const id = this.nextId++;
533
+ return new Promise((resolve2, reject) => {
534
+ this.pending.set(id, { resolve: resolve2, reject });
535
+ this.sendLine({
536
+ jsonrpc: "2.0",
537
+ id,
538
+ method,
539
+ params
540
+ });
541
+ });
542
+ }
543
+ async ensureInitialized() {
544
+ if (this.initialized) return;
545
+ if (this.initializePromise) {
546
+ await this.initializePromise;
547
+ return;
548
+ }
549
+ this.initializePromise = this.call("initialize", {
550
+ clientInfo: {
551
+ name: "codex-web-local",
552
+ version: "0.1.0"
553
+ }
554
+ }).then(() => {
555
+ this.initialized = true;
556
+ }).finally(() => {
557
+ this.initializePromise = null;
558
+ });
559
+ await this.initializePromise;
560
+ }
561
+ async rpc(method, params) {
562
+ await this.ensureInitialized();
563
+ return this.call(method, params);
564
+ }
565
+ onNotification(listener) {
566
+ this.notificationListeners.add(listener);
567
+ return () => {
568
+ this.notificationListeners.delete(listener);
569
+ };
570
+ }
571
+ async respondToServerRequest(payload) {
572
+ await this.ensureInitialized();
573
+ const body = asRecord(payload);
574
+ if (!body) {
575
+ throw new Error("Invalid response payload: expected object");
576
+ }
577
+ const id = body.id;
578
+ if (typeof id !== "number" || !Number.isInteger(id)) {
579
+ throw new Error('Invalid response payload: "id" must be an integer');
580
+ }
581
+ const rawError = asRecord(body.error);
582
+ if (rawError) {
583
+ const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
584
+ const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
585
+ this.resolvePendingServerRequest(id, { error: { code, message } });
586
+ return;
587
+ }
588
+ if (!("result" in body)) {
589
+ throw new Error('Invalid response payload: expected "result" or "error"');
590
+ }
591
+ this.resolvePendingServerRequest(id, { result: body.result });
592
+ }
593
+ listPendingServerRequests() {
594
+ return Array.from(this.pendingServerRequests.values());
595
+ }
596
+ dispose() {
597
+ if (!this.process) return;
598
+ const proc = this.process;
599
+ this.stopping = true;
600
+ this.process = null;
601
+ this.initialized = false;
602
+ this.initializePromise = null;
603
+ this.readBuffer = "";
604
+ const failure = new Error("codex app-server stopped");
605
+ for (const request of this.pending.values()) {
606
+ request.reject(failure);
607
+ }
608
+ this.pending.clear();
609
+ this.pendingServerRequests.clear();
610
+ try {
611
+ proc.stdin.end();
612
+ } catch {
613
+ }
614
+ try {
615
+ proc.kill("SIGTERM");
616
+ } catch {
617
+ }
618
+ const forceKillTimer = setTimeout(() => {
619
+ if (!proc.killed) {
620
+ try {
621
+ proc.kill("SIGKILL");
622
+ } catch {
623
+ }
624
+ }
625
+ }, 1500);
626
+ forceKillTimer.unref();
627
+ }
628
+ };
629
+ var MethodCatalog = class {
630
+ constructor() {
631
+ this.methodCache = null;
632
+ this.notificationCache = null;
633
+ }
634
+ async runGenerateSchemaCommand(outDir) {
635
+ await new Promise((resolve2, reject) => {
636
+ const process2 = spawn("codex", ["app-server", "generate-json-schema", "--out", outDir], {
637
+ stdio: ["ignore", "ignore", "pipe"]
638
+ });
639
+ let stderr = "";
640
+ process2.stderr.setEncoding("utf8");
641
+ process2.stderr.on("data", (chunk) => {
642
+ stderr += chunk;
643
+ });
644
+ process2.on("error", reject);
645
+ process2.on("exit", (code) => {
646
+ if (code === 0) {
647
+ resolve2();
648
+ return;
649
+ }
650
+ reject(new Error(stderr.trim() || `generate-json-schema exited with code ${String(code)}`));
651
+ });
652
+ });
653
+ }
654
+ extractMethodsFromClientRequest(payload) {
655
+ const root = asRecord(payload);
656
+ const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
657
+ const methods = /* @__PURE__ */ new Set();
658
+ for (const entry of oneOf) {
659
+ const row = asRecord(entry);
660
+ const properties = asRecord(row?.properties);
661
+ const methodDef = asRecord(properties?.method);
662
+ const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
663
+ for (const item of methodEnum) {
664
+ if (typeof item === "string" && item.length > 0) {
665
+ methods.add(item);
666
+ }
667
+ }
668
+ }
669
+ return Array.from(methods).sort((a, b) => a.localeCompare(b));
670
+ }
671
+ extractMethodsFromServerNotification(payload) {
672
+ const root = asRecord(payload);
673
+ const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
674
+ const methods = /* @__PURE__ */ new Set();
675
+ for (const entry of oneOf) {
676
+ const row = asRecord(entry);
677
+ const properties = asRecord(row?.properties);
678
+ const methodDef = asRecord(properties?.method);
679
+ const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
680
+ for (const item of methodEnum) {
681
+ if (typeof item === "string" && item.length > 0) {
682
+ methods.add(item);
683
+ }
684
+ }
685
+ }
686
+ return Array.from(methods).sort((a, b) => a.localeCompare(b));
687
+ }
688
+ async listMethods() {
689
+ if (this.methodCache) {
690
+ return this.methodCache;
691
+ }
692
+ const outDir = await mkdtemp(join(tmpdir(), "codex-web-local-schema-"));
693
+ await this.runGenerateSchemaCommand(outDir);
694
+ const clientRequestPath = join(outDir, "ClientRequest.json");
695
+ const raw = await readFile(clientRequestPath, "utf8");
696
+ const parsed = JSON.parse(raw);
697
+ const methods = this.extractMethodsFromClientRequest(parsed);
698
+ this.methodCache = methods;
699
+ return methods;
700
+ }
701
+ async listNotificationMethods() {
702
+ if (this.notificationCache) {
703
+ return this.notificationCache;
704
+ }
705
+ const outDir = await mkdtemp(join(tmpdir(), "codex-web-local-schema-"));
706
+ await this.runGenerateSchemaCommand(outDir);
707
+ const serverNotificationPath = join(outDir, "ServerNotification.json");
708
+ const raw = await readFile(serverNotificationPath, "utf8");
709
+ const parsed = JSON.parse(raw);
710
+ const methods = this.extractMethodsFromServerNotification(parsed);
711
+ this.notificationCache = methods;
712
+ return methods;
713
+ }
714
+ };
715
+ var SHARED_BRIDGE_KEY = "__codexRemoteSharedBridge__";
716
+ function getSharedBridgeState() {
717
+ const globalScope = globalThis;
718
+ const existing = globalScope[SHARED_BRIDGE_KEY];
719
+ if (existing) return existing;
720
+ const created = {
721
+ appServer: new AppServerProcess(),
722
+ methodCatalog: new MethodCatalog()
723
+ };
724
+ globalScope[SHARED_BRIDGE_KEY] = created;
725
+ return created;
726
+ }
727
+ function createCodexBridgeMiddleware() {
728
+ const { appServer, methodCatalog } = getSharedBridgeState();
729
+ const middleware = async (req, res, next) => {
730
+ try {
731
+ if (!req.url) {
732
+ next();
733
+ return;
734
+ }
735
+ const url = new URL(req.url, "http://localhost");
736
+ if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
737
+ const payload = await readJsonBody(req);
738
+ const body = asRecord(payload);
739
+ if (!body || typeof body.method !== "string" || body.method.length === 0) {
740
+ setJson(res, 400, { error: "Invalid body: expected { method, params? }" });
741
+ return;
742
+ }
743
+ const result = await appServer.rpc(body.method, body.params ?? null);
744
+ setJson(res, 200, { result });
745
+ return;
746
+ }
747
+ if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
748
+ const auth = await readCodexAuth();
749
+ if (!auth) {
750
+ setJson(res, 401, { error: "No auth token available for transcription" });
751
+ return;
752
+ }
753
+ const rawBody = await readRawBody(req);
754
+ const incomingCt = req.headers["content-type"] ?? "application/octet-stream";
755
+ const upstream = await proxyTranscribe(rawBody, incomingCt, auth.accessToken, auth.accountId);
756
+ res.statusCode = upstream.status;
757
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
758
+ res.end(upstream.body);
759
+ return;
760
+ }
761
+ if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
762
+ const payload = await readJsonBody(req);
763
+ await appServer.respondToServerRequest(payload);
764
+ setJson(res, 200, { ok: true });
765
+ return;
766
+ }
767
+ if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
768
+ setJson(res, 200, { data: appServer.listPendingServerRequests() });
769
+ return;
770
+ }
771
+ if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
772
+ const methods = await methodCatalog.listMethods();
773
+ setJson(res, 200, { data: methods });
774
+ return;
775
+ }
776
+ if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
777
+ const methods = await methodCatalog.listNotificationMethods();
778
+ setJson(res, 200, { data: methods });
779
+ return;
780
+ }
781
+ if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
782
+ const state = await readWorkspaceRootsState();
783
+ setJson(res, 200, { data: state });
784
+ return;
785
+ }
786
+ if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
787
+ const payload = await readJsonBody(req);
788
+ const record = asRecord(payload);
789
+ if (!record) {
790
+ setJson(res, 400, { error: "Invalid body: expected object" });
791
+ return;
792
+ }
793
+ const nextState = {
794
+ order: normalizeStringArray(record.order),
795
+ labels: normalizeStringRecord(record.labels),
796
+ active: normalizeStringArray(record.active)
797
+ };
798
+ await writeWorkspaceRootsState(nextState);
799
+ setJson(res, 200, { ok: true });
800
+ return;
801
+ }
802
+ if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
803
+ const payload = asRecord(await readJsonBody(req));
804
+ const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
805
+ const createIfMissing = payload?.createIfMissing === true;
806
+ const label = typeof payload?.label === "string" ? payload.label : "";
807
+ if (!rawPath) {
808
+ setJson(res, 400, { error: "Missing path" });
809
+ return;
810
+ }
811
+ const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
812
+ let pathExists = true;
813
+ try {
814
+ const info = await stat(normalizedPath);
815
+ if (!info.isDirectory()) {
816
+ setJson(res, 400, { error: "Path exists but is not a directory" });
817
+ return;
818
+ }
819
+ } catch {
820
+ pathExists = false;
821
+ }
822
+ if (!pathExists && createIfMissing) {
823
+ await mkdir(normalizedPath, { recursive: true });
824
+ } else if (!pathExists) {
825
+ setJson(res, 404, { error: "Directory does not exist" });
826
+ return;
827
+ }
828
+ const existingState = await readWorkspaceRootsState();
829
+ const nextOrder = [normalizedPath, ...existingState.order.filter((item) => item !== normalizedPath)];
830
+ const nextActive = [normalizedPath, ...existingState.active.filter((item) => item !== normalizedPath)];
831
+ const nextLabels = { ...existingState.labels };
832
+ if (label.trim().length > 0) {
833
+ nextLabels[normalizedPath] = label.trim();
834
+ }
835
+ await writeWorkspaceRootsState({
836
+ order: nextOrder,
837
+ labels: nextLabels,
838
+ active: nextActive
839
+ });
840
+ setJson(res, 200, { data: { path: normalizedPath } });
841
+ return;
842
+ }
843
+ if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
844
+ const basePath = url.searchParams.get("basePath")?.trim() ?? "";
845
+ if (!basePath) {
846
+ setJson(res, 400, { error: "Missing basePath" });
847
+ return;
848
+ }
849
+ const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
850
+ try {
851
+ const baseInfo = await stat(normalizedBasePath);
852
+ if (!baseInfo.isDirectory()) {
853
+ setJson(res, 400, { error: "basePath is not a directory" });
854
+ return;
855
+ }
856
+ } catch {
857
+ setJson(res, 404, { error: "basePath does not exist" });
858
+ return;
859
+ }
860
+ let index = 1;
861
+ while (index < 1e5) {
862
+ const candidateName = `New Project (${String(index)})`;
863
+ const candidatePath = join(normalizedBasePath, candidateName);
864
+ try {
865
+ await stat(candidatePath);
866
+ index += 1;
867
+ continue;
868
+ } catch {
869
+ setJson(res, 200, { data: { name: candidateName, path: candidatePath } });
870
+ return;
871
+ }
872
+ }
873
+ setJson(res, 500, { error: "Failed to compute project name suggestion" });
874
+ return;
875
+ }
876
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
877
+ const cache = await readThreadTitleCache();
878
+ setJson(res, 200, { data: cache });
879
+ return;
880
+ }
881
+ if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
882
+ const payload = asRecord(await readJsonBody(req));
883
+ const id = typeof payload?.id === "string" ? payload.id : "";
884
+ const title = typeof payload?.title === "string" ? payload.title : "";
885
+ if (!id) {
886
+ setJson(res, 400, { error: "Missing id" });
887
+ return;
888
+ }
889
+ const cache = await readThreadTitleCache();
890
+ const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
891
+ await writeThreadTitleCache(next2);
892
+ setJson(res, 200, { ok: true });
893
+ return;
894
+ }
895
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
896
+ try {
897
+ const q = url.searchParams.get("q") || "";
898
+ const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
899
+ const sort = url.searchParams.get("sort") || "date";
900
+ const allEntries = await fetchSkillsTree();
901
+ const installedMap = await scanInstalledSkillsFromDisk();
902
+ try {
903
+ const result = await appServer.rpc("skills/list", {});
904
+ for (const entry of result.data ?? []) {
905
+ for (const skill of entry.skills ?? []) {
906
+ if (skill.name) {
907
+ installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
908
+ }
909
+ }
910
+ }
911
+ } catch {
912
+ }
913
+ const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
914
+ await fetchMetaBatch(installedHubEntries);
915
+ const installed = [];
916
+ for (const [, info] of installedMap) {
917
+ const hubEntry = allEntries.find((e) => e.name === info.name);
918
+ const base = hubEntry ? buildHubEntry(hubEntry) : {
919
+ name: info.name,
920
+ owner: "local",
921
+ description: "",
922
+ displayName: "",
923
+ publishedAt: 0,
924
+ avatarUrl: "",
925
+ url: "",
926
+ installed: false
927
+ };
928
+ installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
929
+ }
930
+ const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
931
+ setJson(res, 200, { data: results, installed, total: allEntries.length });
932
+ } catch (error) {
933
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
934
+ }
935
+ return;
936
+ }
937
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
938
+ try {
939
+ const owner = url.searchParams.get("owner") || "";
940
+ const name = url.searchParams.get("name") || "";
941
+ if (!owner || !name) {
942
+ setJson(res, 400, { error: "Missing owner or name" });
943
+ return;
944
+ }
945
+ const rawUrl = `https://raw.githubusercontent.com/openclaw/skills/main/skills/${owner}/${name}/SKILL.md`;
946
+ const resp = await fetch(rawUrl);
947
+ if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
948
+ const content = await resp.text();
949
+ setJson(res, 200, { content });
950
+ } catch (error) {
951
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
952
+ }
953
+ return;
954
+ }
955
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
956
+ try {
957
+ const payload = asRecord(await readJsonBody(req));
958
+ const owner = typeof payload?.owner === "string" ? payload.owner : "";
959
+ const name = typeof payload?.name === "string" ? payload.name : "";
960
+ if (!owner || !name) {
961
+ setJson(res, 400, { error: "Missing owner or name" });
962
+ return;
963
+ }
964
+ const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
965
+ const installDest = await detectUserSkillsDir(appServer);
966
+ const skillPathInRepo = `skills/${owner}/${name}`;
967
+ await runCommand("python3", [
968
+ installerScript,
969
+ "--repo",
970
+ "openclaw/skills",
971
+ "--path",
972
+ skillPathInRepo,
973
+ "--dest",
974
+ installDest,
975
+ "--method",
976
+ "git"
977
+ ]);
978
+ const skillDir = join(installDest, name);
979
+ await ensureInstalledSkillIsValid(appServer, skillDir);
980
+ setJson(res, 200, { ok: true, path: skillDir });
981
+ } catch (error) {
982
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
983
+ }
984
+ return;
985
+ }
986
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
987
+ try {
988
+ const payload = asRecord(await readJsonBody(req));
989
+ const name = typeof payload?.name === "string" ? payload.name : "";
990
+ const path = typeof payload?.path === "string" ? payload.path : "";
991
+ const target = path || (name ? join(getSkillsInstallDir(), name) : "");
992
+ if (!target) {
993
+ setJson(res, 400, { error: "Missing name or path" });
994
+ return;
995
+ }
996
+ await rm(target, { recursive: true, force: true });
997
+ try {
998
+ await appServer.rpc("skills/list", { forceReload: true });
999
+ } catch {
1000
+ }
1001
+ setJson(res, 200, { ok: true, deletedPath: target });
1002
+ } catch (error) {
1003
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
1004
+ }
1005
+ return;
1006
+ }
1007
+ if (req.method === "GET" && url.pathname === "/codex-api/events") {
1008
+ res.statusCode = 200;
1009
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
1010
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1011
+ res.setHeader("Connection", "keep-alive");
1012
+ res.setHeader("X-Accel-Buffering", "no");
1013
+ const unsubscribe = appServer.onNotification((notification) => {
1014
+ if (res.writableEnded || res.destroyed) return;
1015
+ const payload = {
1016
+ ...notification,
1017
+ atIso: (/* @__PURE__ */ new Date()).toISOString()
1018
+ };
1019
+ res.write(`data: ${JSON.stringify(payload)}
1020
+
1021
+ `);
1022
+ });
1023
+ res.write(`event: ready
1024
+ data: ${JSON.stringify({ ok: true })}
1025
+
1026
+ `);
1027
+ const keepAlive = setInterval(() => {
1028
+ res.write(": ping\n\n");
1029
+ }, 15e3);
1030
+ const close = () => {
1031
+ clearInterval(keepAlive);
1032
+ unsubscribe();
1033
+ if (!res.writableEnded) {
1034
+ res.end();
1035
+ }
1036
+ };
1037
+ req.on("close", close);
1038
+ req.on("aborted", close);
1039
+ return;
1040
+ }
1041
+ next();
1042
+ } catch (error) {
1043
+ const message = getErrorMessage(error, "Unknown bridge error");
1044
+ setJson(res, 502, { error: message });
1045
+ }
1046
+ };
1047
+ middleware.dispose = () => {
1048
+ appServer.dispose();
1049
+ };
1050
+ return middleware;
1051
+ }
1052
+
1053
+ // src/server/authMiddleware.ts
1054
+ import { randomBytes, timingSafeEqual } from "crypto";
1055
+ var TOKEN_COOKIE = "codex_web_local_token";
1056
+ function isLocalhostRequest(req) {
1057
+ const remote = req.socket.remoteAddress ?? "";
1058
+ if (remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1") {
1059
+ return true;
1060
+ }
1061
+ const host = (req.headers.host ?? "").toLowerCase();
1062
+ return host.startsWith("localhost:") || host === "localhost" || host.startsWith("127.0.0.1:");
1063
+ }
1064
+ function constantTimeCompare(a, b) {
1065
+ const bufA = Buffer.from(a);
1066
+ const bufB = Buffer.from(b);
1067
+ if (bufA.length !== bufB.length) return false;
1068
+ return timingSafeEqual(bufA, bufB);
1069
+ }
1070
+ function parseCookies(header) {
1071
+ const cookies = {};
1072
+ if (!header) return cookies;
1073
+ for (const pair of header.split(";")) {
1074
+ const idx = pair.indexOf("=");
1075
+ if (idx === -1) continue;
1076
+ const key = pair.slice(0, idx).trim();
1077
+ const value = pair.slice(idx + 1).trim();
1078
+ cookies[key] = value;
1079
+ }
1080
+ return cookies;
1081
+ }
1082
+ var LOGIN_PAGE_HTML = `<!DOCTYPE html>
1083
+ <html lang="en">
1084
+ <head>
1085
+ <meta charset="utf-8">
1086
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1087
+ <title>Codex Web Local &mdash; Login</title>
1088
+ <style>
1089
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
1090
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0a0a0a;color:#e5e5e5;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:1rem}
1091
+ .card{background:#171717;border:1px solid #262626;border-radius:12px;padding:2rem;width:100%;max-width:380px}
1092
+ h1{font-size:1.25rem;font-weight:600;margin-bottom:1.5rem;text-align:center;color:#fafafa}
1093
+ label{display:block;font-size:.875rem;color:#a3a3a3;margin-bottom:.5rem}
1094
+ input{width:100%;padding:.625rem .75rem;background:#0a0a0a;border:1px solid #404040;border-radius:8px;color:#fafafa;font-size:1rem;outline:none;transition:border-color .15s}
1095
+ input:focus{border-color:#3b82f6}
1096
+ button{width:100%;padding:.625rem;margin-top:1rem;background:#3b82f6;color:#fff;border:none;border-radius:8px;font-size:.9375rem;font-weight:500;cursor:pointer;transition:background .15s}
1097
+ button:hover{background:#2563eb}
1098
+ .error{color:#ef4444;font-size:.8125rem;margin-top:.75rem;text-align:center;display:none}
1099
+ </style>
1100
+ </head>
1101
+ <body>
1102
+ <div class="card">
1103
+ <h1>Codex Web Local</h1>
1104
+ <form id="f">
1105
+ <label for="pw">Password</label>
1106
+ <input id="pw" name="password" type="password" autocomplete="current-password" autofocus required>
1107
+ <button type="submit">Sign in</button>
1108
+ <p class="error" id="err">Incorrect password</p>
1109
+ </form>
1110
+ </div>
1111
+ <script>
1112
+ const form=document.getElementById('f');
1113
+ const errEl=document.getElementById('err');
1114
+ form.addEventListener('submit',async e=>{
1115
+ e.preventDefault();
1116
+ errEl.style.display='none';
1117
+ const res=await fetch('/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:document.getElementById('pw').value})});
1118
+ if(res.ok){window.location.reload()}else{errEl.style.display='block';document.getElementById('pw').value='';document.getElementById('pw').focus()}
1119
+ });
1120
+ </script>
1121
+ </body>
1122
+ </html>`;
1123
+ function createAuthMiddleware(password) {
1124
+ const validTokens = /* @__PURE__ */ new Set();
1125
+ return (req, res, next) => {
1126
+ if (isLocalhostRequest(req)) {
1127
+ next();
1128
+ return;
1129
+ }
1130
+ if (req.method === "POST" && req.path === "/auth/login") {
1131
+ let body = "";
1132
+ req.setEncoding("utf8");
1133
+ req.on("data", (chunk) => {
1134
+ body += chunk;
1135
+ });
1136
+ req.on("end", () => {
1137
+ try {
1138
+ const parsed = JSON.parse(body);
1139
+ const provided = typeof parsed.password === "string" ? parsed.password : "";
1140
+ if (!constantTimeCompare(provided, password)) {
1141
+ res.status(401).json({ error: "Invalid password" });
1142
+ return;
1143
+ }
1144
+ const token2 = randomBytes(32).toString("hex");
1145
+ validTokens.add(token2);
1146
+ res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token2}; Path=/; HttpOnly; SameSite=Strict`);
1147
+ res.json({ ok: true });
1148
+ } catch {
1149
+ res.status(400).json({ error: "Invalid request body" });
1150
+ }
1151
+ });
1152
+ return;
1153
+ }
1154
+ const cookies = parseCookies(req.headers.cookie);
1155
+ const token = cookies[TOKEN_COOKIE];
1156
+ if (token && validTokens.has(token)) {
1157
+ next();
1158
+ return;
1159
+ }
1160
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1161
+ res.status(200).send(LOGIN_PAGE_HTML);
1162
+ };
1163
+ }
1164
+
1165
+ // src/server/httpServer.ts
1166
+ var __dirname = dirname(fileURLToPath(import.meta.url));
1167
+ var distDir = join2(__dirname, "..", "dist");
1168
+ var IMAGE_CONTENT_TYPES = {
1169
+ ".avif": "image/avif",
1170
+ ".bmp": "image/bmp",
1171
+ ".gif": "image/gif",
1172
+ ".jpeg": "image/jpeg",
1173
+ ".jpg": "image/jpeg",
1174
+ ".png": "image/png",
1175
+ ".svg": "image/svg+xml",
1176
+ ".webp": "image/webp"
1177
+ };
1178
+ function normalizeLocalImagePath(rawPath) {
1179
+ const trimmed = rawPath.trim();
1180
+ if (!trimmed) return "";
1181
+ if (trimmed.startsWith("file://")) {
1182
+ try {
1183
+ return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
1184
+ } catch {
1185
+ return trimmed.replace(/^file:\/\//u, "");
1186
+ }
1187
+ }
1188
+ return trimmed;
1189
+ }
1190
+ function createServer(options = {}) {
1191
+ const app = express();
1192
+ const bridge = createCodexBridgeMiddleware();
1193
+ if (options.password) {
1194
+ app.use(createAuthMiddleware(options.password));
1195
+ }
1196
+ app.use(bridge);
1197
+ app.get("/codex-local-image", (req, res) => {
1198
+ const rawPath = typeof req.query.path === "string" ? req.query.path : "";
1199
+ const localPath = normalizeLocalImagePath(rawPath);
1200
+ if (!localPath || !isAbsolute2(localPath)) {
1201
+ res.status(400).json({ error: "Expected absolute local file path." });
1202
+ return;
1203
+ }
1204
+ const contentType = IMAGE_CONTENT_TYPES[extname(localPath).toLowerCase()];
1205
+ if (!contentType) {
1206
+ res.status(415).json({ error: "Unsupported image type." });
1207
+ return;
1208
+ }
1209
+ res.type(contentType);
1210
+ res.setHeader("Cache-Control", "private, max-age=300");
1211
+ res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
1212
+ if (!error) return;
1213
+ if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
1214
+ });
1215
+ });
1216
+ app.use(express.static(distDir));
1217
+ app.use((_req, res) => {
1218
+ res.sendFile(join2(distDir, "index.html"));
1219
+ });
1220
+ return {
1221
+ app,
1222
+ dispose: () => bridge.dispose()
1223
+ };
1224
+ }
1225
+
1226
+ // src/server/password.ts
1227
+ import { randomInt } from "crypto";
1228
+ var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
1229
+ function randomGroup(length) {
1230
+ let result = "";
1231
+ for (let i = 0; i < length; i++) {
1232
+ result += CHARS[randomInt(CHARS.length)];
1233
+ }
1234
+ return result;
1235
+ }
1236
+ function generatePassword() {
1237
+ return `${randomGroup(3)}-${randomGroup(3)}-${randomGroup(3)}`;
1238
+ }
1239
+
1240
+ // src/cli/index.ts
1241
+ var program = new Command().name("codexui").description("Web interface for Codex app-server");
1242
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
1243
+ async function readCliVersion() {
1244
+ try {
1245
+ const packageJsonPath = join3(__dirname2, "..", "package.json");
1246
+ const raw = await readFile2(packageJsonPath, "utf8");
1247
+ const parsed = JSON.parse(raw);
1248
+ return typeof parsed.version === "string" ? parsed.version : "unknown";
1249
+ } catch {
1250
+ return "unknown";
1251
+ }
1252
+ }
1253
+ function isTermuxRuntime() {
1254
+ return Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("/com.termux/"));
1255
+ }
1256
+ function canRun(command, args = []) {
1257
+ const result = spawnSync(command, args, { stdio: "ignore" });
1258
+ return result.status === 0;
1259
+ }
1260
+ function runOrFail(command, args, label) {
1261
+ const result = spawnSync(command, args, { stdio: "inherit" });
1262
+ if (result.status !== 0) {
1263
+ throw new Error(`${label} failed with exit code ${String(result.status ?? -1)}`);
1264
+ }
1265
+ }
1266
+ function resolveCodexCommand() {
1267
+ if (canRun("codex", ["--version"])) {
1268
+ return "codex";
1269
+ }
1270
+ const prefix = process.env.PREFIX?.trim();
1271
+ if (!prefix) {
1272
+ return null;
1273
+ }
1274
+ const candidate = join3(prefix, "bin", "codex");
1275
+ if (existsSync(candidate) && canRun(candidate, ["--version"])) {
1276
+ return candidate;
1277
+ }
1278
+ return null;
1279
+ }
1280
+ function hasCodexAuth() {
1281
+ const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
1282
+ return existsSync(join3(codexHome, "auth.json"));
1283
+ }
1284
+ function ensureTermuxCodexInstalled() {
1285
+ if (!isTermuxRuntime()) {
1286
+ return resolveCodexCommand();
1287
+ }
1288
+ let codexCommand = resolveCodexCommand();
1289
+ if (!codexCommand) {
1290
+ console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
1291
+ runOrFail("npm", ["install", "-g", "@mmmbuto/codex-cli-termux"], "Codex CLI install");
1292
+ codexCommand = resolveCodexCommand();
1293
+ if (!codexCommand) {
1294
+ console.log("\nTermux npm package did not expose `codex`. Installing official CLI fallback...\n");
1295
+ runOrFail("npm", ["install", "-g", "@openai/codex"], "Codex CLI fallback install");
1296
+ codexCommand = resolveCodexCommand();
1297
+ }
1298
+ if (!codexCommand) {
1299
+ throw new Error("Codex CLI install completed but binary is still not available in PATH");
1300
+ }
1301
+ console.log("\nCodex CLI installed.\n");
1302
+ }
1303
+ return codexCommand;
1304
+ }
1305
+ function resolvePassword(input) {
1306
+ if (input === false) {
1307
+ return void 0;
1308
+ }
1309
+ if (typeof input === "string") {
1310
+ return input;
1311
+ }
1312
+ return generatePassword();
1313
+ }
1314
+ function printTermuxKeepAlive(lines) {
1315
+ if (!isTermuxRuntime()) {
1316
+ return;
1317
+ }
1318
+ lines.push("");
1319
+ lines.push(" Android/Termux keep-alive:");
1320
+ lines.push(" 1) Keep this Termux session open (do not swipe it away).");
1321
+ lines.push(" 2) Disable battery optimization for Termux in Android settings.");
1322
+ lines.push(" 3) Optional: run `termux-wake-lock` in another shell.");
1323
+ }
1324
+ function openBrowser(url) {
1325
+ const command = process.platform === "darwin" ? { cmd: "open", args: [url] } : process.platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", url] } : { cmd: "xdg-open", args: [url] };
1326
+ const child = spawn2(command.cmd, command.args, { detached: true, stdio: "ignore" });
1327
+ child.on("error", () => {
1328
+ });
1329
+ child.unref();
1330
+ }
1331
+ function listenWithFallback(server, startPort) {
1332
+ return new Promise((resolve2, reject) => {
1333
+ const attempt = (port) => {
1334
+ const onError = (error) => {
1335
+ server.off("listening", onListening);
1336
+ if (error.code === "EADDRINUSE" || error.code === "EACCES") {
1337
+ attempt(port + 1);
1338
+ return;
1339
+ }
1340
+ reject(error);
1341
+ };
1342
+ const onListening = () => {
1343
+ server.off("error", onError);
1344
+ resolve2(port);
1345
+ };
1346
+ server.once("error", onError);
1347
+ server.once("listening", onListening);
1348
+ server.listen(port);
1349
+ };
1350
+ attempt(startPort);
1351
+ });
1352
+ }
1353
+ async function startServer(options) {
1354
+ const version = await readCliVersion();
1355
+ const codexCommand = ensureTermuxCodexInstalled() ?? resolveCodexCommand();
1356
+ if (!hasCodexAuth() && codexCommand) {
1357
+ console.log("\nCodex is not logged in. Starting `codex login`...\n");
1358
+ runOrFail(codexCommand, ["login"], "Codex login");
1359
+ }
1360
+ const requestedPort = parseInt(options.port, 10);
1361
+ const password = resolvePassword(options.password);
1362
+ const { app, dispose } = createServer({ password });
1363
+ const server = createServer2(app);
1364
+ const port = await listenWithFallback(server, requestedPort);
1365
+ const lines = [
1366
+ "",
1367
+ "Codex Web Local is running!",
1368
+ ` Version: ${version}`,
1369
+ "",
1370
+ ` Local: http://localhost:${String(port)}`
1371
+ ];
1372
+ if (port !== requestedPort) {
1373
+ lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
1374
+ }
1375
+ if (password) {
1376
+ lines.push(` Password: ${password}`);
1377
+ }
1378
+ printTermuxKeepAlive(lines);
1379
+ lines.push("");
1380
+ console.log(lines.join("\n"));
1381
+ openBrowser(`http://localhost:${String(port)}`);
1382
+ function shutdown() {
1383
+ console.log("\nShutting down...");
1384
+ server.close(() => {
1385
+ dispose();
1386
+ process.exit(0);
1387
+ });
1388
+ setTimeout(() => {
1389
+ dispose();
1390
+ process.exit(1);
1391
+ }, 5e3).unref();
1392
+ }
1393
+ process.on("SIGINT", shutdown);
1394
+ process.on("SIGTERM", shutdown);
1395
+ }
1396
+ async function runLogin() {
1397
+ const codexCommand = ensureTermuxCodexInstalled() ?? "codex";
1398
+ console.log("\nStarting `codex login`...\n");
1399
+ runOrFail(codexCommand, ["login"], "Codex login");
1400
+ }
1401
+ program.option("-p, --port <port>", "port to listen on", "3000").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").action(async (opts) => {
1402
+ await startServer(opts);
1403
+ });
1404
+ program.command("login").description("Install/check Codex CLI in Termux and run `codex login`").action(runLogin);
1405
+ program.command("help").description("Show codexui command help").action(() => {
1406
+ program.outputHelp();
1407
+ });
1408
+ program.parseAsync(process.argv).catch((error) => {
1409
+ const message = error instanceof Error ? error.message : String(error);
1410
+ console.error(`
1411
+ Failed to run codexui: ${message}`);
1412
+ process.exit(1);
1413
+ });
1414
+ //# sourceMappingURL=index.js.map