From 4df98757e3bedec1470d36d69d14687517c79db6 Mon Sep 17 00:00:00 2001 From: lemon07r Date: Thu, 16 Apr 2026 23:24:46 -0400 Subject: [PATCH] Add constants and X-Msh-* header builder with persistent ~/.kimi/device_id --- src/constants.ts | 25 +++++++++++++++++++++++++ src/headers.ts | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/constants.ts create mode 100644 src/headers.ts diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..4da5df1 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,25 @@ +// Values mirror kimi-cli v1.35.0 1:1. When upstream bumps, update here and +// nothing else in the codebase should hard-code these strings. +// +// Source of truth: research/kimi-cli/src/kimi_cli/constant.py, +// research/kimi-cli/src/kimi_cli/auth/oauth.py +// +// NOTE: client_id is a public constant shipped inside the official CLI, not a +// secret. scope `kimi-code` is what routes the issued JWT to K2.6. + +export const KIMI_CLI_VERSION = "1.35.0" +export const USER_AGENT = `KimiCodeCLI/${KIMI_CLI_VERSION}` + +export const OAUTH_HOST = "https://auth.kimi.com" +export const OAUTH_DEVICE_AUTH_URL = `${OAUTH_HOST}/api/oauth/device_authorization` +export const OAUTH_TOKEN_URL = `${OAUTH_HOST}/api/oauth/token` +export const OAUTH_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098" +export const OAUTH_SCOPE = "kimi-code" +export const OAUTH_DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code" +export const OAUTH_REFRESH_GRANT = "refresh_token" + +export const API_BASE_URL = "https://api.kimi.com/coding/v1" +export const MODEL_ID = "kimi-for-coding" + +// Refresh a bit before the server-reported expiry so we never race it. +export const REFRESH_SAFETY_WINDOW_MS = 60_000 diff --git a/src/headers.ts b/src/headers.ts new file mode 100644 index 0000000..e805010 --- /dev/null +++ b/src/headers.ts @@ -0,0 +1,47 @@ +import os from "node:os" +import fs from "node:fs" +import path from "node:path" +import crypto from "node:crypto" +import { KIMI_CLI_VERSION, USER_AGENT } from "./constants.ts" + +// kimi-cli persists its device id at `~/.kimi/device_id` as a plain UUIDv4 +// hex string (no dashes). We intentionally share the same path so users who +// also run the real kimi CLI keep a single stable fingerprint. See +// research/kimi-cli/src/kimi_cli/auth/oauth.py (get_device_id). +const DEVICE_ID_DIR = path.join(os.homedir(), ".kimi") +const DEVICE_ID_PATH = path.join(DEVICE_ID_DIR, "device_id") + +function ensureDir(dir: string) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 }) +} + +export function getDeviceId(): string { + ensureDir(DEVICE_ID_DIR) + if (fs.existsSync(DEVICE_ID_PATH)) { + const existing = fs.readFileSync(DEVICE_ID_PATH, "utf8").trim() + if (existing) return existing + } + const id = crypto.randomUUID().replace(/-/g, "") + fs.writeFileSync(DEVICE_ID_PATH, id, { mode: 0o600 }) + return id +} + +// Non-ASCII characters in HTTP headers will be rejected by Node's undici +// fetch (`TypeError: Invalid character in header content`). kimi-cli does the +// same sanitization in oauth._ascii_header_value. +function ascii(value: string): string { + return value.replace(/[^\x20-\x7e]/g, "?") +} + +/** Builds the 7 X-Msh-* / UA headers kimi-cli sends on every request. */ +export function kimiHeaders(): Record { + return { + "User-Agent": USER_AGENT, + "X-Msh-Platform": "kimi_cli", + "X-Msh-Version": KIMI_CLI_VERSION, + "X-Msh-Device-Name": ascii(os.hostname() || "unknown"), + "X-Msh-Device-Model": ascii(os.machine?.() || os.arch()), + "X-Msh-Device-Id": getDeviceId(), + "X-Msh-Os-Version": ascii(`${os.type()} ${os.release()}`), + } +}