@songsid/agend 2.1.6-beta.12 → 2.1.6-beta.14
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/dist/backend/codex.d.ts +8 -0
- package/dist/backend/codex.js +385 -29
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.d.ts +9 -0
- package/dist/backend/kiro.js +129 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +12 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/daemon.d.ts +35 -7
- package/dist/daemon.js +406 -64
- package/dist/daemon.js.map +1 -1
- package/dist/instance-lifecycle.js +7 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/muse-usage-relay.js +14 -2
- package/dist/muse-usage-relay.js.map +1 -1
- package/dist/tmux-manager.d.ts +2 -0
- package/dist/tmux-manager.js +20 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/usage/providers.js +10 -2
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +0 -12
- package/dist/usage/usage-api.js +24 -3
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +2 -1
package/dist/backend/codex.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare class CodexBackend implements CliBackend {
|
|
|
7
7
|
private readonly isolatedCodexHome;
|
|
8
8
|
/** Which subscription this instance runs on, or null for the shared login. */
|
|
9
9
|
private credentialProfile;
|
|
10
|
+
/** Set only after preTrust wrote and read back this instance's private config. */
|
|
11
|
+
private authorizedTrust;
|
|
10
12
|
constructor(instanceDir: string);
|
|
11
13
|
supportsQueuedInput(): boolean;
|
|
12
14
|
/**
|
|
@@ -26,6 +28,8 @@ export declare class CodexBackend implements CliBackend {
|
|
|
26
28
|
* (updatePickerDialog) before any delivery path reads this pattern.
|
|
27
29
|
*/
|
|
28
30
|
getBottomReadyPattern(): RegExp | null;
|
|
31
|
+
/** Observed on real Codex 0.156.0: the live input row precedes its footer. */
|
|
32
|
+
isDeliveryInputReadyPane(pane: string): boolean;
|
|
29
33
|
/** Live status chrome that must veto the broad prompt/context ready match. */
|
|
30
34
|
getBusyPattern(): RegExp;
|
|
31
35
|
/**
|
|
@@ -127,9 +131,13 @@ export declare class CodexBackend implements CliBackend {
|
|
|
127
131
|
*/
|
|
128
132
|
private cleanSharedConfig;
|
|
129
133
|
getReadyPattern(): RegExp;
|
|
134
|
+
/** A proxy reply filters chrome per line; whole-pane readiness is separate. */
|
|
135
|
+
isProxyReplyChromeLine(line: string): boolean;
|
|
130
136
|
getErrorPatterns(): ErrorPattern[];
|
|
131
137
|
getStartupDialogs(): StartupDialog[];
|
|
138
|
+
private trustHoldDialog;
|
|
132
139
|
private updatePickerDialog;
|
|
140
|
+
private unknownSelectionHoldDialog;
|
|
133
141
|
getRuntimeDialogs(): RuntimeDialog[];
|
|
134
142
|
getInputUnavailableTransients(): InputUnavailableTransient[];
|
|
135
143
|
getContextUsage(): number | null;
|
package/dist/backend/codex.js
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
|
-
import { chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readdirSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { execFile } from "node:child_process";
|
|
3
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
-
import { lastNonBlankRow } from "../pane-input-residue.js";
|
|
6
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
7
6
|
import { probeCliVersion, resolveBinary, shellQuote, validateModel, validateProvider, warnIfModelMismatch } from "./types.js";
|
|
8
7
|
import { credentialHomeSpec, credentialProfileHome, prepareCredentialProfileHome, resolveCredentialProfile, } from "./credential-profile.js";
|
|
9
8
|
import { getAgendHome } from "../paths.js";
|
|
10
9
|
import { appendWithMarker, removeMarker } from "./marker-utils.js";
|
|
11
10
|
import { t } from "../locale.js";
|
|
11
|
+
import { parse as parseToml } from "smol-toml";
|
|
12
12
|
const CODEX_PROJECT_DOC_MAX_BYTES = 32_768;
|
|
13
13
|
const CODEX_MODELS_CACHE_MAX_BYTES = 5 * 1024 * 1024;
|
|
14
14
|
const SAFE_MODEL_ID_RE = /^[A-Za-z0-9._:/-]+$/;
|
|
15
|
+
/**
|
|
16
|
+
* Whether a pane row is Codex's context footer. Kept when #913 was reverted:
|
|
17
|
+
* #913 introduced it, but #914's pane/ready detection is built on it, and it
|
|
18
|
+
* is pane parsing, not session handling.
|
|
19
|
+
*/
|
|
20
|
+
function isCodexContextFooter(row) {
|
|
21
|
+
const context = String.raw `Context\s+\d+%\s+(?:left|used)`;
|
|
22
|
+
const legacy = new RegExp(String.raw `^\s*${context}(?:\s+⚠\s+\d+\s+warnings?\b[^\r\n]*)?(?:\s+·\s+\S[^\r\n]*)?\s*$`, "i");
|
|
23
|
+
if (legacy.test(row))
|
|
24
|
+
return true;
|
|
25
|
+
// A narrow Codex 0.156 pane may truncate the context item after the
|
|
26
|
+
// authoritative first `session-id` item. Keep structural readiness while
|
|
27
|
+
// /ctx honestly reports context unavailable from a truncated percentage.
|
|
28
|
+
return /^\s*[0-9a-f-]{36}\s+·\s+Context\b[^\r\n]*$/i.test(row);
|
|
29
|
+
}
|
|
15
30
|
const AGEND_MCP_CLEANUP_LOCK = ".agend-mcp-cleanup.lock";
|
|
16
31
|
const AGEND_MCP_CLEANUP_LOCK_STALE_MS = 30_000;
|
|
17
32
|
const SQLITE_SIDECAR_RE = /-(?:wal|shm|journal)$/;
|
|
@@ -62,6 +77,259 @@ function tomlString(value) {
|
|
|
62
77
|
// JSON strings are valid TOML basic strings for the values AgEnD emits.
|
|
63
78
|
return JSON.stringify(value);
|
|
64
79
|
}
|
|
80
|
+
/** Codex 0.156 trusts a linked worktree's common repository root, not its CWD. */
|
|
81
|
+
function codexTrustPaths(workingDirectory) {
|
|
82
|
+
const cwd = realpathSync(resolve(workingDirectory));
|
|
83
|
+
try {
|
|
84
|
+
const commonDir = execFileSync("git", ["-C", cwd, "rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
85
|
+
encoding: "utf-8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"],
|
|
86
|
+
}).trim();
|
|
87
|
+
const canonicalCommonDir = realpathSync(commonDir);
|
|
88
|
+
if (basename(canonicalCommonDir) === ".git")
|
|
89
|
+
return { cwd, root: dirname(canonicalCommonDir) };
|
|
90
|
+
// Submodules keep their common dir in another repository's .git/modules.
|
|
91
|
+
const topLevel = execFileSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
92
|
+
encoding: "utf-8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"],
|
|
93
|
+
}).trim();
|
|
94
|
+
return { cwd, root: realpathSync(topLevel) };
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// A non-Git folder is its own Codex trust root.
|
|
98
|
+
return { cwd, root: cwd };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function tomlTable(value) {
|
|
102
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)
|
|
103
|
+
? value : null;
|
|
104
|
+
}
|
|
105
|
+
function projectTrustTable(config, root) {
|
|
106
|
+
const projects = tomlTable(tomlTable(config)?.projects);
|
|
107
|
+
return projects ? tomlTable(projects[root]) : null;
|
|
108
|
+
}
|
|
109
|
+
function effectiveProjectTrust(content, root) {
|
|
110
|
+
return projectTrustTable(parseToml(content), root)?.trust_level;
|
|
111
|
+
}
|
|
112
|
+
/** Change only the private project's trust value; parse before and after editing. */
|
|
113
|
+
function setProjectTrusted(content, root) {
|
|
114
|
+
const section = `[projects.${tomlString(root)}]`;
|
|
115
|
+
const parsed = parseToml(content);
|
|
116
|
+
if (projectTrustTable(parsed, root)?.trust_level === "trusted")
|
|
117
|
+
return content;
|
|
118
|
+
const lines = content.split("\n");
|
|
119
|
+
const tableRows = [];
|
|
120
|
+
let multiline = null;
|
|
121
|
+
for (let row = 0; row < lines.length; row++) {
|
|
122
|
+
const line = lines[row];
|
|
123
|
+
if (!multiline && /^\s*\[.*\]\s*(?:#.*)?$/.test(line))
|
|
124
|
+
tableRows.push(row);
|
|
125
|
+
// A multiline config value can quote an entire pane/config. Treat a
|
|
126
|
+
// project-looking row inside it as data, never as effective TOML.
|
|
127
|
+
for (const delimiter of ['"""', "'''"]) {
|
|
128
|
+
if (multiline && multiline !== delimiter)
|
|
129
|
+
continue;
|
|
130
|
+
let count = 0;
|
|
131
|
+
let pos = 0;
|
|
132
|
+
while ((pos = line.indexOf(delimiter, pos)) !== -1) {
|
|
133
|
+
if (delimiter === "'''" || pos === 0 || line[pos - 1] !== "\\")
|
|
134
|
+
count++;
|
|
135
|
+
pos += delimiter.length;
|
|
136
|
+
}
|
|
137
|
+
if (count % 2 === 1)
|
|
138
|
+
multiline = multiline === delimiter ? null : delimiter;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const headers = tableRows.filter(row => {
|
|
142
|
+
// TOML permits whitespace around dots and quoted/literal table keys.
|
|
143
|
+
// Parse each real header instead of comparing its raw spelling.
|
|
144
|
+
try {
|
|
145
|
+
return projectTrustTable(parseToml(`${lines[row]}\n__agend_trust_probe__ = true\n`), root)?.__agend_trust_probe__ === true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
if (headers.length > 1)
|
|
152
|
+
throw new Error("Duplicate Codex trust project table");
|
|
153
|
+
if (headers.length === 0 && projectTrustTable(parsed, root)) {
|
|
154
|
+
throw new Error("Cannot safely edit Codex trust project table");
|
|
155
|
+
}
|
|
156
|
+
let updated;
|
|
157
|
+
if (headers.length === 0) {
|
|
158
|
+
updated = `${content.trimEnd()}\n\n${section}\ntrust_level = "trusted"\n`;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const header = headers[0];
|
|
162
|
+
const end = tableRows.find(row => row > header) ?? lines.length;
|
|
163
|
+
const trustRows = [];
|
|
164
|
+
for (let row = header + 1; row < end; row++) {
|
|
165
|
+
if (/^\s*(?:trust_level|"trust_level"|'trust_level')\s*=/.test(lines[row]))
|
|
166
|
+
trustRows.push(row);
|
|
167
|
+
}
|
|
168
|
+
if (trustRows.length > 1)
|
|
169
|
+
throw new Error("Duplicate Codex trust_level key");
|
|
170
|
+
if (trustRows.length === 1)
|
|
171
|
+
lines[trustRows[0]] = 'trust_level = "trusted"';
|
|
172
|
+
else
|
|
173
|
+
lines.splice(header + 1, 0, 'trust_level = "trusted"');
|
|
174
|
+
updated = lines.join("\n");
|
|
175
|
+
}
|
|
176
|
+
// Candidate validation is before atomic write. A spelling the narrow text
|
|
177
|
+
// editor cannot handle must fail closed, never leave Codex with invalid TOML.
|
|
178
|
+
if (effectiveProjectTrust(updated, root) !== "trusted")
|
|
179
|
+
throw new Error("Codex project trust is not effective");
|
|
180
|
+
return updated;
|
|
181
|
+
}
|
|
182
|
+
/** Only the bottom, live Codex 0.156 folder-access screen can own stdin. */
|
|
183
|
+
function codexTrustPromptState(pane) {
|
|
184
|
+
const noPrompt = { active: false, folder: null, root: null, rootNote: "absent", safeChoice: false };
|
|
185
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
186
|
+
let last = rows.length - 1;
|
|
187
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
188
|
+
last--;
|
|
189
|
+
if (last < 0)
|
|
190
|
+
return noPrompt;
|
|
191
|
+
let access = -1;
|
|
192
|
+
for (let index = rows.length - 1; index >= 0; index--) {
|
|
193
|
+
if (/^\s{0,2}Folder access\s*$/.test(rows[index])) {
|
|
194
|
+
access = index;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (access < 0 || last - access > 60)
|
|
199
|
+
return noPrompt;
|
|
200
|
+
const question = rows.findIndex((row, index) => index > access && /^\s{0,2}Trust this folder\?/.test(row));
|
|
201
|
+
if (question < 0 || question >= last)
|
|
202
|
+
return noPrompt;
|
|
203
|
+
// A ready input row or transcript continuation below the question means the
|
|
204
|
+
// trust dialog is history, not the current interactive region.
|
|
205
|
+
if (rows.slice(question + 1, last + 1).some(row => /^\s*[›❯>]\s*(?!\d+\.)\S/.test(row)))
|
|
206
|
+
return noPrompt;
|
|
207
|
+
const noteRows = rows.slice(access + 1, question);
|
|
208
|
+
// The folder must be the first content row after the title. Do not let a
|
|
209
|
+
// later repository-root path stand in for a missing folder path.
|
|
210
|
+
const folderRow = noteRows.find(row => row.trim() !== "");
|
|
211
|
+
const folder = folderRow && /^\s{0,2}\//.test(folderRow) ? folderRow.trim() : null;
|
|
212
|
+
const hasRootNote = noteRows.some(row => /\bNote:|\brepository root\b|Trusting will apply/i.test(row));
|
|
213
|
+
const rootLabel = noteRows.findIndex(row => /\brepository root:/i.test(row));
|
|
214
|
+
const inlineRoot = rootLabel < 0 ? "" : noteRows[rootLabel].split(/\brepository root:/i)[1]?.trim() ?? "";
|
|
215
|
+
const followingRoot = rootLabel < 0 ? "" : noteRows.slice(rootLabel + 1).find(row => row.trim() !== "")?.trim() ?? "";
|
|
216
|
+
const candidateRoot = inlineRoot || followingRoot;
|
|
217
|
+
const root = candidateRoot.startsWith("/") ? candidateRoot : null;
|
|
218
|
+
const rootNote = !hasRootNote ? "absent" : root ? "parsed" : "invalid";
|
|
219
|
+
const choices = rows.slice(question + 1, last + 1).flatMap((row, offset) => {
|
|
220
|
+
const match = row.match(/^\s*([›❯>]?)\s*(\d+)\.\s+(.+?)\s*$/);
|
|
221
|
+
return match ? [{ row: question + 1 + offset, cursor: match[1], number: match[2], text: match[3] }] : [];
|
|
222
|
+
});
|
|
223
|
+
// Unknown option orders, a moved cursor, a third option, or any different
|
|
224
|
+
// footer are held for a human. Never guess where Enter would land.
|
|
225
|
+
const safeChoice = choices.length === 2
|
|
226
|
+
&& choices[0].row + 1 === choices[1].row
|
|
227
|
+
&& rows.slice(choices[1].row + 1, last).every(row => row.trim() === "")
|
|
228
|
+
&& choices[0].cursor === "›" && choices[0].number === "1" && choices[0].text === "Trust and continue"
|
|
229
|
+
&& choices[1].cursor === "" && choices[1].number === "2" && choices[1].text === "Quit"
|
|
230
|
+
&& /^\s*enter continue\s*·\s*esc quit\s*$/i.test(rows[last]);
|
|
231
|
+
return { active: true, folder, root, rootNote, safeChoice };
|
|
232
|
+
}
|
|
233
|
+
/** Unknown/older trust layouts are still input-blocking, never auto-answered. */
|
|
234
|
+
function codexTrustVariantActive(pane) {
|
|
235
|
+
if (codexTrustPromptState(pane).active)
|
|
236
|
+
return true;
|
|
237
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
238
|
+
let last = rows.length - 1;
|
|
239
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
240
|
+
last--;
|
|
241
|
+
if (last < 0)
|
|
242
|
+
return false;
|
|
243
|
+
const start = Math.max(0, last - 22);
|
|
244
|
+
const folderAccess = rows.findIndex((row, index) => index >= start && /^\s{0,2}Folder access\s*$/.test(row));
|
|
245
|
+
if (folderAccess >= 0) {
|
|
246
|
+
const tail = rows.slice(folderAccess + 1, last + 1);
|
|
247
|
+
if (!tail.some(row => /^\s*[›❯>]\s*(?!\d+\.)\S/.test(row))
|
|
248
|
+
&& tail.some(row => /^\s*[›❯>]?\s*\d+\.\s+(?:Open restricted|Trust and continue|Quit)\s*$/i.test(row))
|
|
249
|
+
&& /(?:enter|esc|quit|cancel)/i.test(rows[last]))
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
const question = rows.findIndex((row, index) => index >= start
|
|
253
|
+
&& /^\s*(?:Trust this folder\?|Do you trust the files in this folder\?)/i.test(row));
|
|
254
|
+
if (question < 0 || question >= last)
|
|
255
|
+
return false;
|
|
256
|
+
const tail = rows.slice(question + 1, last + 1);
|
|
257
|
+
// A normal input row after a quoted menu makes it history, not live UI.
|
|
258
|
+
if (tail.some(row => /^\s*[›❯>]\s*(?!\d+\.)\S/.test(row)))
|
|
259
|
+
return false;
|
|
260
|
+
return tail.some(row => /^\s*[›❯>]?\s*\d+\.\s+\S/.test(row))
|
|
261
|
+
&& (/(?:enter|esc|quit|cancel)/i.test(rows[last])
|
|
262
|
+
|| /^\s*[›❯>]?\s*\d+\.\s+\S/.test(rows[last]));
|
|
263
|
+
}
|
|
264
|
+
/** Rate-switch prompts are economic choices, not a fixed keyboard position. */
|
|
265
|
+
function codexRateSwitchVisible(pane) {
|
|
266
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
267
|
+
let last = rows.length - 1;
|
|
268
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
269
|
+
last--;
|
|
270
|
+
let title = -1;
|
|
271
|
+
for (let i = last; i >= Math.max(0, last - 18); i--) {
|
|
272
|
+
if (/^\s*(?:Approaching rate limits|Switch to .{1,120} for lower credit usage\?)\s*$/i.test(rows[i])) {
|
|
273
|
+
title = i;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (title < 0)
|
|
278
|
+
return false;
|
|
279
|
+
const tail = rows.slice(title + 1, last + 1);
|
|
280
|
+
// A copied picker in transcript history is not the currently active menu.
|
|
281
|
+
if (tail.some(row => /^[›>]\s+Ask Codex to do anything\b/.test(row) || isCodexContextFooter(row)))
|
|
282
|
+
return false;
|
|
283
|
+
return tail.some(row => /^\s*[›❯>]?\s*\d+\.\s*(?:Switch to|Keep current model)\b/i.test(row));
|
|
284
|
+
}
|
|
285
|
+
/** Unknown pickers own stdin too; never type or press Enter into one. */
|
|
286
|
+
function codexUnknownSelectionVisible(pane) {
|
|
287
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
288
|
+
let last = rows.length - 1;
|
|
289
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
290
|
+
last--;
|
|
291
|
+
if (last < 0 || !(/\benter\b.*\besc\b/i.test(rows[last])
|
|
292
|
+
|| /^\s*Press enter to continue\s*$/i.test(rows[last])))
|
|
293
|
+
return false;
|
|
294
|
+
let selected = -1;
|
|
295
|
+
for (let i = last - 1; i >= Math.max(0, last - 24); i--) {
|
|
296
|
+
if (/^\s*[›❯>]\s+\S/.test(rows[i])) {
|
|
297
|
+
selected = i;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (selected < 0)
|
|
302
|
+
return false;
|
|
303
|
+
if (/^\s*[›❯>]\s+Ask Codex to do anything\b/.test(rows[selected]))
|
|
304
|
+
return false;
|
|
305
|
+
return !rows.slice(selected + 1, last + 1).some(row => /^[›>]\s+Ask Codex to do anything\b/.test(row) || isCodexContextFooter(row));
|
|
306
|
+
}
|
|
307
|
+
/** Only the complete, current Codex installer picker may receive Escape. */
|
|
308
|
+
function codexUpdatePickerVisible(pane) {
|
|
309
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
310
|
+
let last = rows.length - 1;
|
|
311
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
312
|
+
last--;
|
|
313
|
+
if (last < 0 || !/^\s*Press enter to continue\s*$/.test(rows[last]))
|
|
314
|
+
return false;
|
|
315
|
+
for (let first = last - 3; first >= Math.max(0, last - 9); first--) {
|
|
316
|
+
if (!/^\s*[›❯>]\s+1\.\s+Update now\b/.test(rows[first]))
|
|
317
|
+
continue;
|
|
318
|
+
// At 80 columns Codex 0.153 wraps the installer command to the next row.
|
|
319
|
+
// Permit a bounded continuation, but never another option or cursor.
|
|
320
|
+
const second = rows.findIndex((row, index) => index > first && index <= first + 3
|
|
321
|
+
&& /^\s*2\.\s+Skip\s*$/.test(row));
|
|
322
|
+
if (second < 0 || !rows.slice(first + 1, second).every(row => /^\s+\S/.test(row) && !/^\s*[›❯>]?\s*\d+\./.test(row))
|
|
323
|
+
|| !/^\s*3\.\s+Skip until next version\s*$/.test(rows[second + 1] ?? "")
|
|
324
|
+
|| !rows.slice(second + 2, last).every(row => row.trim() === ""))
|
|
325
|
+
continue;
|
|
326
|
+
const intro = rows.slice(Math.max(0, first - 14), first);
|
|
327
|
+
if (intro.some(row => /Update available!/.test(row))
|
|
328
|
+
&& intro.some(row => /^\s*Release notes: https:\/\/github\.com\/openai\/codex\/releases\/latest\s*$/.test(row)))
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
65
333
|
function renderMcpServer(name, entry, instanceName) {
|
|
66
334
|
const mcpName = `${name}-${instanceName}`.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
67
335
|
const env = { ...entry.env, AGEND_INSTANCE_NAME: instanceName };
|
|
@@ -117,6 +385,8 @@ export class CodexBackend {
|
|
|
117
385
|
isolatedCodexHome;
|
|
118
386
|
/** Which subscription this instance runs on, or null for the shared login. */
|
|
119
387
|
credentialProfile = null;
|
|
388
|
+
/** Set only after preTrust wrote and read back this instance's private config. */
|
|
389
|
+
authorizedTrust = null;
|
|
120
390
|
constructor(instanceDir) {
|
|
121
391
|
this.instanceDir = instanceDir;
|
|
122
392
|
this.binaryPath = resolveBinary("codex");
|
|
@@ -145,6 +415,29 @@ export class CodexBackend {
|
|
|
145
415
|
getBottomReadyPattern() {
|
|
146
416
|
return /^\s*›\s?/;
|
|
147
417
|
}
|
|
418
|
+
/** Observed on real Codex 0.156.0: the live input row precedes its footer. */
|
|
419
|
+
isDeliveryInputReadyPane(pane) {
|
|
420
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
421
|
+
while (rows.length && !rows[rows.length - 1].trim())
|
|
422
|
+
rows.pop();
|
|
423
|
+
const footer = rows.pop() ?? "";
|
|
424
|
+
// Codex preserves other configured status-line items after the context
|
|
425
|
+
// meter (observed on 0.156.0: "Context 100% left · GPT-6-Astra"). They
|
|
426
|
+
// are footer chrome, not evidence that the input row is unavailable.
|
|
427
|
+
if (!isCodexContextFooter(footer))
|
|
428
|
+
return false;
|
|
429
|
+
// Pasted text may wrap over several continuation rows before the footer.
|
|
430
|
+
// Search only its immediate tail, not a historical transcript prompt.
|
|
431
|
+
for (let i = rows.length - 1; i >= Math.max(0, rows.length - 8); i--) {
|
|
432
|
+
if (/^[>›]\s+\d+\./.test(rows[i]))
|
|
433
|
+
return false;
|
|
434
|
+
if (/^[>›]\s+\S/.test(rows[i]))
|
|
435
|
+
return true;
|
|
436
|
+
if (/^[•■⚠]/.test(rows[i]))
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
148
441
|
/** Live status chrome that must veto the broad prompt/context ready match. */
|
|
149
442
|
getBusyPattern() {
|
|
150
443
|
return /(?:^|\n)•\s+Working\b[^\n]*\besc to interrupt\b/i;
|
|
@@ -178,7 +471,7 @@ export class CodexBackend {
|
|
|
178
471
|
return false;
|
|
179
472
|
let footer = -1;
|
|
180
473
|
for (let i = prompt + 1; i < Math.min(rows.length, prompt + 7); i++) {
|
|
181
|
-
if (
|
|
474
|
+
if (isCodexContextFooter(rows[i])) {
|
|
182
475
|
footer = i;
|
|
183
476
|
break;
|
|
184
477
|
}
|
|
@@ -232,13 +525,17 @@ export class CodexBackend {
|
|
|
232
525
|
// AgEnD instances are unattended processes: an interactive self-update
|
|
233
526
|
// picker blocks delivery and must never be enabled by a copied global or
|
|
234
527
|
// managed config layer. Keep this last so the CLI override is authoritative.
|
|
235
|
-
|
|
528
|
+
// Both observed Codex 0.155.0 and 0.156.1 support this launch flag. Pin
|
|
529
|
+
// the initial layout so a user/global fullscreen preference cannot make a
|
|
530
|
+
// header-only pane look ready. A later unknown layout still fails closed.
|
|
531
|
+
cmd += " -c check_for_update_on_startup=false --no-alt-screen";
|
|
236
532
|
// CODEX_HOME is the only Codex-supported way to isolate the complete base
|
|
237
533
|
// config. A profile only layers over the shared config and would therefore
|
|
238
534
|
// still load every globally registered AgEnD MCP server.
|
|
239
535
|
return `CODEX_HOME=${shellQuote(this.isolatedCodexHome)} ${cmd}`;
|
|
240
536
|
}
|
|
241
537
|
writeConfig(config) {
|
|
538
|
+
this.authorizedTrust = null;
|
|
242
539
|
// Set before the home is prepared: which login this instance gets is a
|
|
243
540
|
// property of the home, and the home is built here.
|
|
244
541
|
this.credentialProfile = this.readProfile(config);
|
|
@@ -401,16 +698,23 @@ export class CodexBackend {
|
|
|
401
698
|
return home;
|
|
402
699
|
}
|
|
403
700
|
preTrust(workDir) {
|
|
701
|
+
this.authorizedTrust = null;
|
|
702
|
+
const paths = codexTrustPaths(workDir);
|
|
404
703
|
const configPath = join(this.isolatedCodexHome, "config.toml");
|
|
405
704
|
let content = "";
|
|
406
705
|
try {
|
|
407
706
|
content = readFileSync(configPath, "utf-8");
|
|
408
707
|
}
|
|
409
708
|
catch { }
|
|
410
|
-
const
|
|
411
|
-
if (content
|
|
412
|
-
|
|
413
|
-
|
|
709
|
+
const updated = setProjectTrusted(content, paths.root);
|
|
710
|
+
if (updated !== content)
|
|
711
|
+
atomicWritePrivate(configPath, updated);
|
|
712
|
+
// Do not authorize an automatic Enter merely because the write returned:
|
|
713
|
+
// a stale/untrusted section in the effective isolated config must fail shut.
|
|
714
|
+
const onDisk = readFileSync(configPath, "utf-8");
|
|
715
|
+
if (effectiveProjectTrust(onDisk, paths.root) !== "trusted")
|
|
716
|
+
throw new Error("Codex project trust did not persist");
|
|
717
|
+
this.authorizedTrust = paths;
|
|
414
718
|
}
|
|
415
719
|
/**
|
|
416
720
|
* Preserve Codex login/session/cache behavior while isolating config.toml.
|
|
@@ -654,11 +958,20 @@ export class CodexBackend {
|
|
|
654
958
|
}
|
|
655
959
|
}
|
|
656
960
|
getReadyPattern() {
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
|
|
961
|
+
// Header and context text persist in inline scrollback and even while the
|
|
962
|
+
// CLI is loading or a modal owns stdin. Require the live prompt followed
|
|
963
|
+
// by the Context footer at the *end* of the capture. getBusyPattern still
|
|
964
|
+
// vetoes a working turn whose empty composer remains visible. Unknown TUI
|
|
965
|
+
// layouts cannot claim readiness by merely rendering the old header.
|
|
966
|
+
// U+22C6 is Codex's observed cosmetic starfield; it can be drawn in the
|
|
967
|
+
// prompt, between prompt/footer, and below the footer. A drafted composer
|
|
968
|
+
// is also idle once this same bottom footer proves it owns the screen.
|
|
969
|
+
return /(?:^|\n)[>›][ \t⋆]+(?!\d+\.)\S[^\r\n]*\r?\n(?:[ \t⋆]*\r?\n){0,3}[ \t⋆]+(?:[0-9a-f-]{36}[ \t]+·[ \t]+)?Context[ \t]+(?:\d+%[ \t]+(?:left|used)|\d+…|…)[^\r\n]*(?:\r?\n[ \t⋆]*)*$/i;
|
|
970
|
+
}
|
|
971
|
+
/** A proxy reply filters chrome per line; whole-pane readiness is separate. */
|
|
972
|
+
isProxyReplyChromeLine(line) {
|
|
973
|
+
return /^\s*[›>]\s+Ask Codex to do anything\s*$/.test(line)
|
|
974
|
+
|| isCodexContextFooter(line);
|
|
662
975
|
}
|
|
663
976
|
getErrorPatterns() {
|
|
664
977
|
return [
|
|
@@ -756,12 +1069,40 @@ export class CodexBackend {
|
|
|
756
1069
|
];
|
|
757
1070
|
}
|
|
758
1071
|
getStartupDialogs() {
|
|
1072
|
+
const trustHold = this.trustHoldDialog();
|
|
759
1073
|
return [
|
|
760
|
-
{
|
|
761
|
-
|
|
1074
|
+
{
|
|
1075
|
+
pattern: /^\s*Trust this folder\?/m,
|
|
1076
|
+
keys: ["Enter"],
|
|
1077
|
+
description: "Codex authorized folder trust dialog",
|
|
1078
|
+
blocksDelivery: true,
|
|
1079
|
+
inputBlocked: true,
|
|
1080
|
+
autoResolutionKey: "codex-authorized-folder-trust",
|
|
1081
|
+
isActive: pane => {
|
|
1082
|
+
const state = codexTrustPromptState(pane);
|
|
1083
|
+
const authorized = this.authorizedTrust;
|
|
1084
|
+
return state.active && state.safeChoice && authorized !== null
|
|
1085
|
+
&& state.folder === authorized.cwd
|
|
1086
|
+
&& (state.rootNote === "absent" ? authorized.root === authorized.cwd
|
|
1087
|
+
: state.rootNote === "parsed" && state.root === authorized.root);
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
trustHold,
|
|
762
1091
|
this.updatePickerDialog(),
|
|
1092
|
+
this.unknownSelectionHoldDialog(),
|
|
763
1093
|
];
|
|
764
1094
|
}
|
|
1095
|
+
trustHoldDialog() {
|
|
1096
|
+
return {
|
|
1097
|
+
pattern: /^\s*(?:Folder access|Trust this folder\?|Do you trust the files in this folder\?)/im,
|
|
1098
|
+
keys: [],
|
|
1099
|
+
description: "Codex folder trust needs human confirmation",
|
|
1100
|
+
holdOnly: true,
|
|
1101
|
+
blocksDelivery: true,
|
|
1102
|
+
inputBlocked: true,
|
|
1103
|
+
isActive: codexTrustVariantActive,
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
765
1106
|
updatePickerDialog() {
|
|
766
1107
|
return {
|
|
767
1108
|
// Defense in depth for config written by older AgEnD versions or a Codex
|
|
@@ -777,26 +1118,41 @@ export class CodexBackend {
|
|
|
777
1118
|
// seconds and a delivery can arrive first (hit live on codex-cli 0.153.4,
|
|
778
1119
|
// which parks on this picker for as long as nobody answers it).
|
|
779
1120
|
blocksDelivery: true,
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1121
|
+
// Daemon.dialogMatches uses isActive INSTEAD OF pattern when present.
|
|
1122
|
+
// Check the complete current picker here; a different Enter-only menu
|
|
1123
|
+
// must never receive this automatic Escape key.
|
|
1124
|
+
isActive: codexUpdatePickerVisible,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
unknownSelectionHoldDialog() {
|
|
1128
|
+
return {
|
|
1129
|
+
pattern: /^\s*[›❯>]\s+\S/m,
|
|
1130
|
+
keys: [],
|
|
1131
|
+
description: "Codex interactive selection needs human input",
|
|
1132
|
+
holdOnly: true,
|
|
1133
|
+
blocksDelivery: true,
|
|
1134
|
+
inputBlocked: true,
|
|
1135
|
+
isActive: codexUnknownSelectionVisible,
|
|
788
1136
|
};
|
|
789
1137
|
}
|
|
790
1138
|
getRuntimeDialogs() {
|
|
791
1139
|
return [
|
|
1140
|
+
this.trustHoldDialog(),
|
|
792
1141
|
{
|
|
793
|
-
// Codex
|
|
794
|
-
//
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1142
|
+
// Codex 0.156 may change the wording/order of this credit-cost choice.
|
|
1143
|
+
// Never navigate it by position: a moved option could switch to a
|
|
1144
|
+
// more expensive model. A live picker holds delivery and notifies a
|
|
1145
|
+
// human; old transcript mentions are excluded by the tail matcher.
|
|
1146
|
+
pattern: /(?:Approaching rate limits|Switch to [^\r\n]{1,120} for lower credit usage\?)/i,
|
|
1147
|
+
keys: [],
|
|
1148
|
+
description: "Codex rate limit model switch dialog needs human choice",
|
|
1149
|
+
holdOnly: true,
|
|
1150
|
+
blocksDelivery: true,
|
|
1151
|
+
inputBlocked: true,
|
|
1152
|
+
isActive: codexRateSwitchVisible,
|
|
798
1153
|
},
|
|
799
1154
|
this.updatePickerDialog(),
|
|
1155
|
+
this.unknownSelectionHoldDialog(),
|
|
800
1156
|
];
|
|
801
1157
|
}
|
|
802
1158
|
getInputUnavailableTransients() {
|