lynkr 8.0.1 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.lynkr/telemetry.db +0 -0
- package/.lynkr/telemetry.db-shm +0 -0
- package/.lynkr/telemetry.db-wal +0 -0
- package/README.md +195 -321
- package/lynkr-skill.tar.gz +0 -0
- package/package.json +4 -3
- package/src/api/openai-router.js +30 -11
- package/src/api/providers-handler.js +171 -3
- package/src/api/router.js +9 -2
- package/src/clients/circuit-breaker.js +10 -247
- package/src/clients/codex-process.js +342 -0
- package/src/clients/codex-utils.js +143 -0
- package/src/clients/databricks.js +210 -63
- package/src/clients/resilience.js +540 -0
- package/src/clients/retry.js +22 -167
- package/src/config/index.js +57 -0
- package/src/context/compression.js +42 -9
- package/src/context/distill.js +492 -0
- package/src/orchestrator/index.js +46 -6
- package/src/routing/complexity-analyzer.js +258 -5
- package/src/routing/index.js +12 -2
- package/src/routing/latency-tracker.js +148 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/quality-scorer.js +113 -0
- package/src/routing/telemetry.js +464 -0
- package/src/server.js +11 -0
- package/src/tools/code-graph.js +538 -0
- package/src/tools/code-mode.js +304 -0
- package/src/tools/lazy-loader.js +11 -0
- package/src/tools/mcp-remote.js +7 -0
- package/src/tools/smart-selection.js +11 -0
- package/src/utils/payload.js +206 -0
- package/src/utils/perf-timer.js +80 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex App-Server Process Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages a persistent `codex app-server` child process that communicates
|
|
5
|
+
* via JSON-RPC over stdio. Inherits the user's local ChatGPT subscription
|
|
6
|
+
* auth, so no API key is needed.
|
|
7
|
+
*
|
|
8
|
+
* @module clients/codex-process
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { spawn, execSync } = require("node:child_process");
|
|
12
|
+
const readline = require("node:readline");
|
|
13
|
+
const logger = require("../logger");
|
|
14
|
+
const config = require("../config");
|
|
15
|
+
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
17
|
+
|
|
18
|
+
class CodexProcess {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.child = null;
|
|
21
|
+
this.pendingRequests = new Map(); // id -> { resolve, reject, timeout }
|
|
22
|
+
this.nextId = 1;
|
|
23
|
+
this.initialized = false;
|
|
24
|
+
this.accountInfo = null;
|
|
25
|
+
this.buffer = "";
|
|
26
|
+
this.restartCount = 0;
|
|
27
|
+
this._turnCollector = null; // active turn content collector
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if the codex binary is available on PATH
|
|
32
|
+
*/
|
|
33
|
+
static isAvailable() {
|
|
34
|
+
try {
|
|
35
|
+
const binaryPath = config.codex?.binaryPath || "codex";
|
|
36
|
+
execSync(`which ${binaryPath}`, { stdio: "ignore" });
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Ensure the codex app-server process is running and initialized
|
|
45
|
+
*/
|
|
46
|
+
async ensureRunning() {
|
|
47
|
+
if (this.child && this.initialized) return;
|
|
48
|
+
|
|
49
|
+
if (this.child) {
|
|
50
|
+
// Process exists but not initialized — wait or restart
|
|
51
|
+
this._killProcess();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const binaryPath = config.codex?.binaryPath || "codex";
|
|
55
|
+
|
|
56
|
+
logger.info({ binaryPath, restart: this.restartCount }, "Spawning codex app-server");
|
|
57
|
+
|
|
58
|
+
this.child = spawn(binaryPath, ["app-server"], {
|
|
59
|
+
cwd: process.cwd(),
|
|
60
|
+
env: { ...process.env },
|
|
61
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
62
|
+
shell: process.platform === "win32",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const rl = readline.createInterface({ input: this.child.stdout });
|
|
66
|
+
rl.on("line", (line) => this._onLine(line));
|
|
67
|
+
|
|
68
|
+
this.child.stderr.on("data", (data) => {
|
|
69
|
+
const msg = data.toString().trim();
|
|
70
|
+
if (msg) {
|
|
71
|
+
logger.debug({ stderr: msg }, "[Codex] stderr");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.child.on("exit", (code, signal) => {
|
|
76
|
+
logger.warn({ code, signal, restartCount: this.restartCount }, "[Codex] Process exited");
|
|
77
|
+
this._rejectAllPending(new Error(`Codex process exited with code ${code}`));
|
|
78
|
+
this.child = null;
|
|
79
|
+
this.initialized = false;
|
|
80
|
+
this.restartCount++;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.child.on("error", (err) => {
|
|
84
|
+
logger.error({ err: err.message }, "[Codex] Process error");
|
|
85
|
+
this._rejectAllPending(err);
|
|
86
|
+
this.child = null;
|
|
87
|
+
this.initialized = false;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Handshake
|
|
91
|
+
await this.sendRequest("initialize", {
|
|
92
|
+
protocolVersion: "2025-01-01",
|
|
93
|
+
capabilities: {},
|
|
94
|
+
clientInfo: { name: "lynkr", version: "1.0.0" },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this._sendNotification("initialized", {});
|
|
98
|
+
|
|
99
|
+
// Read account info
|
|
100
|
+
try {
|
|
101
|
+
const accountResp = await this.sendRequest("account/read", {});
|
|
102
|
+
this.accountInfo = this._parseAccount(accountResp);
|
|
103
|
+
logger.info({
|
|
104
|
+
type: this.accountInfo.type,
|
|
105
|
+
planType: this.accountInfo.planType,
|
|
106
|
+
}, "[Codex] Account info");
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.warn({ err: err.message }, "[Codex] account/read failed");
|
|
109
|
+
this.accountInfo = { type: "unknown", planType: null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.initialized = true;
|
|
113
|
+
logger.info("[Codex] App-server initialized");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Send a JSON-RPC request and wait for response
|
|
118
|
+
*/
|
|
119
|
+
sendRequest(method, params, timeoutMs) {
|
|
120
|
+
const timeout = timeoutMs || config.codex?.timeout || DEFAULT_TIMEOUT_MS;
|
|
121
|
+
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
if (!this.child) {
|
|
124
|
+
reject(new Error("Codex process not running"));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const id = this.nextId++;
|
|
129
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
130
|
+
|
|
131
|
+
const timer = setTimeout(() => {
|
|
132
|
+
this.pendingRequests.delete(id);
|
|
133
|
+
reject(new Error(`Codex request "${method}" timed out after ${timeout}ms`));
|
|
134
|
+
}, timeout);
|
|
135
|
+
|
|
136
|
+
this.pendingRequests.set(id, { resolve, reject, timeout: timer, method });
|
|
137
|
+
|
|
138
|
+
this.child.stdin.write(msg + "\n");
|
|
139
|
+
logger.debug({ id, method }, "[Codex] Sent request");
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Send a turn and collect all streaming content events
|
|
145
|
+
* Returns the accumulated response text
|
|
146
|
+
*/
|
|
147
|
+
async sendTurn(threadId, content, model) {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const collectedContent = [];
|
|
150
|
+
let turnId = null;
|
|
151
|
+
|
|
152
|
+
// Set up collector for streaming notifications
|
|
153
|
+
this._turnCollector = {
|
|
154
|
+
threadId,
|
|
155
|
+
content: collectedContent,
|
|
156
|
+
onComplete: (result) => {
|
|
157
|
+
this._turnCollector = null;
|
|
158
|
+
resolve({
|
|
159
|
+
text: collectedContent.join(""),
|
|
160
|
+
turnId,
|
|
161
|
+
raw: result,
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
onError: (err) => {
|
|
165
|
+
this._turnCollector = null;
|
|
166
|
+
reject(err);
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Send the turn/start request
|
|
171
|
+
const id = this.nextId++;
|
|
172
|
+
const params = { threadId, content };
|
|
173
|
+
if (model) params.model = model;
|
|
174
|
+
|
|
175
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, method: "turn/start", params });
|
|
176
|
+
|
|
177
|
+
const timeout = config.codex?.timeout || DEFAULT_TIMEOUT_MS;
|
|
178
|
+
const timer = setTimeout(() => {
|
|
179
|
+
this.pendingRequests.delete(id);
|
|
180
|
+
this._turnCollector = null;
|
|
181
|
+
reject(new Error(`Codex turn timed out after ${timeout}ms`));
|
|
182
|
+
}, timeout);
|
|
183
|
+
|
|
184
|
+
this.pendingRequests.set(id, {
|
|
185
|
+
resolve: (result) => {
|
|
186
|
+
turnId = result?.turnId || null;
|
|
187
|
+
// The collector's onComplete will be called by turn/completed notification
|
|
188
|
+
// If no streaming, resolve immediately
|
|
189
|
+
if (!this._turnCollector) {
|
|
190
|
+
resolve({ text: collectedContent.join(""), turnId, raw: result });
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
reject: (err) => {
|
|
194
|
+
this._turnCollector = null;
|
|
195
|
+
reject(err);
|
|
196
|
+
},
|
|
197
|
+
timeout: timer,
|
|
198
|
+
method: "turn/start",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
this.child.stdin.write(msg + "\n");
|
|
202
|
+
logger.debug({ id, threadId }, "[Codex] Sent turn/start");
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Handle a line from codex stdout
|
|
208
|
+
*/
|
|
209
|
+
_onLine(line) {
|
|
210
|
+
if (!line.trim()) return;
|
|
211
|
+
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(line);
|
|
215
|
+
} catch {
|
|
216
|
+
logger.debug({ line: line.substring(0, 200) }, "[Codex] Non-JSON stdout");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// JSON-RPC response (has id)
|
|
221
|
+
if (parsed.id !== undefined) {
|
|
222
|
+
const pending = this.pendingRequests.get(parsed.id);
|
|
223
|
+
if (pending) {
|
|
224
|
+
clearTimeout(pending.timeout);
|
|
225
|
+
this.pendingRequests.delete(parsed.id);
|
|
226
|
+
|
|
227
|
+
if (parsed.error) {
|
|
228
|
+
pending.reject(new Error(parsed.error.message || JSON.stringify(parsed.error)));
|
|
229
|
+
} else {
|
|
230
|
+
pending.resolve(parsed.result);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// JSON-RPC notification (no id) — streaming events
|
|
237
|
+
const method = parsed.method;
|
|
238
|
+
const params = parsed.params || {};
|
|
239
|
+
|
|
240
|
+
if (method === "item/message/outputText/delta" && this._turnCollector) {
|
|
241
|
+
const delta = params.delta || params.text || "";
|
|
242
|
+
if (delta) {
|
|
243
|
+
this._turnCollector.content.push(delta);
|
|
244
|
+
}
|
|
245
|
+
} else if (method === "item/message/outputText/done" && this._turnCollector) {
|
|
246
|
+
const text = params.text || "";
|
|
247
|
+
if (text && this._turnCollector.content.length === 0) {
|
|
248
|
+
this._turnCollector.content.push(text);
|
|
249
|
+
}
|
|
250
|
+
} else if (method === "turn/completed" && this._turnCollector) {
|
|
251
|
+
this._turnCollector.onComplete(params);
|
|
252
|
+
} else if (method === "turn/error" && this._turnCollector) {
|
|
253
|
+
this._turnCollector.onError(new Error(params.message || "Codex turn error"));
|
|
254
|
+
} else {
|
|
255
|
+
logger.debug({ method, params: JSON.stringify(params).substring(0, 200) }, "[Codex] Notification");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Send a JSON-RPC notification (no response expected)
|
|
261
|
+
*/
|
|
262
|
+
_sendNotification(method, params) {
|
|
263
|
+
if (!this.child) return;
|
|
264
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
265
|
+
this.child.stdin.write(msg + "\n");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Parse account/read response
|
|
270
|
+
*/
|
|
271
|
+
_parseAccount(response) {
|
|
272
|
+
const account = response?.account || response || {};
|
|
273
|
+
const type = account.type || "unknown";
|
|
274
|
+
const planType = account.planType || null;
|
|
275
|
+
return { type, planType };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Reject all pending requests
|
|
280
|
+
*/
|
|
281
|
+
_rejectAllPending(error) {
|
|
282
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
283
|
+
clearTimeout(pending.timeout);
|
|
284
|
+
pending.reject(error);
|
|
285
|
+
}
|
|
286
|
+
this.pendingRequests.clear();
|
|
287
|
+
|
|
288
|
+
if (this._turnCollector) {
|
|
289
|
+
this._turnCollector.onError(error);
|
|
290
|
+
this._turnCollector = null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Kill the child process
|
|
296
|
+
*/
|
|
297
|
+
_killProcess() {
|
|
298
|
+
if (!this.child) return;
|
|
299
|
+
try {
|
|
300
|
+
if (process.platform === "win32") {
|
|
301
|
+
execSync(`taskkill /pid ${this.child.pid} /T /F`, { stdio: "ignore" });
|
|
302
|
+
} else {
|
|
303
|
+
this.child.kill("SIGTERM");
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
try { this.child.kill("SIGKILL"); } catch { /* ignore */ }
|
|
307
|
+
}
|
|
308
|
+
this.child = null;
|
|
309
|
+
this.initialized = false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Graceful shutdown
|
|
314
|
+
*/
|
|
315
|
+
async shutdown() {
|
|
316
|
+
logger.info("[Codex] Shutting down app-server");
|
|
317
|
+
this._rejectAllPending(new Error("Codex shutting down"));
|
|
318
|
+
this._killProcess();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get cached account info
|
|
323
|
+
*/
|
|
324
|
+
getAccountInfo() {
|
|
325
|
+
return this.accountInfo;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Singleton instance
|
|
330
|
+
let instance = null;
|
|
331
|
+
|
|
332
|
+
function getCodexProcess() {
|
|
333
|
+
if (!instance) {
|
|
334
|
+
instance = new CodexProcess();
|
|
335
|
+
}
|
|
336
|
+
return instance;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = {
|
|
340
|
+
CodexProcess,
|
|
341
|
+
getCodexProcess,
|
|
342
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Format Conversion Utilities
|
|
3
|
+
*
|
|
4
|
+
* Converts between Anthropic/Lynkr internal message format
|
|
5
|
+
* and Codex app-server JSON-RPC format.
|
|
6
|
+
*
|
|
7
|
+
* @module clients/codex-utils
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const logger = require("../logger");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract text content from Anthropic message format
|
|
14
|
+
* Handles both string content and content block arrays
|
|
15
|
+
*/
|
|
16
|
+
function extractText(message) {
|
|
17
|
+
if (!message) return "";
|
|
18
|
+
const content = message.content;
|
|
19
|
+
|
|
20
|
+
if (typeof content === "string") return content;
|
|
21
|
+
if (!Array.isArray(content)) return "";
|
|
22
|
+
|
|
23
|
+
return content
|
|
24
|
+
.map((block) => {
|
|
25
|
+
if (block.type === "text") return block.text || "";
|
|
26
|
+
if (block.type === "tool_result") {
|
|
27
|
+
const result = typeof block.content === "string"
|
|
28
|
+
? block.content
|
|
29
|
+
: JSON.stringify(block.content);
|
|
30
|
+
return `[Tool Result: ${result}]`;
|
|
31
|
+
}
|
|
32
|
+
if (block.type === "tool_use") {
|
|
33
|
+
return `[Tool Call: ${block.name}(${JSON.stringify(block.input)})]`;
|
|
34
|
+
}
|
|
35
|
+
return "";
|
|
36
|
+
})
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert Anthropic request body to a Codex turn prompt
|
|
43
|
+
*
|
|
44
|
+
* Strategy: Codex turn/start takes a simple text prompt.
|
|
45
|
+
* We flatten system + message history into a single prompt.
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} body - Lynkr internal body (Anthropic format)
|
|
48
|
+
* @returns {{ prompt: string, systemContext: string|null }}
|
|
49
|
+
*/
|
|
50
|
+
function convertAnthropicToCodexPrompt(body) {
|
|
51
|
+
const systemContext = body.system || null;
|
|
52
|
+
const messages = body.messages || [];
|
|
53
|
+
|
|
54
|
+
if (messages.length === 0) {
|
|
55
|
+
return { prompt: "", systemContext };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// If only one user message, use it directly
|
|
59
|
+
if (messages.length === 1 && messages[0].role === "user") {
|
|
60
|
+
return {
|
|
61
|
+
prompt: extractText(messages[0]),
|
|
62
|
+
systemContext,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Multiple messages — flatten into a conversation prompt
|
|
67
|
+
// Keep last user message as the main prompt
|
|
68
|
+
// Include prior messages as context
|
|
69
|
+
const lastUserIndex = findLastIndex(messages, (m) => m.role === "user");
|
|
70
|
+
if (lastUserIndex === -1) {
|
|
71
|
+
return { prompt: extractText(messages[messages.length - 1]), systemContext };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const lastUserMessage = extractText(messages[lastUserIndex]);
|
|
75
|
+
|
|
76
|
+
// Build context from prior messages
|
|
77
|
+
const priorMessages = messages.slice(0, lastUserIndex);
|
|
78
|
+
if (priorMessages.length === 0) {
|
|
79
|
+
return { prompt: lastUserMessage, systemContext };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const contextParts = priorMessages.map((m) => {
|
|
83
|
+
const text = extractText(m);
|
|
84
|
+
if (!text) return null;
|
|
85
|
+
const role = m.role === "user" ? "User" : "Assistant";
|
|
86
|
+
return `${role}: ${text}`;
|
|
87
|
+
}).filter(Boolean);
|
|
88
|
+
|
|
89
|
+
const conversationContext = contextParts.join("\n\n");
|
|
90
|
+
|
|
91
|
+
// Combine context + latest question
|
|
92
|
+
const prompt = conversationContext
|
|
93
|
+
? `Previous conversation:\n${conversationContext}\n\nUser: ${lastUserMessage}`
|
|
94
|
+
: lastUserMessage;
|
|
95
|
+
|
|
96
|
+
return { prompt, systemContext };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convert Codex turn response to Anthropic message format
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} turnResult - { text, turnId, raw }
|
|
103
|
+
* @param {string} model - Model name to include in response
|
|
104
|
+
* @returns {Object} Anthropic format response
|
|
105
|
+
*/
|
|
106
|
+
function convertCodexResponseToAnthropic(turnResult, model) {
|
|
107
|
+
const text = turnResult.text || "";
|
|
108
|
+
|
|
109
|
+
// Estimate tokens (rough: 1 token ≈ 4 chars)
|
|
110
|
+
const estimatedOutputTokens = Math.ceil(text.length / 4);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
id: `msg_codex_${Date.now()}`,
|
|
114
|
+
type: "message",
|
|
115
|
+
role: "assistant",
|
|
116
|
+
model: model || "codex",
|
|
117
|
+
content: [{ type: "text", text }],
|
|
118
|
+
stop_reason: "end_turn",
|
|
119
|
+
stop_sequence: null,
|
|
120
|
+
usage: {
|
|
121
|
+
input_tokens: 0, // Codex doesn't report these via app-server
|
|
122
|
+
output_tokens: estimatedOutputTokens,
|
|
123
|
+
cache_creation_input_tokens: 0,
|
|
124
|
+
cache_read_input_tokens: 0,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Array.findLastIndex polyfill
|
|
131
|
+
*/
|
|
132
|
+
function findLastIndex(arr, fn) {
|
|
133
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
134
|
+
if (fn(arr[i])) return i;
|
|
135
|
+
}
|
|
136
|
+
return -1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
convertAnthropicToCodexPrompt,
|
|
141
|
+
convertCodexResponseToAnthropic,
|
|
142
|
+
extractText,
|
|
143
|
+
};
|