coding-usage-bar 0.4.1 → 0.5.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 CHANGED
@@ -17,7 +17,7 @@ Claude Code and Codex data stays local and is read from files those tools alread
17
17
 
18
18
  ## Live SwiftBar Menu
19
19
 
20
- The screenshot is captured from the live SwiftBar plugin. The animation reproduces opening and closing that menu without altering the captured usage data.
20
+ The menu bar title and the dropdown panel are captured from the live SwiftBar plugin and placed on a plain backdrop. The animation reproduces opening and closing that menu without altering the captured usage data.
21
21
 
22
22
  <p align="center">
23
23
  <img src="docs/assets/demo.gif" alt="Opening the live Coding Usage Bar SwiftBar menu with Codex, Claude, GLM, DeepSeek, MiniMax, and Kimi usage" width="800">
@@ -33,6 +33,16 @@ coding-usage-bar status
33
33
 
34
34
  `install` creates a local runtime at `~/.coding-usage-bar/app`, a user-level CLI shim at `~/.local/bin/coding-usage-bar`, a launchd agent, a default config file, and a SwiftBar menu bar plugin. Make sure `~/.local/bin` is in your `PATH`.
35
35
 
36
+ ## Troubleshooting
37
+
38
+ **Menu bar icon not showing after install?**
39
+
40
+ 1. Run `coding-usage-bar doctor --fix`. It relaunches SwiftBar, reinstalls the menu bar plugin if missing, re-adds SwiftBar to login items, and collects status data.
41
+ 2. macOS may fold menu bar icons away: check System Settings → Control Center → Menu Bar.
42
+ 3. If macOS showed an Automation permission prompt (System Events) and it was denied, SwiftBar cannot auto-start at login: allow it under System Settings → Privacy & Security → Automation, then run `coding-usage-bar install` again.
43
+
44
+ `coding-usage-bar install` verifies at the end that the plugin actually renders and prints `Menu bar ready`; if that line is missing, the numbered steps it prints tell you exactly what to do.
45
+
36
46
  ## Commands
37
47
 
38
48
  | Command | Purpose |
@@ -40,6 +50,7 @@ coding-usage-bar status
40
50
  | `coding-usage-bar install` | Install runtime, launchd checker, SwiftBar host/plugin, and Claude ingest when safe |
41
51
  | `coding-usage-bar uninstall` | Remove Coding Usage Bar managed launchd/status line/plugin config |
42
52
  | `coding-usage-bar doctor` | Check local Codex/Claude/GLM usage sources and notification backend |
53
+ | `coding-usage-bar doctor --fix` | Repair common issues: missing SwiftBar/plugin, SwiftBar not running, login item, status data |
43
54
  | `coding-usage-bar status` | Print the rolling windows reported by each account and their pacing state from `~/.coding-usage-bar/status.json` |
44
55
  | `coding-usage-bar status --json` | Print the same status snapshot written to `~/.coding-usage-bar/status.json` |
45
56
  | `coding-usage-bar status --refresh` | Re-collect local usage before printing status |
package/README.zh-CN.md CHANGED
@@ -12,7 +12,7 @@ Claude Code 与 Codex 的数据直接读取它们在本机产生的文件,不
12
12
 
13
13
  ## 真实 SwiftBar 菜单
14
14
 
15
- 以下截图来自正在运行的 SwiftBar 插件;动画复现菜单的展开与收起,不修改截图中的用量数据。
15
+ 菜单栏标题与下拉面板均截自正在运行的 SwiftBar 插件,仅叠加纯色桌面背景;动画复现菜单的展开与收起,不修改截图中的用量数据。
16
16
 
17
17
  <p align="center">
18
18
  <img src="docs/assets/demo.gif" alt="打开 Coding Usage Bar 的真实 SwiftBar 菜单,查看 Codex、Claude、GLM、DeepSeek、MiniMax 和 Kimi 用量" width="800">
@@ -33,6 +33,16 @@ coding-usage-bar status
33
33
  - 每 5 分钟采集一次用量的 launchd 任务
34
34
  - 写入 SwiftBar 当前插件目录的菜单栏插件
35
35
 
36
+ 安装结束时会自动验收菜单栏是否就绪,最后一行出现 `Menu bar ready` 即表示图标已出现在菜单栏右上角。
37
+
38
+ ## 常见问题
39
+
40
+ **安装后菜单栏没有图标?**
41
+
42
+ 1. 执行 `coding-usage-bar doctor --fix`:自动重启 SwiftBar、补装菜单栏插件、补加 SwiftBar 登录项、补采 status 数据。
43
+ 2. macOS 可能把菜单栏图标折叠隐藏了:系统设置 → 控制中心 → 菜单栏,检查隐藏区。
44
+ 3. 如果 macOS 弹过 System Events 的"自动化"权限框且被拒绝,SwiftBar 将无法开机自启:在 系统设置 → 隐私与安全性 → 自动化 中放行,再执行一次 `coding-usage-bar install`。
45
+
36
46
  ## 核心特点
37
47
 
38
48
  - 一个菜单栏同时查看多个 AI 编程 Provider
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { saveUsage } from "./store.js";
5
5
  import { formatStatusRows, formatAnalysisDetail, formatIssues, formatProviderMeta } from "./format.js";
6
6
  import { loadDisplayStatusSnapshot, collectStatusSnapshot } from "./runtime.js";
7
7
  import { runDaemonOnce } from "./daemon.js";
8
- import { doctorHasFailures, formatDoctor, runDoctor } from "./doctor.js";
8
+ import { doctorHasFailures, formatDoctor, runDoctor, runDoctorFix } from "./doctor.js";
9
9
  import { installMenuBar, renderMenuBar, uninstallMenuBar, toggleCompactMode } from "./menubar.js";
10
10
  import { buildPaths } from "./paths.js";
11
11
  import { bakeGlyphAtlases } from "./glyphs.js";
@@ -20,7 +20,7 @@ function printHelp() {
20
20
  Usage:
21
21
  coding-usage-bar install [--dry-run]
22
22
  coding-usage-bar uninstall [--dry-run]
23
- coding-usage-bar doctor [--dry-run]
23
+ coding-usage-bar doctor [--dry-run] [--fix]
24
24
  coding-usage-bar status [--fixtures] [--json] [--refresh]
25
25
  coding-usage-bar menubar render
26
26
  coding-usage-bar menubar install [--dry-run]
@@ -57,9 +57,21 @@ async function main() {
57
57
  return;
58
58
  }
59
59
  if (command === "doctor") {
60
+ const fix = hasFlag(args, "--fix");
61
+ if (fix) {
62
+ const fixes = await runDoctorFix({ dryRun });
63
+ if (fixes.length > 0) {
64
+ console.log(fixes.join("\n"));
65
+ console.log("");
66
+ }
67
+ }
60
68
  const checks = runDoctor({ dryRun });
61
69
  console.log(formatDoctor(checks));
62
70
  if (doctorHasFailures(checks)) {
71
+ if (!fix) {
72
+ console.log("");
73
+ console.log("Run coding-usage-bar doctor --fix to repair common issues automatically.");
74
+ }
63
75
  process.exitCode = 1;
64
76
  }
65
77
  return;
package/dist/doctor.d.ts CHANGED
@@ -2,5 +2,25 @@ import { DoctorCheck } from "./types.js";
2
2
  export declare function runDoctor(options?: {
3
3
  dryRun?: boolean;
4
4
  }): DoctorCheck[];
5
+ export interface DoctorFixProbes {
6
+ swiftBarInstalled?: () => boolean;
7
+ pluginPresent?: () => boolean;
8
+ swiftBarRunning?: () => boolean;
9
+ swiftBarInLoginItems?: () => boolean | null;
10
+ statusFilePresent?: () => boolean;
11
+ }
12
+ export interface DoctorFixEffects {
13
+ installSwiftBar?: () => string[];
14
+ installPlugin?: () => string[];
15
+ launchSwiftBar?: () => string[];
16
+ addLoginItem?: () => string[];
17
+ collectStatus?: () => Promise<string[]>;
18
+ }
19
+ export interface DoctorFixOptions {
20
+ dryRun?: boolean;
21
+ probes?: DoctorFixProbes;
22
+ effects?: DoctorFixEffects;
23
+ }
24
+ export declare function runDoctorFix(options?: DoctorFixOptions): Promise<string[]>;
5
25
  export declare function formatDoctor(checks: DoctorCheck[]): string;
6
26
  export declare function doctorHasFailures(checks: DoctorCheck[]): boolean;
package/dist/doctor.js CHANGED
@@ -7,7 +7,8 @@ import { isDir, isFile } from "./fs-util.js";
7
7
  import { resolveKimiConfig } from "./kimi.js";
8
8
  import { buildPaths, providerLatestPath } from "./paths.js";
9
9
  import { notificationBackend } from "./notifier.js";
10
- import { isSwiftBarInstalled, isSwiftBarRunning, swiftBarPluginPath } from "./menubar.js";
10
+ import { addSwiftBarToLoginItems, ensureSwiftBarInstalled, installMenuBar, isSwiftBarInstalled, isSwiftBarInLoginItems, isSwiftBarRunning, openSwiftBar, swiftBarPluginPath, } from "./menubar.js";
11
+ import { runDaemonOnce } from "./daemon.js";
11
12
  function stableClaudeIngestHint() {
12
13
  const paths = buildPaths();
13
14
  return `printf "%s" "$input" | node "${paths.stateDir}/app/dist/cli.js" ingest claude-statusline >/dev/null`;
@@ -240,17 +241,96 @@ export function runDoctor(options = {}) {
240
241
  const swiftBarRunning = pluginPresent && isSwiftBarRunning();
241
242
  checks.push({
242
243
  name: "Menu bar",
243
- ok: pluginPresent,
244
+ ok: pluginPresent && swiftBarRunning,
244
245
  message: swiftBarInstalled
245
246
  ? isFile(swiftBarPlugin.file)
246
247
  ? swiftBarRunning
247
248
  ? `SwiftBar plugin installed and SwiftBar running: ${swiftBarPlugin.file}`
248
- : `SwiftBar plugin installed but SwiftBar is not running (menu bar item hidden); run: open -a SwiftBar`
249
- : `SwiftBar found; run coding-usage-bar menubar install`
250
- : "SwiftBar not found; install SwiftBar, then run coding-usage-bar menubar install",
249
+ : "SwiftBar plugin installed but SwiftBar is not running (menu bar item hidden); run: coding-usage-bar doctor --fix"
250
+ : "SwiftBar found; run: coding-usage-bar doctor --fix"
251
+ : "SwiftBar not found; run: coding-usage-bar doctor --fix (installs SwiftBar via Homebrew)",
252
+ });
253
+ if (process.platform === "darwin") {
254
+ if (options.dryRun) {
255
+ checks.push({
256
+ name: "SwiftBar login item",
257
+ ok: true,
258
+ message: "[dry-run] would check whether SwiftBar auto-starts at login",
259
+ });
260
+ }
261
+ else if (!swiftBarInstalled) {
262
+ checks.push({
263
+ name: "SwiftBar login item",
264
+ ok: true,
265
+ message: "skipped (SwiftBar not installed)",
266
+ });
267
+ }
268
+ else {
269
+ const inLoginItems = isSwiftBarInLoginItems();
270
+ checks.push({
271
+ name: "SwiftBar login item",
272
+ ok: inLoginItems !== false,
273
+ message: inLoginItems === null
274
+ ? "could not verify (Automation permission denied?); if the icon disappears after reboot, check System Settings → General → Login Items"
275
+ : inLoginItems
276
+ ? "SwiftBar auto-starts at login"
277
+ : "not in login items; the menu bar icon disappears after reboot. Run: coding-usage-bar doctor --fix",
278
+ });
279
+ }
280
+ }
281
+ checks.push({
282
+ name: "Status data",
283
+ ok: options.dryRun ? true : isFile(paths.statusFile),
284
+ message: options.dryRun
285
+ ? `[dry-run] would read ${paths.statusFile}`
286
+ : isFile(paths.statusFile)
287
+ ? `found ${paths.statusFile}`
288
+ : `missing ${paths.statusFile}; run coding-usage-bar doctor --fix or coding-usage-bar daemon --once`,
251
289
  });
252
290
  return checks;
253
291
  }
292
+ // One-command self-healing for the common "installed but the menu bar icon is
293
+ // gone" failures: missing SwiftBar, missing plugin, SwiftBar not running,
294
+ // SwiftBar missing from login items, and missing status data.
295
+ export async function runDoctorFix(options = {}) {
296
+ if (options.dryRun) {
297
+ return ["[dry-run] would repair SwiftBar, the menu bar plugin, login items, and status data automatically"];
298
+ }
299
+ const paths = buildPaths();
300
+ const probes = {
301
+ swiftBarInstalled: options.probes?.swiftBarInstalled ?? isSwiftBarInstalled,
302
+ pluginPresent: options.probes?.pluginPresent ?? (() => isFile(swiftBarPluginPath(paths).file)),
303
+ swiftBarRunning: options.probes?.swiftBarRunning ?? isSwiftBarRunning,
304
+ swiftBarInLoginItems: options.probes?.swiftBarInLoginItems ?? isSwiftBarInLoginItems,
305
+ statusFilePresent: options.probes?.statusFilePresent ?? (() => isFile(paths.statusFile)),
306
+ };
307
+ const effects = {
308
+ installSwiftBar: options.effects?.installSwiftBar ?? (() => ensureSwiftBarInstalled()),
309
+ installPlugin: options.effects?.installPlugin ?? (() => installMenuBar()),
310
+ launchSwiftBar: options.effects?.launchSwiftBar ?? (() => openSwiftBar()),
311
+ addLoginItem: options.effects?.addLoginItem ?? (() => addSwiftBarToLoginItems()),
312
+ collectStatus: options.effects?.collectStatus ?? (async () => runDaemonOnce()),
313
+ };
314
+ const messages = [];
315
+ let swiftBarInstalled = probes.swiftBarInstalled();
316
+ if (!swiftBarInstalled) {
317
+ messages.push(...effects.installSwiftBar());
318
+ swiftBarInstalled = probes.swiftBarInstalled();
319
+ }
320
+ if (!probes.pluginPresent()) {
321
+ messages.push(...effects.installPlugin());
322
+ }
323
+ if (swiftBarInstalled && !probes.swiftBarRunning()) {
324
+ messages.push(...effects.launchSwiftBar());
325
+ }
326
+ if (probes.swiftBarInLoginItems() === false) {
327
+ messages.push(...effects.addLoginItem());
328
+ }
329
+ if (!probes.statusFilePresent()) {
330
+ messages.push(...(await effects.collectStatus()));
331
+ }
332
+ return messages;
333
+ }
254
334
  export function formatDoctor(checks) {
255
335
  return checks
256
336
  .map((check) => `${check.ok ? "OK" : "MISSING"} ${check.name}: ${check.message}`)
package/dist/install.js CHANGED
@@ -5,7 +5,7 @@ import { execFileSync } from "node:child_process";
5
5
  import { claudeStatusLineHasIngest, getClaudeStatusLineCommand, readClaudeSettings } from "./claude.js";
6
6
  import { ensureConfig, readConfig } from "./config.js";
7
7
  import { ensureDir, isFile, readJsonFile, writeJsonAtomic } from "./fs-util.js";
8
- import { addSwiftBarToLoginItems, ensureSwiftBarInstalled, installMenuBar, openSwiftBar, uninstallMenuBar } from "./menubar.js";
8
+ import { addSwiftBarToLoginItems, ensureSwiftBarInstalled, installMenuBar, openSwiftBar, uninstallMenuBar, verifyMenuBarSetup } from "./menubar.js";
9
9
  import { bakeGlyphAtlases } from "./glyphs.js";
10
10
  import { buildPaths } from "./paths.js";
11
11
  import { stableNodeExecutable } from "./node-runtime.js";
@@ -376,11 +376,13 @@ export function install(options = {}) {
376
376
  if (dryRun) {
377
377
  messages.push(`[dry-run] would write launchd plist: ${paths.launchAgentFile}`);
378
378
  messages.push(`[dry-run] ProgramArguments: ${[...args, "daemon", "--once"].join(" ")}`);
379
+ messages.push(...verifyMenuBarSetup({ dryRun }));
379
380
  return messages;
380
381
  }
381
382
  fs.writeFileSync(paths.launchAgentFile, xml, "utf8");
382
383
  restartLaunchAgent(paths.launchAgentFile);
383
384
  messages.push(`Installed and restarted launchd agent: ${paths.launchAgentFile}`);
385
+ messages.push(...verifyMenuBarSetup());
384
386
  return messages;
385
387
  }
386
388
  export function uninstall(options = {}) {
package/dist/menubar.d.ts CHANGED
@@ -12,9 +12,24 @@ export declare function ensureSwiftBarInstalled(options?: {
12
12
  }): string[];
13
13
  export declare function isSwiftBarRunning(): boolean;
14
14
  export declare function openSwiftBar(): string[];
15
+ export declare function isSwiftBarInLoginItems(): boolean | null;
15
16
  export declare function addSwiftBarToLoginItems(options?: {
16
17
  dryRun?: boolean;
17
18
  }): string[];
19
+ export interface PluginRunResult {
20
+ ok: boolean;
21
+ output: string;
22
+ }
23
+ export interface VerifyMenuBarOptions {
24
+ dryRun?: boolean;
25
+ runPlugin?: (file: string) => PluginRunResult;
26
+ loginItems?: () => boolean | null;
27
+ swiftBarInstalled?: () => boolean;
28
+ swiftBarRunning?: () => boolean;
29
+ repairPlugin?: () => string[];
30
+ launchSwiftBar?: () => string[];
31
+ }
32
+ export declare function verifyMenuBarSetup(options?: VerifyMenuBarOptions): string[];
18
33
  export declare function renderMenuBar(snapshot?: StatusSnapshot, paths?: RuntimePaths, now?: Date): string;
19
34
  export declare function swiftBarStatusItemVisibilityKeys(defaultsOutput: string): string[];
20
35
  export declare function installMenuBar(options?: {
package/dist/menubar.js CHANGED
@@ -268,22 +268,27 @@ export function openSwiftBar() {
268
268
  }
269
269
  return messages;
270
270
  }
271
- export function addSwiftBarToLoginItems(options = {}) {
272
- const dryRun = options.dryRun ?? false;
273
- const appPath = swiftBarAppPath();
274
- if (!appPath) {
275
- return ["SwiftBar is not installed; skipping login item."];
276
- }
271
+ // Whether SwiftBar auto-starts at login. Returns null when the login item list
272
+ // cannot be queried (Automation permission denied or non-interactive context).
273
+ export function isSwiftBarInLoginItems() {
277
274
  try {
278
275
  const result = execFileSync("osascript", [
279
276
  "-e", 'tell application "System Events" to get the name of every login item',
280
277
  ], { encoding: "utf8" });
281
- if (result.includes("SwiftBar")) {
282
- return ["SwiftBar already in login items."];
283
- }
278
+ return result.split(",").map((name) => name.trim()).includes("SwiftBar");
284
279
  }
285
280
  catch {
286
- // Permission or access issue; try to add anyway.
281
+ return null;
282
+ }
283
+ }
284
+ export function addSwiftBarToLoginItems(options = {}) {
285
+ const dryRun = options.dryRun ?? false;
286
+ const appPath = swiftBarAppPath();
287
+ if (!appPath) {
288
+ return ["SwiftBar is not installed; skipping login item."];
289
+ }
290
+ if (isSwiftBarInLoginItems()) {
291
+ return ["SwiftBar already in login items."];
287
292
  }
288
293
  if (dryRun) {
289
294
  return [`[dry-run] would add SwiftBar to login items: ${appPath}`];
@@ -298,6 +303,69 @@ export function addSwiftBarToLoginItems(options = {}) {
298
303
  return [`Failed to add SwiftBar to login items: ${error instanceof Error ? error.message : String(error)}. Add manually in System Settings → General → Login Items.`];
299
304
  }
300
305
  }
306
+ // Execute the installed plugin exactly the way SwiftBar would and check it
307
+ // produces a real menu. The plugin prints "Usage ERR" with exit code 0 on
308
+ // failure, so a non-error status alone proves nothing.
309
+ function runPluginScript(file) {
310
+ try {
311
+ const stdout = execFileSync(file, { encoding: "utf8", timeout: 30000 });
312
+ const output = stdout.trim();
313
+ return { ok: output.length > 0 && !output.startsWith("Usage ERR"), output };
314
+ }
315
+ catch (error) {
316
+ return { ok: false, output: error instanceof Error ? error.message : String(error) };
317
+ }
318
+ }
319
+ const MENU_BAR_MANUAL_STEPS = [
320
+ "Menu bar is NOT ready yet. Fix it with these steps:",
321
+ " 1. If macOS is showing an Automation/Accessibility permission prompt, click Allow.",
322
+ " 2. Open SwiftBar: open -a SwiftBar",
323
+ " 3. If the icon is still missing, macOS may have folded it away: System Settings → Control Center → Menu Bar.",
324
+ " 4. Re-run: coding-usage-bar doctor --fix",
325
+ ];
326
+ const LOGIN_ITEM_WARNING = [
327
+ "WARN SwiftBar is not in login items: the menu bar icon will disappear after a reboot.",
328
+ " If macOS showed a System Events Automation prompt, allow it and re-run coding-usage-bar install,",
329
+ " or add SwiftBar manually: System Settings → General → Login Items.",
330
+ ];
331
+ // End-to-end acceptance at the end of install: the plugin exists, SwiftBar is
332
+ // running, and the plugin actually renders a menu. Auto-repairs once, then
333
+ // falls back to numbered manual steps instead of failing silently.
334
+ export function verifyMenuBarSetup(options = {}) {
335
+ if (options.dryRun) {
336
+ return ["[dry-run] would verify the SwiftBar plugin renders and the menu bar item appears"];
337
+ }
338
+ const paths = buildPaths();
339
+ const plugin = swiftBarPluginPath(paths);
340
+ const isInstalled = options.swiftBarInstalled ?? isSwiftBarInstalled;
341
+ const isRunning = options.swiftBarRunning ?? isSwiftBarRunning;
342
+ const messages = [];
343
+ if (!isInstalled()) {
344
+ return [
345
+ ...MENU_BAR_MANUAL_STEPS,
346
+ ` (SwiftBar is not installed. Install it with: brew install --cask swiftbar, then re-run coding-usage-bar install.)`,
347
+ ];
348
+ }
349
+ if (!isFile(plugin.file)) {
350
+ messages.push(...(options.repairPlugin ?? installMenuBar)());
351
+ }
352
+ if (!isRunning()) {
353
+ messages.push(...(options.launchSwiftBar ?? openSwiftBar)());
354
+ }
355
+ if (!isFile(plugin.file)) {
356
+ return [...messages, ...MENU_BAR_MANUAL_STEPS, ` (Plugin file missing: ${plugin.file})`];
357
+ }
358
+ const run = (options.runPlugin ?? runPluginScript)(plugin.file);
359
+ if (!run.ok || !isRunning()) {
360
+ return [...messages, ...MENU_BAR_MANUAL_STEPS];
361
+ }
362
+ messages.push("");
363
+ messages.push("Menu bar ready — Coding Usage Bar should now appear at the top-right of your menu bar.");
364
+ if ((options.loginItems ?? isSwiftBarInLoginItems)() === false) {
365
+ messages.push(...LOGIN_ITEM_WARNING);
366
+ }
367
+ return messages;
368
+ }
301
369
  function swiftBarEscape(value) {
302
370
  return value.replaceAll("|", "\\|");
303
371
  }
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-usage-bar",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "macOS menu bar usage monitor for Claude Code, Codex, GLM, DeepSeek, and MiniMax.",
5
5
  "author": "hanzhangzzz",
6
6
  "license": "MIT",
@@ -29,7 +29,7 @@
29
29
  ],
30
30
  "type": "module",
31
31
  "bin": {
32
- "coding-usage-bar": "./dist/cli.js"
32
+ "coding-usage-bar": "dist/cli.js"
33
33
  },
34
34
  "files": [
35
35
  "assets",