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.
- package/package.json +1 -1
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
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
|
-
|
|
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:
|
|
978
|
+
payload: clonedPayload,
|
|
949
979
|
headers: capturedHeaders,
|
|
950
980
|
provider: model.provider,
|
|
951
981
|
api: (model as { api?: string }).api,
|