@infomity/remogent 0.0.0-stage → 0.2.1
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 +202 -0
- package/README.md +225 -3
- package/dist/adapters/claude.js +518 -0
- package/dist/adapters/codex.js +510 -0
- package/dist/adapters/gemini.js +287 -0
- package/dist/adapters/rpc.js +83 -0
- package/dist/adapters/util.js +90 -0
- package/dist/firewall.js +75 -0
- package/dist/identity.js +56 -0
- package/dist/index.js +416 -0
- package/dist/logfile.js +39 -0
- package/dist/port.js +19 -0
- package/dist/protocol.js +142 -0
- package/dist/server.js +170 -0
- package/dist/service.js +244 -0
- package/dist/sessions.js +482 -0
- package/dist/store.js +103 -0
- package/dist/title.js +43 -0
- package/package.json +63 -4
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { RpcError, RpcPeer } from "./rpc.js";
|
|
3
|
+
import { findOnPath, killTree, spawnCli } from "./util.js";
|
|
4
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5
|
+
const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
|
|
6
|
+
/** Cap on tool output stored in a transcript event. */
|
|
7
|
+
const MAX_OUTPUT = 64 * 1024;
|
|
8
|
+
const cap = (s) => (s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) : s);
|
|
9
|
+
/** If Codex has not reported the end of an interrupted turn after this long, end it ourselves so the app unlocks. */
|
|
10
|
+
const INTERRUPT_GRACE_MS = 15_000;
|
|
11
|
+
/**
|
|
12
|
+
* Remogent permission mode -> Codex approval policy + sandbox.
|
|
13
|
+
* ask: everything except known read-only commands needs approval; plan: read-only and never ask (it cannot change
|
|
14
|
+
* anything); auto_edit: edits inside the workspace run, anything outside the sandbox asks; auto: no sandbox, no prompts.
|
|
15
|
+
*/
|
|
16
|
+
const POLICIES = {
|
|
17
|
+
ask: { approvalPolicy: "untrusted", sandbox: "workspace-write" },
|
|
18
|
+
plan: { approvalPolicy: "never", sandbox: "read-only" },
|
|
19
|
+
auto_edit: { approvalPolicy: "on-request", sandbox: "workspace-write" },
|
|
20
|
+
auto: { approvalPolicy: "never", sandbox: "danger-full-access" },
|
|
21
|
+
};
|
|
22
|
+
/** Path of the `codex` executable if it is installed on this host. */
|
|
23
|
+
export function findCodex() {
|
|
24
|
+
return findOnPath("codex");
|
|
25
|
+
}
|
|
26
|
+
const stripAnsi = (s) => s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
27
|
+
function lastLine(s) {
|
|
28
|
+
const lines = stripAnsi(s).trim().split(/\r?\n/);
|
|
29
|
+
return lines[lines.length - 1] ?? "";
|
|
30
|
+
}
|
|
31
|
+
// ---- app-server -> Remogent mapping ----
|
|
32
|
+
function actionRisk(actions) {
|
|
33
|
+
if (!Array.isArray(actions) || actions.length === 0)
|
|
34
|
+
return "shell";
|
|
35
|
+
const readOnly = actions.every((a) => isObj(a) && (a["type"] === "read" || a["type"] === "listFiles" || a["type"] === "search"));
|
|
36
|
+
return readOnly ? "read" : "shell";
|
|
37
|
+
}
|
|
38
|
+
function commandTitle(item) {
|
|
39
|
+
const acts = Array.isArray(item["commandActions"]) ? item["commandActions"] : [];
|
|
40
|
+
const cmds = acts.map((a) => (isObj(a) ? str(a["command"]) : undefined)).filter((c) => !!c);
|
|
41
|
+
const t = cmds.length ? cmds.join(" ; ") : (str(item["command"]) ?? "command");
|
|
42
|
+
return t.length > 200 ? t.slice(0, 200) + "..." : t;
|
|
43
|
+
}
|
|
44
|
+
function changePaths(changes) {
|
|
45
|
+
if (!Array.isArray(changes))
|
|
46
|
+
return [];
|
|
47
|
+
return changes.map((c) => (isObj(c) ? str(c["path"]) : undefined)).filter((p) => !!p);
|
|
48
|
+
}
|
|
49
|
+
function changesDiff(changes) {
|
|
50
|
+
if (!Array.isArray(changes))
|
|
51
|
+
return undefined;
|
|
52
|
+
const parts = [];
|
|
53
|
+
for (const c of changes) {
|
|
54
|
+
if (!isObj(c))
|
|
55
|
+
continue;
|
|
56
|
+
const path = str(c["path"]);
|
|
57
|
+
if (!path)
|
|
58
|
+
continue;
|
|
59
|
+
const kind = isObj(c["kind"]) ? str(c["kind"]["type"]) : undefined;
|
|
60
|
+
const diff = typeof c["diff"] === "string" ? c["diff"] : "";
|
|
61
|
+
parts.push(`${kind ?? "change"} ${path}\n${diff}`.trimEnd());
|
|
62
|
+
}
|
|
63
|
+
return parts.length ? cap(parts.join("\n\n")) : undefined;
|
|
64
|
+
}
|
|
65
|
+
function toolStart(item) {
|
|
66
|
+
const toolCallId = str(item["id"]);
|
|
67
|
+
if (!toolCallId)
|
|
68
|
+
return undefined;
|
|
69
|
+
const titled = (title) => (title ? { title } : {});
|
|
70
|
+
switch (item["type"]) {
|
|
71
|
+
case "commandExecution":
|
|
72
|
+
return {
|
|
73
|
+
kind: "tool_call",
|
|
74
|
+
toolCallId,
|
|
75
|
+
name: "shell",
|
|
76
|
+
title: commandTitle(item),
|
|
77
|
+
risk: actionRisk(item["commandActions"]),
|
|
78
|
+
input: { command: item["command"], cwd: item["cwd"] },
|
|
79
|
+
};
|
|
80
|
+
case "fileChange": {
|
|
81
|
+
const paths = changePaths(item["changes"]);
|
|
82
|
+
return { kind: "tool_call", toolCallId, name: "edit", title: paths.length ? `Edit ${paths.join(", ")}` : "Edit files", risk: "edit", input: { files: paths } };
|
|
83
|
+
}
|
|
84
|
+
case "mcpToolCall": {
|
|
85
|
+
const name = [str(item["server"]), str(item["tool"])].filter(Boolean).join(".");
|
|
86
|
+
return { kind: "tool_call", toolCallId, name: name || "mcp", risk: "other", input: item["arguments"] ?? {} };
|
|
87
|
+
}
|
|
88
|
+
case "webSearch":
|
|
89
|
+
return { kind: "tool_call", toolCallId, name: "web_search", ...titled(str(item["query"])), risk: "network", input: { query: item["query"] } };
|
|
90
|
+
case "dynamicToolCall":
|
|
91
|
+
return { kind: "tool_call", toolCallId, name: str(item["tool"]) ?? "tool", risk: "other", input: item["arguments"] ?? {} };
|
|
92
|
+
default:
|
|
93
|
+
return undefined; // userMessage, agentMessage, reasoning, plan, ...
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function toolEnd(item) {
|
|
97
|
+
const toolCallId = str(item["id"]);
|
|
98
|
+
if (!toolCallId)
|
|
99
|
+
return undefined;
|
|
100
|
+
const type = item["type"];
|
|
101
|
+
if (type !== "commandExecution" && type !== "fileChange" && type !== "mcpToolCall" && type !== "webSearch" && type !== "dynamicToolCall") {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const s = str(item["status"]);
|
|
105
|
+
const declined = s === "declined";
|
|
106
|
+
const status = s === "completed" ? "completed" : s === "failed" || declined ? "failed" : "running";
|
|
107
|
+
let output;
|
|
108
|
+
let diff;
|
|
109
|
+
if (type === "commandExecution")
|
|
110
|
+
output = str(item["aggregatedOutput"]);
|
|
111
|
+
else if (type === "fileChange")
|
|
112
|
+
diff = changesDiff(item["changes"]);
|
|
113
|
+
else if (type === "mcpToolCall") {
|
|
114
|
+
const r = item["error"] ?? item["result"];
|
|
115
|
+
if (r !== undefined && r !== null)
|
|
116
|
+
output = cap(typeof r === "string" ? r : JSON.stringify(r));
|
|
117
|
+
}
|
|
118
|
+
if (!output && declined)
|
|
119
|
+
output = "Declined.";
|
|
120
|
+
return {
|
|
121
|
+
kind: "tool_call_update",
|
|
122
|
+
toolCallId,
|
|
123
|
+
status: type === "webSearch" && status === "running" ? "completed" : status,
|
|
124
|
+
...(output ? { output: cap(output) } : {}),
|
|
125
|
+
...(diff ? { diff } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Reads the `data[]` of `model/list` (verified on codex-cli 0.169.0): `{ id, model, displayName, description, hidden, isDefault,
|
|
130
|
+
* supportedReasoningEfforts: [{ reasoningEffort }] }`. `model` is what `thread/start` / `turn/start` take; hidden models are skipped.
|
|
131
|
+
*/
|
|
132
|
+
function parseCodexModels(data) {
|
|
133
|
+
if (!Array.isArray(data))
|
|
134
|
+
return [];
|
|
135
|
+
return data.filter(isObj).flatMap((x) => {
|
|
136
|
+
if (x["hidden"] === true)
|
|
137
|
+
return [];
|
|
138
|
+
const id = str(x["model"]) ?? str(x["id"]);
|
|
139
|
+
if (!id)
|
|
140
|
+
return [];
|
|
141
|
+
const description = str(x["description"]);
|
|
142
|
+
const efforts = Array.isArray(x["supportedReasoningEfforts"])
|
|
143
|
+
? x["supportedReasoningEfforts"].map((e) => (isObj(e) ? str(e["reasoningEffort"]) : str(e))).filter((e) => !!e)
|
|
144
|
+
: [];
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
id,
|
|
148
|
+
name: str(x["displayName"]) ?? id,
|
|
149
|
+
...(description ? { description } : {}),
|
|
150
|
+
...(x["isDefault"] === true ? { isDefault: true } : {}),
|
|
151
|
+
...(efforts.length ? { efforts } : {}),
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** Asks a short-lived `codex app-server` for its model list (`model/list`; no model call is made). */
|
|
157
|
+
async function queryCodexModels(binary) {
|
|
158
|
+
const child = spawnCli(binary, ["app-server"], homedir());
|
|
159
|
+
const rpc = new RpcPeer(child, { jsonrpcField: false });
|
|
160
|
+
let stderrTail = "";
|
|
161
|
+
child.stderr.on("data", (d) => {
|
|
162
|
+
stderrTail = (stderrTail + d.toString()).slice(-2000);
|
|
163
|
+
});
|
|
164
|
+
child.on("error", (e) => rpc.failAll(new Error(`codex failed to start: ${e.message}`)));
|
|
165
|
+
child.on("exit", (code) => rpc.failAll(new Error(stderrTail ? `codex exited (code ${code}): ${lastLine(stderrTail)}` : `codex exited (code ${code})`)));
|
|
166
|
+
rpc.onRequest = async (method) => {
|
|
167
|
+
throw new RpcError(-32601, `unsupported method: ${method}`);
|
|
168
|
+
};
|
|
169
|
+
let timer;
|
|
170
|
+
try {
|
|
171
|
+
const work = (async () => {
|
|
172
|
+
await rpc.request("initialize", { clientInfo: { name: "remogent", title: "Remogent", version: process.env["npm_package_version"] ?? "dev" } });
|
|
173
|
+
rpc.notify("initialized");
|
|
174
|
+
const out = [];
|
|
175
|
+
let cursor;
|
|
176
|
+
for (let page = 0; page < 5; page++) {
|
|
177
|
+
const res = await rpc.request("model/list", cursor ? { cursor } : {});
|
|
178
|
+
out.push(...parseCodexModels(isObj(res) ? res["data"] : undefined));
|
|
179
|
+
cursor = isObj(res) ? str(res["nextCursor"]) : undefined;
|
|
180
|
+
if (!cursor)
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
})();
|
|
185
|
+
const timeout = new Promise((_, reject) => {
|
|
186
|
+
timer = setTimeout(() => reject(new Error("codex did not answer in time")), 20_000);
|
|
187
|
+
});
|
|
188
|
+
return await Promise.race([work, timeout]);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (timer)
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
killTree(child);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export function createCodexAdapter(binary) {
|
|
197
|
+
return {
|
|
198
|
+
engine: "codex",
|
|
199
|
+
resumable: true,
|
|
200
|
+
listModels: () => queryCodexModels(binary),
|
|
201
|
+
async start({ cwd, permissionMode, resumeId, model }, sink) {
|
|
202
|
+
const policy = POLICIES[permissionMode];
|
|
203
|
+
let threadId = resumeId;
|
|
204
|
+
let curModel = model; // sent with thread/start|resume and every turn/start (a switch is just a new value)
|
|
205
|
+
let conn;
|
|
206
|
+
let ready;
|
|
207
|
+
let closed = false; // intentionally closed by the host
|
|
208
|
+
let turn;
|
|
209
|
+
let textInTurn = false;
|
|
210
|
+
let lastMsgItem;
|
|
211
|
+
const deltaItems = new Set();
|
|
212
|
+
const fileChanges = new Map(); // itemId -> changes, for approval diffs
|
|
213
|
+
const finishTurn = (t, status, errorMessage) => {
|
|
214
|
+
if (turn !== t)
|
|
215
|
+
return;
|
|
216
|
+
turn = undefined;
|
|
217
|
+
if (status === "failed" && errorMessage && t.lastError !== errorMessage)
|
|
218
|
+
sink.emit({ kind: "error", message: errorMessage });
|
|
219
|
+
sink.emit({
|
|
220
|
+
kind: "turn_end",
|
|
221
|
+
stopReason: status === "interrupted" ? "cancelled" : status === "failed" ? "error" : "end_turn",
|
|
222
|
+
...(t.usage !== undefined ? { usage: t.usage } : {}),
|
|
223
|
+
});
|
|
224
|
+
t.resolve();
|
|
225
|
+
};
|
|
226
|
+
const handleNotification = (method, params) => {
|
|
227
|
+
if (!isObj(params))
|
|
228
|
+
return;
|
|
229
|
+
const tid = str(params["threadId"]);
|
|
230
|
+
if (tid && threadId && tid !== threadId)
|
|
231
|
+
return; // another (sub-agent) thread
|
|
232
|
+
switch (method) {
|
|
233
|
+
case "item/agentMessage/delta": {
|
|
234
|
+
const delta = typeof params["delta"] === "string" ? params["delta"] : "";
|
|
235
|
+
const itemId = str(params["itemId"]);
|
|
236
|
+
if (!delta)
|
|
237
|
+
return;
|
|
238
|
+
// Each agent message item is its own paragraph (commentary before tools, then the final answer).
|
|
239
|
+
if (itemId && itemId !== lastMsgItem && textInTurn)
|
|
240
|
+
sink.emit({ kind: "agent_text_delta", text: "\n\n" });
|
|
241
|
+
if (itemId) {
|
|
242
|
+
lastMsgItem = itemId;
|
|
243
|
+
deltaItems.add(itemId);
|
|
244
|
+
}
|
|
245
|
+
textInTurn = true;
|
|
246
|
+
sink.emit({ kind: "agent_text_delta", text: delta });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
case "item/reasoning/summaryTextDelta":
|
|
250
|
+
case "item/reasoning/textDelta": {
|
|
251
|
+
const delta = typeof params["delta"] === "string" ? params["delta"] : "";
|
|
252
|
+
if (delta)
|
|
253
|
+
sink.emit({ kind: "agent_thought_delta", text: delta });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
case "item/started": {
|
|
257
|
+
const item = params["item"];
|
|
258
|
+
if (!isObj(item))
|
|
259
|
+
return;
|
|
260
|
+
if (item["type"] === "fileChange")
|
|
261
|
+
fileChanges.set(String(item["id"]), item["changes"]);
|
|
262
|
+
const ev = toolStart(item);
|
|
263
|
+
if (ev)
|
|
264
|
+
sink.emit(ev);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
case "item/fileChange/patchUpdated": {
|
|
268
|
+
const itemId = str(params["itemId"]);
|
|
269
|
+
if (itemId)
|
|
270
|
+
fileChanges.set(itemId, params["changes"]);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
case "item/completed": {
|
|
274
|
+
const item = params["item"];
|
|
275
|
+
if (!isObj(item))
|
|
276
|
+
return;
|
|
277
|
+
if (item["type"] === "agentMessage") {
|
|
278
|
+
// Whole message without streamed deltas (some items arrive complete): show it now.
|
|
279
|
+
const id = str(item["id"]);
|
|
280
|
+
const text = str(item["text"]);
|
|
281
|
+
if (text && (!id || !deltaItems.has(id))) {
|
|
282
|
+
if (textInTurn)
|
|
283
|
+
sink.emit({ kind: "agent_text_delta", text: "\n\n" });
|
|
284
|
+
textInTurn = true;
|
|
285
|
+
if (id)
|
|
286
|
+
lastMsgItem = id;
|
|
287
|
+
sink.emit({ kind: "agent_text_delta", text });
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const ev = toolEnd(item);
|
|
292
|
+
if (ev)
|
|
293
|
+
sink.emit(ev);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
case "thread/tokenUsage/updated": {
|
|
297
|
+
if (turn && params["tokenUsage"] !== undefined)
|
|
298
|
+
turn.usage = params["tokenUsage"];
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
case "error": {
|
|
302
|
+
const err = isObj(params["error"]) ? params["error"] : {};
|
|
303
|
+
const message = str(err["message"]);
|
|
304
|
+
if (params["willRetry"] === true || !message)
|
|
305
|
+
return;
|
|
306
|
+
if (turn)
|
|
307
|
+
turn.lastError = message;
|
|
308
|
+
sink.emit({ kind: "error", message });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
case "turn/completed": {
|
|
312
|
+
const t = turn;
|
|
313
|
+
const tn = isObj(params["turn"]) ? params["turn"] : {};
|
|
314
|
+
if (!t)
|
|
315
|
+
return;
|
|
316
|
+
if (t.id && str(tn["id"]) && t.id !== tn["id"])
|
|
317
|
+
return;
|
|
318
|
+
const err = isObj(tn["error"]) ? str(tn["error"]["message"]) : undefined;
|
|
319
|
+
finishTurn(t, str(tn["status"]) ?? "completed", err ?? "the turn failed");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
default:
|
|
323
|
+
return; // thread/*, mcpServer/*, account/*, rate limits, warnings, ...
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const decide = (d) => (d === "deny" ? "decline" : "accept");
|
|
327
|
+
const handleRequest = async (method, params) => {
|
|
328
|
+
const p = isObj(params) ? params : {};
|
|
329
|
+
switch (method) {
|
|
330
|
+
case "item/commandExecution/requestApproval": {
|
|
331
|
+
const itemId = str(p["itemId"]);
|
|
332
|
+
const decision = await sink.requestPermission({
|
|
333
|
+
...(itemId ? { toolCallId: itemId } : {}),
|
|
334
|
+
tool: "shell",
|
|
335
|
+
risk: p["networkApprovalContext"] ? "network" : actionRisk(p["commandActions"]),
|
|
336
|
+
summary: str(p["reason"]) ?? commandTitle(p),
|
|
337
|
+
input: { command: p["command"], cwd: p["cwd"], reason: p["reason"] },
|
|
338
|
+
});
|
|
339
|
+
// allow_session is tracked by the host; toward Codex it is a one-time accept, so Codex never persists an
|
|
340
|
+
// exec-policy rule on disk.
|
|
341
|
+
return { decision: decide(decision) };
|
|
342
|
+
}
|
|
343
|
+
case "item/fileChange/requestApproval": {
|
|
344
|
+
const itemId = str(p["itemId"]);
|
|
345
|
+
const changes = itemId ? fileChanges.get(itemId) : undefined;
|
|
346
|
+
const paths = changePaths(changes);
|
|
347
|
+
const diff = changesDiff(changes);
|
|
348
|
+
const decision = await sink.requestPermission({
|
|
349
|
+
...(itemId ? { toolCallId: itemId } : {}),
|
|
350
|
+
tool: "edit",
|
|
351
|
+
risk: "edit",
|
|
352
|
+
summary: str(p["reason"]) ?? (paths.length ? `Edit ${paths.join(", ")}` : "Apply file changes"),
|
|
353
|
+
input: { files: paths, ...(p["grantRoot"] ? { grantRoot: p["grantRoot"] } : {}) },
|
|
354
|
+
...(diff ? { diff } : {}),
|
|
355
|
+
});
|
|
356
|
+
return { decision: decide(decision) };
|
|
357
|
+
}
|
|
358
|
+
case "item/permissions/requestApproval": {
|
|
359
|
+
const itemId = str(p["itemId"]);
|
|
360
|
+
const wanted = isObj(p["permissions"]) ? p["permissions"] : {};
|
|
361
|
+
const decision = await sink.requestPermission({
|
|
362
|
+
...(itemId ? { toolCallId: itemId } : {}),
|
|
363
|
+
tool: "permissions",
|
|
364
|
+
risk: wanted["network"] ? "network" : "edit",
|
|
365
|
+
summary: str(p["reason"]) ?? "Codex asks for extra permissions",
|
|
366
|
+
input: wanted,
|
|
367
|
+
});
|
|
368
|
+
return { permissions: decision === "deny" ? {} : wanted, scope: "turn" };
|
|
369
|
+
}
|
|
370
|
+
case "mcpServer/elicitation/request":
|
|
371
|
+
return { action: "decline" };
|
|
372
|
+
case "item/tool/requestUserInput":
|
|
373
|
+
return { answers: {} };
|
|
374
|
+
case "applyPatchApproval":
|
|
375
|
+
case "execCommandApproval":
|
|
376
|
+
return { decision: "denied" }; // legacy (v1) approvals: never used by this adapter's threads
|
|
377
|
+
default:
|
|
378
|
+
throw new RpcError(-32601, `unsupported method: ${method}`);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
const boot = async () => {
|
|
382
|
+
const child = spawnCli(binary, ["app-server"], cwd);
|
|
383
|
+
const rpc = new RpcPeer(child, { jsonrpcField: false });
|
|
384
|
+
const c = { child, rpc, alive: true };
|
|
385
|
+
let stderrTail = "";
|
|
386
|
+
child.stderr.on("data", (d) => {
|
|
387
|
+
stderrTail = (stderrTail + d.toString()).slice(-2000); // kept only for exit diagnostics; never logged
|
|
388
|
+
});
|
|
389
|
+
const died = (why) => {
|
|
390
|
+
if (!c.alive)
|
|
391
|
+
return;
|
|
392
|
+
c.alive = false;
|
|
393
|
+
const err = new Error(stderrTail ? `${why}: ${lastLine(stderrTail)}` : why);
|
|
394
|
+
rpc.failAll(err);
|
|
395
|
+
const t = turn;
|
|
396
|
+
if (closed || !t)
|
|
397
|
+
return;
|
|
398
|
+
turn = undefined;
|
|
399
|
+
t.reject(err); // the prompt rejection reports the death; the next prompt respawns and resumes the thread
|
|
400
|
+
};
|
|
401
|
+
child.on("exit", (code) => died(`codex exited (code ${code})`));
|
|
402
|
+
child.on("error", (e) => died(`codex failed to start: ${e.message}`));
|
|
403
|
+
rpc.onNotification = handleNotification;
|
|
404
|
+
rpc.onRequest = handleRequest;
|
|
405
|
+
try {
|
|
406
|
+
await rpc.request("initialize", {
|
|
407
|
+
clientInfo: { name: "remogent", title: "Remogent", version: process.env["npm_package_version"] ?? "dev" },
|
|
408
|
+
});
|
|
409
|
+
rpc.notify("initialized");
|
|
410
|
+
const settings = { cwd, approvalPolicy: policy.approvalPolicy, sandbox: policy.sandbox, ...(curModel ? { model: curModel } : {}) };
|
|
411
|
+
if (threadId) {
|
|
412
|
+
await rpc.request("thread/resume", { threadId, excludeTurns: true, ...settings });
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
const res = await rpc.request("thread/start", settings);
|
|
416
|
+
const thread = isObj(res) ? res["thread"] : undefined;
|
|
417
|
+
threadId = isObj(thread) ? str(thread["id"]) : undefined;
|
|
418
|
+
if (!threadId)
|
|
419
|
+
throw new Error("codex did not return a thread id");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
c.alive = false;
|
|
424
|
+
killTree(child);
|
|
425
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
426
|
+
throw new Error(/auth|login|sign.?in|unauthor/i.test(msg) ? `${msg} (run \`codex login\` once on the host to sign in)` : msg);
|
|
427
|
+
}
|
|
428
|
+
return c;
|
|
429
|
+
};
|
|
430
|
+
const ensure = () => {
|
|
431
|
+
if (conn?.alive)
|
|
432
|
+
return Promise.resolve(conn);
|
|
433
|
+
ready ??= boot()
|
|
434
|
+
.then((c) => (conn = c))
|
|
435
|
+
.finally(() => {
|
|
436
|
+
ready = undefined;
|
|
437
|
+
});
|
|
438
|
+
return ready;
|
|
439
|
+
};
|
|
440
|
+
// A new chat starts the CLI now (so a missing login shows up immediately); a restored chat waits for its next prompt.
|
|
441
|
+
if (!resumeId)
|
|
442
|
+
await ensure();
|
|
443
|
+
return {
|
|
444
|
+
async prompt(text) {
|
|
445
|
+
if (closed)
|
|
446
|
+
throw new Error("codex session is closed");
|
|
447
|
+
const c = await ensure();
|
|
448
|
+
const t = { cancelled: false, resolve: () => { }, reject: () => { } };
|
|
449
|
+
const done = new Promise((resolve, reject) => {
|
|
450
|
+
t.resolve = resolve;
|
|
451
|
+
t.reject = reject;
|
|
452
|
+
});
|
|
453
|
+
turn = t;
|
|
454
|
+
textInTurn = false;
|
|
455
|
+
lastMsgItem = undefined;
|
|
456
|
+
deltaItems.clear();
|
|
457
|
+
try {
|
|
458
|
+
const res = await c.rpc.request("turn/start", {
|
|
459
|
+
threadId,
|
|
460
|
+
input: [{ type: "text", text, text_elements: [] }],
|
|
461
|
+
...(curModel ? { model: curModel } : {}),
|
|
462
|
+
});
|
|
463
|
+
const tn = isObj(res) ? res["turn"] : undefined;
|
|
464
|
+
const id = isObj(tn) ? str(tn["id"]) : undefined;
|
|
465
|
+
if (turn === t && id) {
|
|
466
|
+
t.id = id;
|
|
467
|
+
if (t.cancelled)
|
|
468
|
+
void c.rpc.request("turn/interrupt", { threadId, turnId: id }).catch(() => { });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
if (turn === t)
|
|
473
|
+
turn = undefined;
|
|
474
|
+
throw e;
|
|
475
|
+
}
|
|
476
|
+
await done;
|
|
477
|
+
},
|
|
478
|
+
async setModel(next) {
|
|
479
|
+
if (closed)
|
|
480
|
+
throw new Error("codex session is closed");
|
|
481
|
+
if (turn)
|
|
482
|
+
throw new Error("a turn is already running");
|
|
483
|
+
curModel = next; // applied by the next turn/start (and by thread/resume after a respawn)
|
|
484
|
+
},
|
|
485
|
+
cancel() {
|
|
486
|
+
const t = turn;
|
|
487
|
+
const c = conn;
|
|
488
|
+
if (!t || !c?.alive || t.cancelled)
|
|
489
|
+
return;
|
|
490
|
+
t.cancelled = true;
|
|
491
|
+
if (t.id)
|
|
492
|
+
void c.rpc.request("turn/interrupt", { threadId, turnId: t.id }).catch(() => { });
|
|
493
|
+
setTimeout(() => finishTurn(t, "interrupted"), INTERRUPT_GRACE_MS).unref();
|
|
494
|
+
},
|
|
495
|
+
close() {
|
|
496
|
+
closed = true;
|
|
497
|
+
const t = turn;
|
|
498
|
+
turn = undefined;
|
|
499
|
+
t?.resolve();
|
|
500
|
+
if (conn) {
|
|
501
|
+
conn.alive = false;
|
|
502
|
+
killTree(conn.child);
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
engineSessionId: () => threadId,
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
//# sourceMappingURL=codex.js.map
|