codex-monitor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/bin/token-monitor-codex-qr.js +182 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# codex-monitor
|
|
2
|
+
|
|
3
|
+
Generate a local QR code that imports your Codex login state into Token Monitor on iOS.
|
|
4
|
+
|
|
5
|
+
The command reads `~/.codex/auth.json` on your Mac, builds a `tokenmonitor://import-codex?...` QR payload, and writes a PNG locally. It does not start a server and does not upload tokens anywhere.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
From this package directory:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
After publishing:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g codex-monitor
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
codex-monitor --open
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then open Token Monitor on iPhone:
|
|
28
|
+
|
|
29
|
+
1. Go to `Codex dynamic import`.
|
|
30
|
+
2. Tap `Scan Codex credential QR`.
|
|
31
|
+
3. Scan the generated QR code.
|
|
32
|
+
|
|
33
|
+
The iOS app stores the imported credential in the device Keychain and refreshes Codex quota directly from the device.
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
codex-monitor --auth-file ~/.codex/auth.json --output codex-import-qr.png
|
|
39
|
+
codex-monitor --no-terminal --open
|
|
40
|
+
codex-monitor --json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The QR code contains a refresh token and should be treated as a secret. Do not share it.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, chmod, mkdir } from "node:fs/promises";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import QRCode from "qrcode";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_AUTH_PATH = "~/.codex/auth.json";
|
|
10
|
+
const DEFAULT_OUTPUT = "codex-import-qr.png";
|
|
11
|
+
|
|
12
|
+
main().catch((error) => {
|
|
13
|
+
console.error(`codex-monitor: ${error.message}`);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
const options = parseArgs(process.argv.slice(2));
|
|
19
|
+
if (options.help) {
|
|
20
|
+
printHelp();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const authPath = expandHome(options.authFile ?? DEFAULT_AUTH_PATH);
|
|
25
|
+
const outputPath = resolve(process.cwd(), options.output ?? DEFAULT_OUTPUT);
|
|
26
|
+
const auth = await readCodexAuth(authPath);
|
|
27
|
+
const qrPayload = buildImportURL(auth);
|
|
28
|
+
|
|
29
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
30
|
+
await QRCode.toFile(outputPath, qrPayload, {
|
|
31
|
+
errorCorrectionLevel: "M",
|
|
32
|
+
margin: 4,
|
|
33
|
+
scale: 10,
|
|
34
|
+
type: "png"
|
|
35
|
+
});
|
|
36
|
+
await chmod(outputPath, 0o600);
|
|
37
|
+
|
|
38
|
+
if (options.terminal !== false) {
|
|
39
|
+
const terminalQR = await QRCode.toString(qrPayload, { type: "terminal" });
|
|
40
|
+
console.log(terminalQR);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (options.json) {
|
|
44
|
+
console.log(JSON.stringify({
|
|
45
|
+
output: outputPath,
|
|
46
|
+
payloadChars: qrPayload.length
|
|
47
|
+
}, null, 2));
|
|
48
|
+
} else {
|
|
49
|
+
console.log(`QR code written to ${outputPath}`);
|
|
50
|
+
console.log("Scan it in Token Monitor: Codex dynamic import -> Scan Codex credential QR.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (options.open) {
|
|
54
|
+
openFile(outputPath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArgs(args) {
|
|
59
|
+
const options = {
|
|
60
|
+
terminal: true,
|
|
61
|
+
help: false,
|
|
62
|
+
json: false,
|
|
63
|
+
open: false
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
67
|
+
const arg = args[index];
|
|
68
|
+
switch (arg) {
|
|
69
|
+
case "--help":
|
|
70
|
+
case "-h":
|
|
71
|
+
options.help = true;
|
|
72
|
+
break;
|
|
73
|
+
case "--auth-file":
|
|
74
|
+
options.authFile = requireValue(args, index, arg);
|
|
75
|
+
index += 1;
|
|
76
|
+
break;
|
|
77
|
+
case "--output":
|
|
78
|
+
case "-o":
|
|
79
|
+
options.output = requireValue(args, index, arg);
|
|
80
|
+
index += 1;
|
|
81
|
+
break;
|
|
82
|
+
case "--no-terminal":
|
|
83
|
+
options.terminal = false;
|
|
84
|
+
break;
|
|
85
|
+
case "--json":
|
|
86
|
+
options.json = true;
|
|
87
|
+
options.terminal = false;
|
|
88
|
+
break;
|
|
89
|
+
case "--open":
|
|
90
|
+
options.open = true;
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
throw new Error(`unknown option: ${arg}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return options;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function requireValue(args, index, optionName) {
|
|
101
|
+
const value = args[index + 1];
|
|
102
|
+
if (!value || value.startsWith("-")) {
|
|
103
|
+
throw new Error(`${optionName} requires a value`);
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readCodexAuth(authPath) {
|
|
109
|
+
let raw;
|
|
110
|
+
try {
|
|
111
|
+
raw = await readFile(authPath, "utf8");
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`cannot read Codex auth file at ${authPath}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let auth;
|
|
117
|
+
try {
|
|
118
|
+
auth = JSON.parse(raw);
|
|
119
|
+
} catch {
|
|
120
|
+
throw new Error(`Codex auth file is not valid JSON: ${authPath}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const tokens = auth.tokens ?? {};
|
|
124
|
+
if (!tokens.refresh_token || !tokens.account_id) {
|
|
125
|
+
throw new Error("Codex auth file must contain tokens.refresh_token and tokens.account_id");
|
|
126
|
+
}
|
|
127
|
+
return auth;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildImportURL(auth) {
|
|
131
|
+
const tokens = auth.tokens ?? {};
|
|
132
|
+
const loginState = {
|
|
133
|
+
auth_mode: auth.auth_mode,
|
|
134
|
+
tokens: {
|
|
135
|
+
refresh_token: tokens.refresh_token,
|
|
136
|
+
account_id: tokens.account_id
|
|
137
|
+
},
|
|
138
|
+
last_refresh: auth.last_refresh
|
|
139
|
+
};
|
|
140
|
+
const envelope = {
|
|
141
|
+
kind: "token-monitor.codex-credential",
|
|
142
|
+
version: 1,
|
|
143
|
+
credentialType: "codex-login-state",
|
|
144
|
+
displayName: "Codex Desktop refresh token",
|
|
145
|
+
payload: JSON.stringify(loginState),
|
|
146
|
+
createdAt: new Date().toISOString()
|
|
147
|
+
};
|
|
148
|
+
const encoded = Buffer.from(JSON.stringify(envelope), "utf8")
|
|
149
|
+
.toString("base64url");
|
|
150
|
+
return `tokenmonitor://import-codex?payload=${encoded}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function expandHome(path) {
|
|
154
|
+
if (path === "~") {
|
|
155
|
+
return homedir();
|
|
156
|
+
}
|
|
157
|
+
if (path.startsWith("~/")) {
|
|
158
|
+
return resolve(homedir(), path.slice(2));
|
|
159
|
+
}
|
|
160
|
+
return resolve(process.cwd(), path);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function openFile(path) {
|
|
164
|
+
if (process.platform !== "darwin") {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
spawnSync("open", [path], { stdio: "ignore" });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function printHelp() {
|
|
171
|
+
console.log(`
|
|
172
|
+
Usage: codex-monitor [options]
|
|
173
|
+
|
|
174
|
+
Options:
|
|
175
|
+
--auth-file <path> Codex auth file. Default: ${DEFAULT_AUTH_PATH}
|
|
176
|
+
-o, --output <png> PNG output path. Default: ${DEFAULT_OUTPUT}
|
|
177
|
+
--no-terminal Do not render a QR code in the terminal
|
|
178
|
+
--open Open the generated PNG on macOS
|
|
179
|
+
--json Print metadata as JSON
|
|
180
|
+
-h, --help Show help
|
|
181
|
+
`);
|
|
182
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-monitor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate a local QR code for importing Codex login state into Token Monitor.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"codex-monitor": "bin/token-monitor-codex-qr.js",
|
|
8
|
+
"token-monitor-codex-qr": "bin/token-monitor-codex-qr.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --check bin/token-monitor-codex-qr.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"qrcode": "^1.5.4"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org/"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT"
|
|
28
|
+
}
|