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 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
+ }