codex-multi-auth 2.3.0 → 2.3.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/.codex-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/account-manager-commands.js +1 -0
- package/dist/lib/codex-manager/commands/history.d.ts +23 -0
- package/dist/lib/codex-manager/commands/history.d.ts.map +1 -0
- package/dist/lib/codex-manager/commands/history.js +312 -0
- package/dist/lib/codex-manager/help.d.ts.map +1 -1
- package/dist/lib/codex-manager/help.js +1 -0
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +2 -0
- package/dist/lib/runtime/app-bind.d.ts +7 -0
- package/dist/lib/runtime/app-bind.d.ts.map +1 -1
- package/dist/lib/runtime/app-bind.js +46 -5
- package/dist/lib/runtime/config-toml.d.ts +18 -0
- package/dist/lib/runtime/config-toml.d.ts.map +1 -1
- package/dist/lib/runtime/config-toml.js +79 -8
- package/package.json +1 -1
- package/scripts/codex-routing.js +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.",
|
|
5
5
|
"interface": {
|
|
6
6
|
"composerIcon": "./assets/codex-multi-auth-icon.svg"
|
package/README.md
CHANGED
|
@@ -383,7 +383,9 @@ codex-multi-auth doctor --json
|
|
|
383
383
|
|
|
384
384
|
## Release Notes
|
|
385
385
|
|
|
386
|
-
- Current stable: [docs/releases/v2.3.
|
|
386
|
+
- Current stable: [docs/releases/v2.3.2.md](docs/releases/v2.3.2.md) — install via `npm i -g codex-multi-auth`
|
|
387
|
+
- Previous stable: [docs/releases/v2.3.1.md](docs/releases/v2.3.1.md)
|
|
388
|
+
- Previous stable: [docs/releases/v2.3.0.md](docs/releases/v2.3.0.md)
|
|
387
389
|
- Previous stable: [docs/releases/v2.2.2.md](docs/releases/v2.2.2.md)
|
|
388
390
|
- Previous stable: [docs/releases/v2.2.1.md](docs/releases/v2.2.1.md)
|
|
389
391
|
- Previous stable: [docs/releases/v2.2.0.md](docs/releases/v2.2.0.md)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-manager-commands.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/account-manager-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,
|
|
1
|
+
{"version":3,"file":"account-manager-commands.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/account-manager-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,aA8BnC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface HistorySessionSummary {
|
|
2
|
+
id: string;
|
|
3
|
+
threadName: string;
|
|
4
|
+
updatedAt: string;
|
|
5
|
+
provider: string | null;
|
|
6
|
+
originator: string | null;
|
|
7
|
+
cwd: string | null;
|
|
8
|
+
path: string;
|
|
9
|
+
}
|
|
10
|
+
export interface HistorySessionDetail extends HistorySessionSummary {
|
|
11
|
+
cliVersion: string | null;
|
|
12
|
+
messages: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface HistoryCommandDeps {
|
|
15
|
+
getCodexHome?: () => string;
|
|
16
|
+
readDirRecursive?: (dir: string) => string[];
|
|
17
|
+
readFile?: (path: string) => string;
|
|
18
|
+
statMtime?: (path: string) => Date;
|
|
19
|
+
logInfo?: (message: string) => void;
|
|
20
|
+
logError?: (message: string) => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function runHistoryCommand(args: string[], deps?: HistoryCommandDeps): number;
|
|
23
|
+
//# sourceMappingURL=history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../../../lib/codex-manager/commands/history.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,oBAAqB,SAAQ,qBAAqB;IAClE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IAClC,YAAY,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAiVD,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,kBAAuB,GAC3B,MAAM,CA2BR"}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getCodexHomeDir } from "../../runtime-paths.js";
|
|
4
|
+
/**
|
|
5
|
+
* `codex-multi-auth history` — provider-agnostic local session browser.
|
|
6
|
+
*
|
|
7
|
+
* Codex CLI's `/resume` view filters local rollout threads by the model
|
|
8
|
+
* provider that is currently active in `config.toml`. When runtime rotation /
|
|
9
|
+
* app bind is enabled the active provider becomes
|
|
10
|
+
* `codex-multi-auth-runtime-proxy`, so sessions recorded under the native
|
|
11
|
+
* `openai` provider (or vice versa) become invisible in `/resume` even though
|
|
12
|
+
* the rollout files still live side-by-side under `~/.codex/sessions/`. Users
|
|
13
|
+
* perceive this as "history is not shared across accounts" (issue #612), but
|
|
14
|
+
* the split is by provider name, not by account.
|
|
15
|
+
*
|
|
16
|
+
* This command reads the rollout files directly and lists every local session
|
|
17
|
+
* regardless of the provider it was created under, giving a complete view that
|
|
18
|
+
* `/resume` cannot. It performs no network calls and never mutates state.
|
|
19
|
+
*/
|
|
20
|
+
const ROLLOUT_FILENAME_PATTERN = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
21
|
+
const DEFAULT_PREVIEW_MESSAGE_COUNT = 3;
|
|
22
|
+
const MAX_THREAD_NAME_LENGTH = 80;
|
|
23
|
+
function readStringField(record, key) {
|
|
24
|
+
const value = record?.[key];
|
|
25
|
+
return typeof value === "string" && value.trim().length > 0
|
|
26
|
+
? value.trim()
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
function defaultReadDirRecursive(dir) {
|
|
30
|
+
const results = [];
|
|
31
|
+
let entries = [];
|
|
32
|
+
try {
|
|
33
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const entryPath = join(dir, entry.name);
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
results.push(...defaultReadDirRecursive(entryPath));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (entry.isFile() && ROLLOUT_FILENAME_PATTERN.test(entry.name)) {
|
|
45
|
+
results.push(entryPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
function extractIdFromFilename(fileName) {
|
|
51
|
+
const match = fileName.match(ROLLOUT_FILENAME_PATTERN);
|
|
52
|
+
return match?.[1] ?? null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Separator-agnostic basename. Rollout paths may carry Windows separators even
|
|
56
|
+
* when this runs on POSIX (e.g. injected paths in tests, or a CODEX_HOME on a
|
|
57
|
+
* mounted Windows volume), so split on both `/` and `\` rather than relying on
|
|
58
|
+
* the platform-specific node:path basename.
|
|
59
|
+
*/
|
|
60
|
+
function baseNameOf(filePath) {
|
|
61
|
+
const segments = filePath.split(/[\\/]/);
|
|
62
|
+
return segments[segments.length - 1] ?? filePath;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse a single rollout file into a session summary.
|
|
66
|
+
*
|
|
67
|
+
* Mirrors the index-building logic the wrapper uses (scripts/codex.js
|
|
68
|
+
* parseRolloutIndexEntry) but additionally surfaces the `model_provider` and
|
|
69
|
+
* `originator` from `session_meta`, which is the field that actually drives the
|
|
70
|
+
* `/resume` visibility split. Returns null for files without a `session_meta`
|
|
71
|
+
* record or that cannot be read, so partially-written rollouts are skipped
|
|
72
|
+
* rather than crashing the listing.
|
|
73
|
+
*/
|
|
74
|
+
function parseRollout(rolloutPath, deps, previewLimit) {
|
|
75
|
+
const idFromName = extractIdFromFilename(baseNameOf(rolloutPath));
|
|
76
|
+
if (!idFromName)
|
|
77
|
+
return null;
|
|
78
|
+
let content;
|
|
79
|
+
try {
|
|
80
|
+
content = deps.readFile(rolloutPath);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const lines = content.split(/\r?\n/);
|
|
86
|
+
let id = idFromName;
|
|
87
|
+
let threadName = "";
|
|
88
|
+
let updatedAt = null;
|
|
89
|
+
let provider = null;
|
|
90
|
+
let originator = null;
|
|
91
|
+
let cwd = null;
|
|
92
|
+
let cliVersion = null;
|
|
93
|
+
let hasSessionMeta = false;
|
|
94
|
+
const messages = [];
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
if (line.trim().length === 0)
|
|
97
|
+
continue;
|
|
98
|
+
let record;
|
|
99
|
+
try {
|
|
100
|
+
record = JSON.parse(line);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Tolerate malformed/partial lines; a single bad line must not drop
|
|
104
|
+
// the whole session from the listing.
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (typeof record.timestamp === "string") {
|
|
108
|
+
updatedAt = record.timestamp;
|
|
109
|
+
}
|
|
110
|
+
if (record.type === "session_meta") {
|
|
111
|
+
const payload = record.payload;
|
|
112
|
+
const metaId = readStringField(payload, "id");
|
|
113
|
+
if (metaId)
|
|
114
|
+
id = metaId;
|
|
115
|
+
provider = readStringField(payload, "model_provider") ?? provider;
|
|
116
|
+
originator = readStringField(payload, "originator") ?? originator;
|
|
117
|
+
cwd = readStringField(payload, "cwd") ?? cwd;
|
|
118
|
+
cliVersion = readStringField(payload, "cli_version") ?? cliVersion;
|
|
119
|
+
hasSessionMeta = true;
|
|
120
|
+
}
|
|
121
|
+
if (record.type === "event_msg") {
|
|
122
|
+
const payload = record.payload;
|
|
123
|
+
const payloadType = readStringField(payload, "type");
|
|
124
|
+
if (!threadName && payloadType) {
|
|
125
|
+
const message = readStringField(payload, "message");
|
|
126
|
+
if (message) {
|
|
127
|
+
threadName = message;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (payloadType === "user_message" &&
|
|
131
|
+
messages.length < previewLimit) {
|
|
132
|
+
const message = readStringField(payload, "message");
|
|
133
|
+
if (message) {
|
|
134
|
+
messages.push(message);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!hasSessionMeta) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (!updatedAt) {
|
|
143
|
+
try {
|
|
144
|
+
updatedAt = deps.statMtime(rolloutPath).toISOString();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
updatedAt = new Date().toISOString();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!threadName) {
|
|
151
|
+
threadName = "Codex session";
|
|
152
|
+
}
|
|
153
|
+
if (threadName.length > MAX_THREAD_NAME_LENGTH) {
|
|
154
|
+
threadName = `${threadName.slice(0, MAX_THREAD_NAME_LENGTH - 3)}...`;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
id,
|
|
158
|
+
threadName,
|
|
159
|
+
updatedAt,
|
|
160
|
+
provider,
|
|
161
|
+
originator,
|
|
162
|
+
cwd,
|
|
163
|
+
cliVersion,
|
|
164
|
+
messages,
|
|
165
|
+
path: rolloutPath,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function resolveDeps(deps) {
|
|
169
|
+
return {
|
|
170
|
+
getCodexHome: deps.getCodexHome ?? getCodexHomeDir,
|
|
171
|
+
readDirRecursive: deps.readDirRecursive ?? defaultReadDirRecursive,
|
|
172
|
+
readFile: deps.readFile ?? ((path) => readFileSync(path, "utf8")),
|
|
173
|
+
statMtime: deps.statMtime ?? ((path) => statSync(path).mtime),
|
|
174
|
+
logInfo: deps.logInfo ?? ((message) => console.log(message)),
|
|
175
|
+
logError: deps.logError ?? ((message) => console.error(message)),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function collectSessions(resolved, previewLimit) {
|
|
179
|
+
const sessionsDir = join(resolved.getCodexHome(), "sessions");
|
|
180
|
+
let files;
|
|
181
|
+
try {
|
|
182
|
+
files = resolved.readDirRecursive(sessionsDir);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// A missing or unreadable sessions directory is a normal "no history yet"
|
|
186
|
+
// state, not an error — return an empty listing rather than crashing.
|
|
187
|
+
files = [];
|
|
188
|
+
}
|
|
189
|
+
const sessions = [];
|
|
190
|
+
for (const file of files) {
|
|
191
|
+
const parsed = parseRollout(file, { readFile: resolved.readFile, statMtime: resolved.statMtime }, previewLimit);
|
|
192
|
+
if (parsed)
|
|
193
|
+
sessions.push(parsed);
|
|
194
|
+
}
|
|
195
|
+
// Most-recent first, matching how /resume presents threads.
|
|
196
|
+
sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
197
|
+
return sessions;
|
|
198
|
+
}
|
|
199
|
+
function toSummary(detail) {
|
|
200
|
+
const { cliVersion: _cliVersion, messages: _messages, ...summary } = detail;
|
|
201
|
+
void _cliVersion;
|
|
202
|
+
void _messages;
|
|
203
|
+
return summary;
|
|
204
|
+
}
|
|
205
|
+
function runHistoryList(args, resolved) {
|
|
206
|
+
const json = args.includes("--json") || args.includes("-j");
|
|
207
|
+
const sessions = collectSessions(resolved, DEFAULT_PREVIEW_MESSAGE_COUNT);
|
|
208
|
+
if (json) {
|
|
209
|
+
resolved.logInfo(JSON.stringify({ count: sessions.length, sessions: sessions.map(toSummary) }, null, 2));
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
if (sessions.length === 0) {
|
|
213
|
+
resolved.logInfo(`No local Codex sessions found under ${join(resolved.getCodexHome(), "sessions")}.`);
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
resolved.logInfo(`Local Codex sessions (${sessions.length}) — all providers, bypassing /resume provider filtering:`);
|
|
217
|
+
resolved.logInfo("");
|
|
218
|
+
for (const session of sessions) {
|
|
219
|
+
const provider = session.provider ?? "unknown-provider";
|
|
220
|
+
resolved.logInfo(` ${session.updatedAt} [${provider}]`);
|
|
221
|
+
resolved.logInfo(` id: ${session.id}`);
|
|
222
|
+
resolved.logInfo(` thread: ${session.threadName}`);
|
|
223
|
+
if (session.cwd) {
|
|
224
|
+
resolved.logInfo(` cwd: ${session.cwd}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
resolved.logInfo("");
|
|
228
|
+
resolved.logInfo("Resume any session with: codex resume <id> (or `codex resume` for the picker).");
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
function runHistoryShow(args, resolved) {
|
|
232
|
+
const json = args.includes("--json") || args.includes("-j");
|
|
233
|
+
const sessionId = args.find((arg) => !arg.startsWith("-"));
|
|
234
|
+
if (!sessionId) {
|
|
235
|
+
resolved.logError("Missing session id. Usage: codex-multi-auth history show <session-id> [--json]");
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
const sessions = collectSessions(resolved, DEFAULT_PREVIEW_MESSAGE_COUNT);
|
|
239
|
+
const match = sessions.find((session) => session.id === sessionId);
|
|
240
|
+
if (!match) {
|
|
241
|
+
resolved.logError(`Session not found: ${sessionId}`);
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
if (json) {
|
|
245
|
+
resolved.logInfo(JSON.stringify(match, null, 2));
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
resolved.logInfo(`Session ${match.id}`);
|
|
249
|
+
resolved.logInfo(` provider: ${match.provider ?? "unknown"}`);
|
|
250
|
+
resolved.logInfo(` originator: ${match.originator ?? "unknown"}`);
|
|
251
|
+
resolved.logInfo(` updated: ${match.updatedAt}`);
|
|
252
|
+
if (match.cliVersion) {
|
|
253
|
+
resolved.logInfo(` cli: ${match.cliVersion}`);
|
|
254
|
+
}
|
|
255
|
+
if (match.cwd) {
|
|
256
|
+
resolved.logInfo(` cwd: ${match.cwd}`);
|
|
257
|
+
}
|
|
258
|
+
resolved.logInfo(` file: ${match.path}`);
|
|
259
|
+
resolved.logInfo("");
|
|
260
|
+
if (match.messages.length === 0) {
|
|
261
|
+
resolved.logInfo(" (no user messages recorded)");
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
resolved.logInfo(" First messages:");
|
|
265
|
+
for (const message of match.messages) {
|
|
266
|
+
const firstLine = message.split(/\r?\n/, 1)[0] ?? "";
|
|
267
|
+
const preview = firstLine.length > 120 ? `${firstLine.slice(0, 117)}...` : firstLine;
|
|
268
|
+
resolved.logInfo(` - ${preview}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
resolved.logInfo("");
|
|
272
|
+
resolved.logInfo(`Resume with: codex resume ${match.id}`);
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
function printHistoryUsage(resolved) {
|
|
276
|
+
resolved.logInfo([
|
|
277
|
+
"Usage: codex-multi-auth history <list|show> [options]",
|
|
278
|
+
"",
|
|
279
|
+
" list [--json] List every local session across all providers",
|
|
280
|
+
" show <id> [--json] Show provider metadata and first messages for a session",
|
|
281
|
+
"",
|
|
282
|
+
"Lists rollout files under <codex-home>/sessions (default",
|
|
283
|
+
"~/.codex/sessions, honoring CODEX_HOME) directly, so sessions",
|
|
284
|
+
"created under a different model provider (e.g. while runtime rotation",
|
|
285
|
+
"or app bind is active) remain visible even when `codex resume` hides",
|
|
286
|
+
"them. See docs/troubleshooting.md for background.",
|
|
287
|
+
].join("\n"));
|
|
288
|
+
}
|
|
289
|
+
export function runHistoryCommand(args, deps = {}) {
|
|
290
|
+
const resolved = resolveDeps(deps);
|
|
291
|
+
const [subcommand, ...rest] = args;
|
|
292
|
+
if (subcommand === "--help" || subcommand === "-h") {
|
|
293
|
+
printHistoryUsage(resolved);
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
// Default to `list` when no subcommand is given, or when the first arg is a
|
|
297
|
+
// flag (e.g. `history --json`), so the documented `[list] [--json]` form
|
|
298
|
+
// works without an explicit `list`. A leading flag is forwarded as a list
|
|
299
|
+
// argument rather than mistaken for a subcommand.
|
|
300
|
+
if (!subcommand || subcommand === "list") {
|
|
301
|
+
return runHistoryList(rest, resolved);
|
|
302
|
+
}
|
|
303
|
+
if (subcommand.startsWith("-")) {
|
|
304
|
+
return runHistoryList([subcommand, ...rest], resolved);
|
|
305
|
+
}
|
|
306
|
+
if (subcommand === "show") {
|
|
307
|
+
return runHistoryShow(rest, resolved);
|
|
308
|
+
}
|
|
309
|
+
resolved.logError(`Unknown history command: ${subcommand}`);
|
|
310
|
+
printHistoryUsage(resolved);
|
|
311
|
+
return 1;
|
|
312
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/help.ts"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/help.ts"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,IAAI,IAAI,CAkDjC;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,GACvC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,mBAAmB,CAgEtE;AAED,UAAU,cAAc;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,wBAAgB,cAAc,IAAI,IAAI,CAgBrC;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,cAAc,CAyD5D"}
|
|
@@ -35,6 +35,7 @@ export function printUsage() {
|
|
|
35
35
|
" codex-multi-auth rotation <enable|disable|status|bind-app|unbind-app>",
|
|
36
36
|
" codex-multi-auth rotation reset-rate-limits [--all | --account <idx>] [--dry-run] [--json]",
|
|
37
37
|
" codex-multi-auth why-selected [--now | --last] [--json]",
|
|
38
|
+
" codex-multi-auth history [list|show <id>] [--json] (all local sessions, ignores /resume provider filtering)",
|
|
38
39
|
"",
|
|
39
40
|
"Advanced:",
|
|
40
41
|
" codex-multi-auth report [--live] [--json] [--explain] [--model <model>] [--out <path>]",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex-manager.d.ts","sourceRoot":"","sources":["../../lib/codex-manager.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"codex-manager.d.ts","sourceRoot":"","sources":["../../lib/codex-manager.ts"],"names":[],"mappings":"AAqFA,OAAO,EAEN,mBAAmB,EAMnB,sBAAsB,EAGtB,MAAM,qCAAqC,CAAC;AAiD7C,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,CAAC;AA+HvD,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,OAAO,CAAC,CAuGrE;AAwRD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA4C7E"}
|
|
@@ -31,6 +31,7 @@ import { runRotationCommand } from "./codex-manager/commands/rotation.js";
|
|
|
31
31
|
import { runFeaturesCommand, runStatusCommand, } from "./codex-manager/commands/status.js";
|
|
32
32
|
import { loadPersistedRuntimeObservabilitySnapshot } from "./runtime/runtime-observability.js";
|
|
33
33
|
import { runSwitchCommand } from "./codex-manager/commands/switch.js";
|
|
34
|
+
import { runHistoryCommand } from "./codex-manager/commands/history.js";
|
|
34
35
|
import { runUnpinCommand } from "./codex-manager/commands/unpin.js";
|
|
35
36
|
import { runWorkspaceCommand } from "./codex-manager/commands/workspace.js";
|
|
36
37
|
import { runUsageCommand } from "./codex-manager/commands/usage.js";
|
|
@@ -450,6 +451,7 @@ const CLI_COMMAND_HANDLERS = new Map([
|
|
|
450
451
|
sanitizeEmail,
|
|
451
452
|
}),
|
|
452
453
|
],
|
|
454
|
+
["history", (rest) => runHistoryCommand(rest)],
|
|
453
455
|
[
|
|
454
456
|
"verify",
|
|
455
457
|
(rest) => runVerifyCommand(rest, {
|
|
@@ -44,6 +44,13 @@ export interface AppBindRouterStatus {
|
|
|
44
44
|
export interface AppBindStatus {
|
|
45
45
|
bound: boolean;
|
|
46
46
|
running: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* True when config.toml is bound to the runtime proxy but the app-bind state
|
|
49
|
+
* file is gone (orphaned bind, #614). In this case `bound` is also true and
|
|
50
|
+
* `state` is null — the config needs `unbind-app` to recover even though the
|
|
51
|
+
* normal state-file tracking is missing.
|
|
52
|
+
*/
|
|
53
|
+
unmanagedBind: boolean;
|
|
47
54
|
state: AppBindState | null;
|
|
48
55
|
router: AppBindRouterStatus | null;
|
|
49
56
|
paths: AppBindPaths;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-bind.d.ts","sourceRoot":"","sources":["../../../lib/runtime/app-bind.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-bind.d.ts","sourceRoot":"","sources":["../../../lib/runtime/app-bind.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,7 +8,7 @@ import process from "node:process";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { withFileOperationRetry } from "../fs-retry.js";
|
|
10
10
|
import { getCodexMultiAuthDir } from "../runtime-paths.js";
|
|
11
|
-
import { restoreConfigTomlFromRuntimeRotationProvider, rewriteConfigTomlForRuntimeRotationProvider, } from "./config-toml.js";
|
|
11
|
+
import { configHasRuntimeRotationProvider, restoreConfigTomlFromRuntimeRotationProvider, restoreConfigTomlFromRuntimeRotationProviderWithoutBackup, rewriteConfigTomlForRuntimeRotationProvider, } from "./config-toml.js";
|
|
12
12
|
const APP_BIND_DIR_NAME = "app-bind";
|
|
13
13
|
const APP_BIND_STATE_FILE = "runtime-rotation-app-bind.json";
|
|
14
14
|
const APP_BIND_BACKUP_FILE = "codex-config-backup.json";
|
|
@@ -464,9 +464,19 @@ export async function getAppBindStatus(options = {}) {
|
|
|
464
464
|
const paths = resolveAppBindPaths(options);
|
|
465
465
|
const state = await readAppBindState(paths.statePath);
|
|
466
466
|
const router = await readRouterStatus(paths.statusPath);
|
|
467
|
+
// When no state file is present, the bind may still be live in config.toml
|
|
468
|
+
// (orphaned bind, #614). Detect that from the config directly so status and
|
|
469
|
+
// downstream callers don't report a bound config as "not configured".
|
|
470
|
+
let unmanagedBind = false;
|
|
471
|
+
if (state === null) {
|
|
472
|
+
const current = await readConfigIfExists(paths.configPath);
|
|
473
|
+
unmanagedBind =
|
|
474
|
+
current.existed && configHasRuntimeRotationProvider(current.content);
|
|
475
|
+
}
|
|
467
476
|
return {
|
|
468
|
-
bound: state !== null,
|
|
477
|
+
bound: state !== null || unmanagedBind,
|
|
469
478
|
running: router !== null && router.state === "running" && isProcessAlive(router.pid),
|
|
479
|
+
unmanagedBind,
|
|
470
480
|
state,
|
|
471
481
|
router,
|
|
472
482
|
paths,
|
|
@@ -587,6 +597,7 @@ async function unbindCodexAppRuntimeRotationLocked(options, paths) {
|
|
|
587
597
|
await removeAppBindStartup(state);
|
|
588
598
|
}
|
|
589
599
|
const backup = await readAppBindBackup(paths.backupPath);
|
|
600
|
+
let selfHealed = false;
|
|
590
601
|
if (backup) {
|
|
591
602
|
const current = await readConfigIfExists(backup.configPath);
|
|
592
603
|
if (state && current.existed && sha256(current.content) !== state.boundConfigHash) {
|
|
@@ -606,6 +617,18 @@ async function unbindCodexAppRuntimeRotationLocked(options, paths) {
|
|
|
606
617
|
await atomicWriteFile(state.configPath, restoreConfigTomlFromAppBind(current.content, ""));
|
|
607
618
|
}
|
|
608
619
|
}
|
|
620
|
+
else {
|
|
621
|
+
// Orphaned-bind recovery (#614): no backup and no state file, but the
|
|
622
|
+
// config may still be bound to the runtime proxy (e.g. the state/backup
|
|
623
|
+
// were lost while config.toml stayed rewritten). The state-file checks
|
|
624
|
+
// above can't see this, so consult the config directly and self-heal it
|
|
625
|
+
// back to a working provider when it is bound.
|
|
626
|
+
const current = await readConfigIfExists(paths.configPath);
|
|
627
|
+
if (current.existed && configHasRuntimeRotationProvider(current.content)) {
|
|
628
|
+
await atomicWriteFile(paths.configPath, restoreConfigTomlFromRuntimeRotationProviderWithoutBackup(current.content));
|
|
629
|
+
selfHealed = true;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
609
632
|
for (const candidate of [
|
|
610
633
|
paths.statePath,
|
|
611
634
|
paths.backupPath,
|
|
@@ -619,14 +642,32 @@ async function unbindCodexAppRuntimeRotationLocked(options, paths) {
|
|
|
619
642
|
}
|
|
620
643
|
}
|
|
621
644
|
const status = await getAppBindStatus(options);
|
|
645
|
+
let message;
|
|
646
|
+
if (backup) {
|
|
647
|
+
message = `Unbound Codex app config ${backup.configPath}`;
|
|
648
|
+
}
|
|
649
|
+
else if (selfHealed) {
|
|
650
|
+
message = `Restored Codex app config ${paths.configPath} from an orphaned runtime-proxy bind (no backup was present)`;
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
message = "Codex app bind was not configured";
|
|
654
|
+
}
|
|
622
655
|
return {
|
|
623
656
|
status,
|
|
624
|
-
message
|
|
625
|
-
? `Unbound Codex app config ${backup.configPath}`
|
|
626
|
-
: "Codex app bind was not configured",
|
|
657
|
+
message,
|
|
627
658
|
};
|
|
628
659
|
}
|
|
629
660
|
export function formatAppBindStatus(status) {
|
|
661
|
+
if (status.unmanagedBind && !status.state) {
|
|
662
|
+
return [
|
|
663
|
+
`Codex app bind: bound but unmanaged (config=${status.paths.configPath} points at the runtime proxy, but no app-bind state/backup is present)`,
|
|
664
|
+
[
|
|
665
|
+
"Run `codex-multi-auth rotation unbind-app` to restore the original",
|
|
666
|
+
"Codex provider/config. This recovers the orphaned bind even though no",
|
|
667
|
+
"backup was saved (#614).",
|
|
668
|
+
].join(" "),
|
|
669
|
+
].join("\n");
|
|
670
|
+
}
|
|
630
671
|
if (!status.bound || !status.state)
|
|
631
672
|
return "Codex app bind: not configured";
|
|
632
673
|
const parts = [
|
|
@@ -3,4 +3,22 @@ export declare function restoreTopLevelModelProvider(currentConfig: string, orig
|
|
|
3
3
|
export declare function restoreTopLevelResponseStorage(currentConfig: string, originalConfig: string): string;
|
|
4
4
|
export declare function rewriteConfigTomlForRuntimeRotationProvider(rawConfig: string, baseUrl: string, clientApiKey?: string): string;
|
|
5
5
|
export declare function restoreConfigTomlFromRuntimeRotationProvider(currentConfig: string, originalConfig: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Detects whether a config.toml is currently bound to the runtime rotation
|
|
8
|
+
* proxy — either the top-level `model_provider` points at the proxy id, or the
|
|
9
|
+
* proxy `[model_providers.<id>]` block is present. Used to recover an orphaned
|
|
10
|
+
* bind whose app-bind state/backup files were lost: in that situation the
|
|
11
|
+
* state-file-based status check reports "not configured" even though the config
|
|
12
|
+
* is still bound, so unbind/status must consult the config itself.
|
|
13
|
+
*/
|
|
14
|
+
export declare function configHasRuntimeRotationProvider(rawConfig: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Restores a bound config when no backup of the user's original config exists
|
|
17
|
+
* (the orphaned-bind recovery path). Strips the proxy provider block and any
|
|
18
|
+
* bind-written top-level lines, and — because there is no original
|
|
19
|
+
* `model_provider` line to bring back — falls back to `defaultProvider`
|
|
20
|
+
* (Codex's native `"openai"`) so the config is left on a working provider
|
|
21
|
+
* rather than the dangling proxy id.
|
|
22
|
+
*/
|
|
23
|
+
export declare function restoreConfigTomlFromRuntimeRotationProviderWithoutBackup(currentConfig: string, defaultProvider?: string): string;
|
|
6
24
|
//# sourceMappingURL=config-toml.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-toml.d.ts","sourceRoot":"","sources":["../../../lib/runtime/config-toml.ts"],"names":[],"mappings":"AAEA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAqBvD;AA4FD,wBAAgB,4BAA4B,CAC3C,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACpB,MAAM,
|
|
1
|
+
{"version":3,"file":"config-toml.d.ts","sourceRoot":"","sources":["../../../lib/runtime/config-toml.ts"],"names":[],"mappings":"AAEA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAqBvD;AA4FD,wBAAgB,4BAA4B,CAC3C,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACpB,MAAM,CAiDR;AAED,wBAAgB,8BAA8B,CAC7C,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACpB,MAAM,CAkDR;AA2BD,wBAAgB,2CAA2C,CAC1D,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,YAAY,SAAK,GACf,MAAM,CAkBR;AAED,wBAAgB,4CAA4C,CAC3D,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACpB,MAAM,CAYR;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAoB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,yDAAyD,CACxE,aAAa,EAAE,MAAM,EACrB,eAAe,SAAW,GACxB,MAAM,CAkBR"}
|
|
@@ -121,14 +121,32 @@ export function restoreTopLevelModelProvider(currentConfig, originalConfig) {
|
|
|
121
121
|
output.push(line);
|
|
122
122
|
}
|
|
123
123
|
if (!handled && originalLine) {
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
// Only splice the original line back when the current config has no
|
|
125
|
+
// top-level model_provider at all (bind stripped it). If a non-proxy
|
|
126
|
+
// top-level model_provider already exists — e.g. a half-orphaned config
|
|
127
|
+
// where the proxy block is present but the provider line already points
|
|
128
|
+
// elsewhere — inserting another line would create a duplicate top-level
|
|
129
|
+
// key and produce invalid TOML. In that case the existing line is
|
|
130
|
+
// already correct, so leave it untouched.
|
|
131
|
+
const hasTopLevelModelProvider = (() => {
|
|
132
|
+
for (const line of output) {
|
|
133
|
+
if (readTomlTableName(line) !== null)
|
|
134
|
+
return false;
|
|
135
|
+
if (/^\s*model_provider\s*=/.test(line))
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
})();
|
|
140
|
+
if (!hasTopLevelModelProvider) {
|
|
141
|
+
// Splice the restored line into the root table — appending at tail
|
|
142
|
+
// would land it inside whatever section appears last in `output`.
|
|
143
|
+
const firstSectionIdx = output.findIndex((line) => readTomlTableName(line) !== null);
|
|
144
|
+
if (firstSectionIdx === -1) {
|
|
145
|
+
output.push(originalLine);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
output.splice(firstSectionIdx, 0, originalLine);
|
|
149
|
+
}
|
|
132
150
|
}
|
|
133
151
|
}
|
|
134
152
|
return output.join(lineEnding);
|
|
@@ -204,3 +222,56 @@ export function restoreConfigTomlFromRuntimeRotationProvider(currentConfig, orig
|
|
|
204
222
|
const withResponseStorage = restoreTopLevelResponseStorage(withoutProvider, originalConfig);
|
|
205
223
|
return ensureTomlTrailingNewline(restoreTopLevelModelProvider(withResponseStorage, originalConfig).replace(/[\r\n]*$/, ""));
|
|
206
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Detects whether a config.toml is currently bound to the runtime rotation
|
|
227
|
+
* proxy — either the top-level `model_provider` points at the proxy id, or the
|
|
228
|
+
* proxy `[model_providers.<id>]` block is present. Used to recover an orphaned
|
|
229
|
+
* bind whose app-bind state/backup files were lost: in that situation the
|
|
230
|
+
* state-file-based status check reports "not configured" even though the config
|
|
231
|
+
* is still bound, so unbind/status must consult the config itself.
|
|
232
|
+
*/
|
|
233
|
+
export function configHasRuntimeRotationProvider(rawConfig) {
|
|
234
|
+
if (rawConfig.length === 0)
|
|
235
|
+
return false;
|
|
236
|
+
const providerTable = `model_providers.${RUNTIME_ROTATION_PROXY_PROVIDER_ID}`;
|
|
237
|
+
let inTopLevel = true;
|
|
238
|
+
for (const line of rawConfig.split(/\r?\n/)) {
|
|
239
|
+
const tableName = readTomlTableName(line);
|
|
240
|
+
if (tableName !== null) {
|
|
241
|
+
if (tableName === providerTable)
|
|
242
|
+
return true;
|
|
243
|
+
inTopLevel = false;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (inTopLevel &&
|
|
247
|
+
/^\s*model_provider\s*=/.test(line) &&
|
|
248
|
+
line.includes(RUNTIME_ROTATION_PROXY_PROVIDER_ID)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Restores a bound config when no backup of the user's original config exists
|
|
256
|
+
* (the orphaned-bind recovery path). Strips the proxy provider block and any
|
|
257
|
+
* bind-written top-level lines, and — because there is no original
|
|
258
|
+
* `model_provider` line to bring back — falls back to `defaultProvider`
|
|
259
|
+
* (Codex's native `"openai"`) so the config is left on a working provider
|
|
260
|
+
* rather than the dangling proxy id.
|
|
261
|
+
*/
|
|
262
|
+
export function restoreConfigTomlFromRuntimeRotationProviderWithoutBackup(currentConfig, defaultProvider = "openai") {
|
|
263
|
+
const lineEnding = currentConfig.includes("\r\n") ? "\r\n" : "\n";
|
|
264
|
+
// Synthesize a minimal "original" config carrying only the default
|
|
265
|
+
// top-level model_provider, so the shared restore path rewrites the proxy
|
|
266
|
+
// line back to a usable provider instead of leaving it dangling.
|
|
267
|
+
const syntheticOriginal = `model_provider = ${tomlStringLiteral(defaultProvider)}${lineEnding}`;
|
|
268
|
+
const restored = restoreConfigTomlFromRuntimeRotationProvider(currentConfig, syntheticOriginal);
|
|
269
|
+
// Normalize line endings to match the input config. The shared restore path
|
|
270
|
+
// derives its EOL from intermediate state, which can collapse to "\n" when
|
|
271
|
+
// the bound config was almost entirely proxy content; pin it back to the
|
|
272
|
+
// original style so a CRLF (Windows-authored) config stays CRLF.
|
|
273
|
+
if (lineEnding === "\r\n") {
|
|
274
|
+
return restored.replace(/\r?\n/g, "\r\n");
|
|
275
|
+
}
|
|
276
|
+
return restored.replace(/\r\n/g, "\n");
|
|
277
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|