ever-terminal 1.0.0

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,16 @@
1
+ export function codexThreadStatus(thread) {
2
+ const statusType = String(thread?.status?.type ?? thread?.status ?? "");
3
+ if (statusType === "active")
4
+ return "busy";
5
+ if (statusType === "waitingOnApproval" ||
6
+ statusType === "waitingOnUserInput")
7
+ return "awaiting";
8
+ if (statusType === "idle" ||
9
+ statusType === "completed" ||
10
+ statusType === "archived")
11
+ return "idle";
12
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
13
+ if (turns.length === 0)
14
+ return "idle";
15
+ return turns[turns.length - 1]?.completedAt != null ? "idle" : "busy";
16
+ }
@@ -0,0 +1,83 @@
1
+ import { codexThreadStatus } from "./status.js";
2
+ import { debugLog } from "../debug.js";
3
+ export async function listCodexSessions(client, limit, cwd) {
4
+ const result = await client.threadList({
5
+ limit,
6
+ ...(cwd ? { cwd } : {}),
7
+ archived: false,
8
+ sortKey: "updated_at",
9
+ });
10
+ return result.data.slice(0, 10).map((t) => ({
11
+ id: String(t.id ?? ""),
12
+ title: String(t.name ?? t.preview ?? "Codex session").slice(0, 64),
13
+ timestamp: new Date(Number(t.updatedAt ?? t.createdAt ?? Date.now() / 1000) * 1000).toISOString(),
14
+ cwd: String(t.cwd ?? ""),
15
+ status: codexThreadStatus(t),
16
+ }));
17
+ }
18
+ const MAX_HISTORY_ITEMS = 10;
19
+ // Turns fetched per page. Independent of `limit`; correctness comes from the
20
+ // pagination loop, this constant only paces it.
21
+ const TURNS_PAGE_SIZE = 10;
22
+ export async function getCodexSessionHistory(client, sessionId, limit) {
23
+ const t0 = Date.now();
24
+ const returnCount = Math.min(limit, MAX_HISTORY_ITEMS);
25
+ const messages = [];
26
+ let cursor = undefined;
27
+ let pages = 0;
28
+ let scannedTurns = 0;
29
+ let scannedItems = 0;
30
+ outer: while (messages.length < returnCount) {
31
+ const page = await client.threadTurnsList({
32
+ threadId: sessionId,
33
+ limit: TURNS_PAGE_SIZE,
34
+ sortDirection: "desc",
35
+ itemsView: "full",
36
+ ...(cursor ? { cursor } : {}),
37
+ });
38
+ pages++;
39
+ for (const turn of page.data) {
40
+ scannedTurns++;
41
+ const items = Array.isArray(turn?.items)
42
+ ? [...turn.items].reverse()
43
+ : [];
44
+ for (const item of items) {
45
+ scannedItems++;
46
+ if (item?.type === "userMessage") {
47
+ const text = extractUserMessageText(item);
48
+ if (text)
49
+ messages.push({ role: "user", text });
50
+ }
51
+ else if (item?.type === "agentMessage") {
52
+ const text = extractAgentMessageText(item);
53
+ if (text)
54
+ messages.push({ role: "assistant", text });
55
+ }
56
+ if (messages.length >= returnCount)
57
+ break outer;
58
+ }
59
+ }
60
+ if (!page.nextCursor || page.data.length === 0)
61
+ break;
62
+ cursor = page.nextCursor;
63
+ }
64
+ const result = messages.slice(0, returnCount).reverse();
65
+ const tEnd = Date.now();
66
+ debugLog("codex-history", `sessionId=${sessionId} totalMs=${tEnd - t0} pages=${pages} scannedTurns=${scannedTurns} scannedItems=${scannedItems} extracted=${messages.length} returned=${result.length}`);
67
+ return result;
68
+ }
69
+ function extractUserMessageText(item) {
70
+ const content = Array.isArray(item?.content) ? item.content : [];
71
+ const parts = [];
72
+ for (const c of content) {
73
+ if (c?.type === "text" && typeof c?.text === "string" && c.text.trim()) {
74
+ parts.push(c.text.trim());
75
+ }
76
+ }
77
+ return parts.join("\n").trim();
78
+ }
79
+ function extractAgentMessageText(item) {
80
+ if (typeof item?.text === "string" && item.text.trim())
81
+ return item.text.trim();
82
+ return "";
83
+ }
@@ -0,0 +1,69 @@
1
+ import { truncate } from "../summary-format.js";
2
+ export function mapCodexItemTypeToToolName(type) {
3
+ switch (type) {
4
+ case "commandExecution":
5
+ return "Shell";
6
+ case "fileChange":
7
+ return "FileEdit";
8
+ case "mcpToolCall":
9
+ return "Mcp";
10
+ case "dynamicToolCall":
11
+ return "Tool";
12
+ case "webSearch":
13
+ return "WebSearch";
14
+ case "imageView":
15
+ return "ImageView";
16
+ case "imageGeneration":
17
+ return "ImageGeneration";
18
+ case "collabAgentToolCall":
19
+ return "Agent";
20
+ default:
21
+ return String(type ?? "Tool");
22
+ }
23
+ }
24
+ export function summarizeCodexItem(item) {
25
+ const type = String(item?.type ?? "tool");
26
+ if (type === "commandExecution") {
27
+ const cmd = String(item?.command ?? "").trim();
28
+ return cmd ? `Shell ${truncate(cmd, 50)}` : "Shell command";
29
+ }
30
+ if (type === "fileChange") {
31
+ const changes = Array.isArray(item?.changes) ? item.changes.length : 0;
32
+ return changes > 0 ? `FileEdit (${changes} files)` : "FileEdit";
33
+ }
34
+ if (type === "webSearch") {
35
+ const q = String(item?.query ?? "").trim();
36
+ return q ? `Search "${truncate(q, 40)}"` : "Web search";
37
+ }
38
+ return mapCodexItemTypeToToolName(type);
39
+ }
40
+ export function summarizeCodexRequest(params, method) {
41
+ if (method.includes("commandExecution") ||
42
+ method === "execCommandApproval") {
43
+ const cmd = typeof params?.command === "string"
44
+ ? params.command
45
+ : Array.isArray(params?.command)
46
+ ? params.command.join(" ")
47
+ : "";
48
+ return cmd ? `Shell ${truncate(cmd, 50)}` : "Run command";
49
+ }
50
+ if (method.includes("fileChange") || method === "applyPatchApproval") {
51
+ return "Apply file changes";
52
+ }
53
+ if (method.includes("permissions")) {
54
+ return "Request additional permissions";
55
+ }
56
+ return method;
57
+ }
58
+ export function mapCodexRequestMethodToToolName(method) {
59
+ if (method.includes("commandExecution") ||
60
+ method === "execCommandApproval")
61
+ return "Shell";
62
+ if (method.includes("fileChange") || method === "applyPatchApproval")
63
+ return "FileEdit";
64
+ if (method.includes("permissions"))
65
+ return "Config";
66
+ if (method.includes("mcp"))
67
+ return "Mcp";
68
+ return "Tool";
69
+ }
package/dist/debug.js ADDED
@@ -0,0 +1,9 @@
1
+ import { writeToLogFile } from "./logger.js";
2
+ export function debugLog(tag, ...args) {
3
+ if (process.env.VERBOSE === "1") {
4
+ console.log(`[${tag}]`, ...args);
5
+ }
6
+ else {
7
+ writeToLogFile(`[${tag}]`, ...args);
8
+ }
9
+ }
@@ -0,0 +1,14 @@
1
+ const provider = {
2
+ name: "bore",
3
+ program: "bore",
4
+ buildArgs(port) {
5
+ return ["local", String(port), "--to", "bore.pub"];
6
+ },
7
+ parseUrl(output) {
8
+ const match = output.match(/\blistening at\s+bore\.pub:(\d+)\b/i);
9
+ if (!match)
10
+ return undefined;
11
+ return "http://bore.pub:" + match[1];
12
+ },
13
+ };
14
+ export default provider;
@@ -0,0 +1,35 @@
1
+ const provider = {
2
+ name: "ngrok",
3
+ program: "ngrok",
4
+ buildArgs(port) {
5
+ return ["http", String(port), "--log=stdout", "--log-format=json"];
6
+ },
7
+ parseUrl(output) {
8
+ for (const line of output.split(/\r?\n/)) {
9
+ const trimmed = line.trim();
10
+ if (!trimmed)
11
+ continue;
12
+ let event;
13
+ try {
14
+ event = JSON.parse(trimmed);
15
+ }
16
+ catch {
17
+ continue;
18
+ }
19
+ if (!event || typeof event !== "object")
20
+ continue;
21
+ const record = event;
22
+ if (!("addr" in record))
23
+ continue;
24
+ const url = record.url;
25
+ if (typeof url !== "string")
26
+ continue;
27
+ if (!/^https?:\/\/[^/]+\.ngrok[^/]*/.test(url))
28
+ continue;
29
+ return url;
30
+ }
31
+ return undefined;
32
+ },
33
+ failureHint: "ngrok requires a free account and authtoken. Sign up at https://dashboard.ngrok.com/signup, then run: ngrok config add-authtoken <YOUR_TOKEN>",
34
+ };
35
+ export default provider;
@@ -0,0 +1,22 @@
1
+ const provider = {
2
+ name: "pinggy",
3
+ program: "ssh",
4
+ buildArgs(port) {
5
+ return [
6
+ "-o",
7
+ "StrictHostKeyChecking=no",
8
+ "-p",
9
+ "443",
10
+ "-R0:localhost:" + String(port),
11
+ "a.pinggy.io",
12
+ ];
13
+ },
14
+ parseUrl(output) {
15
+ const httpsMatch = output.match(/https:\/\/[^\s]+\.pinggy(?:-free)?\.link/);
16
+ if (httpsMatch)
17
+ return httpsMatch[0];
18
+ const httpMatch = output.match(/http:\/\/[^\s]+\.pinggy(?:-free)?\.link/);
19
+ return httpMatch?.[0];
20
+ },
21
+ };
22
+ export default provider;
@@ -0,0 +1,22 @@
1
+ import bore from "./providers/bore.js";
2
+ import ngrok from "./providers/ngrok.js";
3
+ import pinggy from "./providers/pinggy.js";
4
+ const providers = [pinggy, bore, ngrok];
5
+ function ensureValidExposeProviders(list) {
6
+ for (const provider of list) {
7
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(provider.name)) {
8
+ console.error(`[expose] invalid provider name "${provider.name}". Provider names must start with a letter and then contain only letters or digits.`);
9
+ process.exit(1);
10
+ }
11
+ }
12
+ }
13
+ ensureValidExposeProviders(providers);
14
+ export function getExposeProviders() {
15
+ return providers.slice();
16
+ }
17
+ export function getExposeProviderNames() {
18
+ return providers.map((provider) => provider.name);
19
+ }
20
+ export function getExposeProvider(name) {
21
+ return providers.find((provider) => provider.name === name);
22
+ }
@@ -0,0 +1,75 @@
1
+ import { getExposeProvider } from "./registry.js";
2
+ import { buildClientQuery, printQRCode, rawLog } from "../startup/common.js";
3
+ import { spawnShim } from "../util/spawn-shim.js";
4
+ function getSelectedExposeProviderName() {
5
+ return process.env.EVER_TERMINAL_EXPOSE_PROVIDER;
6
+ }
7
+ function getProviderProgramPathEnvName(provider) {
8
+ return provider.name.toUpperCase() + "_PROGRAM_PATH";
9
+ }
10
+ function getProviderProgram(provider) {
11
+ return process.env[getProviderProgramPathEnvName(provider)] || provider.program;
12
+ }
13
+ function attachCleanup(child) {
14
+ const cleanup = () => {
15
+ if (!child.killed)
16
+ child.kill();
17
+ };
18
+ process.on("exit", cleanup);
19
+ process.on("SIGINT", () => {
20
+ cleanup();
21
+ process.exit(0);
22
+ });
23
+ process.on("SIGTERM", () => {
24
+ cleanup();
25
+ process.exit(0);
26
+ });
27
+ }
28
+ export function startExposeProvider(port, token) {
29
+ const providerName = getSelectedExposeProviderName();
30
+ if (!providerName)
31
+ return;
32
+ const provider = getExposeProvider(providerName);
33
+ if (!provider) {
34
+ console.error(`error: unknown expose provider "${providerName}"`);
35
+ process.exit(1);
36
+ }
37
+ const program = getProviderProgram(provider);
38
+ const programPathEnvName = getProviderProgramPathEnvName(provider);
39
+ const child = spawnShim(program, provider.buildArgs(port), {
40
+ stdio: ["ignore", "pipe", "pipe"],
41
+ });
42
+ let foundUrl = false;
43
+ let buffer = "";
44
+ const handleOutput = (chunk) => {
45
+ buffer += chunk.toString();
46
+ const parsedUrl = provider.parseUrl(buffer);
47
+ if (!parsedUrl || foundUrl)
48
+ return;
49
+ foundUrl = true;
50
+ const fullUrl = `${parsedUrl}?${buildClientQuery(token).toString()}`;
51
+ rawLog("");
52
+ rawLog(` Public expose (${provider.name}): ${parsedUrl}`);
53
+ rawLog("");
54
+ rawLog(` ${fullUrl}`);
55
+ printQRCode(fullUrl, () => rawLog(""));
56
+ };
57
+ rawLog(` Starting quick public expose via ${provider.name}...`);
58
+ child.stdout.on("data", handleOutput);
59
+ child.stderr.on("data", handleOutput);
60
+ child.on("error", (err) => {
61
+ console.error(` Failed to start ${provider.name}: ${err.message}`);
62
+ console.error(` Checked program: ${program}`);
63
+ console.error(` Override with ${programPathEnvName}=... if needed.`);
64
+ if (provider.failureHint)
65
+ console.error(` Hint: ${provider.failureHint}`);
66
+ });
67
+ child.on("exit", (code) => {
68
+ if (code !== null && code !== 0 && !foundUrl) {
69
+ console.error(` ${provider.name} exited with code ${code}`);
70
+ if (provider.failureHint)
71
+ console.error(` Hint: ${provider.failureHint}`);
72
+ }
73
+ });
74
+ attachCleanup(child);
75
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import express from "express";
3
+ import cors from "cors";
4
+ import eventsRouter from "./routes/events.js";
5
+ import coreRouter from "./routes/core.js";
6
+ import { CODEX_APP_SERVER_PORT, printServerBanner, stopCodexAppServer, } from "./startup/common.js";
7
+ import { removeInstancePidfile, writeInstancePidfile, } from "./startup/instance.js";
8
+ import { startExposeProvider } from "./expose/run.js";
9
+ import { installTimestampLogging } from "./logger.js";
10
+ // ── Config ─────────────────────────────────────────────
11
+ const PORT = parseInt(process.env.PORT ?? "3456", 10);
12
+ const TOKEN = process.env.BRIDGE_TOKEN ?? randomBytes(16).toString("hex");
13
+ // ── App ────────────────────────────────────────────────
14
+ const app = express();
15
+ app.use(cors());
16
+ app.use((req, res, next) => {
17
+ const startedAt = process.hrtime.bigint();
18
+ res.on("finish", () => {
19
+ const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
20
+ console.log(`[${req.ip}] ${res.statusCode} ${req.method} ${req.originalUrl} ${durationMs.toFixed(1)}ms`);
21
+ });
22
+ next();
23
+ });
24
+ app.use(express.json({ limit: "10mb" }));
25
+ // ── Auth middleware ────────────────────────────────────
26
+ function auth(req, res, next) {
27
+ const header = req.headers.authorization;
28
+ const queryToken = req.query.token;
29
+ const provided = header?.startsWith("Bearer ")
30
+ ? header.slice(7)
31
+ : queryToken;
32
+ if (provided !== TOKEN) {
33
+ console.warn(`[auth] 401 ${req.method} ${req.url} (ip=${req.ip})`);
34
+ res.status(401).json({ error: "Unauthorized" });
35
+ return;
36
+ }
37
+ next();
38
+ }
39
+ // ── Routes ─────────────────────────────────────────────
40
+ app.use("/api", auth, eventsRouter);
41
+ app.use("/api", auth, coreRouter);
42
+ // ── Start ──────────────────────────────────────────────
43
+ app.listen(PORT, "0.0.0.0", () => {
44
+ printServerBanner(PORT, TOKEN, process.env.PROJECT_DIR || process.cwd(), true);
45
+ try {
46
+ writeInstancePidfile({
47
+ port: PORT,
48
+ token: TOKEN,
49
+ cwd: process.env.PROJECT_DIR || process.cwd(),
50
+ codexAppServerPort: CODEX_APP_SERVER_PORT,
51
+ });
52
+ }
53
+ catch (err) {
54
+ console.error(`[server] WARN: failed to write instance pidfile: ${err?.message}`);
55
+ }
56
+ startExposeProvider(PORT, TOKEN);
57
+ installTimestampLogging();
58
+ });
59
+ // ── Process-level error handlers ──────────────────────
60
+ process.on("uncaughtException", (err) => {
61
+ console.error(`[server] UNCAUGHT EXCEPTION: ${err.message}\n${err.stack}`);
62
+ });
63
+ process.on("unhandledRejection", (reason) => {
64
+ console.error(`[server] UNHANDLED REJECTION: ${reason}`);
65
+ });
66
+ function shutdown() {
67
+ stopCodexAppServer();
68
+ removeInstancePidfile();
69
+ }
70
+ process.on("exit", shutdown);
71
+ process.on("SIGINT", () => {
72
+ shutdown();
73
+ process.exit(0);
74
+ });
75
+ process.on("SIGTERM", () => {
76
+ shutdown();
77
+ process.exit(0);
78
+ });
package/dist/logger.js ADDED
@@ -0,0 +1,44 @@
1
+ import { createWriteStream } from "node:fs";
2
+ import { format } from "node:util";
3
+ const methods = ["log", "info", "warn", "error", "debug"];
4
+ let installed = false;
5
+ let fileStream = null;
6
+ function ts() {
7
+ return new Date().toLocaleString("sv");
8
+ }
9
+ function getLogFilePath() {
10
+ const i = process.argv.indexOf("--log-file");
11
+ return i >= 0 ? process.argv[i + 1] : undefined;
12
+ }
13
+ /** Write directly to log file only (bypass console). Used by debugLog to
14
+ * always capture verbose output in the log file regardless of VERBOSE flag. */
15
+ export function writeToLogFile(...args) {
16
+ if (fileStream)
17
+ fileStream.write(format(`[${ts()}]`, ...args) + "\n");
18
+ }
19
+ /** Monkey-patch console.* to prepend an ISO timestamp. If --log-file is on
20
+ * argv, also tee every line to that file. */
21
+ export function installTimestampLogging() {
22
+ if (installed)
23
+ return;
24
+ installed = true;
25
+ const logFile = getLogFilePath();
26
+ if (logFile) {
27
+ try {
28
+ fileStream = createWriteStream(logFile, { flags: "a" });
29
+ console.log(`[server] Logging to ${logFile}`);
30
+ }
31
+ catch (err) {
32
+ console.error(`error: failed to open log file ${logFile}: ${err.message}`);
33
+ }
34
+ }
35
+ for (const m of methods) {
36
+ const orig = console[m].bind(console);
37
+ console[m] = (...args) => {
38
+ const stamp = `[${ts()}]`;
39
+ orig(stamp, ...args);
40
+ if (fileStream)
41
+ fileStream.write(format(stamp, ...args) + "\n");
42
+ };
43
+ }
44
+ }