@codex-native/sdk 0.0.1 → 0.0.2
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/README.md +482 -5
- package/codex_native.darwin-arm64.node +0 -0
- package/dist/chunk-ZTUGAPWF.mjs +1996 -0
- package/dist/chunk-ZTUGAPWF.mjs.map +1 -0
- package/dist/cli.cjs +3507 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +1658 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.cjs +4722 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +1944 -272
- package/dist/index.d.ts +2234 -0
- package/dist/index.mjs +2501 -454
- package/dist/index.mjs.map +1 -1
- package/npm/darwin-arm64/README.md +2 -2
- package/npm/darwin-arm64/package.json +3 -3
- package/npm/darwin-x64/README.md +2 -2
- package/npm/darwin-x64/package.json +3 -3
- package/npm/linux-arm64-gnu/README.md +2 -2
- package/npm/linux-arm64-gnu/package.json +3 -3
- package/npm/linux-arm64-musl/README.md +2 -2
- package/npm/linux-arm64-musl/package.json +3 -3
- package/npm/linux-x64-gnu/README.md +2 -2
- package/npm/linux-x64-gnu/package.json +3 -3
- package/npm/linux-x64-musl/README.md +2 -2
- package/npm/linux-x64-musl/package.json +3 -3
- package/npm/win32-arm64-msvc/README.md +2 -2
- package/npm/win32-arm64-msvc/package.json +3 -3
- package/npm/win32-x64-msvc/README.md +2 -2
- package/npm/win32-x64-msvc/package.json +3 -3
- package/package.json +56 -20
|
@@ -0,0 +1,1996 @@
|
|
|
1
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
5
|
+
var getDirname = () => path.dirname(getFilename());
|
|
6
|
+
var __dirname = /* @__PURE__ */ getDirname();
|
|
7
|
+
var __filename = /* @__PURE__ */ getFilename();
|
|
8
|
+
|
|
9
|
+
// src/nativeBinding.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
import path2 from "path";
|
|
13
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14
|
+
var CLI_ENTRYPOINT_ENV = "CODEX_NODE_CLI_ENTRYPOINT";
|
|
15
|
+
function ensureCliEntrypointEnv() {
|
|
16
|
+
if (process.env[CLI_ENTRYPOINT_ENV]) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const filename = fileURLToPath2(import.meta.url);
|
|
20
|
+
const dirname3 = path2.dirname(filename);
|
|
21
|
+
const candidates = [
|
|
22
|
+
path2.resolve(dirname3, "cli.cjs"),
|
|
23
|
+
path2.resolve(dirname3, "../cli.cjs"),
|
|
24
|
+
path2.resolve(dirname3, "../dist/cli.cjs")
|
|
25
|
+
];
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
if (fs.existsSync(candidate)) {
|
|
28
|
+
process.env[CLI_ENTRYPOINT_ENV] = candidate;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
ensureCliEntrypointEnv();
|
|
34
|
+
var cachedBinding;
|
|
35
|
+
function getImportMetaUrl() {
|
|
36
|
+
try {
|
|
37
|
+
return Function(
|
|
38
|
+
"return typeof import.meta !== 'undefined' && import.meta.url ? import.meta.url : undefined;"
|
|
39
|
+
)();
|
|
40
|
+
} catch {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function resolveBindingEntryPath() {
|
|
45
|
+
if (typeof __dirname === "string") {
|
|
46
|
+
return path2.resolve(__dirname, "..", "index.js");
|
|
47
|
+
}
|
|
48
|
+
const importMetaUrl = getImportMetaUrl();
|
|
49
|
+
if (importMetaUrl) {
|
|
50
|
+
try {
|
|
51
|
+
const filePath = fileURLToPath2(importMetaUrl);
|
|
52
|
+
return path2.resolve(path2.dirname(filePath), "..", "index.js");
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return path2.resolve(process.cwd(), "index.js");
|
|
57
|
+
}
|
|
58
|
+
function resolveRequire() {
|
|
59
|
+
const globalRequire = globalThis.require;
|
|
60
|
+
if (typeof globalRequire === "function") {
|
|
61
|
+
return globalRequire;
|
|
62
|
+
}
|
|
63
|
+
if (typeof __filename === "string") {
|
|
64
|
+
try {
|
|
65
|
+
return createRequire(__filename);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const importMetaUrl = getImportMetaUrl();
|
|
70
|
+
if (importMetaUrl) {
|
|
71
|
+
try {
|
|
72
|
+
return createRequire(importMetaUrl);
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const fallbackBase = typeof __dirname === "string" ? __dirname : process.cwd();
|
|
77
|
+
const fallbackPath = path2.join(fallbackBase, "noop.js");
|
|
78
|
+
return createRequire(fallbackPath);
|
|
79
|
+
}
|
|
80
|
+
function getNativeBinding() {
|
|
81
|
+
if (cachedBinding !== void 0) {
|
|
82
|
+
return cachedBinding;
|
|
83
|
+
}
|
|
84
|
+
const requireFn = resolveRequire();
|
|
85
|
+
const envPath = process.env.CODEX_NATIVE_BINDING;
|
|
86
|
+
if (envPath && envPath.length > 0) {
|
|
87
|
+
process.env.NAPI_RS_NATIVE_LIBRARY_PATH = envPath;
|
|
88
|
+
}
|
|
89
|
+
const bindingEntryPath = resolveBindingEntryPath();
|
|
90
|
+
try {
|
|
91
|
+
const binding = requireFn(bindingEntryPath);
|
|
92
|
+
binding.ensureTokioRuntime?.();
|
|
93
|
+
cachedBinding = binding;
|
|
94
|
+
return cachedBinding;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn("Failed to load native NAPI binding:", error);
|
|
97
|
+
cachedBinding = null;
|
|
98
|
+
return cachedBinding;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function runApplyPatch(patch) {
|
|
102
|
+
if (!patch) {
|
|
103
|
+
throw new Error("apply_patch requires patch contents");
|
|
104
|
+
}
|
|
105
|
+
const binding = getNativeBinding();
|
|
106
|
+
if (!binding) throw new Error("Native binding not available");
|
|
107
|
+
binding.runApplyPatch(patch);
|
|
108
|
+
}
|
|
109
|
+
async function reverieListConversations(codexHomePath, limit, offset) {
|
|
110
|
+
const binding = getNativeBinding();
|
|
111
|
+
if (!binding?.reverieListConversations) throw new Error("Native binding not available or reverie functions not supported");
|
|
112
|
+
return binding.reverieListConversations(codexHomePath, limit, offset);
|
|
113
|
+
}
|
|
114
|
+
async function reverieSearchConversations(codexHomePath, query, limit) {
|
|
115
|
+
const binding = getNativeBinding();
|
|
116
|
+
if (!binding?.reverieSearchConversations) throw new Error("Native binding not available or reverie functions not supported");
|
|
117
|
+
return binding.reverieSearchConversations(codexHomePath, query, limit);
|
|
118
|
+
}
|
|
119
|
+
async function reverieSearchSemantic(codexHomePath, context, options) {
|
|
120
|
+
const binding = getNativeBinding();
|
|
121
|
+
if (!binding?.reverieSearchSemantic) throw new Error("Native binding not available or reverie functions not supported");
|
|
122
|
+
return binding.reverieSearchSemantic(codexHomePath, context, options);
|
|
123
|
+
}
|
|
124
|
+
async function reverieIndexSemantic(codexHomePath, options) {
|
|
125
|
+
const binding = getNativeBinding();
|
|
126
|
+
if (!binding?.reverieIndexSemantic) throw new Error("Native binding not available or reverie functions not supported");
|
|
127
|
+
return binding.reverieIndexSemantic(codexHomePath, options);
|
|
128
|
+
}
|
|
129
|
+
async function reverieGetConversationInsights(conversationPath, query) {
|
|
130
|
+
const binding = getNativeBinding();
|
|
131
|
+
if (!binding?.reverieGetConversationInsights) throw new Error("Native binding not available or reverie functions not supported");
|
|
132
|
+
return binding.reverieGetConversationInsights(conversationPath, query);
|
|
133
|
+
}
|
|
134
|
+
function encodeToToon(value) {
|
|
135
|
+
const binding = getNativeBinding();
|
|
136
|
+
if (!binding?.toonEncode) throw new Error("Native binding not available or toon encoder not supported");
|
|
137
|
+
return binding.toonEncode(value);
|
|
138
|
+
}
|
|
139
|
+
async function fastEmbedInit(options) {
|
|
140
|
+
const binding = getNativeBinding();
|
|
141
|
+
if (!binding?.fastEmbedInit) throw new Error("Native binding not available or FastEmbed functions not supported");
|
|
142
|
+
await binding.fastEmbedInit(options);
|
|
143
|
+
}
|
|
144
|
+
async function fastEmbedEmbed(request) {
|
|
145
|
+
const binding = getNativeBinding();
|
|
146
|
+
if (!binding?.fastEmbedEmbed) throw new Error("Native binding not available or FastEmbed functions not supported");
|
|
147
|
+
return binding.fastEmbedEmbed(request);
|
|
148
|
+
}
|
|
149
|
+
function tokenizerCount(text, options) {
|
|
150
|
+
const binding = getNativeBinding();
|
|
151
|
+
if (!binding?.tokenizerCount) throw new Error("Native binding not available or tokenizer functions not supported");
|
|
152
|
+
return binding.tokenizerCount(text, options);
|
|
153
|
+
}
|
|
154
|
+
function tokenizerEncode(text, options) {
|
|
155
|
+
const binding = getNativeBinding();
|
|
156
|
+
if (!binding?.tokenizerEncode) throw new Error("Native binding not available or tokenizer functions not supported");
|
|
157
|
+
return binding.tokenizerEncode(text, options);
|
|
158
|
+
}
|
|
159
|
+
function tokenizerDecode(tokens, options) {
|
|
160
|
+
const binding = getNativeBinding();
|
|
161
|
+
if (!binding?.tokenizerDecode) throw new Error("Native binding not available or tokenizer functions not supported");
|
|
162
|
+
return binding.tokenizerDecode(tokens, options);
|
|
163
|
+
}
|
|
164
|
+
async function collectRepoDiffSummary(options) {
|
|
165
|
+
const binding = getNativeBinding();
|
|
166
|
+
if (!binding?.collectRepoDiffSummary) {
|
|
167
|
+
throw new Error("Native binding not available or repo diff helpers not supported");
|
|
168
|
+
}
|
|
169
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
170
|
+
const nativeOptions = options && (options.maxFiles !== void 0 || options.diffContextLines !== void 0 || options.diffCharLimit !== void 0) ? {
|
|
171
|
+
maxFiles: options.maxFiles,
|
|
172
|
+
diffContextLines: options.diffContextLines,
|
|
173
|
+
diffCharLimit: options.diffCharLimit
|
|
174
|
+
} : void 0;
|
|
175
|
+
return binding.collectRepoDiffSummary(cwd, options?.baseBranchOverride, nativeOptions);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/tui.ts
|
|
179
|
+
function startTui(request) {
|
|
180
|
+
const binding = getNativeBinding();
|
|
181
|
+
if (!binding) {
|
|
182
|
+
throw new Error("Native binding is not available");
|
|
183
|
+
}
|
|
184
|
+
if (typeof binding.startTui === "function") {
|
|
185
|
+
const nativeSession = binding.startTui(request);
|
|
186
|
+
return wrapNativeSession(nativeSession);
|
|
187
|
+
}
|
|
188
|
+
if (typeof binding.runTui === "function") {
|
|
189
|
+
return createLegacySession(binding, request);
|
|
190
|
+
}
|
|
191
|
+
throw new Error("Native binding does not expose startTui or runTui");
|
|
192
|
+
}
|
|
193
|
+
async function runTui(request, options = {}) {
|
|
194
|
+
const session = startTui(request);
|
|
195
|
+
const { signal } = options;
|
|
196
|
+
let abortListener;
|
|
197
|
+
try {
|
|
198
|
+
if (signal) {
|
|
199
|
+
if (signal.aborted) {
|
|
200
|
+
session.shutdown();
|
|
201
|
+
} else {
|
|
202
|
+
abortListener = () => session.shutdown();
|
|
203
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return await session.wait();
|
|
207
|
+
} finally {
|
|
208
|
+
if (abortListener && signal) {
|
|
209
|
+
signal.removeEventListener("abort", abortListener);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function wrapNativeSession(nativeSession) {
|
|
214
|
+
return {
|
|
215
|
+
wait: () => nativeSession.wait(),
|
|
216
|
+
shutdown: () => nativeSession.shutdown(),
|
|
217
|
+
get closed() {
|
|
218
|
+
return nativeSession.closed;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function createLegacySession(binding, request) {
|
|
223
|
+
if (typeof binding.runTui !== "function") {
|
|
224
|
+
throw new Error("Native binding does not expose runTui");
|
|
225
|
+
}
|
|
226
|
+
let closed = false;
|
|
227
|
+
const promise = binding.runTui(request).then(
|
|
228
|
+
(result) => {
|
|
229
|
+
closed = true;
|
|
230
|
+
return result;
|
|
231
|
+
},
|
|
232
|
+
(error) => {
|
|
233
|
+
closed = true;
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
return {
|
|
238
|
+
wait: () => promise,
|
|
239
|
+
shutdown() {
|
|
240
|
+
throw new Error(
|
|
241
|
+
"Programmatic shutdown is not supported by this native binding build. Rebuild the SDK to enable startTui()."
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
get closed() {
|
|
245
|
+
return closed;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/lsp/servers.ts
|
|
251
|
+
import * as fs2 from "fs";
|
|
252
|
+
import * as path3 from "path";
|
|
253
|
+
var MARKERS_NODE = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "bun.lock"];
|
|
254
|
+
var MARKERS_PY = ["pyproject.toml", "requirements.txt", "Pipfile", "setup.py", "setup.cfg", "poetry.lock"];
|
|
255
|
+
var MARKERS_RUST = ["Cargo.toml"];
|
|
256
|
+
var DEFAULT_SERVERS = [
|
|
257
|
+
{
|
|
258
|
+
id: "typescript",
|
|
259
|
+
displayName: "TypeScript Language Server",
|
|
260
|
+
command: ["typescript-language-server", "--stdio"],
|
|
261
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
262
|
+
workspace: { type: "markers", include: MARKERS_NODE }
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: "pyright",
|
|
266
|
+
displayName: "Pyright",
|
|
267
|
+
command: ["pyright-langserver", "--stdio"],
|
|
268
|
+
extensions: [".py", ".pyi"],
|
|
269
|
+
workspace: { type: "markers", include: MARKERS_PY }
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "rust-analyzer",
|
|
273
|
+
displayName: "rust-analyzer",
|
|
274
|
+
command: ["rust-analyzer"],
|
|
275
|
+
extensions: [".rs"],
|
|
276
|
+
workspace: { type: "markers", include: MARKERS_RUST }
|
|
277
|
+
}
|
|
278
|
+
];
|
|
279
|
+
function findServerForFile(filePath) {
|
|
280
|
+
const lower = filePath.toLowerCase();
|
|
281
|
+
return DEFAULT_SERVERS.find((server) => server.extensions.some((ext) => lower.endsWith(ext)));
|
|
282
|
+
}
|
|
283
|
+
function resolveWorkspaceRoot(filePath, locator, fallbackDir) {
|
|
284
|
+
if (!locator) {
|
|
285
|
+
return fallbackDir;
|
|
286
|
+
}
|
|
287
|
+
if (locator.type === "fixed") {
|
|
288
|
+
return locator.path;
|
|
289
|
+
}
|
|
290
|
+
const include = locator.include ?? [];
|
|
291
|
+
const exclude = locator.exclude ?? [];
|
|
292
|
+
let current = fs2.statSync(filePath, { throwIfNoEntry: false })?.isDirectory() ? filePath : path3.dirname(filePath);
|
|
293
|
+
const root = path3.parse(current).root;
|
|
294
|
+
while (true) {
|
|
295
|
+
if (exclude.some((pattern) => fs2.existsSync(path3.join(current, pattern)))) {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
if (include.some((pattern) => fs2.existsSync(path3.join(current, pattern)))) {
|
|
299
|
+
return current;
|
|
300
|
+
}
|
|
301
|
+
if (current === root) {
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
const parent = path3.dirname(current);
|
|
305
|
+
if (parent === current) {
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
current = parent;
|
|
309
|
+
}
|
|
310
|
+
return fallbackDir;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/lsp/manager.ts
|
|
314
|
+
import * as path5 from "path";
|
|
315
|
+
|
|
316
|
+
// src/lsp/client.ts
|
|
317
|
+
import { spawn } from "child_process";
|
|
318
|
+
import * as fs3 from "fs/promises";
|
|
319
|
+
import * as path4 from "path";
|
|
320
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
321
|
+
import { EventEmitter } from "events";
|
|
322
|
+
import { createMessageConnection } from "vscode-jsonrpc";
|
|
323
|
+
import { StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/lib/node/main.js";
|
|
324
|
+
import { DiagnosticSeverity } from "vscode-languageserver-types";
|
|
325
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
326
|
+
var LspClient = class _LspClient {
|
|
327
|
+
constructor(config, root) {
|
|
328
|
+
this.config = config;
|
|
329
|
+
this.root = root;
|
|
330
|
+
}
|
|
331
|
+
connection = null;
|
|
332
|
+
process = null;
|
|
333
|
+
diagnostics = /* @__PURE__ */ new Map();
|
|
334
|
+
versions = /* @__PURE__ */ new Map();
|
|
335
|
+
emitter = new EventEmitter();
|
|
336
|
+
static async start(server, root) {
|
|
337
|
+
const client = new _LspClient(server, root);
|
|
338
|
+
await client.initialize();
|
|
339
|
+
return client;
|
|
340
|
+
}
|
|
341
|
+
async initialize() {
|
|
342
|
+
const [command, ...args] = this.config.command;
|
|
343
|
+
if (!command) {
|
|
344
|
+
throw new Error(`LSP server ${this.config.id} is missing a command executable`);
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
this.process = spawn(command, args, {
|
|
348
|
+
cwd: this.root,
|
|
349
|
+
env: { ...process.env, ...this.config.env },
|
|
350
|
+
stdio: "pipe"
|
|
351
|
+
});
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw new Error(`Failed to spawn ${this.config.displayName} (${command}): ${String(error)}`);
|
|
354
|
+
}
|
|
355
|
+
const child = this.process;
|
|
356
|
+
child.stderr.on("data", (_chunk) => {
|
|
357
|
+
});
|
|
358
|
+
const reader = new StreamMessageReader(child.stdout);
|
|
359
|
+
const writer = new StreamMessageWriter(child.stdin);
|
|
360
|
+
this.connection = createMessageConnection(reader, writer);
|
|
361
|
+
this.connection.onNotification("textDocument/publishDiagnostics", (payload) => {
|
|
362
|
+
const fsPath = fileURLToPath3(payload.uri);
|
|
363
|
+
this.diagnostics.set(fsPath, payload.diagnostics);
|
|
364
|
+
this.emitter.emit(`diagnostics:${fsPath}`);
|
|
365
|
+
});
|
|
366
|
+
this.connection.onError((err) => {
|
|
367
|
+
console.warn(`[lsp:${this.config.id}] connection error`, err);
|
|
368
|
+
});
|
|
369
|
+
this.connection.listen();
|
|
370
|
+
await this.connection.sendRequest("initialize", {
|
|
371
|
+
rootUri: pathToFileURL(this.root).href,
|
|
372
|
+
processId: process.pid,
|
|
373
|
+
initializationOptions: this.config.initializationOptions ?? {},
|
|
374
|
+
capabilities: {
|
|
375
|
+
textDocument: {
|
|
376
|
+
synchronization: {
|
|
377
|
+
didOpen: true,
|
|
378
|
+
didChange: true
|
|
379
|
+
},
|
|
380
|
+
publishDiagnostics: {
|
|
381
|
+
versionSupport: true
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
workspace: {
|
|
385
|
+
workspaceFolders: true
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
workspaceFolders: [
|
|
389
|
+
{
|
|
390
|
+
name: path4.basename(this.root),
|
|
391
|
+
uri: pathToFileURL(this.root).href
|
|
392
|
+
}
|
|
393
|
+
]
|
|
394
|
+
});
|
|
395
|
+
await this.connection.sendNotification("initialized", {});
|
|
396
|
+
}
|
|
397
|
+
async openFile(filePath, waitForDiagnostics) {
|
|
398
|
+
if (!this.connection) return;
|
|
399
|
+
const absolute = path4.resolve(filePath);
|
|
400
|
+
const text = await fs3.readFile(absolute, "utf8");
|
|
401
|
+
const uri = pathToFileURL(absolute).href;
|
|
402
|
+
const languageId = detectLanguageId(absolute);
|
|
403
|
+
const existingVersion = this.versions.get(absolute);
|
|
404
|
+
if (existingVersion === void 0) {
|
|
405
|
+
this.versions.set(absolute, 0);
|
|
406
|
+
await this.connection.sendNotification("textDocument/didOpen", {
|
|
407
|
+
textDocument: {
|
|
408
|
+
uri,
|
|
409
|
+
languageId,
|
|
410
|
+
version: 0,
|
|
411
|
+
text
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
const next = existingVersion + 1;
|
|
416
|
+
this.versions.set(absolute, next);
|
|
417
|
+
await this.connection.sendNotification("textDocument/didChange", {
|
|
418
|
+
textDocument: {
|
|
419
|
+
uri,
|
|
420
|
+
version: next
|
|
421
|
+
},
|
|
422
|
+
contentChanges: [{ text }]
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (waitForDiagnostics) {
|
|
426
|
+
await this.waitForDiagnostics(absolute);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
getDiagnostics(filePath) {
|
|
430
|
+
const absolute = path4.resolve(filePath);
|
|
431
|
+
return this.diagnostics.get(absolute) ?? [];
|
|
432
|
+
}
|
|
433
|
+
waitForDiagnostics(filePath, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
434
|
+
const absolute = path4.resolve(filePath);
|
|
435
|
+
return new Promise((resolve5) => {
|
|
436
|
+
const timer = setTimeout(resolve5, timeoutMs).unref();
|
|
437
|
+
this.emitter.once(`diagnostics:${absolute}`, () => {
|
|
438
|
+
clearTimeout(timer);
|
|
439
|
+
resolve5();
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async shutdown() {
|
|
444
|
+
try {
|
|
445
|
+
await this.connection?.dispose();
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
if (this.process && !this.process.killed) {
|
|
449
|
+
this.process.kill();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
function detectLanguageId(filePath) {
|
|
454
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
455
|
+
switch (ext) {
|
|
456
|
+
case ".ts":
|
|
457
|
+
case ".mts":
|
|
458
|
+
case ".cts":
|
|
459
|
+
return "typescript";
|
|
460
|
+
case ".tsx":
|
|
461
|
+
return "typescriptreact";
|
|
462
|
+
case ".js":
|
|
463
|
+
case ".mjs":
|
|
464
|
+
case ".cjs":
|
|
465
|
+
return "javascript";
|
|
466
|
+
case ".jsx":
|
|
467
|
+
return "javascriptreact";
|
|
468
|
+
case ".py":
|
|
469
|
+
case ".pyi":
|
|
470
|
+
return "python";
|
|
471
|
+
case ".rs":
|
|
472
|
+
return "rust";
|
|
473
|
+
default:
|
|
474
|
+
return "plaintext";
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function normalizeSeverity(severity) {
|
|
478
|
+
switch (severity) {
|
|
479
|
+
case DiagnosticSeverity.Error:
|
|
480
|
+
return "error";
|
|
481
|
+
case DiagnosticSeverity.Warning:
|
|
482
|
+
return "warning";
|
|
483
|
+
case DiagnosticSeverity.Information:
|
|
484
|
+
return "info";
|
|
485
|
+
case DiagnosticSeverity.Hint:
|
|
486
|
+
return "hint";
|
|
487
|
+
default:
|
|
488
|
+
return "error";
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/lsp/manager.ts
|
|
493
|
+
var LspManager = class {
|
|
494
|
+
constructor(options) {
|
|
495
|
+
this.options = options;
|
|
496
|
+
}
|
|
497
|
+
clients = /* @__PURE__ */ new Map();
|
|
498
|
+
async collectDiagnostics(files) {
|
|
499
|
+
const unique = Array.from(new Set(files.map((file) => path5.resolve(file))));
|
|
500
|
+
const results = [];
|
|
501
|
+
for (const filePath of unique) {
|
|
502
|
+
const server = findServerForFile(filePath);
|
|
503
|
+
if (!server) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const root = resolveWorkspaceRoot(filePath, server.workspace, this.options.workingDirectory);
|
|
507
|
+
const client = await this.getClient(server, root);
|
|
508
|
+
if (!client) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
await client.openFile(filePath, this.options.waitForDiagnostics !== false);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
console.warn(`[lsp] failed to open ${filePath}:`, error);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const normalized = client.getDiagnostics(filePath).map((diag) => normalizeDiagnostic(diag)).filter((diag) => diag.message.trim().length > 0);
|
|
518
|
+
if (normalized.length > 0) {
|
|
519
|
+
results.push({ path: filePath, diagnostics: normalized });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return results;
|
|
523
|
+
}
|
|
524
|
+
async dispose() {
|
|
525
|
+
await Promise.all(
|
|
526
|
+
Array.from(this.clients.values()).map(async (promise) => {
|
|
527
|
+
const client = await promise;
|
|
528
|
+
await client?.shutdown();
|
|
529
|
+
})
|
|
530
|
+
);
|
|
531
|
+
this.clients.clear();
|
|
532
|
+
}
|
|
533
|
+
async getClient(server, root) {
|
|
534
|
+
const key = `${server.id}:${root}`;
|
|
535
|
+
let existing = this.clients.get(key);
|
|
536
|
+
if (!existing) {
|
|
537
|
+
existing = this.createClient(server, root);
|
|
538
|
+
this.clients.set(key, existing);
|
|
539
|
+
}
|
|
540
|
+
const client = await existing;
|
|
541
|
+
if (!client) {
|
|
542
|
+
this.clients.delete(key);
|
|
543
|
+
}
|
|
544
|
+
return client;
|
|
545
|
+
}
|
|
546
|
+
async createClient(server, root) {
|
|
547
|
+
try {
|
|
548
|
+
return await LspClient.start(server, root);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
console.warn(`[lsp] unable to start ${server.displayName}:`, error);
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
function normalizeDiagnostic(diag) {
|
|
556
|
+
return {
|
|
557
|
+
message: diag.message ?? "",
|
|
558
|
+
severity: normalizeSeverity(diag.severity),
|
|
559
|
+
source: diag.source,
|
|
560
|
+
code: diag.code,
|
|
561
|
+
range: diag.range
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/lsp/format.ts
|
|
566
|
+
import * as path6 from "path";
|
|
567
|
+
var MAX_DIAGNOSTICS_PER_FILE = 5;
|
|
568
|
+
function formatDiagnosticsForTool(diagnostics) {
|
|
569
|
+
return diagnostics.map(({ path: filePath, diagnostics: entries }) => {
|
|
570
|
+
const rel = filePath;
|
|
571
|
+
const lines = entries.slice(0, MAX_DIAGNOSTICS_PER_FILE).map((diag) => {
|
|
572
|
+
const { line, character } = diag.range.start;
|
|
573
|
+
const location = `${line + 1}:${character + 1}`;
|
|
574
|
+
const source = diag.source ? ` \xB7 ${diag.source}` : "";
|
|
575
|
+
return ` - [${diag.severity.toUpperCase()}] ${diag.message} (${location}${source})`;
|
|
576
|
+
});
|
|
577
|
+
const trimmed = entries.length > MAX_DIAGNOSTICS_PER_FILE ? " - \u2026" : "";
|
|
578
|
+
return [`\u2022 ${rel}`, ...lines, trimmed].filter(Boolean).join("\n");
|
|
579
|
+
}).join("\n");
|
|
580
|
+
}
|
|
581
|
+
function formatDiagnosticsForBackgroundEvent(diagnostics, cwd) {
|
|
582
|
+
return diagnostics.map(({ path: filePath, diagnostics: entries }) => {
|
|
583
|
+
const rel = path6.relative(cwd, filePath) || filePath;
|
|
584
|
+
const lines = entries.slice(0, MAX_DIAGNOSTICS_PER_FILE).map((diag) => {
|
|
585
|
+
const { line, character } = diag.range.start;
|
|
586
|
+
const location = `${line + 1}:${character + 1}`;
|
|
587
|
+
const source = diag.source ? ` \xB7 ${diag.source}` : "";
|
|
588
|
+
return ` - [${diag.severity.toUpperCase()}] ${diag.message} (${location}${source})`;
|
|
589
|
+
});
|
|
590
|
+
const trimmed = entries.length > MAX_DIAGNOSTICS_PER_FILE ? " - \u2026" : "";
|
|
591
|
+
return [`\u2022 ${rel}`, ...lines, trimmed].filter(Boolean).join("\n");
|
|
592
|
+
}).join("\n");
|
|
593
|
+
}
|
|
594
|
+
function filterBySeverity(diagnostics, minSeverity = "error") {
|
|
595
|
+
const severityOrder = {
|
|
596
|
+
error: 0,
|
|
597
|
+
warning: 1,
|
|
598
|
+
info: 2,
|
|
599
|
+
hint: 3
|
|
600
|
+
};
|
|
601
|
+
const threshold = severityOrder[minSeverity];
|
|
602
|
+
return diagnostics.map((file) => ({
|
|
603
|
+
...file,
|
|
604
|
+
diagnostics: file.diagnostics.filter(
|
|
605
|
+
(diag) => severityOrder[diag.severity] <= threshold
|
|
606
|
+
)
|
|
607
|
+
})).filter((file) => file.diagnostics.length > 0);
|
|
608
|
+
}
|
|
609
|
+
function summarizeDiagnostics(diagnostics) {
|
|
610
|
+
let errorCount = 0;
|
|
611
|
+
let warningCount = 0;
|
|
612
|
+
let infoCount = 0;
|
|
613
|
+
let hintCount = 0;
|
|
614
|
+
for (const file of diagnostics) {
|
|
615
|
+
for (const diag of file.diagnostics) {
|
|
616
|
+
switch (diag.severity) {
|
|
617
|
+
case "error":
|
|
618
|
+
errorCount++;
|
|
619
|
+
break;
|
|
620
|
+
case "warning":
|
|
621
|
+
warningCount++;
|
|
622
|
+
break;
|
|
623
|
+
case "info":
|
|
624
|
+
infoCount++;
|
|
625
|
+
break;
|
|
626
|
+
case "hint":
|
|
627
|
+
hintCount++;
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
fileCount: diagnostics.length,
|
|
634
|
+
errorCount,
|
|
635
|
+
warningCount,
|
|
636
|
+
infoCount,
|
|
637
|
+
hintCount,
|
|
638
|
+
totalCount: errorCount + warningCount + infoCount + hintCount
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function formatDiagnosticsWithSummary(diagnostics, cwd, options = {}) {
|
|
642
|
+
const filtered = options.minSeverity ? filterBySeverity(diagnostics, options.minSeverity) : diagnostics;
|
|
643
|
+
if (filtered.length === 0) {
|
|
644
|
+
return "No diagnostics found.";
|
|
645
|
+
}
|
|
646
|
+
const summary = summarizeDiagnostics(filtered);
|
|
647
|
+
const maxPerFile = options.maxPerFile ?? MAX_DIAGNOSTICS_PER_FILE;
|
|
648
|
+
const header = `LSP Diagnostics Summary: ${summary.errorCount} error${summary.errorCount !== 1 ? "s" : ""}, ${summary.warningCount} warning${summary.warningCount !== 1 ? "s" : ""} across ${summary.fileCount} file${summary.fileCount !== 1 ? "s" : ""}`;
|
|
649
|
+
const details = filtered.map(({ path: filePath, diagnostics: entries }) => {
|
|
650
|
+
const rel = path6.relative(cwd, filePath) || filePath;
|
|
651
|
+
const lines = entries.slice(0, maxPerFile).map((diag) => {
|
|
652
|
+
const { line, character } = diag.range.start;
|
|
653
|
+
const location = `${line + 1}:${character + 1}`;
|
|
654
|
+
const source = diag.source ? ` \xB7 ${diag.source}` : "";
|
|
655
|
+
return ` - [${diag.severity.toUpperCase()}] ${diag.message} (${location}${source})`;
|
|
656
|
+
});
|
|
657
|
+
const trimmed = entries.length > maxPerFile ? ` - \u2026 (${entries.length - maxPerFile} more)` : "";
|
|
658
|
+
return [`\u2022 ${rel}`, ...lines, trimmed].filter(Boolean).join("\n");
|
|
659
|
+
}).join("\n");
|
|
660
|
+
return `${header}
|
|
661
|
+
|
|
662
|
+
${details}`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/lsp/bridge.ts
|
|
666
|
+
import * as path7 from "path";
|
|
667
|
+
var LspDiagnosticsBridge = class {
|
|
668
|
+
constructor(options) {
|
|
669
|
+
this.options = options;
|
|
670
|
+
this.manager = new LspManager(options);
|
|
671
|
+
}
|
|
672
|
+
manager;
|
|
673
|
+
attached = /* @__PURE__ */ new WeakSet();
|
|
674
|
+
attach(thread) {
|
|
675
|
+
if (this.attached.has(thread)) {
|
|
676
|
+
return () => {
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
this.attached.add(thread);
|
|
680
|
+
const unsubscribe = thread.onEvent((event) => {
|
|
681
|
+
if (event.type !== "item.completed") {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (event.item.type === "file_change") {
|
|
685
|
+
const targets = event.item.changes.filter((change) => change.kind !== "delete").map((change) => path7.resolve(this.options.workingDirectory, change.path));
|
|
686
|
+
if (targets.length === 0) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
void this.processDiagnostics(thread, targets);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (event.item.type === "mcp_tool_call") {
|
|
693
|
+
const targets = extractReadFileTargets(event.item, this.options.workingDirectory);
|
|
694
|
+
if (targets.length === 0) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
void this.processDiagnostics(thread, targets);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
return () => {
|
|
701
|
+
this.attached.delete(thread);
|
|
702
|
+
unsubscribe();
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
async dispose() {
|
|
706
|
+
await this.manager.dispose();
|
|
707
|
+
}
|
|
708
|
+
async processDiagnostics(thread, files) {
|
|
709
|
+
try {
|
|
710
|
+
const diagnostics = await this.manager.collectDiagnostics(files);
|
|
711
|
+
if (diagnostics.length === 0) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const summary = formatDiagnosticsForBackgroundEvent(
|
|
715
|
+
diagnostics,
|
|
716
|
+
this.options.workingDirectory
|
|
717
|
+
);
|
|
718
|
+
console.log(`
|
|
719
|
+
\u{1F4DF} LSP diagnostics detected:
|
|
720
|
+
${summary}
|
|
721
|
+
`);
|
|
722
|
+
try {
|
|
723
|
+
await thread.sendBackgroundEvent(`LSP diagnostics detected:
|
|
724
|
+
${summary}`);
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
} catch (error) {
|
|
728
|
+
console.warn("[lsp] failed to collect diagnostics", error);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
function extractReadFileTargets(item, cwd) {
|
|
733
|
+
if (item.type !== "mcp_tool_call") {
|
|
734
|
+
return [];
|
|
735
|
+
}
|
|
736
|
+
const toolName = item.tool?.toLowerCase?.();
|
|
737
|
+
if (toolName !== "read_file" && toolName !== "read_file_v2") {
|
|
738
|
+
return [];
|
|
739
|
+
}
|
|
740
|
+
let args = item.arguments;
|
|
741
|
+
if (typeof args === "string") {
|
|
742
|
+
try {
|
|
743
|
+
args = JSON.parse(args);
|
|
744
|
+
} catch {
|
|
745
|
+
return [];
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (!args || typeof args !== "object") {
|
|
749
|
+
return [];
|
|
750
|
+
}
|
|
751
|
+
const filePath = args.file_path ?? args.path;
|
|
752
|
+
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
753
|
+
return [];
|
|
754
|
+
}
|
|
755
|
+
const resolved = path7.isAbsolute(filePath) ? filePath : path7.resolve(cwd, filePath);
|
|
756
|
+
return [resolved];
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/lsp/hooks.ts
|
|
760
|
+
function attachLspDiagnostics(thread, options) {
|
|
761
|
+
const bridge = new LspDiagnosticsBridge(options);
|
|
762
|
+
const detach = bridge.attach(thread);
|
|
763
|
+
return () => {
|
|
764
|
+
detach();
|
|
765
|
+
void bridge.dispose().catch((error) => {
|
|
766
|
+
console.warn("Failed to dispose LSP bridge", error);
|
|
767
|
+
});
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/thread.ts
|
|
772
|
+
import * as fs5 from "fs";
|
|
773
|
+
import * as path9 from "path";
|
|
774
|
+
|
|
775
|
+
// src/events/convert.ts
|
|
776
|
+
function convertRustEventToThreadEvent(rustEvent) {
|
|
777
|
+
if (rustEvent?.ThreadStarted) {
|
|
778
|
+
return {
|
|
779
|
+
type: "thread.started",
|
|
780
|
+
thread_id: rustEvent.ThreadStarted.thread_id
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (rustEvent?.TurnStarted) {
|
|
784
|
+
return { type: "turn.started" };
|
|
785
|
+
}
|
|
786
|
+
if (rustEvent?.TurnCompleted) {
|
|
787
|
+
return {
|
|
788
|
+
type: "turn.completed",
|
|
789
|
+
usage: rustEvent.TurnCompleted.usage
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
if (rustEvent?.TurnFailed) {
|
|
793
|
+
return {
|
|
794
|
+
type: "turn.failed",
|
|
795
|
+
error: rustEvent.TurnFailed.error
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
if (rustEvent?.ItemStarted) {
|
|
799
|
+
return {
|
|
800
|
+
type: "item.started",
|
|
801
|
+
item: rustEvent.ItemStarted.item
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
if (rustEvent?.ItemUpdated) {
|
|
805
|
+
return {
|
|
806
|
+
type: "item.updated",
|
|
807
|
+
item: rustEvent.ItemUpdated.item
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
if (rustEvent?.ItemCompleted) {
|
|
811
|
+
return {
|
|
812
|
+
type: "item.completed",
|
|
813
|
+
item: rustEvent.ItemCompleted.item
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
if (rustEvent?.Error) {
|
|
817
|
+
return {
|
|
818
|
+
type: "error",
|
|
819
|
+
message: rustEvent.Error.message
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (rustEvent?.BackgroundEvent) {
|
|
823
|
+
return {
|
|
824
|
+
type: "background_event",
|
|
825
|
+
message: rustEvent.BackgroundEvent.message
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
if (rustEvent?.type === "background_event" && typeof rustEvent.message === "string") {
|
|
829
|
+
return {
|
|
830
|
+
type: "background_event",
|
|
831
|
+
message: rustEvent.message
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
if (rustEvent?.type === "plan_update_scheduled" && rustEvent.plan) {
|
|
835
|
+
const planData = rustEvent.plan;
|
|
836
|
+
const planItems = planData.plan || [];
|
|
837
|
+
return {
|
|
838
|
+
type: "item.completed",
|
|
839
|
+
item: {
|
|
840
|
+
id: `plan-${Date.now()}`,
|
|
841
|
+
type: "todo_list",
|
|
842
|
+
items: planItems.map((item) => ({
|
|
843
|
+
text: item.step,
|
|
844
|
+
completed: item.status === "completed"
|
|
845
|
+
}))
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (rustEvent?.type) {
|
|
850
|
+
return rustEvent;
|
|
851
|
+
}
|
|
852
|
+
return rustEvent;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/outputSchemaFile.ts
|
|
856
|
+
import { promises as fs4 } from "fs";
|
|
857
|
+
import os from "os";
|
|
858
|
+
import path8 from "path";
|
|
859
|
+
function normalizeOutputSchema(schema) {
|
|
860
|
+
if (schema === void 0) {
|
|
861
|
+
return void 0;
|
|
862
|
+
}
|
|
863
|
+
if (isJsonObject(schema) && (schema.type === "json_schema" || schema.type === "json-schema") && isJsonObject(schema.json_schema) && isJsonObject(schema.json_schema.schema)) {
|
|
864
|
+
const strict = typeof schema.json_schema.strict === "boolean" ? schema.json_schema.strict : true;
|
|
865
|
+
return normalizeJsonSchemaObject(schema.json_schema.schema, strict);
|
|
866
|
+
}
|
|
867
|
+
if (isJsonObject(schema) && isJsonObject(schema.schema)) {
|
|
868
|
+
const strict = typeof schema.strict === "boolean" ? schema.strict : true;
|
|
869
|
+
return normalizeJsonSchemaObject(schema.schema, strict);
|
|
870
|
+
}
|
|
871
|
+
if (!isJsonObject(schema)) {
|
|
872
|
+
throw new Error(
|
|
873
|
+
"outputSchema must be a plain JSON object or an OpenAI-style json_schema wrapper"
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
return normalizeJsonSchemaObject(schema, true);
|
|
877
|
+
}
|
|
878
|
+
async function createOutputSchemaFile(schema) {
|
|
879
|
+
const normalizedSchema = normalizeOutputSchema(schema);
|
|
880
|
+
if (!normalizedSchema) {
|
|
881
|
+
return { cleanup: async () => {
|
|
882
|
+
} };
|
|
883
|
+
}
|
|
884
|
+
const schemaDir = await fs4.mkdtemp(path8.join(os.tmpdir(), "codex-output-schema-"));
|
|
885
|
+
const schemaPath = path8.join(schemaDir, "schema.json");
|
|
886
|
+
const cleanup = async () => {
|
|
887
|
+
try {
|
|
888
|
+
await fs4.rm(schemaDir, { recursive: true, force: true });
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
try {
|
|
893
|
+
await fs4.writeFile(schemaPath, JSON.stringify(normalizedSchema), "utf8");
|
|
894
|
+
return { schemaPath, cleanup };
|
|
895
|
+
} catch (error) {
|
|
896
|
+
await cleanup();
|
|
897
|
+
throw error;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function isJsonObject(value) {
|
|
901
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
902
|
+
}
|
|
903
|
+
function normalizeJsonSchemaObject(schema, strict) {
|
|
904
|
+
const record = { ...schema };
|
|
905
|
+
const hasExplicitAdditional = typeof record.additionalProperties === "boolean" || typeof record.additionalProperties === "object";
|
|
906
|
+
const additionalProperties = hasExplicitAdditional ? record.additionalProperties : strict ? false : record.additionalProperties;
|
|
907
|
+
return {
|
|
908
|
+
...record,
|
|
909
|
+
...hasExplicitAdditional || strict ? { additionalProperties } : {}
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// src/thread.ts
|
|
914
|
+
var UNTRUSTED_DIRECTORY_ERROR = "Not inside a trusted directory and --skip-git-repo-check was not specified.";
|
|
915
|
+
function findGitRoot(startDir) {
|
|
916
|
+
let current = path9.resolve(startDir);
|
|
917
|
+
while (true) {
|
|
918
|
+
const gitPath = path9.join(current, ".git");
|
|
919
|
+
if (fs5.existsSync(gitPath)) {
|
|
920
|
+
try {
|
|
921
|
+
const stats = fs5.statSync(gitPath);
|
|
922
|
+
if (stats.isDirectory() || stats.isFile()) {
|
|
923
|
+
return current;
|
|
924
|
+
}
|
|
925
|
+
} catch {
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const parent = path9.dirname(current);
|
|
929
|
+
if (parent === current) {
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
current = parent;
|
|
933
|
+
}
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
function assertTrustedDirectory(workingDirectory) {
|
|
937
|
+
const directory = workingDirectory ? path9.resolve(workingDirectory) : process.cwd();
|
|
938
|
+
if (!findGitRoot(directory)) {
|
|
939
|
+
throw new Error(UNTRUSTED_DIRECTORY_ERROR);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
var Thread = class _Thread {
|
|
943
|
+
_exec;
|
|
944
|
+
_options;
|
|
945
|
+
_id;
|
|
946
|
+
_threadOptions;
|
|
947
|
+
_eventListeners = [];
|
|
948
|
+
_approvalHandler = null;
|
|
949
|
+
/** Returns the ID of the thread. Populated after the first turn starts. */
|
|
950
|
+
get id() {
|
|
951
|
+
return this._id;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Register an event listener for thread events.
|
|
955
|
+
* @param listener Callback function that receives ThreadEvent objects
|
|
956
|
+
* @returns Unsubscribe function to remove the listener
|
|
957
|
+
*/
|
|
958
|
+
onEvent(listener) {
|
|
959
|
+
this._eventListeners.push(listener);
|
|
960
|
+
return () => {
|
|
961
|
+
const index = this._eventListeners.indexOf(listener);
|
|
962
|
+
if (index !== -1) {
|
|
963
|
+
this._eventListeners.splice(index, 1);
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Remove an event listener.
|
|
969
|
+
* @param listener The listener function to remove
|
|
970
|
+
*/
|
|
971
|
+
offEvent(listener) {
|
|
972
|
+
const index = this._eventListeners.indexOf(listener);
|
|
973
|
+
if (index !== -1) {
|
|
974
|
+
this._eventListeners.splice(index, 1);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Register a callback to handle approval requests from the agent.
|
|
979
|
+
* The handler should return true to approve the action, false to deny it.
|
|
980
|
+
*
|
|
981
|
+
* @param handler Callback function that receives ApprovalRequest and returns approval decision
|
|
982
|
+
* @example
|
|
983
|
+
* ```typescript
|
|
984
|
+
* thread.onApprovalRequest(async (request) => {
|
|
985
|
+
* console.log(`Approval requested for ${request.type}`);
|
|
986
|
+
* return true; // Auto-approve
|
|
987
|
+
* });
|
|
988
|
+
* ```
|
|
989
|
+
*/
|
|
990
|
+
onApprovalRequest(handler) {
|
|
991
|
+
this._approvalHandler = handler;
|
|
992
|
+
const binding = getNativeBinding();
|
|
993
|
+
if (binding && typeof binding.registerApprovalCallback === "function") {
|
|
994
|
+
binding.registerApprovalCallback(handler);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Emit a background notification while the agent is running the current turn.
|
|
999
|
+
* The message is surfaced to event subscribers but does not modify the user input queue.
|
|
1000
|
+
*
|
|
1001
|
+
* @throws Error if the thread has not been started yet.
|
|
1002
|
+
*/
|
|
1003
|
+
async sendBackgroundEvent(message) {
|
|
1004
|
+
const trimmed = message?.toString();
|
|
1005
|
+
if (!trimmed || trimmed.trim().length === 0) {
|
|
1006
|
+
throw new Error("Background event message must be a non-empty string");
|
|
1007
|
+
}
|
|
1008
|
+
if (!this._id) {
|
|
1009
|
+
throw new Error("Cannot emit a background event before the thread has started");
|
|
1010
|
+
}
|
|
1011
|
+
const binding = getNativeBinding();
|
|
1012
|
+
if (!binding || typeof binding.emitBackgroundEvent !== "function") {
|
|
1013
|
+
throw new Error("emitBackgroundEvent is not available in this build");
|
|
1014
|
+
}
|
|
1015
|
+
await binding.emitBackgroundEvent({ threadId: this._id, message: trimmed });
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Programmatically update the agent's plan/todo list.
|
|
1019
|
+
* The plan will be applied at the start of the next turn.
|
|
1020
|
+
*
|
|
1021
|
+
* @param args The plan update arguments
|
|
1022
|
+
* @throws Error if no thread ID is available
|
|
1023
|
+
*/
|
|
1024
|
+
updatePlan(args) {
|
|
1025
|
+
if (!this._id) {
|
|
1026
|
+
throw new Error("Cannot update plan: no active thread");
|
|
1027
|
+
}
|
|
1028
|
+
const binding = getNativeBinding();
|
|
1029
|
+
if (!binding || typeof binding.emitPlanUpdate !== "function") {
|
|
1030
|
+
throw new Error("emitPlanUpdate is not available in this build");
|
|
1031
|
+
}
|
|
1032
|
+
binding.emitPlanUpdate({
|
|
1033
|
+
threadId: this._id,
|
|
1034
|
+
explanation: args.explanation,
|
|
1035
|
+
plan: args.plan
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Modify the agent's plan/todo list with granular operations.
|
|
1040
|
+
* Changes will be applied at the start of the next turn.
|
|
1041
|
+
*
|
|
1042
|
+
* @param operations Array of operations to perform on the plan
|
|
1043
|
+
* @throws Error if no thread ID is available
|
|
1044
|
+
*/
|
|
1045
|
+
modifyPlan(operations) {
|
|
1046
|
+
if (!this._id) {
|
|
1047
|
+
throw new Error("Cannot modify plan: no active thread");
|
|
1048
|
+
}
|
|
1049
|
+
const binding = getNativeBinding();
|
|
1050
|
+
if (!binding || typeof binding.modifyPlan !== "function") {
|
|
1051
|
+
throw new Error("modifyPlan is not available in this build");
|
|
1052
|
+
}
|
|
1053
|
+
binding.modifyPlan({
|
|
1054
|
+
threadId: this._id,
|
|
1055
|
+
operations
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Add a new todo item to the agent's plan.
|
|
1060
|
+
*
|
|
1061
|
+
* @param step The todo step description
|
|
1062
|
+
* @param status The initial status (defaults to "pending")
|
|
1063
|
+
*/
|
|
1064
|
+
addTodo(step, status = "pending") {
|
|
1065
|
+
this.modifyPlan([{ type: "add", item: { step, status } }]);
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Update an existing todo item.
|
|
1069
|
+
*
|
|
1070
|
+
* @param index The index of the todo item to update
|
|
1071
|
+
* @param updates The updates to apply
|
|
1072
|
+
*/
|
|
1073
|
+
updateTodo(index, updates) {
|
|
1074
|
+
this.modifyPlan([{ type: "update", index, updates }]);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Remove a todo item from the plan.
|
|
1078
|
+
*
|
|
1079
|
+
* @param index The index of the todo item to remove
|
|
1080
|
+
*/
|
|
1081
|
+
removeTodo(index) {
|
|
1082
|
+
this.modifyPlan([{ type: "remove", index }]);
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Reorder the todo items in the plan.
|
|
1086
|
+
*
|
|
1087
|
+
* @param newOrder Array of indices representing the new order
|
|
1088
|
+
*/
|
|
1089
|
+
reorderTodos(newOrder) {
|
|
1090
|
+
this.modifyPlan([{ type: "reorder", newOrder }]);
|
|
1091
|
+
}
|
|
1092
|
+
/** Compacts the conversation history for this thread using Codex's builtin compaction. */
|
|
1093
|
+
async compact() {
|
|
1094
|
+
const skipGitRepoCheck = this._threadOptions?.skipGitRepoCheck ?? (typeof process !== "undefined" && process.env && process.env.CODEX_TEST_SKIP_GIT_REPO_CHECK === "1");
|
|
1095
|
+
if (!skipGitRepoCheck) {
|
|
1096
|
+
assertTrustedDirectory(this._threadOptions?.workingDirectory);
|
|
1097
|
+
}
|
|
1098
|
+
const events = await this._exec.compact({
|
|
1099
|
+
input: "compact",
|
|
1100
|
+
threadId: this._id,
|
|
1101
|
+
baseUrl: this._options.baseUrl,
|
|
1102
|
+
apiKey: this._options.apiKey,
|
|
1103
|
+
model: this._threadOptions?.model ?? this._options.defaultModel,
|
|
1104
|
+
sandboxMode: this._threadOptions?.sandboxMode,
|
|
1105
|
+
approvalMode: this._threadOptions?.approvalMode,
|
|
1106
|
+
workspaceWriteOptions: this._threadOptions?.workspaceWriteOptions,
|
|
1107
|
+
workingDirectory: this._threadOptions?.workingDirectory,
|
|
1108
|
+
skipGitRepoCheck,
|
|
1109
|
+
modelProvider: this._options.modelProvider
|
|
1110
|
+
});
|
|
1111
|
+
if (!Array.isArray(events)) {
|
|
1112
|
+
throw new Error("Compact did not return event list");
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Fork this thread at the specified user message, returning a new thread that starts
|
|
1117
|
+
* from the conversation history prior to that message.
|
|
1118
|
+
*
|
|
1119
|
+
* @param options Fork configuration including which user message to branch before and optional thread overrides.
|
|
1120
|
+
*/
|
|
1121
|
+
async fork(options) {
|
|
1122
|
+
if (!this._id) {
|
|
1123
|
+
throw new Error("Cannot fork: no active thread");
|
|
1124
|
+
}
|
|
1125
|
+
const nthUserMessage = options?.nthUserMessage;
|
|
1126
|
+
if (typeof nthUserMessage !== "number" || !Number.isInteger(nthUserMessage) || nthUserMessage < 0) {
|
|
1127
|
+
throw new Error("nthUserMessage must be a non-negative integer");
|
|
1128
|
+
}
|
|
1129
|
+
const overrides = options.threadOptions ?? {};
|
|
1130
|
+
const nextThreadOptions = {
|
|
1131
|
+
...this._threadOptions,
|
|
1132
|
+
...overrides
|
|
1133
|
+
};
|
|
1134
|
+
const skipGitRepoCheck = nextThreadOptions.skipGitRepoCheck ?? (typeof process !== "undefined" && process.env && process.env.CODEX_TEST_SKIP_GIT_REPO_CHECK === "1");
|
|
1135
|
+
nextThreadOptions.skipGitRepoCheck = skipGitRepoCheck;
|
|
1136
|
+
if (!skipGitRepoCheck) {
|
|
1137
|
+
assertTrustedDirectory(nextThreadOptions.workingDirectory);
|
|
1138
|
+
}
|
|
1139
|
+
const forkArgs = {
|
|
1140
|
+
threadId: this._id,
|
|
1141
|
+
nthUserMessage,
|
|
1142
|
+
baseUrl: this._options.baseUrl,
|
|
1143
|
+
apiKey: this._options.apiKey,
|
|
1144
|
+
model: nextThreadOptions.model ?? this._options.defaultModel,
|
|
1145
|
+
oss: nextThreadOptions.oss,
|
|
1146
|
+
sandboxMode: nextThreadOptions.sandboxMode,
|
|
1147
|
+
approvalMode: nextThreadOptions.approvalMode,
|
|
1148
|
+
workspaceWriteOptions: nextThreadOptions.workspaceWriteOptions,
|
|
1149
|
+
workingDirectory: nextThreadOptions.workingDirectory,
|
|
1150
|
+
skipGitRepoCheck,
|
|
1151
|
+
fullAuto: nextThreadOptions.fullAuto,
|
|
1152
|
+
modelProvider: this._options.modelProvider
|
|
1153
|
+
};
|
|
1154
|
+
const result = await this._exec.fork(forkArgs);
|
|
1155
|
+
return new _Thread(
|
|
1156
|
+
this._exec,
|
|
1157
|
+
this._options,
|
|
1158
|
+
nextThreadOptions,
|
|
1159
|
+
result.threadId
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
/* @internal */
|
|
1163
|
+
constructor(exec, options, threadOptions, id = null) {
|
|
1164
|
+
this._exec = exec;
|
|
1165
|
+
this._options = options;
|
|
1166
|
+
this._id = id;
|
|
1167
|
+
this._threadOptions = threadOptions;
|
|
1168
|
+
}
|
|
1169
|
+
/** Provides the input to the agent and streams events as they are produced during the turn. */
|
|
1170
|
+
async runStreamed(input, turnOptions = {}) {
|
|
1171
|
+
return { events: this.runStreamedInternal(input, turnOptions, false) };
|
|
1172
|
+
}
|
|
1173
|
+
async *runStreamedInternal(input, turnOptions = {}, emitRawEvents = true) {
|
|
1174
|
+
const normalizedSchema = normalizeOutputSchema(turnOptions.outputSchema);
|
|
1175
|
+
const needsSchemaFile = this._exec.requiresOutputSchemaFile();
|
|
1176
|
+
const schemaFile = needsSchemaFile ? await createOutputSchemaFile(normalizedSchema) : { schemaPath: void 0, cleanup: async () => {
|
|
1177
|
+
} };
|
|
1178
|
+
const options = this._threadOptions;
|
|
1179
|
+
const { prompt, images } = normalizeInput(input);
|
|
1180
|
+
const skipGitRepoCheck = options?.skipGitRepoCheck ?? (typeof process !== "undefined" && process.env && process.env.CODEX_TEST_SKIP_GIT_REPO_CHECK === "1");
|
|
1181
|
+
if (!skipGitRepoCheck) {
|
|
1182
|
+
assertTrustedDirectory(options?.workingDirectory);
|
|
1183
|
+
}
|
|
1184
|
+
const generator = this._exec.run({
|
|
1185
|
+
input: prompt,
|
|
1186
|
+
baseUrl: this._options.baseUrl,
|
|
1187
|
+
apiKey: this._options.apiKey,
|
|
1188
|
+
threadId: this._id,
|
|
1189
|
+
images,
|
|
1190
|
+
model: options?.model,
|
|
1191
|
+
oss: turnOptions?.oss ?? options?.oss,
|
|
1192
|
+
sandboxMode: options?.sandboxMode,
|
|
1193
|
+
approvalMode: options?.approvalMode,
|
|
1194
|
+
workspaceWriteOptions: options?.workspaceWriteOptions,
|
|
1195
|
+
workingDirectory: options?.workingDirectory,
|
|
1196
|
+
skipGitRepoCheck,
|
|
1197
|
+
outputSchemaFile: schemaFile.schemaPath,
|
|
1198
|
+
outputSchema: normalizedSchema,
|
|
1199
|
+
fullAuto: options?.fullAuto
|
|
1200
|
+
});
|
|
1201
|
+
try {
|
|
1202
|
+
for await (const item of generator) {
|
|
1203
|
+
let parsed;
|
|
1204
|
+
try {
|
|
1205
|
+
parsed = JSON.parse(item);
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
throw new Error(`Failed to parse item: ${item}. Parse error: ${error}`);
|
|
1208
|
+
}
|
|
1209
|
+
if (parsed === null) {
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (emitRawEvents) {
|
|
1213
|
+
yield { type: "raw_event", raw: parsed };
|
|
1214
|
+
}
|
|
1215
|
+
const threadEvent = convertRustEventToThreadEvent(parsed);
|
|
1216
|
+
if (threadEvent.type === "thread.started") {
|
|
1217
|
+
this._id = threadEvent.thread_id;
|
|
1218
|
+
}
|
|
1219
|
+
for (const listener of this._eventListeners) {
|
|
1220
|
+
try {
|
|
1221
|
+
listener(threadEvent);
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
console.warn("Thread event listener threw error:", error);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
yield threadEvent;
|
|
1227
|
+
}
|
|
1228
|
+
} finally {
|
|
1229
|
+
await schemaFile.cleanup();
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
/** Provides the input to the agent and returns the completed turn. */
|
|
1233
|
+
async run(input, turnOptions = {}) {
|
|
1234
|
+
const generator = this.runStreamedInternal(input, turnOptions, true);
|
|
1235
|
+
const items = [];
|
|
1236
|
+
let finalResponse = "";
|
|
1237
|
+
let usage = null;
|
|
1238
|
+
let turnFailure = null;
|
|
1239
|
+
for await (const event of generator) {
|
|
1240
|
+
if (event.type === "item.completed") {
|
|
1241
|
+
if (event.item.type === "agent_message") {
|
|
1242
|
+
finalResponse = event.item.text;
|
|
1243
|
+
}
|
|
1244
|
+
items.push(event.item);
|
|
1245
|
+
} else if (event.type === "turn.completed") {
|
|
1246
|
+
usage = event.usage;
|
|
1247
|
+
} else if (event.type === "turn.failed") {
|
|
1248
|
+
turnFailure = event.error;
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (turnFailure) {
|
|
1253
|
+
throw new Error(turnFailure.message);
|
|
1254
|
+
}
|
|
1255
|
+
return { items, finalResponse, usage };
|
|
1256
|
+
}
|
|
1257
|
+
buildTuiRequest(overrides = {}) {
|
|
1258
|
+
const skipGitRepoCheck = this._threadOptions?.skipGitRepoCheck ?? (typeof process !== "undefined" && process.env && process.env.CODEX_TEST_SKIP_GIT_REPO_CHECK === "1");
|
|
1259
|
+
if (!skipGitRepoCheck) {
|
|
1260
|
+
assertTrustedDirectory(this._threadOptions?.workingDirectory);
|
|
1261
|
+
}
|
|
1262
|
+
const request = { ...overrides };
|
|
1263
|
+
const assignIfUndefined = (key, value) => {
|
|
1264
|
+
if (request[key] === void 0 && value !== void 0) {
|
|
1265
|
+
request[key] = value;
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
assignIfUndefined("model", this._threadOptions?.model ?? this._options.defaultModel);
|
|
1269
|
+
assignIfUndefined("oss", this._threadOptions?.oss);
|
|
1270
|
+
assignIfUndefined("sandboxMode", this._threadOptions?.sandboxMode);
|
|
1271
|
+
assignIfUndefined("approvalMode", this._threadOptions?.approvalMode);
|
|
1272
|
+
assignIfUndefined("fullAuto", this._threadOptions?.fullAuto);
|
|
1273
|
+
assignIfUndefined("workingDirectory", this._threadOptions?.workingDirectory);
|
|
1274
|
+
assignIfUndefined("baseUrl", this._options.baseUrl);
|
|
1275
|
+
assignIfUndefined("apiKey", this._options.apiKey);
|
|
1276
|
+
if (request.resumeSessionId === void 0 && request.resumePicker !== true && request.resumeLast !== true && this._id) {
|
|
1277
|
+
request.resumeSessionId = this._id;
|
|
1278
|
+
}
|
|
1279
|
+
return request;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Launches the interactive Codex TUI (Terminal User Interface) for this thread and returns a session handle.
|
|
1283
|
+
*
|
|
1284
|
+
* The handle allows advanced workflows where the TUI can be started and stopped programmatically,
|
|
1285
|
+
* while preserving the underlying conversation state.
|
|
1286
|
+
*/
|
|
1287
|
+
launchTui(overrides = {}) {
|
|
1288
|
+
const request = this.buildTuiRequest(overrides);
|
|
1289
|
+
const detachLsp = this.attachDefaultLspBridge(request);
|
|
1290
|
+
const session = startTui(request);
|
|
1291
|
+
return this.wrapTuiSession(session, detachLsp);
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Launches the interactive Codex TUI (Terminal User Interface) for this thread.
|
|
1295
|
+
*
|
|
1296
|
+
* This method enables seamless transition from programmatic agent interaction to
|
|
1297
|
+
* interactive terminal chat within the same session. The TUI takes over the terminal
|
|
1298
|
+
* and allows you to continue the conversation interactively.
|
|
1299
|
+
*
|
|
1300
|
+
* @param overrides - Optional configuration to override thread defaults. Supports all TUI options
|
|
1301
|
+
* including prompt, sandbox mode, approval mode, and resume options.
|
|
1302
|
+
* @param options - Optional run options including an AbortSignal to request shutdown.
|
|
1303
|
+
* @returns A Promise that resolves to TUI exit information including:
|
|
1304
|
+
* - tokenUsage: Token consumption statistics
|
|
1305
|
+
* - conversationId: Session ID for resuming later
|
|
1306
|
+
* - updateAction: Optional suggested update command
|
|
1307
|
+
* @throws {Error} If not in a trusted git repository (unless skipGitRepoCheck is set)
|
|
1308
|
+
* @throws {Error} If the terminal is not interactive (TTY required)
|
|
1309
|
+
*/
|
|
1310
|
+
async tui(overrides = {}, options = {}) {
|
|
1311
|
+
const request = this.buildTuiRequest(overrides);
|
|
1312
|
+
const detachLsp = this.attachDefaultLspBridge(request);
|
|
1313
|
+
try {
|
|
1314
|
+
return await runTui(request, options);
|
|
1315
|
+
} finally {
|
|
1316
|
+
detachLsp();
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
wrapTuiSession(session, cleanup) {
|
|
1320
|
+
let released = false;
|
|
1321
|
+
const release = () => {
|
|
1322
|
+
if (released) {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
released = true;
|
|
1326
|
+
cleanup();
|
|
1327
|
+
};
|
|
1328
|
+
return {
|
|
1329
|
+
wait: async () => {
|
|
1330
|
+
try {
|
|
1331
|
+
return await session.wait();
|
|
1332
|
+
} finally {
|
|
1333
|
+
release();
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
shutdown: () => {
|
|
1337
|
+
release();
|
|
1338
|
+
session.shutdown();
|
|
1339
|
+
},
|
|
1340
|
+
get closed() {
|
|
1341
|
+
return session.closed;
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
attachDefaultLspBridge(request) {
|
|
1346
|
+
const workingDirectory = request.workingDirectory ?? this._threadOptions?.workingDirectory ?? (typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : ".");
|
|
1347
|
+
return attachLspDiagnostics(this, {
|
|
1348
|
+
workingDirectory,
|
|
1349
|
+
waitForDiagnostics: true
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
function normalizeInput(input) {
|
|
1354
|
+
if (typeof input === "string") {
|
|
1355
|
+
return { prompt: input, images: [] };
|
|
1356
|
+
}
|
|
1357
|
+
const promptParts = [];
|
|
1358
|
+
const images = [];
|
|
1359
|
+
for (const item of input) {
|
|
1360
|
+
if (item.type === "text") {
|
|
1361
|
+
promptParts.push(item.text);
|
|
1362
|
+
} else if (item.type === "local_image") {
|
|
1363
|
+
images.push(item.path);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return { prompt: promptParts.join("\n\n"), images };
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/exec.ts
|
|
1370
|
+
var CodexExec = class {
|
|
1371
|
+
native;
|
|
1372
|
+
constructor() {
|
|
1373
|
+
const nativeBinding = getNativeBinding();
|
|
1374
|
+
if (!nativeBinding) {
|
|
1375
|
+
throw new Error(
|
|
1376
|
+
"Native NAPI binding not available. Make sure @openai/codex-native is properly installed and built."
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
this.native = nativeBinding;
|
|
1380
|
+
}
|
|
1381
|
+
requiresOutputSchemaFile() {
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
async *run(args) {
|
|
1385
|
+
const binding = this.native;
|
|
1386
|
+
const queue = new AsyncQueue();
|
|
1387
|
+
validateModel(args.model, args.oss === true);
|
|
1388
|
+
const request = {
|
|
1389
|
+
prompt: args.input,
|
|
1390
|
+
threadId: args.threadId ?? void 0,
|
|
1391
|
+
images: args.images && args.images.length > 0 ? args.images : void 0,
|
|
1392
|
+
model: args.model,
|
|
1393
|
+
oss: args.oss,
|
|
1394
|
+
approvalMode: args.approvalMode,
|
|
1395
|
+
workspaceWriteOptions: args.workspaceWriteOptions,
|
|
1396
|
+
sandboxMode: args.sandboxMode,
|
|
1397
|
+
workingDirectory: args.workingDirectory,
|
|
1398
|
+
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
1399
|
+
outputSchema: args.outputSchema,
|
|
1400
|
+
baseUrl: args.baseUrl,
|
|
1401
|
+
apiKey: args.apiKey,
|
|
1402
|
+
modelProvider: args.modelProvider,
|
|
1403
|
+
fullAuto: args.fullAuto,
|
|
1404
|
+
reviewMode: args.review ? true : void 0,
|
|
1405
|
+
reviewHint: args.review?.userFacingHint
|
|
1406
|
+
};
|
|
1407
|
+
let runPromise = Promise.resolve();
|
|
1408
|
+
try {
|
|
1409
|
+
runPromise = binding.runThreadStream(request, (err, eventJson) => {
|
|
1410
|
+
if (err) {
|
|
1411
|
+
queue.fail(err);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
try {
|
|
1415
|
+
queue.push(eventJson ?? "null");
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
queue.fail(error);
|
|
1418
|
+
}
|
|
1419
|
+
}).then(
|
|
1420
|
+
() => {
|
|
1421
|
+
queue.end();
|
|
1422
|
+
},
|
|
1423
|
+
(error) => {
|
|
1424
|
+
queue.fail(error);
|
|
1425
|
+
}
|
|
1426
|
+
);
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
queue.fail(error);
|
|
1429
|
+
throw error;
|
|
1430
|
+
}
|
|
1431
|
+
let loopError;
|
|
1432
|
+
try {
|
|
1433
|
+
for await (const value of queue) {
|
|
1434
|
+
yield value;
|
|
1435
|
+
}
|
|
1436
|
+
await runPromise;
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
loopError = error;
|
|
1439
|
+
throw error;
|
|
1440
|
+
} finally {
|
|
1441
|
+
queue.end();
|
|
1442
|
+
if (loopError) {
|
|
1443
|
+
await runPromise.catch(() => {
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
async compact(args) {
|
|
1449
|
+
validateModel(args.model, args.oss === true);
|
|
1450
|
+
const request = {
|
|
1451
|
+
prompt: args.input,
|
|
1452
|
+
threadId: args.threadId ?? void 0,
|
|
1453
|
+
images: args.images && args.images.length > 0 ? args.images : void 0,
|
|
1454
|
+
model: args.model,
|
|
1455
|
+
modelProvider: args.modelProvider,
|
|
1456
|
+
oss: args.oss,
|
|
1457
|
+
sandboxMode: args.sandboxMode,
|
|
1458
|
+
approvalMode: args.approvalMode,
|
|
1459
|
+
workspaceWriteOptions: args.workspaceWriteOptions,
|
|
1460
|
+
workingDirectory: args.workingDirectory,
|
|
1461
|
+
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
1462
|
+
outputSchema: args.outputSchema,
|
|
1463
|
+
baseUrl: args.baseUrl,
|
|
1464
|
+
apiKey: args.apiKey,
|
|
1465
|
+
fullAuto: args.fullAuto,
|
|
1466
|
+
reviewMode: args.review ? true : void 0,
|
|
1467
|
+
reviewHint: args.review?.userFacingHint
|
|
1468
|
+
};
|
|
1469
|
+
return this.native.compactThread(request);
|
|
1470
|
+
}
|
|
1471
|
+
async fork(args) {
|
|
1472
|
+
if (!args.threadId) {
|
|
1473
|
+
throw new Error("threadId is required to fork a conversation");
|
|
1474
|
+
}
|
|
1475
|
+
const request = {
|
|
1476
|
+
threadId: args.threadId,
|
|
1477
|
+
nthUserMessage: args.nthUserMessage,
|
|
1478
|
+
model: args.model,
|
|
1479
|
+
oss: args.oss,
|
|
1480
|
+
sandboxMode: args.sandboxMode,
|
|
1481
|
+
approvalMode: args.approvalMode,
|
|
1482
|
+
workspaceWriteOptions: args.workspaceWriteOptions,
|
|
1483
|
+
workingDirectory: args.workingDirectory,
|
|
1484
|
+
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
1485
|
+
baseUrl: args.baseUrl,
|
|
1486
|
+
apiKey: args.apiKey,
|
|
1487
|
+
modelProvider: args.modelProvider,
|
|
1488
|
+
linuxSandboxPath: args.linuxSandboxPath,
|
|
1489
|
+
fullAuto: args.fullAuto
|
|
1490
|
+
};
|
|
1491
|
+
return this.native.forkThread(request);
|
|
1492
|
+
}
|
|
1493
|
+
async listConversations(request) {
|
|
1494
|
+
return this.native.listConversations(request);
|
|
1495
|
+
}
|
|
1496
|
+
async deleteConversation(request) {
|
|
1497
|
+
return this.native.deleteConversation(request);
|
|
1498
|
+
}
|
|
1499
|
+
async resumeConversationFromRollout(request) {
|
|
1500
|
+
return this.native.resumeConversationFromRollout(request);
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
var AsyncQueue = class {
|
|
1504
|
+
buffer = [];
|
|
1505
|
+
waiters = [];
|
|
1506
|
+
ended = false;
|
|
1507
|
+
error;
|
|
1508
|
+
push(value) {
|
|
1509
|
+
if (this.ended) return;
|
|
1510
|
+
if (this.waiters.length > 0) {
|
|
1511
|
+
const waiter = this.waiters.shift();
|
|
1512
|
+
waiter.resolve({ value, done: false });
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
this.buffer.push(value);
|
|
1516
|
+
}
|
|
1517
|
+
end() {
|
|
1518
|
+
if (this.ended) return;
|
|
1519
|
+
this.ended = true;
|
|
1520
|
+
const waiters = this.waiters;
|
|
1521
|
+
this.waiters = [];
|
|
1522
|
+
for (const waiter of waiters) {
|
|
1523
|
+
waiter.resolve({ value: void 0, done: true });
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
fail(error) {
|
|
1527
|
+
if (this.ended) return;
|
|
1528
|
+
this.error = error;
|
|
1529
|
+
this.ended = true;
|
|
1530
|
+
const waiters = this.waiters;
|
|
1531
|
+
this.waiters = [];
|
|
1532
|
+
for (const waiter of waiters) {
|
|
1533
|
+
waiter.reject(error);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
async next() {
|
|
1537
|
+
if (this.buffer.length > 0) {
|
|
1538
|
+
const value = this.buffer.shift();
|
|
1539
|
+
return { value, done: false };
|
|
1540
|
+
}
|
|
1541
|
+
if (this.error) {
|
|
1542
|
+
return Promise.reject(this.error);
|
|
1543
|
+
}
|
|
1544
|
+
if (this.ended) {
|
|
1545
|
+
return { value: void 0, done: true };
|
|
1546
|
+
}
|
|
1547
|
+
return new Promise((resolve5, reject) => {
|
|
1548
|
+
this.waiters.push({ resolve: resolve5, reject });
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
[Symbol.asyncIterator]() {
|
|
1552
|
+
return this;
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
function validateModel(model, oss) {
|
|
1556
|
+
if (!model) return;
|
|
1557
|
+
const trimmed = String(model).trim();
|
|
1558
|
+
if (oss) {
|
|
1559
|
+
if (!trimmed.startsWith("gpt-oss:")) {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`Invalid model "${trimmed}" for OSS mode. Use models prefixed with "gpt-oss:", e.g. "gpt-oss:20b".`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1567
|
+
// GPT models
|
|
1568
|
+
"gpt-5",
|
|
1569
|
+
"gpt-5-codex",
|
|
1570
|
+
"gpt-5-codex-mini",
|
|
1571
|
+
"gpt-5.1",
|
|
1572
|
+
"gpt-5.1-codex",
|
|
1573
|
+
"gpt-5.1-codex-mini",
|
|
1574
|
+
// Claude models
|
|
1575
|
+
"claude-sonnet-4-5-20250929",
|
|
1576
|
+
"claude-sonnet-4-20250514",
|
|
1577
|
+
"claude-opus-4-20250514"
|
|
1578
|
+
]);
|
|
1579
|
+
if (!allowed.has(trimmed) && !trimmed.startsWith("claude-") && !trimmed.startsWith("gpt-")) {
|
|
1580
|
+
throw new Error(
|
|
1581
|
+
`Invalid model "${trimmed}". Supported models: ${Array.from(allowed).map((m) => `"${m}"`).join(", ")}, or any model starting with "claude-" or "gpt-".`
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/reviewOptions.ts
|
|
1587
|
+
function buildReviewPrompt(target) {
|
|
1588
|
+
switch (target.type) {
|
|
1589
|
+
case "current_changes":
|
|
1590
|
+
return {
|
|
1591
|
+
prompt: "Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.",
|
|
1592
|
+
hint: "current changes"
|
|
1593
|
+
};
|
|
1594
|
+
case "branch": {
|
|
1595
|
+
const branch = target.baseBranch;
|
|
1596
|
+
const prompt = `Review the code changes against the base branch '${branch}'. Start by finding the merge diff between the current branch and ${branch}'s upstream e.g. (\`git merge-base HEAD "$(git rev-parse --abbrev-ref "${branch}@{upstream}")"\`), then run \`git diff\` against that SHA to see what changes we would merge into the ${branch} branch. Provide prioritized, actionable findings.`;
|
|
1597
|
+
return {
|
|
1598
|
+
prompt,
|
|
1599
|
+
hint: `changes against '${branch}'`
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
case "commit": {
|
|
1603
|
+
const shortSha = target.sha.slice(0, 7);
|
|
1604
|
+
const subject = target.subject ?? target.sha;
|
|
1605
|
+
return {
|
|
1606
|
+
prompt: `Review the code changes introduced by commit ${target.sha} ("${subject}"). Provide prioritized, actionable findings.`,
|
|
1607
|
+
hint: `commit ${shortSha}`
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
case "custom": {
|
|
1611
|
+
const hint = target.hint ?? "custom review";
|
|
1612
|
+
return {
|
|
1613
|
+
prompt: target.prompt,
|
|
1614
|
+
hint
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
default: {
|
|
1618
|
+
const exhaustive = target;
|
|
1619
|
+
throw new Error(`Unsupported review target: ${String(exhaustive)}`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// src/codex.ts
|
|
1625
|
+
var Codex = class {
|
|
1626
|
+
exec;
|
|
1627
|
+
options;
|
|
1628
|
+
nativeBinding;
|
|
1629
|
+
lspForTools;
|
|
1630
|
+
constructor(options = {}) {
|
|
1631
|
+
const predefinedTools = options.tools ? [...options.tools] : [];
|
|
1632
|
+
const preserveRegisteredTools = options.preserveRegisteredTools === true;
|
|
1633
|
+
this.nativeBinding = getNativeBinding();
|
|
1634
|
+
this.options = { ...options, tools: [] };
|
|
1635
|
+
if (this.nativeBinding) {
|
|
1636
|
+
if (!preserveRegisteredTools && typeof this.nativeBinding.clearRegisteredTools === "function") {
|
|
1637
|
+
this.nativeBinding.clearRegisteredTools();
|
|
1638
|
+
}
|
|
1639
|
+
for (const tool of predefinedTools) {
|
|
1640
|
+
this.registerTool(tool);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
this.lspForTools = this.createLspManagerForTools();
|
|
1644
|
+
if (this.lspForTools && this.nativeBinding) {
|
|
1645
|
+
this.registerDefaultReadFileInterceptor();
|
|
1646
|
+
}
|
|
1647
|
+
this.exec = new CodexExec();
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Register a tool for Codex. When `tool.name` matches a built-in Codex tool,
|
|
1651
|
+
* the native implementation is replaced for this Codex instance.
|
|
1652
|
+
*/
|
|
1653
|
+
registerTool(tool) {
|
|
1654
|
+
if (!this.nativeBinding) {
|
|
1655
|
+
throw new Error("Native tool registration requires the NAPI binding");
|
|
1656
|
+
}
|
|
1657
|
+
if (typeof this.nativeBinding.registerTool !== "function") {
|
|
1658
|
+
console.warn("registerTool is not available in this build - tools feature may be incomplete");
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
const { handler, ...info } = tool;
|
|
1662
|
+
this.nativeBinding.registerTool(info, handler);
|
|
1663
|
+
if (!this.options.tools) {
|
|
1664
|
+
this.options.tools = [];
|
|
1665
|
+
}
|
|
1666
|
+
this.options.tools.push(tool);
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Register a tool interceptor for Codex. Interceptors can modify tool invocations
|
|
1670
|
+
* and results, and can call the built-in implementation.
|
|
1671
|
+
*/
|
|
1672
|
+
registerToolInterceptor(toolName, handler) {
|
|
1673
|
+
if (!this.nativeBinding) {
|
|
1674
|
+
throw new Error("Native tool interceptor registration requires the NAPI binding");
|
|
1675
|
+
}
|
|
1676
|
+
if (typeof this.nativeBinding.registerToolInterceptor !== "function" || typeof this.nativeBinding.callToolBuiltin !== "function") {
|
|
1677
|
+
console.warn("registerToolInterceptor is not available in this build - interceptor feature may be incomplete");
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
this.nativeBinding.registerToolInterceptor(toolName, async (...args) => {
|
|
1681
|
+
const context = args.length === 1 ? args[0] : args[1];
|
|
1682
|
+
if (!context || typeof context !== "object") {
|
|
1683
|
+
throw new Error("Native interceptor callback did not receive a context object");
|
|
1684
|
+
}
|
|
1685
|
+
const { invocation, token } = context;
|
|
1686
|
+
const callBuiltin = (override) => this.nativeBinding.callToolBuiltin(token, override ?? invocation);
|
|
1687
|
+
return handler({ invocation, callBuiltin });
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Clear all registered tools, restoring built-in defaults.
|
|
1692
|
+
*/
|
|
1693
|
+
clearTools() {
|
|
1694
|
+
if (!this.nativeBinding) {
|
|
1695
|
+
throw new Error("Native tool management requires the NAPI binding");
|
|
1696
|
+
}
|
|
1697
|
+
if (typeof this.nativeBinding.clearRegisteredTools === "function") {
|
|
1698
|
+
this.nativeBinding.clearRegisteredTools();
|
|
1699
|
+
}
|
|
1700
|
+
if (this.options.tools) {
|
|
1701
|
+
this.options.tools = [];
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
buildConversationConfig(options = {}) {
|
|
1705
|
+
return {
|
|
1706
|
+
model: options.model ?? this.options.defaultModel,
|
|
1707
|
+
modelProvider: options.modelProvider ?? this.options.modelProvider,
|
|
1708
|
+
oss: options.oss,
|
|
1709
|
+
sandboxMode: options.sandboxMode,
|
|
1710
|
+
approvalMode: options.approvalMode,
|
|
1711
|
+
workspaceWriteOptions: options.workspaceWriteOptions,
|
|
1712
|
+
workingDirectory: options.workingDirectory,
|
|
1713
|
+
skipGitRepoCheck: options.skipGitRepoCheck,
|
|
1714
|
+
reasoningEffort: options.reasoningEffort,
|
|
1715
|
+
reasoningSummary: options.reasoningSummary,
|
|
1716
|
+
fullAuto: options.fullAuto,
|
|
1717
|
+
baseUrl: this.options.baseUrl,
|
|
1718
|
+
apiKey: this.options.apiKey
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
createLspManagerForTools() {
|
|
1722
|
+
const cwd = typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : ".";
|
|
1723
|
+
const options = {
|
|
1724
|
+
workingDirectory: cwd,
|
|
1725
|
+
waitForDiagnostics: true
|
|
1726
|
+
};
|
|
1727
|
+
try {
|
|
1728
|
+
return new LspManager(options);
|
|
1729
|
+
} catch {
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
registerDefaultReadFileInterceptor() {
|
|
1734
|
+
if (!this.lspForTools) {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
try {
|
|
1738
|
+
this.registerToolInterceptor("read_file", async ({ invocation, callBuiltin }) => {
|
|
1739
|
+
let base;
|
|
1740
|
+
try {
|
|
1741
|
+
base = await callBuiltin();
|
|
1742
|
+
} catch (err) {
|
|
1743
|
+
return {
|
|
1744
|
+
success: false,
|
|
1745
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1746
|
+
output: void 0
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
if (!base.output || base.success === false) {
|
|
1750
|
+
return base;
|
|
1751
|
+
}
|
|
1752
|
+
let filePath;
|
|
1753
|
+
if (invocation.arguments) {
|
|
1754
|
+
try {
|
|
1755
|
+
const args = JSON.parse(invocation.arguments);
|
|
1756
|
+
const candidate = typeof args.file_path === "string" && args.file_path || typeof args.path === "string" && args.path || void 0;
|
|
1757
|
+
if (candidate && candidate.trim().length > 0) {
|
|
1758
|
+
filePath = candidate;
|
|
1759
|
+
}
|
|
1760
|
+
} catch {
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
if (!filePath) {
|
|
1764
|
+
return base;
|
|
1765
|
+
}
|
|
1766
|
+
let diagnosticsText = "";
|
|
1767
|
+
try {
|
|
1768
|
+
const results = await this.lspForTools.collectDiagnostics([filePath]);
|
|
1769
|
+
if (!results.length) {
|
|
1770
|
+
return base;
|
|
1771
|
+
}
|
|
1772
|
+
diagnosticsText = formatDiagnosticsForTool(results);
|
|
1773
|
+
} catch {
|
|
1774
|
+
return base;
|
|
1775
|
+
}
|
|
1776
|
+
if (!diagnosticsText) {
|
|
1777
|
+
return base;
|
|
1778
|
+
}
|
|
1779
|
+
const header = `LSP diagnostics for ${filePath}:
|
|
1780
|
+
${diagnosticsText}`;
|
|
1781
|
+
return prependSystemHintToToolResult(base, header);
|
|
1782
|
+
});
|
|
1783
|
+
} catch {
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Register a programmatic approval callback that Codex will call before executing
|
|
1788
|
+
* sensitive operations (e.g., shell commands, file writes).
|
|
1789
|
+
*/
|
|
1790
|
+
setApprovalCallback(handler) {
|
|
1791
|
+
if (!this.nativeBinding || typeof this.nativeBinding.registerApprovalCallback !== "function") {
|
|
1792
|
+
console.warn("Approval callback is not available in this build");
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
this.nativeBinding.registerApprovalCallback(handler);
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Starts a new conversation with an agent.
|
|
1799
|
+
* @returns A new thread instance.
|
|
1800
|
+
*/
|
|
1801
|
+
startThread(options = {}) {
|
|
1802
|
+
const threadOptions = {
|
|
1803
|
+
...options,
|
|
1804
|
+
model: options.model ?? this.options.defaultModel
|
|
1805
|
+
};
|
|
1806
|
+
return new Thread(this.exec, this.options, threadOptions);
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Resumes a conversation with an agent based on the thread id.
|
|
1810
|
+
* Threads are persisted in ~/.codex/sessions.
|
|
1811
|
+
*
|
|
1812
|
+
* @param id The id of the thread to resume.
|
|
1813
|
+
* @returns A new thread instance.
|
|
1814
|
+
*/
|
|
1815
|
+
resumeThread(id, options = {}) {
|
|
1816
|
+
const threadOptions = {
|
|
1817
|
+
...options,
|
|
1818
|
+
model: options.model ?? this.options.defaultModel
|
|
1819
|
+
};
|
|
1820
|
+
return new Thread(this.exec, this.options, threadOptions, id);
|
|
1821
|
+
}
|
|
1822
|
+
async listConversations(options = {}) {
|
|
1823
|
+
const request = {
|
|
1824
|
+
config: this.buildConversationConfig(options),
|
|
1825
|
+
pageSize: options.pageSize,
|
|
1826
|
+
cursor: options.cursor,
|
|
1827
|
+
modelProviders: options.modelProviders
|
|
1828
|
+
};
|
|
1829
|
+
return this.exec.listConversations(request);
|
|
1830
|
+
}
|
|
1831
|
+
async deleteConversation(id, options = {}) {
|
|
1832
|
+
const result = await this.exec.deleteConversation({
|
|
1833
|
+
id,
|
|
1834
|
+
config: this.buildConversationConfig(options)
|
|
1835
|
+
});
|
|
1836
|
+
return result.deleted;
|
|
1837
|
+
}
|
|
1838
|
+
async resumeConversationFromRollout(rolloutPath, options = {}) {
|
|
1839
|
+
const result = await this.exec.resumeConversationFromRollout({
|
|
1840
|
+
rolloutPath,
|
|
1841
|
+
config: this.buildConversationConfig(options)
|
|
1842
|
+
});
|
|
1843
|
+
const threadOptions = {
|
|
1844
|
+
...options,
|
|
1845
|
+
model: options.model ?? this.options.defaultModel
|
|
1846
|
+
};
|
|
1847
|
+
return new Thread(this.exec, this.options, threadOptions, result.threadId);
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Starts a review task using the built-in Codex review flow.
|
|
1851
|
+
*/
|
|
1852
|
+
async review(options) {
|
|
1853
|
+
const generator = this.reviewStreamedInternal(options);
|
|
1854
|
+
const items = [];
|
|
1855
|
+
let finalResponse = "";
|
|
1856
|
+
let usage = null;
|
|
1857
|
+
let turnFailure = null;
|
|
1858
|
+
for await (const event of generator) {
|
|
1859
|
+
if (event === null) continue;
|
|
1860
|
+
if (event.type === "item.completed") {
|
|
1861
|
+
if (event.item.type === "agent_message") {
|
|
1862
|
+
finalResponse = event.item.text;
|
|
1863
|
+
}
|
|
1864
|
+
items.push(event.item);
|
|
1865
|
+
} else if (event.type === "exited_review_mode") {
|
|
1866
|
+
if (event.review_output) {
|
|
1867
|
+
const reviewOutput = event.review_output;
|
|
1868
|
+
let reviewText = "";
|
|
1869
|
+
if (reviewOutput.overall_explanation) {
|
|
1870
|
+
reviewText += reviewOutput.overall_explanation;
|
|
1871
|
+
}
|
|
1872
|
+
if (reviewOutput.findings && reviewOutput.findings.length > 0) {
|
|
1873
|
+
if (reviewText) reviewText += "\n\n";
|
|
1874
|
+
reviewText += "## Review Findings\n\n";
|
|
1875
|
+
reviewOutput.findings.forEach((finding, index) => {
|
|
1876
|
+
reviewText += `### ${index + 1}. ${finding.title}
|
|
1877
|
+
`;
|
|
1878
|
+
reviewText += `${finding.body}
|
|
1879
|
+
`;
|
|
1880
|
+
reviewText += `**Priority:** ${finding.priority} | **Confidence:** ${finding.confidence_score}
|
|
1881
|
+
`;
|
|
1882
|
+
reviewText += `**Location:** ${finding.code_location.absolute_file_path}:${finding.code_location.line_range.start}-${finding.code_location.line_range.end}
|
|
1883
|
+
|
|
1884
|
+
`;
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
finalResponse = reviewText;
|
|
1888
|
+
}
|
|
1889
|
+
} else if (event.type === "turn.completed") {
|
|
1890
|
+
usage = event.usage;
|
|
1891
|
+
} else if (event.type === "turn.failed") {
|
|
1892
|
+
turnFailure = event.error;
|
|
1893
|
+
break;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
if (turnFailure) {
|
|
1897
|
+
throw new Error(turnFailure.message);
|
|
1898
|
+
}
|
|
1899
|
+
return { items, finalResponse, usage };
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Starts a review task and returns the event stream.
|
|
1903
|
+
*/
|
|
1904
|
+
async reviewStreamed(options) {
|
|
1905
|
+
return { events: this.reviewStreamedInternal(options) };
|
|
1906
|
+
}
|
|
1907
|
+
async *reviewStreamedInternal(options) {
|
|
1908
|
+
const { target, threadOptions = {}, turnOptions = {} } = options;
|
|
1909
|
+
const { prompt, hint } = buildReviewPrompt(target);
|
|
1910
|
+
const normalizedSchema = normalizeOutputSchema(turnOptions.outputSchema);
|
|
1911
|
+
const needsSchemaFile = this.exec.requiresOutputSchemaFile();
|
|
1912
|
+
const schemaFile = needsSchemaFile ? await createOutputSchemaFile(normalizedSchema) : { schemaPath: void 0, cleanup: async () => {
|
|
1913
|
+
} };
|
|
1914
|
+
const generator = this.exec.run({
|
|
1915
|
+
input: prompt,
|
|
1916
|
+
baseUrl: this.options.baseUrl,
|
|
1917
|
+
apiKey: this.options.apiKey,
|
|
1918
|
+
model: threadOptions.model,
|
|
1919
|
+
modelProvider: threadOptions.modelProvider ?? this.options.modelProvider,
|
|
1920
|
+
oss: threadOptions.oss,
|
|
1921
|
+
sandboxMode: threadOptions.sandboxMode,
|
|
1922
|
+
approvalMode: threadOptions.approvalMode,
|
|
1923
|
+
workspaceWriteOptions: threadOptions.workspaceWriteOptions,
|
|
1924
|
+
workingDirectory: threadOptions.workingDirectory,
|
|
1925
|
+
skipGitRepoCheck: threadOptions.skipGitRepoCheck,
|
|
1926
|
+
outputSchemaFile: schemaFile.schemaPath,
|
|
1927
|
+
outputSchema: normalizedSchema,
|
|
1928
|
+
fullAuto: threadOptions.fullAuto,
|
|
1929
|
+
review: {
|
|
1930
|
+
userFacingHint: hint
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
try {
|
|
1934
|
+
for await (const item of generator) {
|
|
1935
|
+
let parsed;
|
|
1936
|
+
try {
|
|
1937
|
+
parsed = JSON.parse(item);
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
throw new Error(`Failed to parse item: ${item}`, { cause: error });
|
|
1940
|
+
}
|
|
1941
|
+
yield parsed;
|
|
1942
|
+
}
|
|
1943
|
+
} finally {
|
|
1944
|
+
await schemaFile.cleanup();
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
function prependSystemHintToToolResult(base, hint) {
|
|
1949
|
+
const trimmedHint = hint.trim();
|
|
1950
|
+
if (!trimmedHint) {
|
|
1951
|
+
return base;
|
|
1952
|
+
}
|
|
1953
|
+
const existing = base.output ?? "";
|
|
1954
|
+
const separator = existing.length === 0 || existing.startsWith("\n") ? "\n\n" : "\n\n";
|
|
1955
|
+
const output = existing.length === 0 ? `[SYSTEM_HINT]
|
|
1956
|
+
${trimmedHint}` : `[SYSTEM_HINT]
|
|
1957
|
+
${trimmedHint}${separator}${existing}`;
|
|
1958
|
+
return {
|
|
1959
|
+
...base,
|
|
1960
|
+
output
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
export {
|
|
1965
|
+
convertRustEventToThreadEvent,
|
|
1966
|
+
getNativeBinding,
|
|
1967
|
+
runApplyPatch,
|
|
1968
|
+
reverieListConversations,
|
|
1969
|
+
reverieSearchConversations,
|
|
1970
|
+
reverieSearchSemantic,
|
|
1971
|
+
reverieIndexSemantic,
|
|
1972
|
+
reverieGetConversationInsights,
|
|
1973
|
+
encodeToToon,
|
|
1974
|
+
fastEmbedInit,
|
|
1975
|
+
fastEmbedEmbed,
|
|
1976
|
+
tokenizerCount,
|
|
1977
|
+
tokenizerEncode,
|
|
1978
|
+
tokenizerDecode,
|
|
1979
|
+
collectRepoDiffSummary,
|
|
1980
|
+
startTui,
|
|
1981
|
+
runTui,
|
|
1982
|
+
DEFAULT_SERVERS,
|
|
1983
|
+
findServerForFile,
|
|
1984
|
+
resolveWorkspaceRoot,
|
|
1985
|
+
LspManager,
|
|
1986
|
+
formatDiagnosticsForTool,
|
|
1987
|
+
formatDiagnosticsForBackgroundEvent,
|
|
1988
|
+
filterBySeverity,
|
|
1989
|
+
summarizeDiagnostics,
|
|
1990
|
+
formatDiagnosticsWithSummary,
|
|
1991
|
+
LspDiagnosticsBridge,
|
|
1992
|
+
attachLspDiagnostics,
|
|
1993
|
+
Thread,
|
|
1994
|
+
Codex
|
|
1995
|
+
};
|
|
1996
|
+
//# sourceMappingURL=chunk-ZTUGAPWF.mjs.map
|