pi-provider-kimi-code 0.6.6 → 0.6.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
5
5
  "keywords": [
6
6
  "ai-provider",
@@ -49,12 +49,16 @@
49
49
  "format:check": "oxfmt . --check",
50
50
  "pre-commit": "prek run"
51
51
  },
52
+ "dependencies": {
53
+ "proper-lockfile": "^4.1.2"
54
+ },
52
55
  "devDependencies": {
53
- "@earendil-works/pi-ai": "^0.79.10",
54
- "@earendil-works/pi-coding-agent": "^0.79.10",
55
- "@earendil-works/pi-tui": "^0.79.10",
56
+ "@earendil-works/pi-ai": "^0.80.9",
57
+ "@earendil-works/pi-coding-agent": "^0.80.9",
58
+ "@earendil-works/pi-tui": "^0.80.9",
56
59
  "@j178/prek": "^0.4.4",
57
60
  "@types/node": "^24.0.0",
61
+ "@types/proper-lockfile": "^4.1.4",
58
62
  "oxfmt": "^0.44.0",
59
63
  "oxlint": "^1.59.0",
60
64
  "typescript": "^5.9.0"
package/src/constants.ts CHANGED
@@ -41,7 +41,7 @@ export function getDefaultBaseUrl(protocol: KimiWireProtocol): string {
41
41
 
42
42
  export const DEFAULT_BASE_URL = getDefaultBaseUrl(ENV_KIMI_CODE_PROTOCOL);
43
43
 
44
- export const PROVIDER_VERSION = "0.6.6";
44
+ export const PROVIDER_VERSION = "0.6.7";
45
45
 
46
46
  // Upstream kimi-code CLI version — used in User-Agent and X-Msh-Version
47
47
  // headers to match the official client's identity. Update this when
package/src/oauth.ts CHANGED
@@ -2,13 +2,94 @@
2
2
  // reuse, the login / refresh handlers wired into pi's OAuth interface, and
3
3
  // the stream-level auth refresh used by the streaming handler.
4
4
 
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import os from "node:os";
7
7
  import { dirname, join } from "node:path";
8
8
 
9
+ import { lock as acquireFileLock } from "proper-lockfile";
10
+
9
11
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
10
- import type { OAuthCredential } from "@earendil-works/pi-coding-agent";
11
- import { AuthStorage } from "@earendil-works/pi-coding-agent";
12
+ import * as piAgent from "@earendil-works/pi-coding-agent";
13
+
14
+ // Structural mirrors of pi's stored credential shapes. The exported names
15
+ // moved between pi versions (pi-coding-agent <=0.79 vs pi-ai >=0.80.8), so
16
+ // these are defined locally to type-check against either surface.
17
+ type StoredCredential = { type: string } & Record<string, unknown>;
18
+
19
+ export interface StoredOAuthCredential {
20
+ type: "oauth";
21
+ access: string;
22
+ refresh: string;
23
+ expires: number;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ // pi renamed its auth storage surface in 0.80.8: the exported AuthStorage
28
+ // class (sync get/set under a file lock) was replaced by readStoredCredential
29
+ // for one-off reads, with no exported write path. Detect whichever the host
30
+ // pi provides so the extension keeps loading on both sides of the rename.
31
+ const piAgentRuntime = piAgent as unknown as {
32
+ getAgentDir(): string;
33
+ readStoredCredential?(providerId: string): StoredCredential | undefined;
34
+ AuthStorage?: {
35
+ create(): {
36
+ get(provider: string): StoredCredential | undefined;
37
+ set(provider: string, credential: StoredCredential): void;
38
+ };
39
+ };
40
+ };
41
+
42
+ export function readStoredOAuthCredential(providerId: string): StoredOAuthCredential | null {
43
+ const credential = piAgentRuntime.readStoredCredential
44
+ ? piAgentRuntime.readStoredCredential(providerId)
45
+ : piAgentRuntime.AuthStorage?.create().get(providerId);
46
+ return credential?.type === "oauth" ? (credential as StoredOAuthCredential) : null;
47
+ }
48
+
49
+ // Mutual exclusion with pi's own credential writes: use the same library
50
+ // (proper-lockfile) on the same file pi's FileAuthStorageBackend locks, with
51
+ // the same options. The shared implementation keeps the lock mtime fresh
52
+ // while held, recovers stale locks from crashed processes, and retries for
53
+ // ~40s so a live holder running a slow network refresh inside the lock is
54
+ // waited out.
55
+ async function acquireAuthFileLock(authPath: string): Promise<() => Promise<void>> {
56
+ return acquireFileLock(authPath, {
57
+ realpath: false,
58
+ stale: 30_000,
59
+ retries: { retries: 10, factor: 2, minTimeout: 100, maxTimeout: 10_000, randomize: true },
60
+ });
61
+ }
62
+
63
+ async function writeStoredCredential(
64
+ providerId: string,
65
+ credential: StoredOAuthCredential,
66
+ ): Promise<void> {
67
+ const AuthStorage = piAgentRuntime.AuthStorage;
68
+ if (AuthStorage) {
69
+ AuthStorage.create().set(providerId, credential);
70
+ return;
71
+ }
72
+ // pi >=0.80.8: no exported write path, so do a locked read-modify-write of
73
+ // auth.json in pi's on-disk format (indent 2). The read happens inside the
74
+ // lock so concurrent updates from other processes are never clobbered, and
75
+ // the explicit chmod covers pre-existing files (writeFileSync's mode only
76
+ // applies on creation).
77
+ const authPath = join(piAgentRuntime.getAgentDir(), "auth.json");
78
+ const release = await acquireAuthFileLock(authPath);
79
+ try {
80
+ let data: Record<string, unknown> = {};
81
+ try {
82
+ data = JSON.parse(readFileSync(authPath, "utf-8"));
83
+ } catch {
84
+ // Missing or unreadable auth.json — start from an empty store.
85
+ }
86
+ data[providerId] = credential;
87
+ writeFileSync(authPath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 0o600 });
88
+ chmodSync(authPath, 0o600);
89
+ } finally {
90
+ await release();
91
+ }
92
+ }
12
93
 
13
94
  import { CLIENT_ID, PROVIDER_ID, RETRYABLE_REFRESH_STATUSES, getOAuthHost } from "./constants.ts";
14
95
  import { getCommonHeaders } from "./device.ts";
@@ -424,8 +505,8 @@ export async function refreshKimiCodeToken(
424
505
  // `expires` is in the past. If the server rotates/revokes the access token
425
506
  // before that (common with short-lived session tokens), every request keeps
426
507
  // returning 401. The streaming handler invokes refreshKimiAuthToken on the
427
- // first auth error to force a refresh through AuthStorage (which persists the
428
- // new credentials under a file lock), and retry once.
508
+ // first auth error to force a refresh and persist the new credentials, and
509
+ // retry once.
429
510
  // =============================================================================
430
511
 
431
512
  export function getKimiApiKey(credentials: OAuthCredentials): string {
@@ -453,9 +534,7 @@ export async function refreshKimiAuthToken(
453
534
  ): Promise<string | null> {
454
535
  try {
455
536
  const kimiCred = readKimiCliCredentials();
456
- const storage = AuthStorage.create();
457
- const piCred = storage.get(PROVIDER_ID);
458
- const piOAuth = piCred?.type === "oauth" ? piCred : null;
537
+ const piOAuth = readStoredOAuthCredential(PROVIDER_ID);
459
538
 
460
539
  if (piOAuth) {
461
540
  if (piOAuth.access !== currentKey && Date.now() < piOAuth.expires) {
@@ -471,14 +550,14 @@ export async function refreshKimiAuthToken(
471
550
  if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
472
551
  const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
473
552
  if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
474
- const recovered: OAuthCredential = {
553
+ const recovered: StoredOAuthCredential = {
475
554
  ...piOAuth,
476
555
  type: "oauth",
477
556
  access: kimiCred.access_token!,
478
557
  refresh: kimiCred.refresh_token,
479
558
  expires: kimiExpiresMs,
480
559
  };
481
- storage.set(PROVIDER_ID, recovered);
560
+ await writeStoredCredential(PROVIDER_ID, recovered);
482
561
  console.error("[kimi-coding] auth refresh: recovered newer kimi-code token");
483
562
  return recovered.access;
484
563
  }
@@ -487,14 +566,14 @@ export async function refreshKimiAuthToken(
487
566
  }
488
567
 
489
568
  const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
490
- const newCred: OAuthCredential = {
569
+ const newCred: StoredOAuthCredential = {
491
570
  ...piOAuth,
492
571
  type: "oauth",
493
572
  access: refreshed.access_token,
494
573
  refresh: refreshed.refresh_token,
495
574
  expires: newExpiresMs,
496
575
  };
497
- storage.set(PROVIDER_ID, newCred);
576
+ await writeStoredCredential(PROVIDER_ID, newCred);
498
577
  writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
499
578
  console.error("[kimi-coding] auth refresh: new token persisted");
500
579
  return newCred.access;
package/src/stream.ts CHANGED
@@ -12,11 +12,46 @@ import type {
12
12
  Model,
13
13
  SimpleStreamOptions,
14
14
  } from "@earendil-works/pi-ai";
15
- import {
16
- createAssistantMessageEventStream,
17
- streamSimpleAnthropic,
18
- streamSimpleOpenAICompletions,
19
- } from "@earendil-works/pi-ai";
15
+ import * as piAi from "@earendil-works/pi-ai";
16
+ import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
17
+
18
+ type KimiStreamSimple = (
19
+ model: Model<Api>,
20
+ context: Context,
21
+ options?: SimpleStreamOptions,
22
+ ) => AssistantMessageEventStream;
23
+
24
+ // pi 0.80.8 unexported the global streamSimple* functions. Depending on how
25
+ // this module is loaded, the pi-ai root now offers one of three shapes:
26
+ // - pi extension loader (jiti/Bun): root aliases to the compat entrypoint,
27
+ // which exposes lazy per-API factories (anthropicMessagesApi et al.)
28
+ // - pi <=0.79: root only has the legacy streamSimple* globals
29
+ // - plain Node with pi-ai >=0.80.8 (tests, SDK use): root has neither, but
30
+ // lazyApi + a dynamic subpath import reaches the same implementation
31
+ // Detect whichever shape is present so the extension loads in all three.
32
+ const piAiRuntime = piAi as unknown as {
33
+ anthropicMessagesApi?: () => { streamSimple: KimiStreamSimple };
34
+ openAICompletionsApi?: () => { streamSimple: KimiStreamSimple };
35
+ streamSimpleAnthropic?: KimiStreamSimple;
36
+ streamSimpleOpenAICompletions?: KimiStreamSimple;
37
+ lazyApi?: (load: () => Promise<object>) => { streamSimple: KimiStreamSimple };
38
+ };
39
+
40
+ // The ./api/* subpath exports only exist on pi-ai >=0.80.8, so a literal
41
+ // specifier would fail type resolution against older pi-ai. Computed
42
+ // specifiers keep the imports invisible to TypeScript; they are only ever
43
+ // evaluated in plain-Node environments where the subpaths do resolve.
44
+ const anthropicMessagesModule = "@earendil-works/pi-ai/api/anthropic-messages";
45
+ const openAICompletionsModule = "@earendil-works/pi-ai/api/openai-completions";
46
+
47
+ const streamSimpleAnthropic: KimiStreamSimple =
48
+ piAiRuntime.anthropicMessagesApi?.().streamSimple ??
49
+ piAiRuntime.streamSimpleAnthropic ??
50
+ piAiRuntime.lazyApi?.(() => import(anthropicMessagesModule)).streamSimple!;
51
+ const streamSimpleOpenAICompletions: KimiStreamSimple =
52
+ piAiRuntime.openAICompletionsApi?.().streamSimple ??
53
+ piAiRuntime.streamSimpleOpenAICompletions ??
54
+ piAiRuntime.lazyApi?.(() => import(openAICompletionsModule)).streamSimple!;
20
55
  import {
21
56
  DEFAULT_KIMI_CODE_CONFIG,
22
57
  type KimiCodeConfig,
@@ -162,7 +197,16 @@ export function resolveKimiApiKey(apiKey: string | undefined): string {
162
197
  return apiKey || process.env.KIMI_API_KEY || "";
163
198
  }
164
199
 
165
- export function mergeKimiRequestHeaders(headers?: Record<string, string>): Record<string, string> {
200
+ // SimpleStreamOptions.headers is Record<string, string> on pi <=0.79 and
201
+ // ProviderHeaders (Record<string, string | null>, null = suppress the header
202
+ // when pi-ai assembles the request) on >=0.80.8. Accept and preserve both
203
+ // shapes; defined locally because ProviderHeaders is not exported on old pi.
204
+ type HeaderMap = Record<string, string | null>;
205
+
206
+ export function mergeKimiRequestHeaders(headers?: HeaderMap): HeaderMap {
207
+ // Caller values override our Kimi defaults. Nulls are retained rather than
208
+ // deleted so the suppression semantics reach pi-ai, which strips null
209
+ // entries (including its own defaults) when building the request.
166
210
  return { ...getKimiProviderHeaders(), ...headers };
167
211
  }
168
212
 
@@ -231,7 +275,9 @@ export function streamSimpleKimi(
231
275
  return {
232
276
  ...options,
233
277
  ...apiKeyOverride,
234
- headers: mergeKimiRequestHeaders(options?.headers),
278
+ // Cast to whatever headers shape the installed pi-ai declares:
279
+ // Record<string, string> on pi <=0.79, ProviderHeaders on >=0.80.8.
280
+ headers: mergeKimiRequestHeaders(options?.headers) as SimpleStreamOptions["headers"],
235
281
  onPayload: async (payload, modelData) => {
236
282
  let nextPayload: unknown = payload;
237
283
 
@@ -1,9 +1,13 @@
1
- import { AuthStorage, type AgentToolResult } from "@earendil-works/pi-coding-agent";
1
+ import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth } from "@earendil-works/pi-tui";
3
3
 
4
4
  import { PROVIDER_ID, getBaseUrl } from "../constants.ts";
5
5
  import { getKimiProviderHeaders } from "../device.ts";
6
- import { KIMI_LOGIN_REQUIRED_MESSAGE, refreshKimiAuthToken } from "../oauth.ts";
6
+ import {
7
+ KIMI_LOGIN_REQUIRED_MESSAGE,
8
+ readStoredOAuthCredential,
9
+ refreshKimiAuthToken,
10
+ } from "../oauth.ts";
7
11
 
8
12
  export const KIMI_TOOL_TIMEOUT_MS = 180_000;
9
13
 
@@ -19,9 +23,7 @@ export interface BuildKimiToolOptions {
19
23
  }
20
24
 
21
25
  function defaultGetAccessToken(): string | null {
22
- const credential = AuthStorage.create().get(PROVIDER_ID);
23
- if (!credential || credential.type !== "oauth") return null;
24
- return credential.access;
26
+ return readStoredOAuthCredential(PROVIDER_ID)?.access ?? null;
25
27
  }
26
28
 
27
29
  export function buildKimiToolDeps(options: BuildKimiToolOptions = {}): KimiToolDeps {
package/src/usage.ts CHANGED
@@ -1,8 +1,6 @@
1
- import { AuthStorage } from "@earendil-works/pi-coding-agent";
2
-
3
1
  import { PROVIDER_ID, getBaseUrl } from "./constants.ts";
4
2
  import { getKimiProviderHeaders } from "./device.ts";
5
- import { refreshKimiAuthToken } from "./oauth.ts";
3
+ import { readStoredOAuthCredential, refreshKimiAuthToken } from "./oauth.ts";
6
4
 
7
5
  const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
8
6
  LEVEL_FREE: "Free",
@@ -52,8 +50,8 @@ interface BoosterWalletInfo {
52
50
  const FIXED_POINT_CENTS = 1_000_000n;
53
51
 
54
52
  export function getKimiUsageToken(): string | null {
55
- const credential = AuthStorage.create().get(PROVIDER_ID);
56
- if (credential?.type === "oauth" && credential.access) return credential.access;
53
+ const credential = readStoredOAuthCredential(PROVIDER_ID);
54
+ if (credential?.access) return credential.access;
57
55
  const apiKey = process.env.KIMI_API_KEY?.trim();
58
56
  return apiKey || null;
59
57
  }