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.
- package/LICENSE +21 -0
- package/README.md +266 -0
- package/dist/claude/provider.js +234 -0
- package/dist/claude/session.js +719 -0
- package/dist/claude/summarize.js +97 -0
- package/dist/cli.js +414 -0
- package/dist/codex/app-server.js +300 -0
- package/dist/codex/memory.js +61 -0
- package/dist/codex/provider.js +362 -0
- package/dist/codex/session.js +1091 -0
- package/dist/codex/status.js +16 -0
- package/dist/codex/storage.js +83 -0
- package/dist/codex/summarize.js +69 -0
- package/dist/debug.js +9 -0
- package/dist/expose/providers/bore.js +14 -0
- package/dist/expose/providers/ngrok.js +35 -0
- package/dist/expose/providers/pinggy.js +22 -0
- package/dist/expose/registry.js +22 -0
- package/dist/expose/run.js +75 -0
- package/dist/expose/types.js +1 -0
- package/dist/index.js +78 -0
- package/dist/logger.js +44 -0
- package/dist/routes/core.js +290 -0
- package/dist/routes/events.js +104 -0
- package/dist/session.js +18 -0
- package/dist/startup/common.js +318 -0
- package/dist/startup/instance.js +89 -0
- package/dist/summary-format.js +17 -0
- package/dist/types.js +1 -0
- package/dist/update.js +56 -0
- package/dist/util/spawn-shim.js +25 -0
- package/package.json +79 -0
|
@@ -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;
|
package/dist/session.js
ADDED
|
@@ -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
|
+
}
|