diff --git a/CHANGELOG.md b/CHANGELOG.md index 021d063534..7554ff0e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.47.1 — Unreleased ### Added +- Kimi: enrich Code API and CLI usage with the monthly membership pool from a signed-in Kimi Desktop session, using WAL-safe read-only cookie access (#2351). Thanks @Leehow! - Kimi/GLM: distinguish Kimi Code from the regional Open Platform, bind China and international keys to their issuing hosts, and show GLM Coding Plan's 5-hour window as primary with MCP separate (#2351). Thanks @Leehow! ### Changed diff --git a/Sources/CodexBarCore/Providers/Kimi/KimiCookieImporter.swift b/Sources/CodexBarCore/Providers/Kimi/KimiCookieImporter.swift index b3f6deef69..5e95d2c1d1 100644 --- a/Sources/CodexBarCore/Providers/Kimi/KimiCookieImporter.swift +++ b/Sources/CodexBarCore/Providers/Kimi/KimiCookieImporter.swift @@ -4,6 +4,10 @@ import Foundation import SweetCookieKit public enum KimiCookieImporter { + public static func desktopAuthToken() -> String? { + KimiDesktopAuthToken.load() + } + private static let log = CodexBarLog.logger(LogCategories.kimiCookie) private static let cookieClient = BrowserCookieClient() private static let cookieDomains = ["www.kimi.com", "kimi.com"] diff --git a/Sources/CodexBarCore/Providers/Kimi/KimiDesktopAuthToken.swift b/Sources/CodexBarCore/Providers/Kimi/KimiDesktopAuthToken.swift new file mode 100644 index 0000000000..46952c650e --- /dev/null +++ b/Sources/CodexBarCore/Providers/Kimi/KimiDesktopAuthToken.swift @@ -0,0 +1,127 @@ +import Foundation + +#if canImport(SQLite3) +import SQLite3 +#elseif canImport(CSQLite3) +import CSQLite3 +#endif + +#if canImport(SQLite3) || canImport(CSQLite3) +/// Read-only access to the official Kimi Desktop Chromium cookie store. +public enum KimiDesktopAuthToken: Sendable { + private static let log = CodexBarLog.logger(LogCategories.kimiCookie) + + public static func cookiesDatabaseURL( + homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser) -> URL + { + homeDirectory + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Application Support", isDirectory: true) + .appendingPathComponent("kimi-desktop", isDirectory: true) + .appendingPathComponent("Cookies", isDirectory: false) + } + + public static func load( + homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser) -> String? + { + self.load(databaseURL: self.cookiesDatabaseURL(homeDirectory: homeDirectory)) + } + + static func load(databaseURL: URL) -> String? { + guard FileManager.default.isReadableFile(atPath: databaseURL.path) else { return nil } + do { + return try self.read(databaseURL: databaseURL, immutable: false) + } catch let failure as SQLiteReadFailure { + // Chromium can leave the main database in WAL mode after a clean shutdown removes both sidecars. + // Immutable mode reads that idle file without recreating sidecars; active WAL databases stay on the + // normal read-only path so committed WAL records remain visible. + guard failure.code == SQLITE_CANTOPEN, self.walSidecarsAreMissing(databaseURL: databaseURL) else { + Self.log.debug("Kimi Desktop Cookies read failed: \(failure.message)") + return nil + } + do { + return try self.read(databaseURL: databaseURL, immutable: true) + } catch let fallbackFailure as SQLiteReadFailure { + Self.log.debug("Kimi Desktop Cookies immutable read failed: \(fallbackFailure.message)") + return nil + } catch { + return nil + } + } catch { + return nil + } + } + + private static func read(databaseURL: URL, immutable: Bool) throws -> String? { + var db: OpaquePointer? + let filename = immutable ? "\(databaseURL.absoluteURL.absoluteString)?immutable=1" : databaseURL.path + let flags = immutable ? SQLITE_OPEN_READONLY | SQLITE_OPEN_URI : SQLITE_OPEN_READONLY + let openResult = sqlite3_open_v2(filename, &db, flags, nil) + guard openResult == SQLITE_OK else { + let failure = self.sqliteFailure(db: db, resultCode: openResult) + sqlite3_close(db) + throw failure + } + defer { sqlite3_close(db) } + sqlite3_busy_timeout(db, 250) + + let sql = """ + SELECT value + FROM cookies + WHERE name = 'kimi-auth' + AND host_key IN ('www.kimi.com', '.www.kimi.com', '.kimi.com', 'kimi.com') + ORDER BY last_access_utc DESC + LIMIT 1 + """ + var statement: OpaquePointer? + let prepareResult = sqlite3_prepare_v2(db, sql, -1, &statement, nil) + guard prepareResult == SQLITE_OK else { + throw self.sqliteFailure(db: db, resultCode: prepareResult) + } + defer { sqlite3_finalize(statement) } + + let step = sqlite3_step(statement) + if step == SQLITE_DONE { + return nil + } + guard step == SQLITE_ROW else { + throw self.sqliteFailure(db: db, resultCode: step) + } + guard let text = sqlite3_column_text(statement, 0) else { return nil } + let token = String(cString: text).trimmingCharacters(in: .whitespacesAndNewlines) + return token.isEmpty ? nil : token + } + + private static func walSidecarsAreMissing(databaseURL: URL) -> Bool { + !FileManager.default.fileExists(atPath: databaseURL.path + "-wal") && + !FileManager.default.fileExists(atPath: databaseURL.path + "-shm") + } + + private static func sqliteFailure(db: OpaquePointer?, resultCode: Int32) -> SQLiteReadFailure { + SQLiteReadFailure( + code: db.map { sqlite3_errcode($0) } ?? resultCode, + message: db.map { String(cString: sqlite3_errmsg($0)) } ?? "unknown error") + } + + private struct SQLiteReadFailure: Error { + let code: Int32 + let message: String + } +} +#else +public enum KimiDesktopAuthToken: Sendable { + public static func cookiesDatabaseURL( + homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser) -> URL + { + homeDirectory + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Application Support", isDirectory: true) + .appendingPathComponent("kimi-desktop", isDirectory: true) + .appendingPathComponent("Cookies", isDirectory: false) + } + + public static func load(homeDirectory _: URL = FileManager.default.homeDirectoryForCurrentUser) -> String? { + nil + } +} +#endif diff --git a/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift index 052e0e9408..1d816ebd8e 100644 --- a/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift @@ -66,9 +66,15 @@ struct KimiAPIFetchStrategy: ProviderFetchStrategy { let id: String = "kimi.api" let kind: ProviderFetchKind = .apiToken private let transport: any ProviderHTTPTransport + private let resolveWebAuthToken: @Sendable (ProviderFetchContext) -> String? - init(transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) { + init( + transport: any ProviderHTTPTransport = ProviderHTTPClient.shared, + resolveWebAuthToken: @escaping @Sendable (ProviderFetchContext) -> String? = + KimiWebEnrichmentTokenResolver.resolve) + { self.transport = transport + self.resolveWebAuthToken = resolveWebAuthToken } func isAvailable(_ context: ProviderFetchContext) async -> Bool { @@ -83,6 +89,7 @@ struct KimiAPIFetchStrategy: ProviderFetchStrategy { let snapshot = try await KimiUsageFetcher.fetchCodeAPIUsage( apiKey: apiKey, baseURL: baseURL, + webAuthToken: self.enrichmentToken(context), transport: self.transport) return self.makeResult( usage: snapshot.toUsageSnapshot(), @@ -92,15 +99,26 @@ struct KimiAPIFetchStrategy: ProviderFetchStrategy { func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool { KimiCodeAPIFallbackPolicy.shouldFallback(on: error, context: context) } + + private func enrichmentToken(_ context: ProviderFetchContext) -> String? { + guard let settings = context.settings?.kimi, settings.cookieSource != .off else { return nil } + return self.resolveWebAuthToken(context) + } } struct KimiCLICredentialFetchStrategy: ProviderFetchStrategy { let id: String = "kimi.cli" let kind: ProviderFetchKind = .oauth private let transport: any ProviderHTTPTransport + private let resolveWebAuthToken: @Sendable (ProviderFetchContext) -> String? - init(transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) { + init( + transport: any ProviderHTTPTransport = ProviderHTTPClient.shared, + resolveWebAuthToken: @escaping @Sendable (ProviderFetchContext) -> String? = + KimiWebEnrichmentTokenResolver.resolve) + { self.transport = transport + self.resolveWebAuthToken = resolveWebAuthToken } func isAvailable(_ context: ProviderFetchContext) async -> Bool { @@ -120,6 +138,7 @@ struct KimiCLICredentialFetchStrategy: ProviderFetchStrategy { apiKey: token, baseURL: baseURL, identityHeaders: identityHeaders, + webAuthToken: self.enrichmentToken(context), transport: self.transport) } catch { throw Self.normalizedCodeAPIError(error) @@ -137,6 +156,29 @@ struct KimiCLICredentialFetchStrategy: ProviderFetchStrategy { guard case KimiAPIError.invalidAPIKey = error else { return error } return KimiAPIError.invalidCodeCredential } + + private func enrichmentToken(_ context: ProviderFetchContext) -> String? { + guard let settings = context.settings?.kimi, settings.cookieSource != .off else { return nil } + return self.resolveWebAuthToken(context) + } +} + +enum KimiWebEnrichmentTokenResolver { + static func resolve(_ context: ProviderFetchContext) -> String? { + guard let settings = context.settings?.kimi, settings.cookieSource != .off else { return nil } + if let override = KimiCookieHeader.resolveCookieOverride(context: context) { + return override.token + } + #if os(macOS) + if let token = KimiCookieImporter.desktopAuthToken() { + return token + } + if let token = try? KimiCookieImporter.importSession().authToken { + return token + } + #endif + return nil + } } private enum KimiCodeAPIFallbackPolicy { @@ -183,6 +225,9 @@ struct KimiWebFetchStrategy: ProviderFetchStrategy { #if os(macOS) if context.settings?.kimi?.cookieSource != .off { + if KimiCookieImporter.desktopAuthToken() != nil { + return true + } return KimiCookieImporter.hasSession() } #endif @@ -220,6 +265,9 @@ struct KimiWebFetchStrategy: ProviderFetchStrategy { // Try browser cookie import when auto mode is enabled #if os(macOS) if context.settings?.kimi?.cookieSource != .off { + if let token = KimiCookieImporter.desktopAuthToken() { + return token + } do { let session = try KimiCookieImporter.importSession() if let token = session.authToken { diff --git a/Sources/CodexBarCore/Providers/Kimi/KimiUsageFetcher.swift b/Sources/CodexBarCore/Providers/Kimi/KimiUsageFetcher.swift index 115fa75f2e..021ab11995 100644 --- a/Sources/CodexBarCore/Providers/Kimi/KimiUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Kimi/KimiUsageFetcher.swift @@ -16,6 +16,7 @@ public struct KimiUsageFetcher: Sendable { apiKey: String, baseURL: URL = KimiSettingsReader.defaultCodeAPIBaseURL, identityHeaders: [String: String] = [:], + webAuthToken: String? = nil, now: Date = Date(), transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> KimiUsageSnapshot { @@ -44,7 +45,13 @@ public struct KimiUsageFetcher: Sendable { throw self.codeAPIError(statusCode: response.statusCode) } - return try self.parseCodeAPIUsage(from: data, now: now) + let snapshot = try self.parseCodeAPIUsage(from: data, now: now) + guard let webAuthToken else { return snapshot } + return try await self.enrichCodeAPIUsage( + snapshot, + webAuthToken: webAuthToken, + now: now, + transport: transport) } static func _parseCodeAPIUsageForTesting(_ data: Data, now: Date = Date()) throws -> KimiUsageSnapshot { @@ -179,6 +186,35 @@ public struct KimiUsageFetcher: Sendable { return codingUsage } + private static func enrichCodeAPIUsage( + _ snapshot: KimiUsageSnapshot, + webAuthToken: String, + now: Date, + transport: any ProviderHTTPTransport) async throws -> KimiUsageSnapshot + { + let sessionInfo = self.decodeSessionInfo(from: webAuthToken) + let subscriptionStats: KimiSubscriptionStatsResponse? + do { + subscriptionStats = try await self.fetchSubscriptionStats( + authToken: webAuthToken, + sessionInfo: sessionInfo, + transport: transport) + } catch is CancellationError { + throw CancellationError() + } catch { + Self.log.warning("Kimi Code monthly enrichment unavailable: \(error.localizedDescription)") + return snapshot + } + guard let subscriptionStats else { return snapshot } + return KimiUsageSnapshot( + weekly: snapshot.weekly, + rateLimit: snapshot.rateLimit, + rateLimitWindow: snapshot.rateLimitWindow, + subscriptionBalance: subscriptionStats.subscriptionBalance, + subscriptionCodeWeeklyLimit: subscriptionStats.ratelimitCode7d, + updatedAt: now) + } + private static func parseCodeAPIUsage(from data: Data, now: Date) throws -> KimiUsageSnapshot { let response = try JSONDecoder().decode(KimiCodeAPIUsageResponse.self, from: data) let rateLimit = response.limits?.first diff --git a/Tests/CodexBarTests/KimiDesktopAuthTokenTests.swift b/Tests/CodexBarTests/KimiDesktopAuthTokenTests.swift new file mode 100644 index 0000000000..86a55ed83a --- /dev/null +++ b/Tests/CodexBarTests/KimiDesktopAuthTokenTests.swift @@ -0,0 +1,176 @@ +#if os(macOS) + +import Foundation +import SQLite3 +import Testing +@testable import CodexBarCore + +struct KimiDesktopAuthTokenTests { + @Test + func `reads newest plaintext Kimi auth token`() throws { + let environment = try Self.makeEnvironment() + defer { try? FileManager.default.removeItem(at: environment.root) } + try Self.createDatabase(at: environment.databaseURL) + try Self.insertCookie( + databaseURL: environment.databaseURL, + host: "www.kimi.com", + value: "older-token", + lastAccess: 1) + try Self.insertCookie( + databaseURL: environment.databaseURL, + host: ".kimi.com", + value: "newer-token", + lastAccess: 2) + + #expect(KimiDesktopAuthToken.load(homeDirectory: environment.root) == "newer-token") + } + + @Test + func `reads active WAL without copying or mutating the database`() throws { + let environment = try Self.makeEnvironment() + defer { try? FileManager.default.removeItem(at: environment.root) } + var db: OpaquePointer? + guard sqlite3_open(environment.databaseURL.path, &db) == SQLITE_OK else { throw SQLiteTestError.open } + defer { sqlite3_close(db) } + try Self.createSchema(db: db) + try Self.exec(db: db, sql: "PRAGMA journal_mode = WAL;") + try Self.insertCookie(db: db, host: "www.kimi.com", value: "active-wal-token", lastAccess: 3) + + #expect(FileManager.default.fileExists(atPath: environment.databaseURL.path + "-wal")) + #expect(KimiDesktopAuthToken.load(homeDirectory: environment.root) == "active-wal-token") + #expect(FileManager.default.fileExists(atPath: environment.databaseURL.path + "-wal")) + } + + @Test + func `reads idle WAL database without creating sidecars`() throws { + let environment = try Self.makeEnvironment() + defer { try? FileManager.default.removeItem(at: environment.root) } + try Self.createDatabase(at: environment.databaseURL) + try Self.insertCookie( + databaseURL: environment.databaseURL, + host: "www.kimi.com", + value: "idle-wal-token", + lastAccess: 4) + try Self.configureIdleWAL(at: environment.databaseURL) + + let walPath = environment.databaseURL.path + "-wal" + let sharedMemoryPath = environment.databaseURL.path + "-shm" + #expect(!FileManager.default.fileExists(atPath: walPath)) + #expect(!FileManager.default.fileExists(atPath: sharedMemoryPath)) + + #expect(KimiDesktopAuthToken.load(homeDirectory: environment.root) == "idle-wal-token") + #expect(!FileManager.default.fileExists(atPath: walPath)) + #expect(!FileManager.default.fileExists(atPath: sharedMemoryPath)) + } + + @Test + func `ignores tokens from unrelated hosts`() throws { + let environment = try Self.makeEnvironment() + defer { try? FileManager.default.removeItem(at: environment.root) } + try Self.createDatabase(at: environment.databaseURL) + try Self.insertCookie( + databaseURL: environment.databaseURL, + host: "example.com", + value: "wrong-host-token", + lastAccess: 5) + + #expect(KimiDesktopAuthToken.load(homeDirectory: environment.root) == nil) + } + + private static func makeEnvironment() throws -> (root: URL, databaseURL: URL) { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent("KimiDesktopAuthTokenTests-\(UUID().uuidString)", isDirectory: true) + let directory = root + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Application Support", isDirectory: true) + .appendingPathComponent("kimi-desktop", isDirectory: true) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + return (root, directory.appendingPathComponent("Cookies", isDirectory: false)) + } + + private static func createDatabase(at url: URL) throws { + var db: OpaquePointer? + guard sqlite3_open(url.path, &db) == SQLITE_OK else { throw SQLiteTestError.open } + defer { sqlite3_close(db) } + try Self.createSchema(db: db) + } + + private static func createSchema(db: OpaquePointer?) throws { + try self.exec( + db: db, + sql: """ + CREATE TABLE cookies ( + host_key TEXT NOT NULL, + name TEXT NOT NULL, + value TEXT NOT NULL, + encrypted_value BLOB NOT NULL DEFAULT X'', + last_access_utc INTEGER NOT NULL + ); + """) + } + + private static func insertCookie( + databaseURL: URL, + host: String, + value: String, + lastAccess: Int64) throws + { + var db: OpaquePointer? + guard sqlite3_open(databaseURL.path, &db) == SQLITE_OK else { throw SQLiteTestError.open } + defer { sqlite3_close(db) } + try Self.insertCookie(db: db, host: host, value: value, lastAccess: lastAccess) + } + + private static func insertCookie( + db: OpaquePointer?, + host: String, + value: String, + lastAccess: Int64) throws + { + var statement: OpaquePointer? + guard sqlite3_prepare_v2( + db, + "INSERT INTO cookies (host_key, name, value, last_access_utc) VALUES (?, 'kimi-auth', ?, ?)", + -1, + &statement, + nil) == SQLITE_OK + else { throw SQLiteTestError.prepare } + defer { sqlite3_finalize(statement) } + let transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + sqlite3_bind_text(statement, 1, host, -1, transient) + sqlite3_bind_text(statement, 2, value, -1, transient) + sqlite3_bind_int64(statement, 3, lastAccess) + guard sqlite3_step(statement) == SQLITE_DONE else { throw SQLiteTestError.exec } + } + + private static func configureIdleWAL(at url: URL) throws { + var db: OpaquePointer? + guard sqlite3_open(url.path, &db) == SQLITE_OK else { throw SQLiteTestError.open } + do { + try Self.exec(db: db, sql: "PRAGMA journal_mode = WAL; PRAGMA wal_checkpoint(TRUNCATE);") + } catch { + sqlite3_close(db) + throw error + } + guard sqlite3_close(db) == SQLITE_OK else { throw SQLiteTestError.close } + for suffix in ["-wal", "-shm"] { + let path = url.path + suffix + if FileManager.default.fileExists(atPath: path) { + try FileManager.default.removeItem(atPath: path) + } + } + } + + private static func exec(db: OpaquePointer?, sql: String) throws { + guard sqlite3_exec(db, sql, nil, nil, nil) == SQLITE_OK else { throw SQLiteTestError.exec } + } + + private enum SQLiteTestError: Error { + case open + case close + case prepare + case exec + } +} + +#endif diff --git a/Tests/CodexBarTests/KimiProviderTests.swift b/Tests/CodexBarTests/KimiProviderTests.swift index 376cba1cd8..b9a361ec24 100644 --- a/Tests/CodexBarTests/KimiProviderTests.swift +++ b/Tests/CodexBarTests/KimiProviderTests.swift @@ -18,7 +18,8 @@ private struct KimiStubClaudeFetcher: ClaudeUsageFetching { private func makeKimiFetchContext( sourceMode: ProviderSourceMode, - environment: [String: String] = [:]) -> ProviderFetchContext + environment: [String: String] = [:], + settings: ProviderSettingsSnapshot? = nil) -> ProviderFetchContext { let env = environment return ProviderFetchContext( @@ -29,7 +30,7 @@ private func makeKimiFetchContext( webDebugDumpHTML: false, verbose: false, env: env, - settings: nil, + settings: settings, fetcher: UsageFetcher(environment: env), claudeFetcher: KimiStubClaudeFetcher(), browserDetection: BrowserDetection(cacheTTL: 0)) @@ -283,6 +284,73 @@ struct KimiSettingsReaderTests { } struct KimiAPIFetchStrategyTests { + @Test + func `cookie source off skips monthly enrichment resolution`() async throws { + let transport = ProviderHTTPTransportStub { request in + let url = try #require(request.url) + #expect(url.path == "/coding/v1/usages") + let response = try #require(HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "application/json"])) + let data = Data(#"{"usage":{"limit":"100","used":"25","remaining":"75"},"limits":[]}"#.utf8) + return (data, response) + } + let strategy = KimiAPIFetchStrategy( + transport: transport, + resolveWebAuthToken: { _ in + Issue.record("Cookie source Off must not resolve desktop or browser sessions") + return "unexpected-web-token" + }) + let context = makeKimiFetchContext( + sourceMode: .api, + environment: ["KIMI_CODE_API_KEY": "api-token"], + settings: .make(kimi: .init(cookieSource: .off, manualCookieHeader: nil))) + + let result = try await strategy.fetch(context) + + #expect(result.usage.primary?.usedPercent == 25) + #expect(result.usage.extraRateWindows == nil) + #expect(await transport.requests().count == 1) + } + + @Test + func `code API usage enriches monthly pool from explicit web session`() async throws { + let transport = ProviderHTTPTransportStub { request in + let url = try #require(request.url) + let response = try #require(HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "application/json"])) + if url.path == "/coding/v1/usages" { + return ( + Data(#"{"usage":{"limit":"100","used":"25","remaining":"75"},"limits":[]}"#.utf8), + response) + } + #expect(url.path.hasSuffix("/GetSubscriptionStats")) + #expect(request.value(forHTTPHeaderField: "Cookie") == "kimi-auth=desktop-token") + return ( + Data(#"{"subscriptionBalance":{"feature":"FEATURE_OMNI","type":"SUBSCRIPTION","amountUsedRatio":0.42}}"# + .utf8), + response) + } + let strategy = KimiAPIFetchStrategy( + transport: transport, + resolveWebAuthToken: { _ in "desktop-token" }) + let context = makeKimiFetchContext( + sourceMode: .api, + environment: ["KIMI_CODE_API_KEY": "api-token"], + settings: .make(kimi: .init(cookieSource: .auto, manualCookieHeader: nil))) + + let result = try await strategy.fetch(context) + let monthly = result.usage.extraRateWindows?.first { $0.id == "kimi-monthly" } + + #expect(monthly?.window.usedPercent == 42) + #expect(await transport.requests().count == 2) + } + @Test func `auto mode accepts CLI credential and reports expired remediation`() async throws { let home = try makeTemporaryKimiCodeHome() diff --git a/docs/kimi.md b/docs/kimi.md index e4bb95b7cc..ae550912be 100644 --- a/docs/kimi.md +++ b/docs/kimi.md @@ -18,6 +18,7 @@ Code subscription credentials. - Displays weekly request quota (from membership tier) - Shows current 5-hour rate limit usage +- Enriches Code API/CLI usage with the monthly membership pool when a web session is available - API-key, Kimi Code CLI, automatic cookie, and manual cookie authentication methods - Automatic refresh countdown @@ -68,6 +69,11 @@ reuse; use an explicit API key for endpoint-override testing. **Note**: Requires Full Disk Access to read browser cookies (System Settings → Privacy & Security → Full Disk Access → CodexBar). +Automatic mode also checks the official Kimi Desktop app before importing browser cookies. Its Chromium +Cookies database is opened read-only: active WAL databases use SQLite's normal WAL-aware path, while idle +WAL-mode databases with no sidecars use an immutable read-only fallback. CodexBar never creates or modifies +Kimi Desktop database files. + ### Method 4: Manual Token Entry For advanced users or when automatic import fails: @@ -96,7 +102,12 @@ When multiple sources are available, CodexBar uses this order: 2. Fresh Kimi Code CLI access token (`~/.kimi-code/credentials/kimi-code.json`) 3. Manual cookie/token (from Settings UI) when web fallback is used 4. Cookie environment variable (`KIMI_AUTH_TOKEN`) -5. Browser cookies (Arc → Chrome → Safari → Edge → Brave → Chromium) +5. Kimi Desktop `kimi-auth` cookie +6. Browser cookies (Arc → Chrome → Safari → Edge → Brave → Chromium) + +For Code API and CLI results, sources 3–6 are best-effort enrichment only: the required Code usage remains +available if the membership request fails. Setting **Cookie source** to **Off** disables this enrichment and +does not inspect Kimi Desktop or browser cookies. **Note**: Browser cookie import requires Full Disk Access permission.