Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CodexBar 🎚️ - May your tokens never run out.

Tiny macOS 14+ menu bar app that keeps your Codex, Claude, Cursor, Gemini, Antigravity, Droid (Factory), Copilot, z.ai, Kiro, Vertex AI, Augment, Amp, JetBrains AI, OpenRouter, Perplexity, and Abacus AI limits visible (session + weekly where available) and shows when each window resets. One status item per provider (or Merge Icons mode with a provider switcher and optional Overview tab); enable what you use from Settings. No Dock icon, minimal UI, dynamic bar icons in the menu bar.
Tiny macOS 14+ menu bar app that keeps your Codex, Claude, Cursor, Gemini, Antigravity, Droid (Factory), Copilot, z.ai, Manus, Kiro, Vertex AI, Augment, Amp, JetBrains AI, OpenRouter, Perplexity, and Abacus AI limits visible (session + weekly where available) and shows when each window resets. One status item per provider (or Merge Icons mode with a provider switcher and optional Overview tab); enable what you use from Settings. No Dock icon, minimal UI, dynamic bar icons in the menu bar.

<img src="codexbar.png" alt="CodexBar menu screenshot" width="520" />

Expand Down Expand Up @@ -39,6 +39,7 @@ Linux support via Omarchy: community Waybar module and TUI, driven by the `codex
- [Droid](docs/factory.md) — Browser cookies + WorkOS token flows for Factory usage + billing.
- [Copilot](docs/copilot.md) — GitHub device flow + Copilot internal usage API.
- [z.ai](docs/zai.md) — API token (Keychain) for quota + MCP windows.
- [Manus](docs/manus.md) — Browser `session_id` auth for credit balance, monthly credits, and daily refresh tracking.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Update README Manus docs link to an existing file

The new README entry links to docs/manus.md, but that file is not present in the repository, so the provider documentation link is broken for readers. Point this link to an existing page (or add the missing docs file) to avoid dead navigation from the main project page.

Useful? React with 👍 / 👎.

- [Kimi](docs/kimi.md) — Auth token (JWT from `kimi-auth` cookie) for weekly quota + 5‑hour rate limit.
- [Kimi K2](docs/kimi-k2.md) — API key for credit-based usage totals.
- [Kiro](docs/kiro.md) — CLI-based usage via `kiro-cli /usage` command; monthly credits + bonus credits.
Expand Down Expand Up @@ -72,7 +73,7 @@ The menu bar icon is a tiny two-bar meter:
Wondering if CodexBar scans your disk? It doesn’t crawl your filesystem; it reads a small set of known locations (browser cookies/local storage, local JSONL logs) when the related features are enabled. See the discussion and audit notes in [issue #12](https://github.com/steipete/CodexBar/issues/12).

## macOS permissions (why they’re needed)
- **Full Disk Access (optional)**: only required to read Safari cookies/local storage for web-based providers (Codex web, Claude web, Cursor, Droid/Factory). If you don’t grant it, use Chrome/Firefox cookies or CLI-only sources instead.
- **Full Disk Access (optional)**: only required to read Safari cookies/local storage for web-based providers (Codex web, Claude web, Cursor, Droid/Factory, Manus, Abacus AI). If you don’t grant it, use Chrome/Firefox cookies or CLI-only sources instead.
- **Keychain access (prompted by macOS)**:
- Chrome cookie import needs the “Chrome Safe Storage” key to decrypt cookies.
- Claude OAuth credentials (written by the Claude CLI) are read from Keychain when present.
Expand Down
16 changes: 16 additions & 0 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,13 @@ extension UsageMenuCardView.Model {
{
primaryDetailText = detail
}
if input.provider == .manus,
let detail = primary.resetDescription,
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
primaryDetailText = detail
primaryResetText = nil
}
if input.provider == .warp || input.provider == .kilo, primary.resetsAt == nil {
primaryResetText = nil
}
Expand Down Expand Up @@ -1134,6 +1141,12 @@ extension UsageMenuCardView.Model {
{
weeklyDetailText = detail
}
if input.provider == .manus,
let detail = weekly.resetDescription,
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
weeklyDetailText = detail
}
// Perplexity bonus credits don't reset; show balance without "Resets" prefix.
if input.provider == .perplexity,
let detail = weekly.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines),
Expand Down Expand Up @@ -1376,6 +1389,9 @@ extension UsageMenuCardView.Model {
provider: UsageProvider,
cost: ProviderCostSnapshot?) -> ProviderCostSection?
{
if provider == .manus {
return nil
}
guard let cost else { return nil }
guard cost.limit > 0 else { return nil }

Expand Down
109 changes: 109 additions & 0 deletions Sources/CodexBar/Providers/Manus/ManusProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

@ProviderImplementationRegistration
struct ManusProviderImplementation: ProviderImplementation {
let id: UsageProvider = .manus
let supportsLoginFlow: Bool = true

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { _ in "web" }
}

@MainActor
func runLoginFlow(context _: ProviderLoginContext) async -> Bool {
if let url = URL(string: "https://manus.im") {
NSWorkspace.shared.open(url)
}
return false
}

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.manusCookieSource
_ = settings.manusManualCookieHeader
}

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
.manus(context.settings.manusSettingsSnapshot(tokenOverride: context.tokenOverride))
}
Comment on lines +24 to +34

@MainActor
func tokenAccountsVisibility(context: ProviderSettingsContext, support: TokenAccountSupport) -> Bool {
guard support.requiresManualCookieSource else { return true }
if !context.settings.tokenAccounts(for: context.provider).isEmpty { return true }
return context.settings.manusCookieSource == .manual
}

@MainActor
func applyTokenAccountCookieSource(settings: SettingsStore) {
if settings.manusCookieSource != .manual {
settings.manusCookieSource = .manual
}
}

@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let cookieBinding = Binding(
get: { context.settings.manusCookieSource.rawValue },
set: { raw in
context.settings.manusCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
})
let options = ProviderCookieSourceUI.options(
allowsOff: true,
keychainDisabled: context.settings.debugDisableKeychainAccess)

let subtitle: () -> String? = {
ProviderCookieSourceUI.subtitle(
source: context.settings.manusCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Automatically imports browser session cookies.",
manual: "Paste the session_id value or a full Cookie header.",
off: "Manus cookies are disabled.")
}

return [
ProviderSettingsPickerDescriptor(
id: "manus-cookie-source",
title: "Cookie source",
subtitle: "Automatically imports browser session cookies.",
dynamicSubtitle: subtitle,
binding: cookieBinding,
options: options,
isVisible: nil,
onChange: nil),
]
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "manus-cookie",
title: "",
subtitle: "",
kind: .secure,
placeholder: "session_id=...\n\nor paste just the session_id value",
binding: context.stringBinding(\.manusManualCookieHeader),
actions: [
ProviderSettingsActionDescriptor(
id: "manus-open-dashboard",
title: "Open Manus",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://manus.im") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: { context.settings.manusCookieSource == .manual },
onActivate: nil),
]
}
}
60 changes: 60 additions & 0 deletions Sources/CodexBar/Providers/Manus/ManusSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var manusManualCookieHeader: String {
get { self.configSnapshot.providerConfig(for: .manus)?.sanitizedCookieHeader ?? "" }
set {
self.updateProviderConfig(provider: .manus) { entry in
entry.cookieHeader = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .manus, field: "cookieHeader", value: newValue)
}
}

var manusCookieSource: ProviderCookieSource {
get { self.resolvedCookieSource(provider: .manus, fallback: .auto) }
set {
self.updateProviderConfig(provider: .manus) { entry in
entry.cookieSource = newValue
}
self.logProviderModeChange(provider: .manus, field: "cookieSource", value: newValue.rawValue)
}
}
}

extension SettingsStore {
func manusSettingsSnapshot(tokenOverride: TokenAccountOverride?) -> ProviderSettingsSnapshot.ManusProviderSettings {
ProviderSettingsSnapshot.ManusProviderSettings(
cookieSource: self.manusSnapshotCookieSource(tokenOverride: tokenOverride),
manualCookieHeader: self.manusSnapshotCookieHeader(tokenOverride: tokenOverride))
}

private func manusSnapshotCookieHeader(tokenOverride: TokenAccountOverride?) -> String {
let fallback = self.manusManualCookieHeader
guard let support = TokenAccountSupportCatalog.support(for: .manus),
case .cookieHeader = support.injection
else {
return fallback
}
guard let account = ProviderTokenAccountSelection.selectedAccount(
provider: .manus,
settings: self,
override: tokenOverride)
else {
return fallback
}
return TokenAccountSupportCatalog.normalizedCookieHeader(account.token, support: support)
}

private func manusSnapshotCookieSource(tokenOverride: TokenAccountOverride?) -> ProviderCookieSource {
let fallback = self.manusCookieSource
guard let support = TokenAccountSupportCatalog.support(for: .manus),
support.requiresManualCookieSource
else {
return fallback
}
if self.tokenAccounts(for: .manus).isEmpty { return fallback }
return .manual
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum ProviderImplementationRegistry {
case .copilot: CopilotProviderImplementation()
case .zai: ZaiProviderImplementation()
case .minimax: MiniMaxProviderImplementation()
case .manus: ManusProviderImplementation()
case .kimi: KimiProviderImplementation()
case .kilo: KiloProviderImplementation()
case .kiro: KiroProviderImplementation()
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-manus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,7 @@ extension UsageStore {
.alibaba: "Alibaba Coding Plan debug log not yet implemented",
.factory: "Droid debug log not yet implemented",
.copilot: "Copilot debug log not yet implemented",
.manus: "Manus debug log not yet implemented",
.vertexai: "Vertex AI debug log not yet implemented",
.kilo: "Kilo debug log not yet implemented",
.kiro: "Kiro debug log not yet implemented",
Expand Down Expand Up @@ -875,8 +876,8 @@ extension UsageStore {
let hasAny = resolution != nil
let source = resolution?.source.rawValue ?? "none"
return "WARP_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
case .gemini, .antigravity, .opencode, .opencodego, .factory, .copilot, .vertexai, .kilo, .kiro, .kimi,
.kimik2, .jetbrains, .perplexity, .abacus:
case .gemini, .antigravity, .opencode, .opencodego, .factory, .copilot, .manus, .vertexai, .kilo, .kiro,
.kimi, .kimik2, .jetbrains, .perplexity, .abacus:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ struct TokenAccountCLIContext {
cookieSource: cookieSource,
manualCookieHeader: cookieHeader,
apiRegion: self.resolveMiniMaxRegion(config)))
case .manus:
let cookieHeader = self.manualCookieHeader(provider: provider, account: account, config: config)
let cookieSource = self.cookieSource(provider: provider, account: account, config: config)
return self.makeSnapshot(
manus: ProviderSettingsSnapshot.ManusProviderSettings(
cookieSource: cookieSource,
manualCookieHeader: cookieHeader))
case .augment:
let cookieHeader = self.manualCookieHeader(provider: provider, account: account, config: config)
let cookieSource = self.cookieSource(provider: provider, account: account, config: config)
Expand Down Expand Up @@ -207,6 +214,7 @@ struct TokenAccountCLIContext {
alibaba: ProviderSettingsSnapshot.AlibabaCodingPlanProviderSettings? = nil,
factory: ProviderSettingsSnapshot.FactoryProviderSettings? = nil,
minimax: ProviderSettingsSnapshot.MiniMaxProviderSettings? = nil,
manus: ProviderSettingsSnapshot.ManusProviderSettings? = nil,
zai: ProviderSettingsSnapshot.ZaiProviderSettings? = nil,
kilo: ProviderSettingsSnapshot.KiloProviderSettings? = nil,
kimi: ProviderSettingsSnapshot.KimiProviderSettings? = nil,
Expand All @@ -226,6 +234,7 @@ struct TokenAccountCLIContext {
alibaba: alibaba,
factory: factory,
minimax: minimax,
manus: manus,
zai: zai,
kilo: kilo,
kimi: kimi,
Expand Down
Loading