copilot-hub 0.1.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 +215 -0
- package/apps/agent-engine/.env.example +41 -0
- package/apps/agent-engine/LICENSE +21 -0
- package/apps/agent-engine/README.md +57 -0
- package/apps/agent-engine/bot-registry.example.json +28 -0
- package/apps/agent-engine/capabilities/example/index.js +3 -0
- package/apps/agent-engine/capabilities/example/manifest.json +14 -0
- package/apps/agent-engine/dist/agent-worker.js +241 -0
- package/apps/agent-engine/dist/config.js +225 -0
- package/apps/agent-engine/dist/index.js +352 -0
- package/apps/agent-engine/dist/test/project-fingerprint.test.js +40 -0
- package/apps/agent-engine/dist/test/thread-id.test.js +12 -0
- package/apps/agent-engine/package.json +28 -0
- package/apps/control-plane/.env.example +25 -0
- package/apps/control-plane/README.md +35 -0
- package/apps/control-plane/bot-registry.example.json +40 -0
- package/apps/control-plane/capabilities/example/index.js +3 -0
- package/apps/control-plane/capabilities/example/manifest.json +14 -0
- package/apps/control-plane/dist/agent-worker.js +243 -0
- package/apps/control-plane/dist/channels/channel-factory.js +21 -0
- package/apps/control-plane/dist/channels/hub-ops-commands.js +752 -0
- package/apps/control-plane/dist/channels/telegram-channel.js +743 -0
- package/apps/control-plane/dist/channels/whatsapp-channel.js +35 -0
- package/apps/control-plane/dist/config.js +230 -0
- package/apps/control-plane/dist/copilot-hub.js +138 -0
- package/apps/control-plane/dist/index.js +349 -0
- package/apps/control-plane/dist/kernel/admin-contract.js +51 -0
- package/apps/control-plane/dist/test/project-fingerprint.test.js +40 -0
- package/apps/control-plane/dist/test/thread-id.test.js +12 -0
- package/apps/control-plane/package.json +27 -0
- package/package.json +89 -0
- package/packages/contracts/README.md +10 -0
- package/packages/contracts/dist/control-plane.d.ts +24 -0
- package/packages/contracts/dist/control-plane.js +37 -0
- package/packages/contracts/dist/control-plane.js.map +1 -0
- package/packages/contracts/dist/index.d.ts +1 -0
- package/packages/contracts/dist/index.js +2 -0
- package/packages/contracts/dist/index.js.map +1 -0
- package/packages/contracts/package.json +27 -0
- package/packages/core/README.md +33 -0
- package/packages/core/dist/agent-supervisor.d.ts +39 -0
- package/packages/core/dist/agent-supervisor.js +552 -0
- package/packages/core/dist/agent-supervisor.js.map +1 -0
- package/packages/core/dist/bot-manager.d.ts +66 -0
- package/packages/core/dist/bot-manager.js +333 -0
- package/packages/core/dist/bot-manager.js.map +1 -0
- package/packages/core/dist/bot-registry.d.ts +60 -0
- package/packages/core/dist/bot-registry.js +381 -0
- package/packages/core/dist/bot-registry.js.map +1 -0
- package/packages/core/dist/bot-runtime.d.ts +135 -0
- package/packages/core/dist/bot-runtime.js +349 -0
- package/packages/core/dist/bot-runtime.js.map +1 -0
- package/packages/core/dist/bridge-service.d.ts +39 -0
- package/packages/core/dist/bridge-service.js +272 -0
- package/packages/core/dist/bridge-service.js.map +1 -0
- package/packages/core/dist/capability-manager.d.ts +18 -0
- package/packages/core/dist/capability-manager.js +335 -0
- package/packages/core/dist/capability-manager.js.map +1 -0
- package/packages/core/dist/capability-scaffold.d.ts +26 -0
- package/packages/core/dist/capability-scaffold.js +118 -0
- package/packages/core/dist/capability-scaffold.js.map +1 -0
- package/packages/core/dist/channel-factory.d.ts +6 -0
- package/packages/core/dist/channel-factory.js +22 -0
- package/packages/core/dist/channel-factory.js.map +1 -0
- package/packages/core/dist/codex-app-client.d.ts +56 -0
- package/packages/core/dist/codex-app-client.js +762 -0
- package/packages/core/dist/codex-app-client.js.map +1 -0
- package/packages/core/dist/codex-provider.d.ts +31 -0
- package/packages/core/dist/codex-provider.js +64 -0
- package/packages/core/dist/codex-provider.js.map +1 -0
- package/packages/core/dist/control-permission.d.ts +19 -0
- package/packages/core/dist/control-permission.js +106 -0
- package/packages/core/dist/control-permission.js.map +1 -0
- package/packages/core/dist/control-plane-actions.d.ts +1 -0
- package/packages/core/dist/control-plane-actions.js +2 -0
- package/packages/core/dist/control-plane-actions.js.map +1 -0
- package/packages/core/dist/example-capability.d.ts +17 -0
- package/packages/core/dist/example-capability.js +22 -0
- package/packages/core/dist/example-capability.js.map +1 -0
- package/packages/core/dist/extension-contract.d.ts +22 -0
- package/packages/core/dist/extension-contract.js +28 -0
- package/packages/core/dist/extension-contract.js.map +1 -0
- package/packages/core/dist/index.d.ts +26 -0
- package/packages/core/dist/index.js +27 -0
- package/packages/core/dist/index.js.map +1 -0
- package/packages/core/dist/instance-lock.d.ts +9 -0
- package/packages/core/dist/instance-lock.js +74 -0
- package/packages/core/dist/instance-lock.js.map +1 -0
- package/packages/core/dist/kernel-control-plane.d.ts +16 -0
- package/packages/core/dist/kernel-control-plane.js +500 -0
- package/packages/core/dist/kernel-control-plane.js.map +1 -0
- package/packages/core/dist/kernel-version.d.ts +1 -0
- package/packages/core/dist/kernel-version.js +2 -0
- package/packages/core/dist/kernel-version.js.map +1 -0
- package/packages/core/dist/project-fingerprint.d.ts +11 -0
- package/packages/core/dist/project-fingerprint.js +33 -0
- package/packages/core/dist/project-fingerprint.js.map +1 -0
- package/packages/core/dist/provider-factory.d.ts +7 -0
- package/packages/core/dist/provider-factory.js +21 -0
- package/packages/core/dist/provider-factory.js.map +1 -0
- package/packages/core/dist/secret-store.d.ts +18 -0
- package/packages/core/dist/secret-store.js +110 -0
- package/packages/core/dist/secret-store.js.map +1 -0
- package/packages/core/dist/state-store.d.ts +50 -0
- package/packages/core/dist/state-store.js +324 -0
- package/packages/core/dist/state-store.js.map +1 -0
- package/packages/core/dist/telegram-channel.d.ts +27 -0
- package/packages/core/dist/telegram-channel.js +951 -0
- package/packages/core/dist/telegram-channel.js.map +1 -0
- package/packages/core/dist/thread-id.d.ts +1 -0
- package/packages/core/dist/thread-id.js +12 -0
- package/packages/core/dist/thread-id.js.map +1 -0
- package/packages/core/dist/whatsapp-channel.d.ts +26 -0
- package/packages/core/dist/whatsapp-channel.js +36 -0
- package/packages/core/dist/whatsapp-channel.js.map +1 -0
- package/packages/core/dist/workspace-paths.d.ts +5 -0
- package/packages/core/dist/workspace-paths.js +77 -0
- package/packages/core/dist/workspace-paths.js.map +1 -0
- package/packages/core/dist/workspace-policy.d.ts +30 -0
- package/packages/core/dist/workspace-policy.js +104 -0
- package/packages/core/dist/workspace-policy.js.map +1 -0
- package/packages/core/package.json +126 -0
- package/scripts/cli.mjs +537 -0
- package/scripts/configure.mjs +254 -0
- package/scripts/ensure-shared-build.mjs +96 -0
- package/scripts/run-node-tests.mjs +52 -0
- package/scripts/supervisor.mjs +332 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import readline from "node:readline";
|
|
7
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
8
|
+
const DEFAULT_TURN_ACTIVITY_TIMEOUT_MS = 3_600_000;
|
|
9
|
+
export class CodexAppClient extends EventEmitter {
|
|
10
|
+
constructor({ codexBin, codexHomeDir, cwd, sandboxMode, approvalPolicy, turnActivityTimeoutMs }) {
|
|
11
|
+
super();
|
|
12
|
+
this.codexBin = String(codexBin ?? "codex");
|
|
13
|
+
this.codexHomeDir = codexHomeDir ? path.resolve(String(codexHomeDir)) : null;
|
|
14
|
+
this.cwd = path.resolve(String(cwd));
|
|
15
|
+
this.sandboxMode = normalizeSandboxMode(sandboxMode);
|
|
16
|
+
this.approvalPolicy = normalizeApprovalPolicy(approvalPolicy);
|
|
17
|
+
this.turnActivityTimeoutMs = normalizeTimeout(turnActivityTimeoutMs, DEFAULT_TURN_ACTIVITY_TIMEOUT_MS);
|
|
18
|
+
this.child = null;
|
|
19
|
+
this.reader = null;
|
|
20
|
+
this.nextRequestId = 1;
|
|
21
|
+
this.startPromise = null;
|
|
22
|
+
this.started = false;
|
|
23
|
+
this.pendingRequests = new Map();
|
|
24
|
+
this.pendingApprovals = new Map();
|
|
25
|
+
this.turnWaiters = new Map();
|
|
26
|
+
this.turnOutput = new Map();
|
|
27
|
+
this.completedTurns = new Map();
|
|
28
|
+
this.activeTurnByThread = new Map();
|
|
29
|
+
}
|
|
30
|
+
setCwd(value) {
|
|
31
|
+
this.cwd = path.resolve(String(value));
|
|
32
|
+
}
|
|
33
|
+
setPolicies({ sandboxMode, approvalPolicy }) {
|
|
34
|
+
this.sandboxMode = normalizeSandboxMode(sandboxMode ?? this.sandboxMode);
|
|
35
|
+
this.approvalPolicy = normalizeApprovalPolicy(approvalPolicy ?? this.approvalPolicy);
|
|
36
|
+
}
|
|
37
|
+
async ensureStarted() {
|
|
38
|
+
if (this.started) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (this.startPromise) {
|
|
42
|
+
await this.startPromise;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.startPromise = this.#startInternal();
|
|
46
|
+
try {
|
|
47
|
+
await this.startPromise;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
this.startPromise = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async shutdown() {
|
|
54
|
+
this.started = false;
|
|
55
|
+
if (this.reader) {
|
|
56
|
+
this.reader.close();
|
|
57
|
+
this.reader = null;
|
|
58
|
+
}
|
|
59
|
+
const child = this.child;
|
|
60
|
+
this.child = null;
|
|
61
|
+
if (child) {
|
|
62
|
+
try {
|
|
63
|
+
child.stdin.end();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Ignore stdin close errors.
|
|
67
|
+
}
|
|
68
|
+
child.kill();
|
|
69
|
+
}
|
|
70
|
+
for (const { reject, timer } of this.pendingRequests.values()) {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
reject(new Error("Codex app-server process stopped."));
|
|
73
|
+
}
|
|
74
|
+
this.pendingRequests.clear();
|
|
75
|
+
for (const { reject, timer } of this.turnWaiters.values()) {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
reject(new Error("Codex app-server process stopped while waiting for turn completion."));
|
|
78
|
+
}
|
|
79
|
+
this.turnWaiters.clear();
|
|
80
|
+
this.pendingApprovals.clear();
|
|
81
|
+
this.completedTurns.clear();
|
|
82
|
+
this.turnOutput.clear();
|
|
83
|
+
this.activeTurnByThread.clear();
|
|
84
|
+
}
|
|
85
|
+
listPendingApprovals({ threadId } = {}) {
|
|
86
|
+
const normalizedThreadId = threadId ? String(threadId) : null;
|
|
87
|
+
const values = [...this.pendingApprovals.values()].filter((entry) => normalizedThreadId ? entry.threadId === normalizedThreadId : true);
|
|
88
|
+
values.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
89
|
+
return values.map((entry) => ({ ...entry }));
|
|
90
|
+
}
|
|
91
|
+
async resolveApproval({ approvalId, decision }) {
|
|
92
|
+
const normalizedApprovalId = String(approvalId ?? "").trim();
|
|
93
|
+
if (!normalizedApprovalId) {
|
|
94
|
+
throw new Error("approvalId is required.");
|
|
95
|
+
}
|
|
96
|
+
const approval = this.pendingApprovals.get(normalizedApprovalId);
|
|
97
|
+
if (!approval) {
|
|
98
|
+
throw new Error(`Unknown approval '${normalizedApprovalId}'.`);
|
|
99
|
+
}
|
|
100
|
+
const normalizedDecision = normalizeApprovalDecision(decision);
|
|
101
|
+
await this.#sendResponse({
|
|
102
|
+
id: approval.serverRequestId,
|
|
103
|
+
result: {
|
|
104
|
+
decision: normalizedDecision,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
this.pendingApprovals.delete(normalizedApprovalId);
|
|
108
|
+
this.#resumeTurnAfterApproval(makeTurnKey(approval.threadId, approval.turnId), normalizedApprovalId);
|
|
109
|
+
return {
|
|
110
|
+
...approval,
|
|
111
|
+
decision: normalizedDecision,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async interruptTurn({ threadId }) {
|
|
115
|
+
await this.ensureStarted();
|
|
116
|
+
const normalizedThreadId = String(threadId ?? "").trim();
|
|
117
|
+
if (!normalizedThreadId) {
|
|
118
|
+
throw new Error("threadId is required.");
|
|
119
|
+
}
|
|
120
|
+
const active = this.activeTurnByThread.get(normalizedThreadId);
|
|
121
|
+
const activeTurnId = String(active?.turnId ?? "").trim();
|
|
122
|
+
if (!activeTurnId) {
|
|
123
|
+
return {
|
|
124
|
+
interrupted: false,
|
|
125
|
+
reason: "no_active_turn",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const attempts = [
|
|
129
|
+
{
|
|
130
|
+
method: "turn/cancel",
|
|
131
|
+
params: { threadId: normalizedThreadId, turnId: activeTurnId },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
method: "turn/cancel",
|
|
135
|
+
params: { threadId: normalizedThreadId },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
method: "turn/interrupt",
|
|
139
|
+
params: { threadId: normalizedThreadId, turnId: activeTurnId },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
method: "turn/interrupt",
|
|
143
|
+
params: { threadId: normalizedThreadId },
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
let lastError = null;
|
|
147
|
+
for (const attempt of attempts) {
|
|
148
|
+
try {
|
|
149
|
+
await this.#sendRequest(attempt.method, attempt.params, 2_000);
|
|
150
|
+
return {
|
|
151
|
+
interrupted: true,
|
|
152
|
+
method: attempt.method,
|
|
153
|
+
turnId: activeTurnId,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
lastError = error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
this.#handleProcessFailure(new Error("Turn interrupted by user (forced process restart)."));
|
|
161
|
+
try {
|
|
162
|
+
await this.ensureStarted();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Best effort restart only.
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
interrupted: true,
|
|
169
|
+
method: "process_restart",
|
|
170
|
+
turnId: activeTurnId,
|
|
171
|
+
warning: lastError ? sanitizeError(lastError) : null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async sendTurn({ threadId, prompt, inputItems = [], turnActivityTimeoutMs, onThreadReady = null, }) {
|
|
175
|
+
await this.ensureStarted();
|
|
176
|
+
const text = String(prompt ?? "").trim();
|
|
177
|
+
const normalizedInputItems = normalizeTurnInputItems({
|
|
178
|
+
prompt: text,
|
|
179
|
+
inputItems,
|
|
180
|
+
});
|
|
181
|
+
const resolvedThreadId = await this.#ensureThread(threadId);
|
|
182
|
+
if (typeof onThreadReady === "function") {
|
|
183
|
+
try {
|
|
184
|
+
await onThreadReady(resolvedThreadId);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
this.emit("warning", {
|
|
188
|
+
type: "thread_ready_callback_failed",
|
|
189
|
+
threadId: resolvedThreadId,
|
|
190
|
+
error: sanitizeError(error),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const response = await this.#sendRequest("turn/start", {
|
|
195
|
+
threadId: resolvedThreadId,
|
|
196
|
+
input: normalizedInputItems,
|
|
197
|
+
});
|
|
198
|
+
const turnId = String(response?.turn?.id ?? "").trim();
|
|
199
|
+
if (!turnId) {
|
|
200
|
+
throw new Error("turn/start did not return a turn id.");
|
|
201
|
+
}
|
|
202
|
+
this.activeTurnByThread.set(resolvedThreadId, {
|
|
203
|
+
turnId,
|
|
204
|
+
startedAt: new Date().toISOString(),
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
const waitTimeout = normalizeTimeout(turnActivityTimeoutMs, this.turnActivityTimeoutMs);
|
|
208
|
+
const turn = response.turn?.status && response.turn.status !== "inProgress"
|
|
209
|
+
? response.turn
|
|
210
|
+
: await this.#waitForTurnCompletion(resolvedThreadId, turnId, waitTimeout);
|
|
211
|
+
if (turn.status === "failed") {
|
|
212
|
+
throw new Error(turn.error?.message || "Turn failed.");
|
|
213
|
+
}
|
|
214
|
+
if (turn.status === "interrupted") {
|
|
215
|
+
throw new Error("Turn was interrupted.");
|
|
216
|
+
}
|
|
217
|
+
const assistantText = await this.#resolveAssistantText(resolvedThreadId, turnId);
|
|
218
|
+
return {
|
|
219
|
+
threadId: resolvedThreadId,
|
|
220
|
+
turnId,
|
|
221
|
+
assistantText,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
const current = this.activeTurnByThread.get(resolvedThreadId);
|
|
226
|
+
if (String(current?.turnId ?? "") === turnId) {
|
|
227
|
+
this.activeTurnByThread.delete(resolvedThreadId);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async #startInternal() {
|
|
232
|
+
if (this.codexHomeDir) {
|
|
233
|
+
fs.mkdirSync(this.codexHomeDir, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
const args = ["-C", normalizeCliPath(this.cwd), "app-server"];
|
|
236
|
+
const env = this.#buildEnvironment();
|
|
237
|
+
const child = spawn(this.codexBin, args, {
|
|
238
|
+
windowsHide: true,
|
|
239
|
+
shell: false,
|
|
240
|
+
env,
|
|
241
|
+
});
|
|
242
|
+
this.child = child;
|
|
243
|
+
this.started = true;
|
|
244
|
+
this.reader = readline.createInterface({
|
|
245
|
+
input: child.stdout,
|
|
246
|
+
crlfDelay: Infinity,
|
|
247
|
+
});
|
|
248
|
+
this.reader.on("line", (line) => {
|
|
249
|
+
this.#handleStdoutLine(line);
|
|
250
|
+
});
|
|
251
|
+
child.stderr.on("data", (chunk) => {
|
|
252
|
+
const text = chunk.toString("utf8").trim();
|
|
253
|
+
if (text) {
|
|
254
|
+
this.emit("stderr", text);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
child.on("error", (error) => {
|
|
258
|
+
this.#handleProcessFailure(annotateSpawnError(error, this.codexBin));
|
|
259
|
+
});
|
|
260
|
+
child.on("close", (code) => {
|
|
261
|
+
this.#handleProcessFailure(new Error(`Codex app-server exited with code ${code ?? -1}.`));
|
|
262
|
+
});
|
|
263
|
+
await this.#sendRequest("initialize", {
|
|
264
|
+
clientInfo: {
|
|
265
|
+
name: "telegram-codex-bridge",
|
|
266
|
+
title: "Telegram Codex Bridge",
|
|
267
|
+
version: "0.2.0",
|
|
268
|
+
},
|
|
269
|
+
capabilities: {
|
|
270
|
+
experimentalApi: true,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
await this.#sendNotification("initialized");
|
|
274
|
+
}
|
|
275
|
+
#buildEnvironment() {
|
|
276
|
+
const env = { ...process.env };
|
|
277
|
+
if (this.codexHomeDir) {
|
|
278
|
+
env.CODEX_HOME = this.codexHomeDir;
|
|
279
|
+
}
|
|
280
|
+
return env;
|
|
281
|
+
}
|
|
282
|
+
async #ensureThread(threadId) {
|
|
283
|
+
const previousThreadId = String(threadId ?? "").trim();
|
|
284
|
+
if (previousThreadId) {
|
|
285
|
+
try {
|
|
286
|
+
await this.#sendRequest("thread/resume", {
|
|
287
|
+
threadId: previousThreadId,
|
|
288
|
+
cwd: normalizeCliPath(this.cwd),
|
|
289
|
+
approvalPolicy: this.approvalPolicy,
|
|
290
|
+
sandbox: this.sandboxMode,
|
|
291
|
+
});
|
|
292
|
+
return previousThreadId;
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
this.emit("warning", {
|
|
296
|
+
type: "thread_resume_failed",
|
|
297
|
+
threadId: previousThreadId,
|
|
298
|
+
error: sanitizeError(error),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const started = await this.#sendRequest("thread/start", {
|
|
303
|
+
cwd: normalizeCliPath(this.cwd),
|
|
304
|
+
approvalPolicy: this.approvalPolicy,
|
|
305
|
+
sandbox: this.sandboxMode,
|
|
306
|
+
experimentalRawEvents: false,
|
|
307
|
+
});
|
|
308
|
+
const nextThreadId = String(started?.thread?.id ?? "").trim();
|
|
309
|
+
if (!nextThreadId) {
|
|
310
|
+
throw new Error("thread/start did not return a thread id.");
|
|
311
|
+
}
|
|
312
|
+
return nextThreadId;
|
|
313
|
+
}
|
|
314
|
+
async #resolveAssistantText(threadId, turnId) {
|
|
315
|
+
const key = makeTurnKey(threadId, turnId);
|
|
316
|
+
const buffered = this.turnOutput.get(key);
|
|
317
|
+
if (buffered?.items?.length) {
|
|
318
|
+
return buffered.items[buffered.items.length - 1];
|
|
319
|
+
}
|
|
320
|
+
if (buffered?.delta?.trim()) {
|
|
321
|
+
return buffered.delta.trim();
|
|
322
|
+
}
|
|
323
|
+
const read = await this.#sendRequest("thread/read", {
|
|
324
|
+
threadId,
|
|
325
|
+
includeTurns: true,
|
|
326
|
+
});
|
|
327
|
+
const turns = Array.isArray(read?.thread?.turns) ? read.thread.turns : [];
|
|
328
|
+
const targetTurn = turns.find((entry) => entry.id === turnId);
|
|
329
|
+
if (!targetTurn || !Array.isArray(targetTurn.items)) {
|
|
330
|
+
return "";
|
|
331
|
+
}
|
|
332
|
+
const messages = targetTurn.items
|
|
333
|
+
.filter((entry) => entry?.type === "agentMessage" && typeof entry.text === "string")
|
|
334
|
+
.map((entry) => entry.text.trim())
|
|
335
|
+
.filter(Boolean);
|
|
336
|
+
if (messages.length === 0) {
|
|
337
|
+
return "";
|
|
338
|
+
}
|
|
339
|
+
return messages[messages.length - 1];
|
|
340
|
+
}
|
|
341
|
+
#waitForTurnCompletion(threadId, turnId, timeoutMs) {
|
|
342
|
+
const key = makeTurnKey(threadId, turnId);
|
|
343
|
+
const cached = this.completedTurns.get(key);
|
|
344
|
+
if (cached) {
|
|
345
|
+
this.completedTurns.delete(key);
|
|
346
|
+
return Promise.resolve(cached);
|
|
347
|
+
}
|
|
348
|
+
return new Promise((resolve, reject) => {
|
|
349
|
+
const waiter = {
|
|
350
|
+
resolve,
|
|
351
|
+
reject,
|
|
352
|
+
turnId: String(turnId),
|
|
353
|
+
timeoutMs,
|
|
354
|
+
timer: null,
|
|
355
|
+
waitingForApproval: false,
|
|
356
|
+
pendingApprovalIds: new Set(),
|
|
357
|
+
};
|
|
358
|
+
this.turnWaiters.set(key, waiter);
|
|
359
|
+
this.#armTurnActivityTimeout(key);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
#armTurnActivityTimeout(turnKey) {
|
|
363
|
+
const waiter = this.turnWaiters.get(turnKey);
|
|
364
|
+
if (!waiter) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (waiter.waitingForApproval) {
|
|
368
|
+
if (waiter.timer) {
|
|
369
|
+
clearTimeout(waiter.timer);
|
|
370
|
+
waiter.timer = null;
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (waiter.timer) {
|
|
375
|
+
clearTimeout(waiter.timer);
|
|
376
|
+
}
|
|
377
|
+
waiter.timer = setTimeout(() => {
|
|
378
|
+
this.turnWaiters.delete(turnKey);
|
|
379
|
+
waiter.reject(new Error(`Turn ${waiter.turnId || "<unknown>"} inactive for ${waiter.timeoutMs}ms.`));
|
|
380
|
+
}, waiter.timeoutMs);
|
|
381
|
+
}
|
|
382
|
+
#touchTurn(threadId, turnId) {
|
|
383
|
+
const key = makeTurnKey(threadId, turnId);
|
|
384
|
+
if (!this.turnWaiters.has(key)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.#armTurnActivityTimeout(key);
|
|
388
|
+
}
|
|
389
|
+
#pauseTurnForApproval(turnKey, approvalId) {
|
|
390
|
+
const waiter = this.turnWaiters.get(turnKey);
|
|
391
|
+
if (!waiter) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
waiter.pendingApprovalIds.add(String(approvalId));
|
|
395
|
+
waiter.waitingForApproval = true;
|
|
396
|
+
if (waiter.timer) {
|
|
397
|
+
clearTimeout(waiter.timer);
|
|
398
|
+
waiter.timer = null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
#resumeTurnAfterApproval(turnKey, approvalId) {
|
|
402
|
+
const waiter = this.turnWaiters.get(turnKey);
|
|
403
|
+
if (!waiter) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
waiter.pendingApprovalIds.delete(String(approvalId));
|
|
407
|
+
if (waiter.pendingApprovalIds.size > 0) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
waiter.waitingForApproval = false;
|
|
411
|
+
this.#armTurnActivityTimeout(turnKey);
|
|
412
|
+
}
|
|
413
|
+
#clearTurnApprovalTracking(turnKey) {
|
|
414
|
+
const waiter = this.turnWaiters.get(turnKey);
|
|
415
|
+
if (!waiter) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
waiter.waitingForApproval = false;
|
|
419
|
+
waiter.pendingApprovalIds.clear();
|
|
420
|
+
}
|
|
421
|
+
async #sendRequest(method, params, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
422
|
+
await this.ensureStarted();
|
|
423
|
+
const requestId = this.nextRequestId;
|
|
424
|
+
this.nextRequestId += 1;
|
|
425
|
+
const message = {
|
|
426
|
+
jsonrpc: "2.0",
|
|
427
|
+
id: requestId,
|
|
428
|
+
method,
|
|
429
|
+
};
|
|
430
|
+
if (params !== undefined) {
|
|
431
|
+
message.params = params;
|
|
432
|
+
}
|
|
433
|
+
return new Promise((resolve, reject) => {
|
|
434
|
+
const timer = setTimeout(() => {
|
|
435
|
+
this.pendingRequests.delete(requestId);
|
|
436
|
+
reject(new Error(`Request '${method}' timed out after ${timeoutMs}ms.`));
|
|
437
|
+
}, timeoutMs);
|
|
438
|
+
this.pendingRequests.set(requestId, {
|
|
439
|
+
method,
|
|
440
|
+
resolve,
|
|
441
|
+
reject,
|
|
442
|
+
timer,
|
|
443
|
+
});
|
|
444
|
+
try {
|
|
445
|
+
this.#writeMessage(message);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
clearTimeout(timer);
|
|
449
|
+
this.pendingRequests.delete(requestId);
|
|
450
|
+
reject(error);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async #sendNotification(method, params) {
|
|
455
|
+
await this.ensureStarted();
|
|
456
|
+
const message = {
|
|
457
|
+
jsonrpc: "2.0",
|
|
458
|
+
method,
|
|
459
|
+
};
|
|
460
|
+
if (params !== undefined) {
|
|
461
|
+
message.params = params;
|
|
462
|
+
}
|
|
463
|
+
this.#writeMessage(message);
|
|
464
|
+
}
|
|
465
|
+
async #sendResponse({ id, result, error }) {
|
|
466
|
+
await this.ensureStarted();
|
|
467
|
+
const message = {
|
|
468
|
+
jsonrpc: "2.0",
|
|
469
|
+
id,
|
|
470
|
+
};
|
|
471
|
+
if (error) {
|
|
472
|
+
message.error = error;
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
message.result = result;
|
|
476
|
+
}
|
|
477
|
+
this.#writeMessage(message);
|
|
478
|
+
}
|
|
479
|
+
#writeMessage(value) {
|
|
480
|
+
if (!this.child || !this.child.stdin) {
|
|
481
|
+
throw new Error("Codex app-server process is not available.");
|
|
482
|
+
}
|
|
483
|
+
this.child.stdin.write(`${JSON.stringify(value)}\n`);
|
|
484
|
+
}
|
|
485
|
+
#handleStdoutLine(rawLine) {
|
|
486
|
+
const line = String(rawLine ?? "").trim();
|
|
487
|
+
if (!line) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
let message;
|
|
491
|
+
try {
|
|
492
|
+
message = JSON.parse(line);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (message &&
|
|
498
|
+
typeof message === "object" &&
|
|
499
|
+
"id" in message &&
|
|
500
|
+
("result" in message || "error" in message) &&
|
|
501
|
+
!message.method) {
|
|
502
|
+
this.#handleResponse(message);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (message &&
|
|
506
|
+
typeof message === "object" &&
|
|
507
|
+
"id" in message &&
|
|
508
|
+
typeof message.method === "string") {
|
|
509
|
+
void this.#handleServerRequest(message);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (message && typeof message === "object" && typeof message.method === "string") {
|
|
513
|
+
this.#handleNotification(message);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
#handleResponse(message) {
|
|
517
|
+
const pending = this.pendingRequests.get(message.id);
|
|
518
|
+
if (!pending) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
this.pendingRequests.delete(message.id);
|
|
522
|
+
clearTimeout(pending.timer);
|
|
523
|
+
if (message.error) {
|
|
524
|
+
const details = typeof message.error?.message === "string"
|
|
525
|
+
? message.error.message
|
|
526
|
+
: JSON.stringify(message.error);
|
|
527
|
+
pending.reject(new Error(details || `Codex request '${pending.method}' failed.`));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
pending.resolve(message.result);
|
|
531
|
+
}
|
|
532
|
+
async #handleServerRequest(message) {
|
|
533
|
+
const method = String(message.method);
|
|
534
|
+
if (method === "item/commandExecution/requestApproval" ||
|
|
535
|
+
method === "item/fileChange/requestApproval") {
|
|
536
|
+
const params = message.params ?? {};
|
|
537
|
+
this.#touchTurn(params.threadId, params.turnId);
|
|
538
|
+
const approval = {
|
|
539
|
+
id: createApprovalId(),
|
|
540
|
+
serverRequestId: message.id,
|
|
541
|
+
kind: method === "item/commandExecution/requestApproval" ? "commandExecution" : "fileChange",
|
|
542
|
+
method,
|
|
543
|
+
threadId: String(params.threadId ?? ""),
|
|
544
|
+
turnId: String(params.turnId ?? ""),
|
|
545
|
+
itemId: String(params.itemId ?? ""),
|
|
546
|
+
command: typeof params.command === "string" ? params.command : null,
|
|
547
|
+
cwd: typeof params.cwd === "string" ? params.cwd : null,
|
|
548
|
+
reason: typeof params.reason === "string" ? params.reason : null,
|
|
549
|
+
commandActions: Array.isArray(params.commandActions) ? params.commandActions : null,
|
|
550
|
+
createdAt: new Date().toISOString(),
|
|
551
|
+
};
|
|
552
|
+
this.pendingApprovals.set(approval.id, approval);
|
|
553
|
+
this.#pauseTurnForApproval(makeTurnKey(approval.threadId, approval.turnId), approval.id);
|
|
554
|
+
this.emit("approvalRequested", { ...approval });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
await this.#sendResponse({
|
|
558
|
+
id: message.id,
|
|
559
|
+
error: {
|
|
560
|
+
code: -32601,
|
|
561
|
+
message: `Method '${method}' is not supported by telegram-codex-bridge.`,
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
#handleNotification(message) {
|
|
566
|
+
const method = String(message.method ?? "");
|
|
567
|
+
const params = message.params ?? {};
|
|
568
|
+
if (method === "item/agentMessage/delta") {
|
|
569
|
+
this.#touchTurn(params.threadId, params.turnId);
|
|
570
|
+
const key = makeTurnKey(params.threadId, params.turnId);
|
|
571
|
+
const buffer = this.turnOutput.get(key) ?? { delta: "", items: [] };
|
|
572
|
+
buffer.delta += String(params.delta ?? "");
|
|
573
|
+
this.turnOutput.set(key, buffer);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (method === "item/completed") {
|
|
577
|
+
this.#touchTurn(params.threadId, params.turnId);
|
|
578
|
+
const key = makeTurnKey(params.threadId, params.turnId);
|
|
579
|
+
const item = params.item ?? {};
|
|
580
|
+
if (item.type === "agentMessage" && typeof item.text === "string") {
|
|
581
|
+
const buffer = this.turnOutput.get(key) ?? { delta: "", items: [] };
|
|
582
|
+
buffer.items.push(item.text.trim());
|
|
583
|
+
this.turnOutput.set(key, buffer);
|
|
584
|
+
}
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (method === "turn/completed") {
|
|
588
|
+
const turn = params.turn;
|
|
589
|
+
const key = makeTurnKey(params.threadId, turn?.id);
|
|
590
|
+
this.#clearTurnApprovalTracking(key);
|
|
591
|
+
const waiter = this.turnWaiters.get(key);
|
|
592
|
+
if (waiter) {
|
|
593
|
+
this.turnWaiters.delete(key);
|
|
594
|
+
clearTimeout(waiter.timer);
|
|
595
|
+
waiter.resolve(turn);
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
this.completedTurns.set(key, turn);
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (method === "error") {
|
|
603
|
+
this.emit("warning", {
|
|
604
|
+
type: "notification_error",
|
|
605
|
+
error: params?.message ? String(params.message) : "Unknown app-server error notification.",
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
#handleProcessFailure(error) {
|
|
610
|
+
const reason = error instanceof Error ? error : new Error(String(error));
|
|
611
|
+
if (!this.started && !this.startPromise) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
this.started = false;
|
|
615
|
+
if (this.reader) {
|
|
616
|
+
this.reader.close();
|
|
617
|
+
this.reader = null;
|
|
618
|
+
}
|
|
619
|
+
this.child = null;
|
|
620
|
+
this.emit("warning", {
|
|
621
|
+
type: "process_failure",
|
|
622
|
+
error: sanitizeError(reason),
|
|
623
|
+
});
|
|
624
|
+
for (const { reject, timer } of this.pendingRequests.values()) {
|
|
625
|
+
clearTimeout(timer);
|
|
626
|
+
reject(reason);
|
|
627
|
+
}
|
|
628
|
+
this.pendingRequests.clear();
|
|
629
|
+
for (const { reject, timer } of this.turnWaiters.values()) {
|
|
630
|
+
clearTimeout(timer);
|
|
631
|
+
reject(reason);
|
|
632
|
+
}
|
|
633
|
+
this.turnWaiters.clear();
|
|
634
|
+
this.pendingApprovals.clear();
|
|
635
|
+
this.turnOutput.clear();
|
|
636
|
+
this.completedTurns.clear();
|
|
637
|
+
this.activeTurnByThread.clear();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function normalizeSandboxMode(value) {
|
|
641
|
+
const mode = String(value ?? "danger-full-access")
|
|
642
|
+
.trim()
|
|
643
|
+
.toLowerCase();
|
|
644
|
+
if (mode === "read-only" || mode === "workspace-write" || mode === "danger-full-access") {
|
|
645
|
+
return mode;
|
|
646
|
+
}
|
|
647
|
+
return "danger-full-access";
|
|
648
|
+
}
|
|
649
|
+
function normalizeApprovalPolicy(value) {
|
|
650
|
+
const mode = String(value ?? "never")
|
|
651
|
+
.trim()
|
|
652
|
+
.toLowerCase();
|
|
653
|
+
if (mode === "untrusted" || mode === "on-failure" || mode === "on-request" || mode === "never") {
|
|
654
|
+
return mode;
|
|
655
|
+
}
|
|
656
|
+
return "never";
|
|
657
|
+
}
|
|
658
|
+
function normalizeApprovalDecision(value) {
|
|
659
|
+
const decision = String(value ?? "")
|
|
660
|
+
.trim()
|
|
661
|
+
.toLowerCase();
|
|
662
|
+
if (decision === "accept" || decision === "approve" || decision === "approved") {
|
|
663
|
+
return "accept";
|
|
664
|
+
}
|
|
665
|
+
if (decision === "acceptforsession" || decision === "always") {
|
|
666
|
+
return "acceptForSession";
|
|
667
|
+
}
|
|
668
|
+
if (decision === "decline" ||
|
|
669
|
+
decision === "deny" ||
|
|
670
|
+
decision === "denied" ||
|
|
671
|
+
decision === "reject") {
|
|
672
|
+
return "decline";
|
|
673
|
+
}
|
|
674
|
+
if (decision === "cancel" || decision === "abort") {
|
|
675
|
+
return "cancel";
|
|
676
|
+
}
|
|
677
|
+
throw new Error("decision must be one of: accept, acceptForSession, decline, cancel.");
|
|
678
|
+
}
|
|
679
|
+
function normalizeTimeout(value, fallback) {
|
|
680
|
+
const parsed = Number.parseInt(String(value ?? fallback), 10);
|
|
681
|
+
if (!Number.isFinite(parsed) || parsed < 1_000) {
|
|
682
|
+
return fallback;
|
|
683
|
+
}
|
|
684
|
+
return parsed;
|
|
685
|
+
}
|
|
686
|
+
function normalizeCliPath(value) {
|
|
687
|
+
if (process.platform !== "win32") {
|
|
688
|
+
return value;
|
|
689
|
+
}
|
|
690
|
+
return String(value).replace(/\\/g, "/");
|
|
691
|
+
}
|
|
692
|
+
function makeTurnKey(threadId, turnId) {
|
|
693
|
+
return `${String(threadId ?? "")}::${String(turnId ?? "")}`;
|
|
694
|
+
}
|
|
695
|
+
function createApprovalId() {
|
|
696
|
+
return `apr_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
697
|
+
}
|
|
698
|
+
function annotateSpawnError(error, command) {
|
|
699
|
+
if (!error || typeof error !== "object") {
|
|
700
|
+
return error;
|
|
701
|
+
}
|
|
702
|
+
if (error.code === "ENOENT") {
|
|
703
|
+
return new Error([
|
|
704
|
+
`Cannot execute Codex binary '${command}' (ENOENT).`,
|
|
705
|
+
"Set CODEX_BIN to a valid executable (example: C:\\Users\\<you>\\...\\codex.exe) or ensure it is on PATH.",
|
|
706
|
+
].join("\n"));
|
|
707
|
+
}
|
|
708
|
+
if (process.platform === "win32" && error.code === "EPERM") {
|
|
709
|
+
return new Error([
|
|
710
|
+
`Cannot execute Codex binary '${command}' (EPERM).`,
|
|
711
|
+
"On Windows, verify CODEX_BIN points to an executable and that permissions allow process spawn.",
|
|
712
|
+
].join("\n"));
|
|
713
|
+
}
|
|
714
|
+
return error;
|
|
715
|
+
}
|
|
716
|
+
function normalizeTurnInputItems({ prompt, inputItems }) {
|
|
717
|
+
const items = [];
|
|
718
|
+
const text = String(prompt ?? "").trim();
|
|
719
|
+
if (text) {
|
|
720
|
+
items.push({
|
|
721
|
+
type: "text",
|
|
722
|
+
text,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
if (Array.isArray(inputItems)) {
|
|
726
|
+
for (const entry of inputItems) {
|
|
727
|
+
const type = String(entry?.type ?? "")
|
|
728
|
+
.trim()
|
|
729
|
+
.toLowerCase();
|
|
730
|
+
if (type === "image") {
|
|
731
|
+
const url = String(entry?.url ?? "").trim();
|
|
732
|
+
if (!url) {
|
|
733
|
+
throw new Error("Image input item requires a non-empty url.");
|
|
734
|
+
}
|
|
735
|
+
items.push({
|
|
736
|
+
type: "image",
|
|
737
|
+
url,
|
|
738
|
+
});
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (type === "localimage") {
|
|
742
|
+
const localPath = String(entry?.path ?? "").trim();
|
|
743
|
+
if (!localPath) {
|
|
744
|
+
throw new Error("localImage input item requires a non-empty path.");
|
|
745
|
+
}
|
|
746
|
+
items.push({
|
|
747
|
+
type: "localImage",
|
|
748
|
+
path: localPath,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (items.length === 0) {
|
|
754
|
+
throw new Error("Prompt cannot be empty.");
|
|
755
|
+
}
|
|
756
|
+
return items;
|
|
757
|
+
}
|
|
758
|
+
function sanitizeError(error) {
|
|
759
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
760
|
+
return raw.split(/\r?\n/).slice(0, 12).join("\n");
|
|
761
|
+
}
|
|
762
|
+
//# sourceMappingURL=codex-app-client.js.map
|