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,290 @@
1
+ import { Router } from "express";
2
+ import { getDefaultProvider, isProvider, parseProvider, SUPPORTED_PROVIDERS, } from "../session.js";
3
+ import { broadcast, pushMessage, getMessages } from "./events.js";
4
+ import { createClaudeProvider } from "../claude/provider.js";
5
+ import { createCodexProvider } from "../codex/provider.js";
6
+ import { CodexAppServerClient } from "../codex/app-server.js";
7
+ import { debugLog } from "../debug.js";
8
+ import { CODEX_APP_SERVER_PORT, ensureCodexAppServerStarted, } from "../startup/common.js";
9
+ import { checkForUpdate, getCurrentAppVersion } from "../update.js";
10
+ const router = Router();
11
+ const emit = (sessionId, msg) => {
12
+ if (!sessionId)
13
+ return;
14
+ const id = pushMessage(sessionId, msg);
15
+ broadcast(sessionId, msg, id);
16
+ };
17
+ const STATUS_CHECK_COUNT = 10;
18
+ function toOneLineJson(value, maxLen = 1200) {
19
+ try {
20
+ const text = JSON.stringify(value);
21
+ if (!text)
22
+ return String(value);
23
+ return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
24
+ }
25
+ catch {
26
+ return String(value);
27
+ }
28
+ }
29
+ const codexClient = new CodexAppServerClient(`ws://127.0.0.1:${CODEX_APP_SERVER_PORT}`);
30
+ const claudeProvider = createClaudeProvider(emit);
31
+ const codexProvider = createCodexProvider(emit, () => codexClient);
32
+ const providerRegistry = {
33
+ claude: claudeProvider,
34
+ codex: codexProvider,
35
+ };
36
+ export { codexClient };
37
+ export function getProvider(name) {
38
+ const resolved = name ? parseProvider(name) : getDefaultProvider();
39
+ const handlers = providerRegistry[resolved];
40
+ if (!handlers)
41
+ throw new Error(`No handlers registered for provider "${resolved}"`);
42
+ return handlers;
43
+ }
44
+ router.use((req, res, next) => {
45
+ for (const provider of [
46
+ req.query.provider,
47
+ req.body?.provider,
48
+ ]) {
49
+ if (provider !== undefined && !isProvider(provider)) {
50
+ res.status(400).json({
51
+ error: `Unsupported provider "${String(provider)}". Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}`,
52
+ });
53
+ return;
54
+ }
55
+ }
56
+ next();
57
+ });
58
+ // GET /api/sessions — list resumable sessions
59
+ router.get("/sessions", async (req, res) => {
60
+ const providerName = req.query.provider;
61
+ const resolvedProvider = providerName
62
+ ? parseProvider(providerName)
63
+ : getDefaultProvider();
64
+ const cwd = req.query.cwd ||
65
+ (resolvedProvider === "codex" ? undefined : process.env.PROJECT_DIR);
66
+ const provider = getProvider(resolvedProvider);
67
+ const limit = Number(req.query.limit) || 10;
68
+ try {
69
+ const sessions = await provider.listSessions(limit, cwd);
70
+ await Promise.all(sessions.slice(0, STATUS_CHECK_COUNT).map(async (s, i) => {
71
+ if (s.status)
72
+ return;
73
+ sessions[i].status = await provider.getSessionStatus(s.id);
74
+ }));
75
+ res.json({ sessions });
76
+ }
77
+ catch (err) {
78
+ res.json({ sessions: [], error: err.message });
79
+ }
80
+ });
81
+ // GET /api/info — account, model, version, provider
82
+ router.get("/info", async (req, res) => {
83
+ const providerName = req.query.provider;
84
+ const provider = getProvider(providerName);
85
+ try {
86
+ const info = await provider.getInfo();
87
+ res.json(info);
88
+ }
89
+ catch (err) {
90
+ res.json({
91
+ account: {},
92
+ model: "Unknown",
93
+ version: "Unknown",
94
+ error: err.message,
95
+ });
96
+ }
97
+ });
98
+ // GET /api/update-check — current app version and latest npm version
99
+ router.get("/update-check", async (_req, res) => {
100
+ try {
101
+ res.json(await checkForUpdate());
102
+ }
103
+ catch (err) {
104
+ res.json({
105
+ ...getCurrentAppVersion(),
106
+ newestVersion: null,
107
+ updateAvailable: null,
108
+ error: err.message,
109
+ });
110
+ }
111
+ });
112
+ // POST /api/prompt — send a prompt to a session (create if needed)
113
+ router.post("/prompt", async (req, res) => {
114
+ const { text, sessionId, provider, cwd } = req.body ?? {};
115
+ console.log(`[prompt] sessionId=${sessionId ?? "(none)"} (provider=${provider}) text=${(text || "").slice(0, 80)}`);
116
+ if (!text || typeof text !== "string") {
117
+ console.warn("[prompt] rejected: missing text field");
118
+ res.status(400).json({ error: "Missing 'text' field" });
119
+ return;
120
+ }
121
+ try {
122
+ const p = provider || getDefaultProvider();
123
+ const targetProvider = getProvider(p);
124
+ const result = await targetProvider.prompt(sessionId, text, cwd);
125
+ res.status(202).json({
126
+ ok: true,
127
+ sessionId: result.sessionId,
128
+ provider: result.provider,
129
+ });
130
+ }
131
+ catch (err) {
132
+ console.error("[prompt] failed:", err.message);
133
+ const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
134
+ res.status(statusCode).json({ error: err.message });
135
+ }
136
+ });
137
+ // POST /api/permission-response
138
+ router.post("/permission-response", (req, res) => {
139
+ const { sessionId, decision, provider } = req.body ?? {};
140
+ console.log(`[permission-response] sessionId=${sessionId ?? "(none)"} provider=${provider ?? "(default)"} decision=${decision ?? "deny"}`);
141
+ debugLog("api", "permission-response body", toOneLineJson(req.body ?? {}));
142
+ if (!sessionId) {
143
+ res.status(400).json({ error: "Missing 'sessionId'" });
144
+ return;
145
+ }
146
+ const targetProvider = getProvider(provider);
147
+ if (!targetProvider.getStatus(sessionId)) {
148
+ res.status(404).json({ error: "Session not found" });
149
+ return;
150
+ }
151
+ targetProvider.respondPermission(sessionId, decision || "deny");
152
+ res.json({ ok: true });
153
+ });
154
+ // POST /api/question-response
155
+ router.post("/question-response", (req, res) => {
156
+ const { sessionId, answer, provider } = req.body ?? {};
157
+ console.log(`[question-response] sessionId=${sessionId ?? "(none)"} provider=${provider ?? "(default)"} answer=${String(answer ?? "skip").slice(0, 120)}`);
158
+ debugLog("api", "question-response body", toOneLineJson(req.body ?? {}));
159
+ if (!sessionId) {
160
+ res.status(400).json({ error: "Missing 'sessionId'" });
161
+ return;
162
+ }
163
+ const targetProvider = getProvider(provider);
164
+ if (!targetProvider.getStatus(sessionId)) {
165
+ res.status(404).json({ error: "Session not found" });
166
+ return;
167
+ }
168
+ targetProvider.respondQuestion(sessionId, answer || "skip");
169
+ res.json({ ok: true });
170
+ });
171
+ // POST /api/interrupt
172
+ router.post("/interrupt", (req, res) => {
173
+ const { sessionId, provider } = req.body ?? {};
174
+ if (!sessionId) {
175
+ res.status(400).json({ error: "Missing 'sessionId'" });
176
+ return;
177
+ }
178
+ const targetProvider = getProvider(provider);
179
+ if (!targetProvider.getStatus(sessionId)) {
180
+ res.status(404).json({ error: "Session not found" });
181
+ return;
182
+ }
183
+ targetProvider.interrupt(sessionId);
184
+ res.json({ ok: true });
185
+ });
186
+ // GET /api/status
187
+ router.get("/status", (req, res) => {
188
+ const sessionId = req.query.sessionId;
189
+ const providerName = req.query.provider;
190
+ if (!sessionId) {
191
+ res.status(400).json({ error: "Missing 'sessionId'" });
192
+ return;
193
+ }
194
+ const status = getProvider(providerName).getStatus(sessionId);
195
+ if (!status) {
196
+ res.status(404).json({ error: "Session not found" });
197
+ return;
198
+ }
199
+ res.json({
200
+ state: status.state,
201
+ sessionId,
202
+ provider: status.provider,
203
+ });
204
+ });
205
+ // GET /api/messages?sessionId=&after=
206
+ router.get("/messages", (req, res) => {
207
+ const after = parseInt(req.query.after) || 0;
208
+ const sessionId = req.query.sessionId;
209
+ const providerName = req.query.provider;
210
+ if (!sessionId) {
211
+ res.status(400).json({ error: "Missing 'sessionId'" });
212
+ return;
213
+ }
214
+ const status = getProvider(providerName).getStatus(sessionId);
215
+ const messages = getMessages(sessionId, after);
216
+ res.json({
217
+ messages,
218
+ state: status?.state ?? "idle",
219
+ sessionId,
220
+ provider: status?.provider ?? providerName ?? null,
221
+ });
222
+ });
223
+ // GET /api/debug/thread/:id — raw app-server / SDK output for debugging
224
+ router.get("/debug/thread/:id", async (req, res) => {
225
+ const id = req.params.id;
226
+ const provider = req.query.provider || getDefaultProvider();
227
+ try {
228
+ if (provider === "codex") {
229
+ const thread = await codexClient.threadRead(id, true);
230
+ res.json(thread);
231
+ }
232
+ else if (provider === "claude") {
233
+ const { getSessionMessages } = await import("@anthropic-ai/claude-agent-sdk");
234
+ const messages = await getSessionMessages(id);
235
+ res.json({ sessionId: id, messages });
236
+ }
237
+ else {
238
+ const history = await getProvider(provider).getHistory(id, 50);
239
+ res.json({ sessionId: id, messages: history });
240
+ }
241
+ }
242
+ catch (err) {
243
+ res.status(500).json({ error: err.message });
244
+ }
245
+ });
246
+ // GET /api/debug/status/:id — test status detection against real data
247
+ router.get("/debug/status/:id", async (req, res) => {
248
+ const id = req.params.id;
249
+ const providerName = req.query.provider || getDefaultProvider();
250
+ const provider = getProvider(providerName);
251
+ try {
252
+ const status = await provider.getSessionStatus(id);
253
+ res.json({ sessionId: id, provider: providerName, status });
254
+ }
255
+ catch (err) {
256
+ res.status(500).json({ error: err.message });
257
+ }
258
+ });
259
+ // GET /api/sessions/:id/history
260
+ router.get("/sessions/:id/history", async (req, res) => {
261
+ const id = req.params.id;
262
+ const limit = Math.min(parseInt(req.query.limit) || 10, 10);
263
+ const providerName = req.query.provider || getDefaultProvider();
264
+ const provider = getProvider(providerName);
265
+ try {
266
+ const history = await provider.getHistory(id, limit);
267
+ res.json({ history });
268
+ }
269
+ catch (err) {
270
+ res.json({ history: [], error: err.message });
271
+ }
272
+ });
273
+ // GET /api/metrics — codex subscription state for monitoring
274
+ router.get("/metrics", (_req, res) => {
275
+ const subscribed = codexProvider.getSubscribedSessions();
276
+ res.json({ codex: { subscribedSessions: subscribed } });
277
+ });
278
+ // POST /api/codex/ensure-app-server — wake the lazy codex app-server.
279
+ router.post("/codex/ensure-app-server", async (_req, res) => {
280
+ try {
281
+ const started = await ensureCodexAppServerStarted();
282
+ res.json({ started, port: CODEX_APP_SERVER_PORT });
283
+ }
284
+ catch (err) {
285
+ res
286
+ .status(500)
287
+ .json({ started: false, error: err?.message ?? String(err) });
288
+ }
289
+ });
290
+ export default router;
@@ -0,0 +1,104 @@
1
+ import { Router } from "express";
2
+ import { writeToLogFile } from "../logger.js";
3
+ const router = Router();
4
+ // ── Per-session message ring buffer + SSE clients ────
5
+ const MAX_MESSAGES_PER_SESSION = 500;
6
+ const sessions = new Map();
7
+ function getSession(sessionId) {
8
+ let s = sessions.get(sessionId);
9
+ if (!s) {
10
+ s = { messages: [], clients: new Set(), nextId: 1 };
11
+ sessions.set(sessionId, s);
12
+ }
13
+ return s;
14
+ }
15
+ export function pushMessage(sessionId, msg) {
16
+ const s = getSession(sessionId);
17
+ const id = s.nextId++;
18
+ s.messages.push({ id, msg });
19
+ if (s.messages.length > MAX_MESSAGES_PER_SESSION) {
20
+ s.messages.shift();
21
+ }
22
+ return id;
23
+ }
24
+ export function getMessages(sessionId, after) {
25
+ const s = sessions.get(sessionId);
26
+ if (!s)
27
+ return [];
28
+ return s.messages
29
+ .filter((m) => m.id > after)
30
+ .map((m) => ({ id: m.id, ...m.msg }));
31
+ }
32
+ export function broadcast(sessionId, msg, id) {
33
+ const s = sessions.get(sessionId);
34
+ const data = JSON.stringify(msg);
35
+ if (process.env.VERBOSE === "1") {
36
+ console.log(`[SSE-${sessionId}]: ${data}`);
37
+ }
38
+ else {
39
+ writeToLogFile(`[SSE-${sessionId}]: ${data}`);
40
+ }
41
+ if (!s || s.clients.size === 0)
42
+ return;
43
+ let deadCount = 0;
44
+ for (const res of s.clients) {
45
+ try {
46
+ res.write(`id: ${id}\ndata: ${data}\n\n`);
47
+ }
48
+ catch {
49
+ s.clients.delete(res);
50
+ deadCount++;
51
+ }
52
+ }
53
+ if (deadCount > 0) {
54
+ console.warn(`[sse] Removed ${deadCount} dead client(s) for session=${sessionId} (remaining: ${s.clients.size})`);
55
+ }
56
+ }
57
+ export function clientCount() {
58
+ let total = 0;
59
+ for (const s of sessions.values())
60
+ total += s.clients.size;
61
+ return total;
62
+ }
63
+ export function sessionHasClients(sessionId) {
64
+ const s = sessions.get(sessionId);
65
+ return !!s && s.clients.size > 0;
66
+ }
67
+ router.get("/events", (req, res) => {
68
+ const sessionId = req.query.sessionId;
69
+ if (!sessionId) {
70
+ res.status(400).json({ error: "Missing 'sessionId' query parameter" });
71
+ return;
72
+ }
73
+ res.setHeader("Content-Type", "text/event-stream");
74
+ res.setHeader("Cache-Control", "no-cache");
75
+ res.setHeader("Connection", "keep-alive");
76
+ res.setHeader("X-Accel-Buffering", "no");
77
+ res.flushHeaders();
78
+ res.write(":ok\n\n");
79
+ const s = getSession(sessionId);
80
+ if (req.query.needReplay === "true" && s.messages.length > 0) {
81
+ // Replay buffered messages for this session
82
+ for (const entry of s.messages) {
83
+ res.write(`id: ${entry.id}\ndata: ${JSON.stringify(entry.msg)}\n\n`);
84
+ }
85
+ }
86
+ s.clients.add(res);
87
+ console.log(`[sse] Client connected session=${sessionId} (session clients: ${s.clients.size}, total: ${clientCount()})`);
88
+ // Heartbeat every 15s
89
+ const heartbeat = setInterval(() => {
90
+ try {
91
+ res.write(":heartbeat\n\n");
92
+ }
93
+ catch {
94
+ clearInterval(heartbeat);
95
+ s.clients.delete(res);
96
+ }
97
+ }, 15000);
98
+ req.on("close", () => {
99
+ clearInterval(heartbeat);
100
+ s.clients.delete(res);
101
+ console.log(`[sse] Client disconnected (total: ${clientCount()})`);
102
+ });
103
+ });
104
+ export default router;
@@ -0,0 +1,18 @@
1
+ // ── Provider ────────────────────────────────────────────
2
+ // cursor/opencode are experimental and hidden in 0.8.1 — re-add them here to re-enable.
3
+ export const SUPPORTED_PROVIDERS = ["claude", "codex"];
4
+ export function isProvider(value) {
5
+ return typeof value === "string" && SUPPORTED_PROVIDERS.includes(value);
6
+ }
7
+ export function parseProvider(value, label = "provider") {
8
+ if (isProvider(value))
9
+ return value;
10
+ throw new Error(`Unsupported ${label} "${String(value)}". Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}`);
11
+ }
12
+ /** Global default provider, read once from env. */
13
+ export function getDefaultProvider() {
14
+ const env = process.env.DEFAULT_PROVIDER;
15
+ if (!env)
16
+ return "claude";
17
+ return parseProvider(env, "DEFAULT_PROVIDER");
18
+ }