easy-codex 0.2.1 → 0.2.3

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/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # 更新记录
2
+
3
+ ## 0.2.3 — 2026-10-19
4
+
5
+ ### 修复
6
+
7
+ - update先在线刷新latest并比较版本,按明确版本号安装,防止旧缓存降级;相同或更高的本地版本不安装,检查失败明确报错。
8
+ - 保留上一版新增的update、show自动重开和绿色时段延长至18:00。
9
+
10
+
11
+ ## 0.2.2 — 2026-10-19
12
+
13
+ ### 新增
14
+
15
+ - 新增 `easy-codex update`,复用npm升级当前安装目录中的正式包。
16
+ - `show` 自动退出自身旧浮窗并打开新版,保留现有设置;无法退出旧实例时明确失败,避免重复窗口。
17
+
18
+ ### 调整
19
+
20
+ - 两个工具统一所选城市的时间颜色:00:00–09:00红、09:00–18:00绿、18:00–24:00橙。
21
+ - 将绿色结束/橙色开始从15:00调整至18:00;午夜与09:00切换点不变,仍自动处理夏令时。
22
+ - 保留升级前的时区、城市、位置和显示选择,额度圆环颜色规则保持原样。
23
+ - README与版本说明随包交付,保留show快捷启动和overlay兼容。
24
+
25
+
26
+ ## 0.2.1 — 2026-10-19
27
+
28
+ ### 新增与调整
29
+
30
+ - 新增 `easy-codex show` 快捷启动,保留 `overlay` 兼容。
31
+ - 菜单增加重置倒计时:周只显示最大单位,满天按天、不足一天按小时、不足一小时按分钟;5小时窗口保留时分组合,无小时则仅分钟。
32
+ - 时间开关改为“显示时间”;城市入口显示“洛杉矶·05:23”,使用所选城市当前时间且点号无空格。
33
+ - 兼容已有设置,升级后保留城市、时区、位置与显示选择。
34
+
35
+ ### 使用示例
36
+
37
+ ```sh
38
+ npm i -g easy-codex@latest
39
+ easy-codex show
40
+ ```
41
+
42
+ 升级前退出旧 EC 浮窗。菜单重置时刻按电脑本地时区;时间浮窗使用所选城市时区。已有时间颜色规则保持:当地00:00–09:00红、09:00–15:00绿、15:00–24:00橙,自动处理夏令时。
43
+
44
+ 尚未发布的 Agent 协作开发入口不属于此版本。
package/README.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  显示当前本机 Codex 登录账号的剩余额度百分比和重置时间。第三方辅助工具,与 OpenAI 无官方隶属关系。
4
4
 
5
+ ## v0.2.3 修复说明
6
+
7
+ - update先在线检查正式版本,再按明确版本号安装,避免npm旧缓存导致降级;已是最新或本地版本更高时不重复安装。
8
+ - 包含上一版update、show自动重开、09:00–18:00绿色时段,以及完整版本说明。
9
+
10
+ ## v0.2.2 新增与调整
11
+
12
+ - **命令更新**:新增 `easy-codex update`,从官方 npm 更新当前全局安装。
13
+ - **自动重开**:`easy-codex show` 会先退出自身旧浮窗,再打开新版;设置保持不变。
14
+
15
+ - 统一时间颜色:所选城市当地时间 **00:00–09:00 红色、09:00–18:00 绿色、18:00–24:00 橙色**,绿色时段延长到18:00。
16
+ - 09:00转绿、18:00转橙、午夜转红,自动适应目标时区夏令时。
17
+ - 保留已有时区、城市、位置和显示选择;额度圆环颜色阈值保持原有规则。
18
+
19
+ 完整版本说明见 [CHANGELOG](CHANGELOG.md)。
20
+
21
+ ## v0.2.1 新增功能
22
+
23
+ - **快捷启动**:新增 `easy-codex show`,原 `overlay` 命令继续可用。
24
+ - **重置倒计时**:周额度显示“5天后”等最大单位;5小时窗口显示“3小时2分后”,不足一小时只显示分钟。同时保留电脑本地时区的重置时刻。
25
+ - **时间菜单优化**:开关改为“显示时间”;城市入口直接显示当前当地时间,如 `洛杉矶·05:23`,点号无空格。
26
+ - **升级兼容**:保留已保存的时区、城市、位置和显示选择。
27
+
28
+ 完整更新记录见 [CHANGELOG](CHANGELOG.md)。
29
+
5
30
  ## 安装与使用
6
31
 
7
32
  安装一次:
@@ -26,6 +51,19 @@ easy-codex show
26
51
 
27
52
  输出每个额度桶的主、次窗口。窗口时长采用服务端数据,重置时间使用本机时区。剩余比例为 `100 - usedPercent`,限制在 0–100%;未返回的字段显示“未知”。额度属于账号,不是当前会话的 Token 数,也不是 API 账单余额。即使有剩余百分比,也不代表其他服务端限制已解除。
28
53
 
54
+ ## 升级与菜单
55
+
56
+ ```sh
57
+ npm i -g easy-codex@latest
58
+ easy-codex show
59
+ ```
60
+
61
+ `show` 从 v0.2.1 正式提供,`overlay` 保留兼容。更新前从 EC 菜单退出旧浮窗,再运行 `show`。已保存的城市、时区、位置与显示选择会保留;“恢复默认位置”只恢复位置,不重置时区。
62
+
63
+ 周额度菜单示例:`周 · 77% · 5天后 25日 06:17 重置`。周倒计时只显示最大单位;若账号返回5小时窗口,其倒计时保留时分组合,例如 `3小时2分后 07:59 重置`,不足一小时仅显示分钟。绝对重置时间按电脑系统时区,独立时间浮窗按所选城市时区。
64
+
65
+ 时间开关为“显示时间”;城市子菜单入口如 `洛杉矶·05:23`,点号两侧无空格。只有实际返回的额度窗口才显示对应圆环和菜单项。
66
+
29
67
  ## 数据与限制
30
68
 
31
69
  通过官方 [App Server 协议](https://developers.openai.com/codex/app-server) 启动 `codex app-server --listen stdio://`,完成握手后调用 `account/rateLimits/read`,随后关闭本工具启动的进程。优先读取多额度桶,兼容单额度桶。
@@ -80,10 +118,13 @@ GitHub Actions 在 `v*` 标签上测试、构建通用 macOS 浮窗,并发布
80
118
 
81
119
  ## 电子时钟
82
120
 
83
- 透明时钟位于圆环上方,竖排居中,默认间距10px,可独立拖动。默认硅谷,城市顺序为硅谷、洛杉矶、西雅图、华盛顿特区、哥伦布(俄亥俄州)、新加坡、上海,也可自定义 IANA 时区;设置自动保存。仅显示时分,按目标当地时间00–09红色、09–15绿色、15–24橙色,自动处理夏令时,不修改电脑系统时区。菜单“恢复默认位置”应用竖排默认布局;已有拖动位置继续保留。
121
+ 透明时钟位于圆环上方,竖排居中,默认间距10px,可独立拖动。默认硅谷,城市顺序为硅谷、洛杉矶、西雅图、华盛顿特区、哥伦布(俄亥俄州)、新加坡、上海,也可自定义 IANA 时区;设置自动保存。仅显示时分,按目标当地时间00–09红色、09–18绿色、18–24橙色,自动处理夏令时,不修改电脑系统时区。菜单“恢复默认位置”应用竖排默认布局;已有拖动位置继续保留。
84
122
 
85
123
 
86
124
 
87
125
  浮窗使用 `easy-codex show` 启动;原 `overlay` 命令保留兼容。
88
126
 
89
127
  周倒计时只显示最大单位,例如“6天后 25日 06:17 重置”,不足一天按小时、不足一小时按分钟。5小时窗口保留时分组合,例如“3小时2分后 07:59 重置”,没有小时则“22分后 07:59 重置”,省略为零的单位。绝对重置时刻使用电脑本地时区;周含日,5小时不含日期。
128
+
129
+
130
+ 命令更新:`easy-codex update`,更新完成后运行 `easy-codex show`,会自动退出旧浮窗并打开新版。旧版本尚未提供 update 时,可先执行 `npm i -g easy-codex@latest`。更新过程中需要正常网络及原安装目录写入权限,不会自动使用 sudo。
package/bin/easy-codex.js CHANGED
@@ -5,16 +5,19 @@ import { readUsage, formatUsage } from '../src/usage.js';
5
5
 
6
6
  const args = process.argv.slice(2);
7
7
  if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
8
- console.log('用法:easy-codex [usage|show|overlay]\n显示已登录 Codex 账号的剩余百分比和重置时间。需要官方 Codex CLI。');
8
+ console.log('用法:easy-codex [usage|show|overlay|update]\n显示已登录 Codex 账号的剩余百分比和重置时间。需要官方 Codex CLI。');
9
9
  } else if (args.length === 1 && ['--version', '-v'].includes(args[0])) {
10
10
  console.log(version);
11
11
  } else if (args.length === 1 && ['show', 'overlay'].includes(args[0])) {
12
12
  try { const { launchOverlay } = await import('../src/overlay.js'); await launchOverlay(); }
13
13
  catch (error) { console.error(`easy-codex: ${error.message}`); process.exitCode = 1; }
14
+ } else if (args.length === 1 && args[0] === 'update') {
15
+ try { const { updatePackage } = await import('../src/update.js'); await updatePackage('easy-codex'); }
16
+ catch (error) { console.error(`easy-codex: ${error.message}`); process.exitCode = 1; }
14
17
  } else if (args.length === 0 || (args.length === 1 && args[0] === 'usage')) {
15
18
  try { console.log(formatUsage(await readUsage())); }
16
19
  catch (error) { console.error(`easy-codex: ${error.message}`); process.exitCode = 1; }
17
20
  } else {
18
- console.error('未知参数。用法:easy-codex [usage|show|overlay],或 easy-codex --help');
21
+ console.error('未知参数。用法:easy-codex [usage|show|overlay|update],或 easy-codex --help');
19
22
  process.exitCode = 1;
20
23
  }
@@ -196,8 +196,8 @@ func clockState(at date: Date, zone: TimeZone) -> RingState {
196
196
  var calendar = Calendar(identifier: .gregorian)
197
197
  calendar.timeZone = zone
198
198
  let hour = calendar.component(.hour, from: date)
199
- if hour >= 9 && hour < 15 { return .green }
200
- if hour >= 15 { return .orange }
199
+ if hour >= 9 && hour < 18 { return .green }
200
+ if hour >= 18 { return .orange }
201
201
  return .red
202
202
  }
203
203
 
@@ -524,10 +524,13 @@ struct Main {
524
524
  precondition(clockText(at: winter, zone: singapore) == "20:00")
525
525
  precondition(TimeZone(identifier: "Invalid/Zone") == nil)
526
526
  let utc = TimeZone(secondsFromGMT: 0)!
527
- for (hour, state) in [(0, RingState.red), (6, .red), (8, .red), (9, .green), (14, .green), (15, .orange), (23, .orange)] {
527
+ for (hour, state) in [(0, RingState.red), (6, .red), (8, .red), (9, .green), (14, .green), (15, .green), (17, .green), (18, .orange), (23, .orange)] {
528
528
  let date = Date(timeIntervalSince1970: Double(hour * 3600))
529
529
  precondition(clockState(at: date, zone: utc) == state)
530
530
  }
531
+ for (seconds, state) in [(32399.0, RingState.red), (32400.0, .green), (64799.0, .green), (64800.0, .orange), (86400.0, .red)] {
532
+ precondition(clockState(at: Date(timeIntervalSince1970: seconds), zone: utc) == state)
533
+ }
531
534
  precondition(clockState(at: winter, zone: la) == .red)
532
535
  precondition(clockState(at: winter, zone: singapore) == .orange)
533
536
  assert(RingState.resolve(51, stale: false) == .green)
@@ -544,6 +547,20 @@ struct Main {
544
547
  print("Swift 圆环颜色与窗口坐标检查通过")
545
548
  return
546
549
  }
550
+ if CommandLine.arguments.contains("--quit-running") {
551
+ let running = NSRunningApplication.runningApplications(withBundleIdentifier: "com.unionschool.easy-codex.overlay")
552
+ .filter { $0.processIdentifier != ProcessInfo.processInfo.processIdentifier }
553
+ for app in running { _ = app.terminate() }
554
+ let deadline = Date().addingTimeInterval(5)
555
+ while running.contains(where: { !$0.isTerminated }), Date() < deadline {
556
+ RunLoop.current.run(until: Date().addingTimeInterval(0.05))
557
+ }
558
+ if running.contains(where: { !$0.isTerminated }) {
559
+ fputs("旧浮窗尚未退出,请从菜单退出后重试。\n", stderr)
560
+ exit(1)
561
+ }
562
+ return
563
+ }
547
564
  let app = NSApplication.shared
548
565
  app.setActivationPolicy(.accessory)
549
566
  if CommandLine.arguments.contains("--diagnose") {
@@ -5,7 +5,7 @@
5
5
  <key>CFBundleIdentifier</key><string>com.unionschool.easy-codex.overlay</string>
6
6
  <key>CFBundleName</key><string>Easy Codex Overlay</string>
7
7
  <key>CFBundlePackageType</key><string>APPL</string>
8
- <key>CFBundleShortVersionString</key><string>0.2.1</string>
8
+ <key>CFBundleShortVersionString</key><string>0.2.3</string>
9
9
  <key>LSMinimumSystemVersion</key><string>14.0</string>
10
10
  <key>LSUIElement</key><true/>
11
11
  <key>NSHighResolutionCapable</key><true/>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-codex",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Codex usage limits and transparent world clock overlay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,8 @@
12
12
  "README.md",
13
13
  "native/Overlay.swift",
14
14
  "native/build/EasyCodexOverlay.app",
15
- "scripts/build-overlay.mjs"
15
+ "scripts/build-overlay.mjs",
16
+ "CHANGELOG.md"
16
17
  ],
17
18
  "engines": {
18
19
  "node": ">=22"
package/src/overlay.js CHANGED
@@ -4,12 +4,14 @@ import { fileURLToPath } from 'node:url';
4
4
  import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
6
 
7
- export async function launchOverlay() {
8
- if (process.platform !== 'darwin') throw new Error('浮窗首版仅支持 macOS。');
7
+ export async function launchOverlay({ platform = process.platform, check = access, run = promisify(execFile) } = {}) {
8
+ if (platform !== 'darwin') throw new Error('浮窗首版仅支持 macOS。');
9
9
  const app = fileURLToPath(new URL('../native/build/EasyCodexOverlay.app', import.meta.url));
10
- try { await access(`${app}/Contents/MacOS/EasyCodexOverlay`, constants.X_OK); }
10
+ try { await check(`${app}/Contents/MacOS/EasyCodexOverlay`, constants.X_OK); }
11
11
  catch { throw new Error('尚未构建浮窗,请在项目目录执行 npm run build:overlay。'); }
12
- await promisify(execFile)('/usr/bin/open', ['-g', '-a', app, '--args',
12
+ try { await run(`${app}/Contents/MacOS/EasyCodexOverlay`, ['--quit-running'], { timeout: 7000, maxBuffer: 4096 }); }
13
+ catch { throw new Error('无法退出旧浮窗,请从菜单退出后重试。'); }
14
+ await run('/usr/bin/open', ['-g', '-a', app, '--args',
13
15
  process.execPath, fileURLToPath(new URL('./overlay-query.js', import.meta.url)), process.env.PATH ?? '', process.env.CODEX_HOME ?? '']);
14
16
  console.log('额度与电子时钟浮窗已启动;Codex 在前台时显示。拖动调整位置,右键或菜单栏 EC 可刷新、切换中心额度、设置时区和退出。');
15
17
  }
package/src/update.js ADDED
@@ -0,0 +1,72 @@
1
+ import { access, realpath } from 'node:fs/promises';
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, basename, join, delimiter } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { spawn, execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
8
+
9
+ export function installedPrefix(root, platform = process.platform) {
10
+ const modules = dirname(root.replace(/[\\/]$/, ''));
11
+ if (basename(modules) !== 'node_modules') return null;
12
+ const parent = dirname(modules);
13
+ return basename(parent) === 'lib' ? dirname(parent) : platform === 'win32' ? parent : null;
14
+ }
15
+
16
+ export function isNewer(latest, current) {
17
+ if (!/^\d+\.\d+\.\d+$/.test(latest) || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(current)) {
18
+ throw new Error('无法识别正式版本号,未执行更新。');
19
+ }
20
+ const a = latest.split('.').map(Number), b = current.split('-')[0].split('.').map(Number);
21
+ for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] > b[i]; }
22
+ return current.includes('-');
23
+ }
24
+
25
+ async function findNpmCli() {
26
+ const directories = [dirname(process.execPath), ...(process.env.PATH ?? '').split(delimiter)];
27
+ for (const directory of directories.filter(Boolean)) {
28
+ for (const path of [join(directory, '../lib/node_modules/npm/bin/npm-cli.js'),
29
+ join(directory, 'node_modules/npm/bin/npm-cli.js'), join(directory, 'npm')]) {
30
+ try { const resolved = await realpath(path); await access(resolved); return resolved; }
31
+ catch { /* npm 可能位于 PATH 中另一套 Node 的旁边。 */ }
32
+ }
33
+ }
34
+ throw new Error('未找到 npm,请先安装 Node.js/npm,再使用 npm i -g 更新。');
35
+ }
36
+
37
+ async function readLatest(npmCli, name) {
38
+ try {
39
+ const { stdout } = await promisify(execFile)(process.execPath, [npmCli, 'view', name, 'dist-tags.latest',
40
+ '--json', '--prefer-online', '--registry=https://registry.npmjs.org'], { timeout: 30000, maxBuffer: 4096 });
41
+ return JSON.parse(stdout);
42
+ } catch { throw new Error('无法在线检查 npm 最新版本,请检查网络后重试。'); }
43
+ }
44
+
45
+ function runNpm(command, args) {
46
+ return new Promise((resolve, reject) => {
47
+ const child = spawn(command, args, { stdio: 'inherit', shell: false });
48
+ child.once('error', () => reject(new Error('无法启动 npm 更新。')));
49
+ child.once('exit', (code, signal) => {
50
+ if (code === 0) resolve();
51
+ else reject(new Error(`npm 更新失败(${signal ?? code ?? '未知状态'}),请检查网络或安装目录权限。`));
52
+ });
53
+ });
54
+ }
55
+
56
+ export async function updatePackage(name, { root = fileURLToPath(new URL('../', import.meta.url)),
57
+ getNpmCli = findNpmCli, getLatest = readLatest, currentVersion = version,
58
+ run = runNpm, platform = process.platform } = {}) {
59
+ if (!['easy-codex', 'easy-claude'].includes(name)) throw new Error('不支持的更新包名。');
60
+ const npmCli = await getNpmCli();
61
+ const latest = await getLatest(npmCli, name);
62
+ if (!isNewer(latest, currentVersion)) {
63
+ console.log(`${name} 当前 ${currentVersion},npm 最新 ${latest},无需更新。`);
64
+ return;
65
+ }
66
+ const prefix = installedPrefix(root, platform);
67
+ // 先在线检查再锁定版本,避免安装阶段复用旧 latest 缓存而降级。
68
+ const args = [npmCli, 'install', '--global', ...(prefix ? ['--prefix', prefix] : []),
69
+ '--prefer-online', '--registry=https://registry.npmjs.org', `${name}@${latest}`];
70
+ await run(process.execPath, args);
71
+ console.log(`${name} 已更新到 ${latest};运行 ${name} show 自动重启浮窗。`);
72
+ }