pi-kimi-keepalive 0.3.2 → 0.3.5

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +52 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-kimi-keepalive",
3
- "version": "0.3.2",
3
+ "version": "0.3.5",
4
4
  "description": "Prompt-cache keepalive for Kimi (kimi-coding) sessions in the Pi coding agent. Replays the last real provider request while idle so the automatic prefix cache never expires.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -51,7 +51,22 @@ import {
51
51
 
52
52
  const STATE_DIR = join(homedir(), ".pi", "cache-keepalive");
53
53
  const STATE_FILE = join(STATE_DIR, "state.json");
54
- const AUTH_FILE = join(process.env.PI_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "auth.json");
54
+
55
+ /** Package version, read from the package.json that ships this extension. */
56
+ function ownVersion(): string {
57
+ try {
58
+ const pkg = JSON.parse(
59
+ readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
60
+ ) as { version?: unknown };
61
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
62
+ } catch {
63
+ return "unknown";
64
+ }
65
+ }
66
+ /** Live OAuth state; resolved lazily so tests can redirect it via PI_AGENT_DIR. */
67
+ function authFile(): string {
68
+ return join(process.env.PI_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "auth.json");
69
+ }
55
70
 
56
71
  interface PersistedConfig {
57
72
  enabled: boolean;
@@ -473,22 +488,26 @@ export default function (pi: ExtensionAPI) {
473
488
  */
474
489
  function probeHeaders(): Record<string, string> | null {
475
490
  if (!capture) return null;
476
- const headers = buildProbeHeaders(capture.headers);
477
- if (!headers.authorization) {
478
- try {
479
- const raw = JSON.parse(readFileSync(AUTH_FILE, "utf8")) as Record<string, unknown>;
480
- const entry = (raw["kimi-coding"] ?? null) as { access?: unknown } | null;
481
- if (entry && typeof entry.access === "string" && entry.access.length > 0) {
482
- headers.authorization = `Bearer ${entry.access}`;
483
- } else {
484
- debug(`no kimi-coding access token in ${AUTH_FILE}`);
485
- }
486
- } catch (error) {
487
- debug(`auth.json unavailable: ${error instanceof Error ? error.message : String(error)}`);
491
+ // Prefer the live token from auth.json: pi refreshes it there on real
492
+ // requests, while captured request headers freeze the token from capture
493
+ // time — hours later that Bearer can already be expired (HTTP 401).
494
+ // Fall back to the captured headers when auth.json is unavailable.
495
+ try {
496
+ const raw = JSON.parse(readFileSync(authFile(), "utf8")) as Record<string, unknown>;
497
+ const entry = (raw["kimi-coding"] ?? null) as { access?: unknown } | null;
498
+ if (entry && typeof entry.access === "string" && entry.access.length > 0) {
499
+ const headers = buildProbeHeaders(capture.headers);
500
+ headers.authorization = `Bearer ${entry.access}`;
501
+ return headers;
488
502
  }
503
+ debug(`no kimi-coding access token in ${authFile()}`);
504
+ } catch (error) {
505
+ debug(
506
+ `auth.json unavailable (${error instanceof Error ? error.message : String(error)}) — falling back to captured headers`,
507
+ );
489
508
  }
490
- if (!headers.authorization) return null;
491
- return headers;
509
+ const headers = buildProbeHeaders(capture.headers);
510
+ return headers.authorization ? headers : null;
492
511
  }
493
512
 
494
513
  async function runProbe(): Promise<boolean> {
@@ -710,7 +729,7 @@ export default function (pi: ExtensionAPI) {
710
729
  const savings = hasPricing(capture?.cost) ? formatUsd(stats.savedUsd) : "n/a (no price data)";
711
730
  ctx.ui.setStatus("cache-keepalive", `♥ ${state}`);
712
731
  ctx.ui.setWidget("cache-keepalive", [
713
- "pi-kimi-keepalive",
732
+ `pi-kimi-keepalive v${ownVersion()}`,
714
733
  ` state: ${state}`,
715
734
  ` probes: ${stats.probes} sent · ${stats.hits} hits · ${stats.misses} misses · ${stats.errors} errors`,
716
735
  ` est.: saved ${savings} · probe spend ${formatUsd(stats.spendUsd)} · cap ${config.spendCapUsd === null ? "none" : formatUsd(config.spendCapUsd)}`,
@@ -733,7 +752,7 @@ export default function (pi: ExtensionAPI) {
733
752
  ? `${capture.provider} (${capture.api ?? "unknown api"}) @ ${capture.baseUrl}`
734
753
  : "none yet — probes start after your first real turn";
735
754
  return [
736
- "pi-kimi-keepalive",
755
+ `pi-kimi-keepalive v${ownVersion()}`,
737
756
  ` state: ${config.enabled ? "on" : "off"}${pausedReason ? ` (paused: ${pausedReason})` : ""}`,
738
757
  ` capture: ${route}`,
739
758
  ` mode: ${config.mode}${config.mode === "smart" ? ` · cadence ${formatDuration(config.intervalMs)}${lastProbeInputTokens > SMART_MAX_CONTEXT_TOKENS ? ` · context ${lastProbeInputTokens.toLocaleString()} > cap, frozen at floor` : ""}` : " (fixed via interval=)"}`,
@@ -952,8 +971,23 @@ export default function (pi: ExtensionAPI) {
952
971
  const raw = payload as Record<string, unknown>;
953
972
  if (!Array.isArray(raw.messages) || raw.messages.length === 0) return;
954
973
  if (raw.system !== undefined && !Array.isArray(raw.system) && typeof raw.system !== "string") return;
974
+ let clonedPayload: Record<string, unknown>;
975
+ try {
976
+ clonedPayload = structuredClone(raw);
977
+ } catch {
978
+ // DataCloneError: the payload carries non-cloneable values (BigInt,
979
+ // symbols, functions). Fall back to a JSON round-trip; probing never
980
+ // mutates the captured payload, so a by-reference capture is the last
981
+ // resort rather than failing the real request.
982
+ try {
983
+ clonedPayload = JSON.parse(JSON.stringify(raw)) as Record<string, unknown>;
984
+ } catch {
985
+ debug("payload not cloneable — capturing by reference");
986
+ clonedPayload = raw;
987
+ }
988
+ }
955
989
  capture = {
956
- payload: structuredClone(raw),
990
+ payload: clonedPayload,
957
991
  headers: capturedHeaders,
958
992
  provider: model.provider,
959
993
  api: (model as { api?: string }).api,