pi-kimi-keepalive 0.3.1 → 0.3.4

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 +49 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-kimi-keepalive",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
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,10 @@ 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
+ /** Live OAuth state; resolved lazily so tests can redirect it via PI_AGENT_DIR. */
55
+ function authFile(): string {
56
+ return join(process.env.PI_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "auth.json");
57
+ }
55
58
 
56
59
  interface PersistedConfig {
57
60
  enabled: boolean;
@@ -202,7 +205,7 @@ export default function (pi: ExtensionAPI) {
202
205
  maxMissStreak: int(raw.maxMissStreak, DEFAULT_CONFIG.maxMissStreak, 1),
203
206
  maxErrorStreak: int(raw.maxErrorStreak, DEFAULT_CONFIG.maxErrorStreak, 1),
204
207
  mode: raw.mode === "smart" ? "smart" : "default",
205
- initialized: raw.initialized === true,
208
+ initialized: raw.initialized === true,
206
209
  };
207
210
  } catch {
208
211
  config = { ...DEFAULT_CONFIG };
@@ -233,8 +236,10 @@ export default function (pi: ExtensionAPI) {
233
236
  }
234
237
 
235
238
  const confirmNext = await wizardCtx.ui.confirm(
236
- "pi-kimi-keepalive — first-time setup",
237
- "Set the keepalive guardrails. Press Esc at any prompt to keep the default. " +
239
+ opts.firstRun
240
+ ? "pi-kimi-keepalive first-time setup"
241
+ : "pi-kimi-keepalive — reconfigure",
242
+ "Set the keepalive guardrails. Press Esc at any prompt to keep the current value. " +
238
243
  "All values are saved to ~/.pi/cache-keepalive/state.json and can be changed later " +
239
244
  "via /keepalive <setting>.",
240
245
  );
@@ -471,22 +476,26 @@ export default function (pi: ExtensionAPI) {
471
476
  */
472
477
  function probeHeaders(): Record<string, string> | null {
473
478
  if (!capture) return null;
474
- const headers = buildProbeHeaders(capture.headers);
475
- if (!headers.authorization) {
476
- try {
477
- const raw = JSON.parse(readFileSync(AUTH_FILE, "utf8")) as Record<string, unknown>;
478
- const entry = (raw["kimi-coding"] ?? null) as { access?: unknown } | null;
479
- if (entry && typeof entry.access === "string" && entry.access.length > 0) {
480
- headers.authorization = `Bearer ${entry.access}`;
481
- } else {
482
- debug(`no kimi-coding access token in ${AUTH_FILE}`);
483
- }
484
- } catch (error) {
485
- debug(`auth.json unavailable: ${error instanceof Error ? error.message : String(error)}`);
479
+ // Prefer the live token from auth.json: pi refreshes it there on real
480
+ // requests, while captured request headers freeze the token from capture
481
+ // time — hours later that Bearer can already be expired (HTTP 401).
482
+ // Fall back to the captured headers when auth.json is unavailable.
483
+ try {
484
+ const raw = JSON.parse(readFileSync(authFile(), "utf8")) as Record<string, unknown>;
485
+ const entry = (raw["kimi-coding"] ?? null) as { access?: unknown } | null;
486
+ if (entry && typeof entry.access === "string" && entry.access.length > 0) {
487
+ const headers = buildProbeHeaders(capture.headers);
488
+ headers.authorization = `Bearer ${entry.access}`;
489
+ return headers;
486
490
  }
491
+ debug(`no kimi-coding access token in ${authFile()}`);
492
+ } catch (error) {
493
+ debug(
494
+ `auth.json unavailable (${error instanceof Error ? error.message : String(error)}) — falling back to captured headers`,
495
+ );
487
496
  }
488
- if (!headers.authorization) return null;
489
- return headers;
497
+ const headers = buildProbeHeaders(capture.headers);
498
+ return headers.authorization ? headers : null;
490
499
  }
491
500
 
492
501
  async function runProbe(): Promise<boolean> {
@@ -670,6 +679,11 @@ export default function (pi: ExtensionAPI) {
670
679
  errorStreak += 1;
671
680
  debug("probe failed:", message);
672
681
  if (/^HTTP 40[13]\b/.test(message)) {
682
+ // Credentials are dead regardless of history; clear the streaks so the
683
+ // automatic recovery after the next real turn starts from a clean slate.
684
+ missStreak = 0;
685
+ errorStreak = 0;
686
+ smartHitStreak = 0;
673
687
  pause("captured credentials rejected; will recapture after your next real turn");
674
688
  return;
675
689
  }
@@ -843,6 +857,7 @@ export default function (pi: ExtensionAPI) {
843
857
  notify("usage: /keepalive maxidle=30m (0 disables the cutoff)", "error");
844
858
  break;
845
859
  }
860
+ config.maxIdleMs = ms;
846
861
  persistConfig();
847
862
  notify(ms === 0 ? "maxidle disabled — probing continues while idle" : `maxidle set to ${formatDuration(ms)}`, "info");
848
863
  break;
@@ -944,8 +959,23 @@ export default function (pi: ExtensionAPI) {
944
959
  const raw = payload as Record<string, unknown>;
945
960
  if (!Array.isArray(raw.messages) || raw.messages.length === 0) return;
946
961
  if (raw.system !== undefined && !Array.isArray(raw.system) && typeof raw.system !== "string") return;
962
+ let clonedPayload: Record<string, unknown>;
963
+ try {
964
+ clonedPayload = structuredClone(raw);
965
+ } catch {
966
+ // DataCloneError: the payload carries non-cloneable values (BigInt,
967
+ // symbols, functions). Fall back to a JSON round-trip; probing never
968
+ // mutates the captured payload, so a by-reference capture is the last
969
+ // resort rather than failing the real request.
970
+ try {
971
+ clonedPayload = JSON.parse(JSON.stringify(raw)) as Record<string, unknown>;
972
+ } catch {
973
+ debug("payload not cloneable — capturing by reference");
974
+ clonedPayload = raw;
975
+ }
976
+ }
947
977
  capture = {
948
- payload: structuredClone(raw),
978
+ payload: clonedPayload,
949
979
  headers: capturedHeaders,
950
980
  provider: model.provider,
951
981
  api: (model as { api?: string }).api,