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,97 @@
|
|
|
1
|
+
import { fileName, truncate } from "../summary-format.js";
|
|
2
|
+
/**
|
|
3
|
+
* Summarize Claude SDK tool calls for glasses display.
|
|
4
|
+
* Style: concise, single-line labels like VS Code Claude Code sidebar.
|
|
5
|
+
*/
|
|
6
|
+
export function summarizeClaudeToolCall(toolName, input) {
|
|
7
|
+
switch (toolName) {
|
|
8
|
+
case "Bash": {
|
|
9
|
+
const description = truncate(input.description ?? input.command ?? "command", 50);
|
|
10
|
+
return `Bash ${description}`;
|
|
11
|
+
}
|
|
12
|
+
case "Read": {
|
|
13
|
+
const name = fileName(input.file_path);
|
|
14
|
+
const offset = input.offset;
|
|
15
|
+
const limit = input.limit;
|
|
16
|
+
if (offset !== undefined && limit !== undefined)
|
|
17
|
+
return `Read ${name} (lines ${offset}-${offset + limit})`;
|
|
18
|
+
if (limit !== undefined)
|
|
19
|
+
return `Read ${name} (${limit} lines)`;
|
|
20
|
+
return `Read ${name}`;
|
|
21
|
+
}
|
|
22
|
+
case "Edit": {
|
|
23
|
+
const name = fileName(input.file_path);
|
|
24
|
+
const newStr = input.new_string;
|
|
25
|
+
const oldStr = input.old_string;
|
|
26
|
+
const added = newStr ? newStr.split("\n").length : 0;
|
|
27
|
+
const removed = oldStr ? oldStr.split("\n").length : 0;
|
|
28
|
+
const delta = added - removed;
|
|
29
|
+
if (delta > 0)
|
|
30
|
+
return `Edit ${name} +${delta} lines`;
|
|
31
|
+
if (delta < 0)
|
|
32
|
+
return `Edit ${name} ${delta} lines`;
|
|
33
|
+
return `Edit ${name} ~${added} lines`;
|
|
34
|
+
}
|
|
35
|
+
case "Write": {
|
|
36
|
+
const name = fileName(input.file_path);
|
|
37
|
+
const lines = input.content ? input.content.split("\n").length : 0;
|
|
38
|
+
return `Write ${name} (${lines} lines)`;
|
|
39
|
+
}
|
|
40
|
+
case "Glob":
|
|
41
|
+
return `Glob ${truncate(input.pattern, 40)}`;
|
|
42
|
+
case "Grep":
|
|
43
|
+
return `Grep "${truncate(input.pattern, 25)}"`;
|
|
44
|
+
case "Agent":
|
|
45
|
+
return `Agent ${truncate(input.description ?? "", 40)}`;
|
|
46
|
+
case "Skill":
|
|
47
|
+
return `Skill ${truncate(String(input.skill ?? input.name ?? ""), 40)}`.trim();
|
|
48
|
+
case "TodoWrite":
|
|
49
|
+
return "TodoWrite update tasks";
|
|
50
|
+
case "WebSearch":
|
|
51
|
+
return `Search "${truncate(input.query ?? "", 30)}"`;
|
|
52
|
+
case "WebFetch":
|
|
53
|
+
return `Fetch ${truncate(input.url ?? "", 40)}`;
|
|
54
|
+
case "ToolSearch":
|
|
55
|
+
return `ToolSearch ${truncate(input.query, 40)}`;
|
|
56
|
+
case "KillShell":
|
|
57
|
+
return `Kill process ${input.pid ?? ""}`.trim();
|
|
58
|
+
case "Config":
|
|
59
|
+
return `Config ${input.action ?? ""} ${truncate(String(input.key ?? ""), 30)}`.trim();
|
|
60
|
+
case "Mcp":
|
|
61
|
+
return `MCP ${input.server_name ?? ""}.${truncate(String(input.tool_name ?? ""), 30)}`;
|
|
62
|
+
case "RemoteTrigger":
|
|
63
|
+
return `Trigger ${input.action ?? "manage"}`;
|
|
64
|
+
case "NotebookEdit":
|
|
65
|
+
return `NotebookEdit ${input.command ?? "edit"}`;
|
|
66
|
+
case "ExitPlanMode":
|
|
67
|
+
return "ExitPlanMode";
|
|
68
|
+
case "ListMcpResources":
|
|
69
|
+
return "ListMcpResources";
|
|
70
|
+
case "ReadMcpResource":
|
|
71
|
+
return `ReadMcpResource ${truncate(String(input.uri ?? ""), 40)}`;
|
|
72
|
+
case "TaskOutput":
|
|
73
|
+
return `Agent ${truncate(String(input.task_id ?? ""), 30)}`.trim();
|
|
74
|
+
case "TaskCreate":
|
|
75
|
+
return `Agent ${truncate(String(input.subject ?? input.description ?? ""), 40)}`;
|
|
76
|
+
case "TaskUpdate":
|
|
77
|
+
return `Agent ${truncate(String(input.subject ?? ""), 40)} ${input.status ?? ""}`.trim();
|
|
78
|
+
case "TaskGet":
|
|
79
|
+
case "TaskList":
|
|
80
|
+
case "TaskStop":
|
|
81
|
+
return `Agent ${toolName.replace("Task", "").toLowerCase()}`;
|
|
82
|
+
case "AskUserQuestion":
|
|
83
|
+
return "AskUserQuestion";
|
|
84
|
+
default: {
|
|
85
|
+
const displayName = toolName.startsWith("Task") ? "Agent" : toolName;
|
|
86
|
+
const detail = input.subject ||
|
|
87
|
+
input.description ||
|
|
88
|
+
input.content ||
|
|
89
|
+
input.taskId ||
|
|
90
|
+
input.action ||
|
|
91
|
+
"";
|
|
92
|
+
return detail
|
|
93
|
+
? `${displayName} ${truncate(String(detail), 40)}`
|
|
94
|
+
: displayName;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { connect as netConnect } from "node:net";
|
|
7
|
+
import { request as httpRequest } from "node:http";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import yargs from "yargs";
|
|
10
|
+
import { hideBin } from "yargs/helpers";
|
|
11
|
+
import { getExposeProviderNames } from "./expose/registry.js";
|
|
12
|
+
import { SUPPORTED_PROVIDERS } from "./session.js";
|
|
13
|
+
import { spawnSyncShim } from "./util/spawn-shim.js";
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf8"));
|
|
16
|
+
const INSTANCE_DIR = join(homedir(), ".ever-terminal", "instances");
|
|
17
|
+
const exposeProviderNames = getExposeProviderNames();
|
|
18
|
+
const exposeProviderList = exposeProviderNames.join(", ");
|
|
19
|
+
const providerNames = [...SUPPORTED_PROVIDERS];
|
|
20
|
+
const optionDefinitions = {
|
|
21
|
+
port: {
|
|
22
|
+
alias: "p",
|
|
23
|
+
type: "number",
|
|
24
|
+
describe: "Server port",
|
|
25
|
+
default: 3456,
|
|
26
|
+
},
|
|
27
|
+
token: {
|
|
28
|
+
alias: "t",
|
|
29
|
+
type: "string",
|
|
30
|
+
describe: "Auth token (default: auto-generated)",
|
|
31
|
+
},
|
|
32
|
+
name: {
|
|
33
|
+
alias: "n",
|
|
34
|
+
type: "string",
|
|
35
|
+
describe: "Client display name",
|
|
36
|
+
},
|
|
37
|
+
cwd: {
|
|
38
|
+
alias: "d",
|
|
39
|
+
type: "string",
|
|
40
|
+
describe: "Project directory (where Claude Code sessions live)",
|
|
41
|
+
},
|
|
42
|
+
provider: {
|
|
43
|
+
type: "string",
|
|
44
|
+
choices: providerNames,
|
|
45
|
+
describe: "Default AI provider",
|
|
46
|
+
},
|
|
47
|
+
tailscale: {
|
|
48
|
+
type: "boolean",
|
|
49
|
+
describe: "Use Tailscale IPv4 address instead of LAN",
|
|
50
|
+
},
|
|
51
|
+
interface: {
|
|
52
|
+
alias: ["i", "if"],
|
|
53
|
+
type: "string",
|
|
54
|
+
describe: "Use the IPv4 address of the named network interface",
|
|
55
|
+
},
|
|
56
|
+
expose: {
|
|
57
|
+
type: "string",
|
|
58
|
+
array: true,
|
|
59
|
+
requiresArg: true,
|
|
60
|
+
choices: exposeProviderNames,
|
|
61
|
+
describe: `Quick public expose provider (${exposeProviderList})`,
|
|
62
|
+
},
|
|
63
|
+
"log-file": {
|
|
64
|
+
type: "string",
|
|
65
|
+
describe: "Tee all logs to a file (default: ./ever-terminal-<ts>.log)",
|
|
66
|
+
},
|
|
67
|
+
verbose: {
|
|
68
|
+
type: "boolean",
|
|
69
|
+
describe: "Print raw SDK messages for debugging",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
function registerCompletionOptions(command) {
|
|
73
|
+
for (const [name, option] of Object.entries(optionDefinitions)) {
|
|
74
|
+
const alias = Array.isArray(option.alias) ? option.alias[0] : option.alias;
|
|
75
|
+
if (option.choices) {
|
|
76
|
+
command.option(name, option.describe, (done) => {
|
|
77
|
+
for (const value of option.choices)
|
|
78
|
+
done(value, value);
|
|
79
|
+
}, alias);
|
|
80
|
+
}
|
|
81
|
+
else if (option.type === "boolean") {
|
|
82
|
+
command.option(name, option.describe, alias);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
command.option(name, option.describe, () => { }, alias);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function registerYargsOptions(parser) {
|
|
90
|
+
let next = parser;
|
|
91
|
+
for (const [name, option] of Object.entries(optionDefinitions)) {
|
|
92
|
+
next = next.option(name, option);
|
|
93
|
+
}
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
96
|
+
function registerCompletionCommands(root) {
|
|
97
|
+
const start = root.command("start", "Start the server (default)");
|
|
98
|
+
const complete = root.command("complete", "Print shell completion script for bash, zsh, fish, or powershell");
|
|
99
|
+
registerCompletionOptions(root);
|
|
100
|
+
registerCompletionOptions(start);
|
|
101
|
+
complete.argument("shell", (done) => {
|
|
102
|
+
done("bash", "Bash shell");
|
|
103
|
+
done("zsh", "Zsh shell");
|
|
104
|
+
done("fish", "Fish shell");
|
|
105
|
+
done("powershell", "PowerShell");
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async function runCompletion(shell, forwardedArgs = []) {
|
|
109
|
+
const t = (await import("@bomb.sh/tab")).default;
|
|
110
|
+
registerCompletionCommands(t);
|
|
111
|
+
if (shell === "--") {
|
|
112
|
+
t.parse(forwardedArgs);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
t.setup("ever-terminal", "ever-terminal", shell);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ── Completion (bash/zsh/fish/powershell) ────────────
|
|
119
|
+
// Handled before yargs so "complete" isn't rejected by strict mode.
|
|
120
|
+
const rawArgs = process.argv.slice(2);
|
|
121
|
+
if (rawArgs[0] === "complete") {
|
|
122
|
+
await runCompletion(rawArgs[1], rawArgs.slice(2));
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
if (rawArgs[0] === "codex") {
|
|
126
|
+
await runCodex(rawArgs.slice(1));
|
|
127
|
+
// unreachable — runCodex calls process.exit
|
|
128
|
+
}
|
|
129
|
+
// ── CLI ──────────────────────────────────────────────
|
|
130
|
+
registerYargsOptions(yargs(hideBin(process.argv))
|
|
131
|
+
.scriptName("ever-terminal")
|
|
132
|
+
.help("help").alias("h", "help")
|
|
133
|
+
.version("version", "Show version number", pkg.version).alias("v", "version")
|
|
134
|
+
.usage("$0 [command] [options]")
|
|
135
|
+
.command("start", "Start the server (default)", {}, run)
|
|
136
|
+
.command("complete <shell>", "Print shell completion script for bash, zsh, fish, or powershell", (command) => command.positional("shell", {
|
|
137
|
+
describe: "Target shell (bash, zsh, fish, powershell)",
|
|
138
|
+
choices: ["bash", "zsh", "fish", "powershell"],
|
|
139
|
+
type: "string",
|
|
140
|
+
}), async (argv) => {
|
|
141
|
+
await runCompletion(argv.shell);
|
|
142
|
+
})
|
|
143
|
+
.command("$0", false, {}, run))
|
|
144
|
+
.check((argv) => {
|
|
145
|
+
if (argv.tailscale && argv.interface) {
|
|
146
|
+
throw new Error("--tailscale and --interface are mutually exclusive");
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(argv.expose) && argv.expose.length > 1) {
|
|
149
|
+
throw new Error("only one --expose provider may be specified");
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
})
|
|
153
|
+
.group(["tailscale", "interface"], "Local network options:")
|
|
154
|
+
.group(["expose"], "Quick public expose options:")
|
|
155
|
+
.strict()
|
|
156
|
+
.example("ever-terminal", "Start with defaults")
|
|
157
|
+
.example("ever-terminal -p 8080", "Start on port 8080")
|
|
158
|
+
.example("ever-terminal -t mytoken123", "Start with a fixed token")
|
|
159
|
+
.example("npx ever-terminal", "Run without installing")
|
|
160
|
+
.example("ever-terminal --expose pinggy", "Start with a quick public expose helper")
|
|
161
|
+
.epilogue("Quick public expose helpers are intended for simple temporary sharing, not long-term use.\n" +
|
|
162
|
+
"For stable setups, prefer a proper network path such as Tailscale or a production tunnel configuration.\n\n" +
|
|
163
|
+
"Shell completion (example usage):\n" +
|
|
164
|
+
" source <(ever-terminal complete zsh)\n" +
|
|
165
|
+
" source <(ever-terminal complete bash)\n" +
|
|
166
|
+
" ever-terminal complete fish > ~/.config/fish/completions/ever-terminal.fish\n" +
|
|
167
|
+
" ever-terminal complete powershell >> $PROFILE")
|
|
168
|
+
.parse();
|
|
169
|
+
async function run(argv) {
|
|
170
|
+
// Set env vars from flags before importing the server
|
|
171
|
+
if (argv.port !== 3456)
|
|
172
|
+
process.env.PORT = String(argv.port);
|
|
173
|
+
if (argv.token)
|
|
174
|
+
process.env.BRIDGE_TOKEN = argv.token;
|
|
175
|
+
if (argv.name)
|
|
176
|
+
process.env.EVER_TERMINAL_NAME = argv.name;
|
|
177
|
+
if (argv.cwd)
|
|
178
|
+
process.env.PROJECT_DIR = resolve(argv.cwd);
|
|
179
|
+
if (argv.provider)
|
|
180
|
+
process.env.DEFAULT_PROVIDER = argv.provider;
|
|
181
|
+
if (argv.verbose)
|
|
182
|
+
process.env.VERBOSE = "1";
|
|
183
|
+
{
|
|
184
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
185
|
+
const filename = argv.logFile || `ever-terminal-${stamp}.log`;
|
|
186
|
+
process.argv.push("--log-file", resolve(filename));
|
|
187
|
+
}
|
|
188
|
+
if (argv.tailscale) {
|
|
189
|
+
process.env.EVER_HOST_MODE = "tailscale";
|
|
190
|
+
}
|
|
191
|
+
else if (argv.interface) {
|
|
192
|
+
process.env.EVER_HOST_MODE = "interface";
|
|
193
|
+
process.env.EVER_HOST_INTERFACE = argv.interface;
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(argv.expose) && argv.expose[0]) {
|
|
196
|
+
process.env.EVER_TERMINAL_EXPOSE_PROVIDER = argv.expose[0];
|
|
197
|
+
}
|
|
198
|
+
// Boot the server
|
|
199
|
+
await import("./index.js");
|
|
200
|
+
}
|
|
201
|
+
function isPidAlive(pid) {
|
|
202
|
+
try {
|
|
203
|
+
process.kill(pid, 0);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
return err && err.code === "EPERM";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function listLiveInstances() {
|
|
211
|
+
let entries;
|
|
212
|
+
try {
|
|
213
|
+
entries = readdirSync(INSTANCE_DIR);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
if (err && err.code === "ENOENT")
|
|
217
|
+
return [];
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
const live = [];
|
|
221
|
+
for (const name of entries) {
|
|
222
|
+
if (!name.endsWith(".json"))
|
|
223
|
+
continue;
|
|
224
|
+
const path = join(INSTANCE_DIR, name);
|
|
225
|
+
let info;
|
|
226
|
+
try {
|
|
227
|
+
info = JSON.parse(readFileSync(path, "utf8"));
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
try {
|
|
231
|
+
unlinkSync(path);
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (typeof info.pid !== "number" || !isPidAlive(info.pid)) {
|
|
237
|
+
try {
|
|
238
|
+
unlinkSync(path);
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
live.push(info);
|
|
244
|
+
}
|
|
245
|
+
live.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
|
|
246
|
+
return live;
|
|
247
|
+
}
|
|
248
|
+
function formatAge(startedAt) {
|
|
249
|
+
const secs = Math.max(0, Math.floor((Date.now() - (startedAt ?? Date.now())) / 1000));
|
|
250
|
+
if (secs < 60)
|
|
251
|
+
return `${secs}s`;
|
|
252
|
+
const mins = Math.floor(secs / 60);
|
|
253
|
+
if (mins < 60)
|
|
254
|
+
return `${mins}m`;
|
|
255
|
+
const hours = Math.floor(mins / 60);
|
|
256
|
+
if (hours < 24)
|
|
257
|
+
return `${hours}h`;
|
|
258
|
+
return `${Math.floor(hours / 24)}d`;
|
|
259
|
+
}
|
|
260
|
+
function promptChoice(instances) {
|
|
261
|
+
return new Promise((resolveChoice, reject) => {
|
|
262
|
+
process.stderr.write("Multiple ever-terminal servers are running. Pick one to wake codex through:\n");
|
|
263
|
+
instances.forEach((inst, i) => {
|
|
264
|
+
process.stderr.write(` [${i + 1}] pid=${inst.pid} port=${inst.port} codex-port=${inst.codexAppServerPort} ` +
|
|
265
|
+
`age=${formatAge(inst.startedAt)} cwd=${inst.cwd}\n`);
|
|
266
|
+
});
|
|
267
|
+
if (!process.stdin.isTTY) {
|
|
268
|
+
reject(new Error("stdin is not a TTY; cannot prompt. Stop all but one ever-terminal server and retry."));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
272
|
+
rl.question(`Choice [1-${instances.length}]: `, (answer) => {
|
|
273
|
+
rl.close();
|
|
274
|
+
const idx = parseInt(answer.trim(), 10);
|
|
275
|
+
if (!Number.isFinite(idx) || idx < 1 || idx > instances.length) {
|
|
276
|
+
reject(new Error(`Invalid choice "${answer.trim()}"`));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
resolveChoice(instances[idx - 1]);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function postEnsureAppServer(instance, timeoutMs = 8000) {
|
|
284
|
+
return new Promise((resolveReq, reject) => {
|
|
285
|
+
const req = httpRequest({
|
|
286
|
+
host: "127.0.0.1",
|
|
287
|
+
port: instance.port,
|
|
288
|
+
path: "/api/codex/ensure-app-server",
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: {
|
|
291
|
+
"Authorization": `Bearer ${instance.token}`,
|
|
292
|
+
"Content-Length": "0",
|
|
293
|
+
},
|
|
294
|
+
timeout: timeoutMs,
|
|
295
|
+
}, (res) => {
|
|
296
|
+
let body = "";
|
|
297
|
+
res.setEncoding("utf8");
|
|
298
|
+
res.on("data", (chunk) => { body += chunk; });
|
|
299
|
+
res.on("end", () => {
|
|
300
|
+
if (res.statusCode !== 200) {
|
|
301
|
+
reject(new Error(`HTTP ${res.statusCode}: ${body.slice(0, 200)}`));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
resolveReq(JSON.parse(body));
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
reject(new Error(`bad JSON from ensure-app-server: ${err.message}`));
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
req.on("timeout", () => { req.destroy(new Error("request timed out")); });
|
|
313
|
+
req.on("error", reject);
|
|
314
|
+
req.end();
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function probePort(port, timeoutMs = 500) {
|
|
318
|
+
return new Promise((resolveProbe) => {
|
|
319
|
+
const socket = netConnect({ host: "127.0.0.1", port, timeout: timeoutMs });
|
|
320
|
+
const done = (ok) => {
|
|
321
|
+
socket.removeAllListeners();
|
|
322
|
+
socket.destroy();
|
|
323
|
+
resolveProbe(ok);
|
|
324
|
+
};
|
|
325
|
+
socket.once("connect", () => done(true));
|
|
326
|
+
socket.once("timeout", () => done(false));
|
|
327
|
+
socket.once("error", () => done(false));
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
async function waitForPort(port, totalMs = 6000) {
|
|
331
|
+
const deadline = Date.now() + totalMs;
|
|
332
|
+
while (Date.now() < deadline) {
|
|
333
|
+
if (await probePort(port))
|
|
334
|
+
return true;
|
|
335
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
async function runCodex(extraArgs) {
|
|
340
|
+
const cwd = process.cwd();
|
|
341
|
+
const envPortRaw = process.env.CODEX_APP_SERVER_PORT;
|
|
342
|
+
const envPort = envPortRaw ? parseInt(envPortRaw, 10) : null;
|
|
343
|
+
let codexPort = envPort ?? 8765;
|
|
344
|
+
if (!(await probePort(codexPort))) {
|
|
345
|
+
let instances;
|
|
346
|
+
try {
|
|
347
|
+
instances = listLiveInstances();
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
console.error(`[codex] WARN: failed to read instance dir: ${err.message}`);
|
|
351
|
+
instances = [];
|
|
352
|
+
}
|
|
353
|
+
if (envPort != null) {
|
|
354
|
+
instances = instances.filter((i) => i.codexAppServerPort === envPort);
|
|
355
|
+
}
|
|
356
|
+
if (instances.length === 0) {
|
|
357
|
+
console.error("[codex] no running ever-terminal server found to wake the codex app-server.\n" +
|
|
358
|
+
"[codex] start one in another shell (e.g. `ever-terminal`) and retry.");
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
let chosen;
|
|
362
|
+
if (instances.length === 1) {
|
|
363
|
+
chosen = instances[0];
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
try {
|
|
367
|
+
chosen = await promptChoice(instances);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
console.error(`[codex] ${err.message}`);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const result = await postEnsureAppServer(chosen);
|
|
376
|
+
if (!result.started) {
|
|
377
|
+
console.error(`[codex] server reported codex app-server failed to start ` +
|
|
378
|
+
`(see ever-terminal logs on pid ${chosen.pid})`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
codexPort = result.port ?? chosen.codexAppServerPort ?? codexPort;
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
console.error(`[codex] failed to signal ever-terminal server pid=${chosen.pid} ` +
|
|
385
|
+
`on port ${chosen.port}: ${err.message}`);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
if (!(await waitForPort(codexPort))) {
|
|
389
|
+
console.error(`[codex] codex app-server did not become reachable on port ${codexPort} within 6s`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const wsUrl = `ws://127.0.0.1:${codexPort}`;
|
|
394
|
+
const args = hasCodexCwdArg(extraArgs)
|
|
395
|
+
? ["--remote", wsUrl, ...extraArgs]
|
|
396
|
+
: ["--remote", wsUrl, "-C", cwd, ...extraArgs];
|
|
397
|
+
const result = spawnSyncShim("codex", args, {
|
|
398
|
+
stdio: "inherit",
|
|
399
|
+
env: process.env,
|
|
400
|
+
cwd,
|
|
401
|
+
});
|
|
402
|
+
if (result.error) {
|
|
403
|
+
process.stderr.write(`ever-terminal: failed to launch codex: ${result.error.message}\n`);
|
|
404
|
+
}
|
|
405
|
+
process.exit(result.status ?? 1);
|
|
406
|
+
}
|
|
407
|
+
function hasCodexCwdArg(args) {
|
|
408
|
+
for (let i = 0; i < args.length; i++) {
|
|
409
|
+
const arg = args[i];
|
|
410
|
+
if (arg === "-C" || arg === "--cd" || arg.startsWith("--cd="))
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|