pi-spark 0.19.0 → 0.19.2

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/README.md CHANGED
@@ -155,7 +155,7 @@ All fields are optional. If the recap model configuration is incomplete, pi-spar
155
155
 
156
156
  | Field | Value | Description |
157
157
  | --- | --- | --- |
158
- | `idle` | number (ms) or duration string | How long the session must stay idle before a recap is generated. Accepts a millisecond number or a [vercel/ms](https://github.com/vercel/ms) string (e.g., `"5m"`); minimum 5000 ms, defaults to 5 minutes. |
158
+ | `idle` | number (ms) or duration string | How long the session must stay idle before a recap is generated. Accepts a millisecond number or a [parse-duration](https://github.com/jkroso/parse-duration) string (e.g., `"5m"`); minimum 5000 ms, defaults to 5 minutes. |
159
159
  | `provider` | string | Provider ID for the recap model. |
160
160
  | `model` | string | Model ID for the recap model. |
161
161
  | `thinkingLevel` | `ModelThinkingLevel` | Thinking level for the recap model. Defaults to `off`. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-spark",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "Pi package that polishes your daily experience.",
5
5
  "keywords": [
6
6
  "pi-coding-agent",
@@ -33,21 +33,22 @@
33
33
  "@grpc/grpc-js": "^1.14.4",
34
34
  "@grpc/proto-loader": "^0.8.1",
35
35
  "defu": "^6.1.7",
36
- "ms": "4.0.0-nightly.202508271359",
36
+ "parse-duration": "^2.1.8",
37
+ "pretty-ms": "^9.3.0",
37
38
  "zod": "^4.4.3"
38
39
  },
39
40
  "peerDependencies": {
40
- "@earendil-works/pi-agent-core": ">=0.80.6",
41
- "@earendil-works/pi-ai": ">=0.80.6",
42
- "@earendil-works/pi-coding-agent": ">=0.80.6",
43
- "@earendil-works/pi-tui": ">=0.80.6",
41
+ "@earendil-works/pi-agent-core": ">=0.80.8",
42
+ "@earendil-works/pi-ai": ">=0.80.8",
43
+ "@earendil-works/pi-coding-agent": ">=0.80.8",
44
+ "@earendil-works/pi-tui": ">=0.80.8",
44
45
  "typebox": "*"
45
46
  },
46
47
  "devDependencies": {
47
- "@earendil-works/pi-agent-core": ">=0.80.6",
48
- "@earendil-works/pi-ai": ">=0.80.6",
49
- "@earendil-works/pi-coding-agent": ">=0.80.6",
50
- "@earendil-works/pi-tui": ">=0.80.6",
48
+ "@earendil-works/pi-agent-core": ">=0.80.8",
49
+ "@earendil-works/pi-ai": ">=0.80.8",
50
+ "@earendil-works/pi-coding-agent": ">=0.80.8",
51
+ "@earendil-works/pi-tui": ">=0.80.8",
51
52
  "@types/node": "*",
52
53
  "typebox": "*",
53
54
  "typescript": "^7.0.2"
@@ -51,7 +51,7 @@ export class CreditsManager {
51
51
  }
52
52
 
53
53
  const signals = AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS), signal]);
54
- const credits = await provider.fetch(ctx, apiKey, signals);
54
+ const credits = await provider.fetch(apiKey, signals);
55
55
 
56
56
  // The active model may have changed while the request was in flight.
57
57
  if (ctx.model?.provider !== provider.id) return;
@@ -18,7 +18,7 @@ export const deepseekProvider: CreditsProvider = {
18
18
  id: PROVIDER,
19
19
  label: "DeepSeek",
20
20
 
21
- async fetch(_ctx, apiKey, signal): Promise<Credits> {
21
+ async fetch(apiKey, signal): Promise<Credits> {
22
22
  const headers: Record<string, string> = {
23
23
  Accept: "application/json",
24
24
  Authorization: `Bearer ${apiKey}`,
@@ -108,7 +108,7 @@ export const fireworksProvider: CreditsProvider = {
108
108
  id: PROVIDER,
109
109
  label: "Fireworks",
110
110
 
111
- async fetch(_ctx, apiKey, signal): Promise<Credits> {
111
+ async fetch(apiKey, signal): Promise<Credits> {
112
112
  const name = await resolveAccount(apiKey, signal);
113
113
  const balance = await unary<Balance>("GetBalance", { name }, apiKey, signal);
114
114
 
@@ -42,7 +42,7 @@ export const kimiCodeProvider: CreditsProvider = {
42
42
  id: PROVIDER,
43
43
  label: "Kimi Code",
44
44
 
45
- async fetch(_ctx, apiKey, signal): Promise<Credits> {
45
+ async fetch(apiKey, signal): Promise<Credits> {
46
46
  const headers: Record<string, string> = {
47
47
  Accept: "application/json",
48
48
  Authorization: `Bearer ${apiKey}`,
@@ -18,7 +18,7 @@ function createMoonshotProvider(id: string, host: string, currency: string): Cre
18
18
  id,
19
19
  label: "Moonshot",
20
20
 
21
- async fetch(_ctx, apiKey, signal): Promise<Credits> {
21
+ async fetch(apiKey, signal): Promise<Credits> {
22
22
  const headers: Record<string, string> = {
23
23
  Accept: "application/json",
24
24
  Authorization: `Bearer ${apiKey}`,
@@ -1,11 +1,13 @@
1
1
  import { uuidv7 } from "@earendil-works/pi-agent-core";
2
+ import { readStoredCredential } from "@earendil-works/pi-coding-agent";
3
+ import prettyMilliseconds from "pretty-ms";
2
4
 
3
5
  import { confirmCodexReset, formatAvailableResets, showCodexResetLoader, showCodexResetSelector } from "./openai-codex-panel";
4
6
  import { toNumber } from "../../../utils/format";
5
7
 
6
8
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
9
  import type { CodexResetPanelData } from "./openai-codex-panel";
8
- import type { Credits, CreditsProvider, RefreshCredits } from "../types";
10
+ import type { Credits, CreditsLane, CreditsProvider, RefreshCredits } from "../types";
9
11
 
10
12
  const PROVIDER = "openai-codex";
11
13
  const BASE_URL = "https://chatgpt.com/backend-api";
@@ -24,6 +26,7 @@ interface CodexUsageResponse {
24
26
  }
25
27
 
26
28
  interface CodexRateWindow {
29
+ limit_window_seconds: number;
27
30
  used_percent?: number | string;
28
31
  }
29
32
 
@@ -50,7 +53,7 @@ async function runCodexReset(ctx: ExtensionContext, refresh: RefreshCredits): Pr
50
53
  const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER);
51
54
  if (!apiKey) return;
52
55
 
53
- const headers = buildHeaders(ctx, apiKey);
56
+ const headers = buildHeaders(apiKey);
54
57
 
55
58
  const credit = await showCodexResetSelector(ctx, async (signal) => {
56
59
  const requestSignal = AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
@@ -74,15 +77,15 @@ async function redeemReset(ctx: ExtensionContext, headers: Record<string, string
74
77
  }).finally(() => refresh(ctx));
75
78
  }
76
79
 
77
- function buildHeaders(ctx: ExtensionContext, apiKey: string): Record<string, string> {
80
+ function buildHeaders(apiKey: string): Record<string, string> {
78
81
  const headers: Record<string, string> = {
79
82
  Accept: "application/json",
80
83
  Authorization: `Bearer ${apiKey}`,
81
84
  };
82
85
 
83
- const credential = ctx.modelRegistry.authStorage.get(PROVIDER);
84
- const accountId = credential?.type === "oauth" ? (credential.accountId as string | undefined) : undefined;
85
- if (accountId) headers["ChatGPT-Account-ID"] = accountId;
86
+ const credential = readStoredCredential(PROVIDER);
87
+ const accountId = credential?.type === "oauth" ? credential.accountId : undefined;
88
+ if (typeof accountId === "string" && accountId.length > 0) headers["ChatGPT-Account-ID"] = accountId;
86
89
 
87
90
  return headers;
88
91
  }
@@ -126,14 +129,20 @@ function toCredits(usage: CodexUsageResponse, suffix?: string): Credits {
126
129
  return {
127
130
  type: "windows",
128
131
  unlimited: usage.credits?.unlimited === true,
129
- lanes: [
130
- { label: "5h", percent: parseUsedPercent(usage.rate_limit?.primary_window) },
131
- { label: "7d", percent: parseUsedPercent(usage.rate_limit?.secondary_window) },
132
- ],
132
+ lanes: [usage.rate_limit?.primary_window, usage.rate_limit?.secondary_window]
133
+ .filter((window): window is CodexRateWindow => window != null)
134
+ .map(toLane),
133
135
  suffix,
134
136
  };
135
137
  }
136
138
 
139
+ function toLane(window: CodexRateWindow): CreditsLane {
140
+ return {
141
+ label: prettyMilliseconds(window.limit_window_seconds * 1_000, { compact: true }),
142
+ percent: parseUsedPercent(window),
143
+ };
144
+ }
145
+
137
146
  function parseUsedPercent(window?: CodexRateWindow | null): number | undefined {
138
147
  const value = toNumber(window?.used_percent);
139
148
  return value === undefined ? undefined : Math.min(100, Math.max(0, value));
@@ -143,8 +152,8 @@ export const openaiCodexProvider: CreditsProvider = {
143
152
  id: PROVIDER,
144
153
  label: "Codex",
145
154
 
146
- async fetch(ctx, apiKey, signal): Promise<Credits> {
147
- const headers = buildHeaders(ctx, apiKey);
155
+ async fetch(apiKey, signal): Promise<Credits> {
156
+ const headers = buildHeaders(apiKey);
148
157
  const usage = await fetchUsage(headers, signal);
149
158
  const availableCount = usage.rate_limit_reset_credits?.available_count ?? 0;
150
159
  const suffix = availableCount > 0 ? `(${formatAvailableResets(availableCount)})` : undefined;
@@ -152,9 +161,8 @@ export const openaiCodexProvider: CreditsProvider = {
152
161
  return toCredits(usage, suffix);
153
162
  },
154
163
 
155
- register(pi, ctx, refresh): void {
156
- const credential = ctx.modelRegistry.authStorage.get(PROVIDER);
157
- if (credential?.type !== "oauth") return;
164
+ register(pi, _ctx, refresh): void {
165
+ if (readStoredCredential(PROVIDER)?.type !== "oauth") return;
158
166
 
159
167
  pi.registerCommand("codex-reset", {
160
168
  description: "Select and redeem a banked OpenAI Codex rate-limit reset",
@@ -16,7 +16,7 @@ export const openrouterProvider: CreditsProvider = {
16
16
  id: PROVIDER,
17
17
  label: "OpenRouter",
18
18
 
19
- async fetch(_ctx, apiKey, signal): Promise<Credits> {
19
+ async fetch(apiKey, signal): Promise<Credits> {
20
20
  const headers: Record<string, string> = {
21
21
  Accept: "application/json",
22
22
  Authorization: `Bearer ${apiKey}`,
@@ -13,7 +13,7 @@ export const vercelAiGatewayProvider: CreditsProvider = {
13
13
  id: PROVIDER,
14
14
  label: "Vercel",
15
15
 
16
- async fetch(_ctx, apiKey, signal): Promise<Credits> {
16
+ async fetch(apiKey, signal): Promise<Credits> {
17
17
  const headers: Record<string, string> = {
18
18
  Accept: "application/json",
19
19
  Authorization: `Bearer ${apiKey}`,
@@ -23,6 +23,6 @@ export type RefreshCredits = (ctx: ExtensionContext) => Promise<void>;
23
23
  export interface CreditsProvider {
24
24
  readonly id: ProviderId;
25
25
  readonly label: string;
26
- fetch(ctx: ExtensionContext, apiKey: string, signal: AbortSignal): Promise<Credits>;
26
+ fetch(apiKey: string, signal: AbortSignal): Promise<Credits>;
27
27
  register?(pi: ExtensionAPI, ctx: ExtensionContext, refresh: RefreshCredits): void;
28
28
  }
@@ -1,8 +1,6 @@
1
- import { ms } from "ms";
1
+ import parseDuration from "parse-duration";
2
2
  import * as z from "zod";
3
3
 
4
- import type { StringValue } from "ms";
5
-
6
4
  const MIN_IDLE_MS = 5_000;
7
5
 
8
6
  /** Accept a millisecond number or a human-readable duration (e.g., "3m"), normalized to milliseconds. */
@@ -11,20 +9,11 @@ export const idleTimeoutSchema = z
11
9
  .transform((value, ctx) => {
12
10
  if (typeof value === "number") return value;
13
11
 
14
- let parsed: number;
15
- try {
16
- parsed = ms(value as StringValue);
17
- } catch (error) {
18
- ctx.addIssue({ code: "custom", message: error instanceof Error ? error.message : String(error) });
19
- return z.NEVER;
20
- }
21
-
22
- if (Number.isNaN(parsed)) {
23
- ctx.addIssue({ code: "custom", message: `Value is not a valid duration. value=${JSON.stringify(value)}` });
24
- return z.NEVER;
25
- }
12
+ const parsed = parseDuration(value);
13
+ if (parsed !== null) return parsed;
26
14
 
27
- return parsed;
15
+ ctx.addIssue({ code: "custom", message: `Value is not a valid duration. value=${JSON.stringify(value)}` });
16
+ return z.NEVER;
28
17
  })
29
18
  .pipe(z.number().min(MIN_IDLE_MS));
30
19