pi-spark 0.7.0 → 0.8.0
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 +10 -10
- package/extensions/recap/idle.ts +27 -1
- package/extensions/shared/config/schema.ts +0 -2
- package/package.json +2 -1
- package/extensions/codex-usage/client.ts +0 -69
- package/extensions/codex-usage/config.ts +0 -5
- package/extensions/codex-usage/index.ts +0 -51
- package/extensions/codex-usage/manager.ts +0 -47
- package/extensions/codex-usage/status.ts +0 -31
package/README.md
CHANGED
|
@@ -6,7 +6,6 @@ A small, opinionated collection of [pi](https://pi.dev/) extensions.
|
|
|
6
6
|
|
|
7
7
|
## Extensions
|
|
8
8
|
|
|
9
|
-
- **Codex usage:** shows your OpenAI Codex (ChatGPT) rate-limit usage as a footer status when a Codex model is active.
|
|
10
9
|
- **Editor:** replaces the default editor with a compact working indicator (inspired by [Amp](https://ampcode.com/)) and current model info.
|
|
11
10
|
- **Footer:** shows session information, extension statuses, cost, and context usage on one line.
|
|
12
11
|
- **Fullscreen:** clears the screen and scrollback on session start, pins the editor and footer to the bottom for a full-screen session, and clears again on exit.
|
|
@@ -46,29 +45,25 @@ Example:
|
|
|
46
45
|
"footer": false,
|
|
47
46
|
"presets": {
|
|
48
47
|
"claude-opus": {
|
|
49
|
-
"model": "claude-opus-4-8",
|
|
50
48
|
"provider": "anthropic",
|
|
49
|
+
"model": "claude-opus-4-8",
|
|
51
50
|
"thinkingLevel": "high"
|
|
52
51
|
},
|
|
53
52
|
"gpt": {
|
|
54
|
-
"model": "gpt-5.5",
|
|
55
53
|
"provider": "openai-codex",
|
|
54
|
+
"model": "gpt-5.5",
|
|
56
55
|
"thinkingLevel": "medium"
|
|
57
56
|
}
|
|
58
57
|
},
|
|
59
58
|
"recap": {
|
|
60
|
-
"idle":
|
|
61
|
-
"model": "gpt-5.4-mini",
|
|
59
|
+
"idle": "5m",
|
|
62
60
|
"provider": "openai-codex",
|
|
61
|
+
"model": "gpt-5.4-mini",
|
|
63
62
|
"thinkingLevel": "off"
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
```
|
|
67
66
|
|
|
68
|
-
### Codex usage
|
|
69
|
-
|
|
70
|
-
- pi-spark queries the ChatGPT backend and shows your Codex 5-hour (`5h`) and 7-day (`7d`) rate-limit usage as a footer status, refreshing on session start, model change, and after billable turns. The status appears only while a Codex (`openai-codex`) model is active and uses its stored OAuth credential.
|
|
71
|
-
|
|
72
67
|
### Editor
|
|
73
68
|
|
|
74
69
|
- `editor.spinner` controls the working indicator style and can be `dots`, `lights`, `tildes`, or `pulse`.
|
|
@@ -98,7 +93,8 @@ Use presets in these ways:
|
|
|
98
93
|
### Recap
|
|
99
94
|
|
|
100
95
|
- pi-spark can generate a short recap after the session has been idle or when you run `/recap` manually.
|
|
101
|
-
- The `recap.idle` value
|
|
96
|
+
- The `recap.idle` value sets how long the session must stay idle before a recap is generated. It accepts either a millisecond number or a human-readable duration string parsed by [vercel/ms](https://github.com/vercel/ms) (e.g., `"3m"`, `"30s"`, `"2 minutes"`), and must resolve to at least 5000ms.
|
|
97
|
+
- The recap model can be customized with `provider`, `model`, and `thinkingLevel`.
|
|
102
98
|
|
|
103
99
|
## Recommended pi settings
|
|
104
100
|
|
|
@@ -111,3 +107,7 @@ Use presets in these ways:
|
|
|
111
107
|
```
|
|
112
108
|
|
|
113
109
|
Project trust is an [input-loading guard](https://pi.dev/docs/latest/security#project-trust), so use `"always"` only if you trust the projects you open.
|
|
110
|
+
|
|
111
|
+
## Other pi packages
|
|
112
|
+
|
|
113
|
+
- [pi-credits](https://github.com/zlliang/pi-credits): shows the active provider's credit balance or rate-limit usage as a footer status.
|
package/extensions/recap/idle.ts
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
+
import { ms } from "ms";
|
|
1
2
|
import * as z from "zod";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import type { StringValue } from "ms";
|
|
5
|
+
|
|
6
|
+
const MIN_IDLE_MS = 5_000;
|
|
7
|
+
|
|
8
|
+
/** Accept a millisecond number or a human-readable duration (e.g., "3m"), normalized to milliseconds. */
|
|
9
|
+
export const idleTimeoutSchema = z
|
|
10
|
+
.union([z.number(), z.string()])
|
|
11
|
+
.transform((value, ctx) => {
|
|
12
|
+
if (typeof value === "number") return value;
|
|
13
|
+
|
|
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
|
+
}
|
|
26
|
+
|
|
27
|
+
return parsed;
|
|
28
|
+
})
|
|
29
|
+
.pipe(z.number().min(MIN_IDLE_MS));
|
|
4
30
|
|
|
5
31
|
type IdleTimeout = z.infer<typeof idleTimeoutSchema>;
|
|
6
32
|
type IdleHash = string | number | boolean;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
|
|
3
|
-
import { codexUsageConfigSchema } from "../../codex-usage/config";
|
|
4
3
|
import { editorConfigSchema } from "../../editor/config";
|
|
5
4
|
import { footerConfigSchema } from "../../footer/config";
|
|
6
5
|
import { fullscreenConfigSchema } from "../../fullscreen/config";
|
|
@@ -9,7 +8,6 @@ import { presetsConfigSchema } from "../../presets/config";
|
|
|
9
8
|
import { recapConfigSchema } from "../../recap/config";
|
|
10
9
|
|
|
11
10
|
export const configSchemas = {
|
|
12
|
-
codexUsage: codexUsageConfigSchema,
|
|
13
11
|
editor: editorConfigSchema,
|
|
14
12
|
footer: footerConfigSchema,
|
|
15
13
|
fullscreen: fullscreenConfigSchema,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-spark",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A small, opinionated collection of pi extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-coding-agent",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"typecheck": "tsc --noEmit"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"ms": "4.0.0-nightly.202508271359",
|
|
31
32
|
"zod": "^4.4.3"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
export const CODEX_PROVIDER = "openai-codex";
|
|
4
|
-
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
5
|
-
const REQUEST_TIMEOUT_MS = 30_000;
|
|
6
|
-
|
|
7
|
-
/** Normalized Codex usage with rate-limit windows reduced to used percentages. */
|
|
8
|
-
export interface CodexUsage {
|
|
9
|
-
unlimited: boolean;
|
|
10
|
-
/** Used percentage of the 5-hour window, clamped to 0-100. */
|
|
11
|
-
primaryPercent: number | undefined;
|
|
12
|
-
/** Used percentage of the 7-day window, clamped to 0-100. */
|
|
13
|
-
secondaryPercent: number | undefined;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface CodexUsageResponse {
|
|
17
|
-
rate_limit?: {
|
|
18
|
-
primary_window?: CodexRateWindow | null;
|
|
19
|
-
secondary_window?: CodexRateWindow | null;
|
|
20
|
-
} | null;
|
|
21
|
-
credits?: {
|
|
22
|
-
unlimited?: boolean;
|
|
23
|
-
} | null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface CodexRateWindow {
|
|
27
|
-
used_percent?: number | string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Read the ChatGPT account id from the stored Codex OAuth credential, if present. */
|
|
31
|
-
export function readAccountId(ctx: ExtensionContext): string | undefined {
|
|
32
|
-
const credential = ctx.modelRegistry.authStorage.get(CODEX_PROVIDER) as { accountId?: string } | undefined;
|
|
33
|
-
const accountId = credential?.accountId;
|
|
34
|
-
|
|
35
|
-
return typeof accountId === "string" && accountId.trim() ? accountId.trim() : undefined;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Fetch and normalize Codex usage, aborting on timeout or when the parent signal aborts. */
|
|
39
|
-
export async function fetchCodexUsage(token: string, accountId?: string, parentSignal?: AbortSignal): Promise<CodexUsage> {
|
|
40
|
-
const signals = [AbortSignal.timeout(REQUEST_TIMEOUT_MS)];
|
|
41
|
-
if (parentSignal) signals.push(parentSignal);
|
|
42
|
-
|
|
43
|
-
const headers: Record<string, string> = {
|
|
44
|
-
Accept: "application/json",
|
|
45
|
-
Authorization: `Bearer ${token}`,
|
|
46
|
-
};
|
|
47
|
-
if (accountId) headers["ChatGPT-Account-Id"] = accountId;
|
|
48
|
-
|
|
49
|
-
const response = await fetch(USAGE_URL, { method: "GET", headers, signal: AbortSignal.any(signals) });
|
|
50
|
-
if (!response.ok) throw new Error("request failed");
|
|
51
|
-
|
|
52
|
-
return normalizeUsage((await response.json()) as CodexUsageResponse);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function normalizeUsage(payload: CodexUsageResponse): CodexUsage {
|
|
56
|
-
return {
|
|
57
|
-
unlimited: payload.credits?.unlimited === true,
|
|
58
|
-
primaryPercent: parseUsedPercent(payload.rate_limit?.primary_window),
|
|
59
|
-
secondaryPercent: parseUsedPercent(payload.rate_limit?.secondary_window),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function parseUsedPercent(window: CodexRateWindow | null | undefined): number | undefined {
|
|
64
|
-
const raw = window?.used_percent;
|
|
65
|
-
if (raw === undefined || raw === null) return undefined;
|
|
66
|
-
|
|
67
|
-
const value = typeof raw === "number" ? raw : Number(raw);
|
|
68
|
-
return Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : undefined;
|
|
69
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { CodexUsageManager } from "./manager";
|
|
2
|
-
import { loadConfig } from "../shared/config";
|
|
3
|
-
import { isUsage } from "../shared/usage";
|
|
4
|
-
|
|
5
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
7
|
-
|
|
8
|
-
function hasCost(message: AgentMessage): boolean {
|
|
9
|
-
const usage = (message as { usage?: unknown }).usage;
|
|
10
|
-
if (!isUsage(usage)) return false;
|
|
11
|
-
|
|
12
|
-
return usage.cost.total > 0 || usage.input > 0 || usage.output > 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export default function (pi: ExtensionAPI) {
|
|
16
|
-
let codexUsageManager: CodexUsageManager | undefined = undefined;
|
|
17
|
-
|
|
18
|
-
pi.on("session_start", (_event, ctx) => {
|
|
19
|
-
if (!ctx.hasUI) return;
|
|
20
|
-
|
|
21
|
-
const config = loadConfig(ctx, "codexUsage");
|
|
22
|
-
if (!config) return;
|
|
23
|
-
|
|
24
|
-
codexUsageManager = new CodexUsageManager();
|
|
25
|
-
codexUsageManager.refresh(ctx);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
pi.on("model_select", (_event, ctx) => {
|
|
29
|
-
codexUsageManager?.refresh(ctx);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
pi.on("turn_end", (event, ctx) => {
|
|
33
|
-
if (!hasCost(event.message)) return;
|
|
34
|
-
|
|
35
|
-
codexUsageManager?.refresh(ctx);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
pi.on("session_compact", (_event, ctx) => {
|
|
39
|
-
codexUsageManager?.refresh(ctx);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
pi.on("session_tree", (event, ctx) => {
|
|
43
|
-
if (!event.summaryEntry) return;
|
|
44
|
-
|
|
45
|
-
codexUsageManager?.refresh(ctx);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
pi.on("session_shutdown", () => {
|
|
49
|
-
codexUsageManager = undefined;
|
|
50
|
-
});
|
|
51
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { CODEX_PROVIDER, fetchCodexUsage, readAccountId } from "./client";
|
|
2
|
-
import { renderError, renderUsage } from "./status";
|
|
3
|
-
|
|
4
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
|
|
6
|
-
const STATUS_KEY = "codex-usage";
|
|
7
|
-
|
|
8
|
-
export class CodexUsageManager {
|
|
9
|
-
private inflight: Promise<void> | undefined = undefined;
|
|
10
|
-
|
|
11
|
-
async refresh(ctx: ExtensionContext): Promise<void> {
|
|
12
|
-
if (!this.isCodexContext(ctx)) {
|
|
13
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (this.inflight) return this.inflight;
|
|
18
|
-
|
|
19
|
-
this.inflight = this.fetch(ctx).finally(() => {
|
|
20
|
-
this.inflight = undefined;
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
return this.inflight;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private async fetch(ctx: ExtensionContext): Promise<void> {
|
|
27
|
-
try {
|
|
28
|
-
const token = await ctx.modelRegistry.getApiKeyForProvider(CODEX_PROVIDER);
|
|
29
|
-
if (!token) {
|
|
30
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const usage = await fetchCodexUsage(token, readAccountId(ctx), ctx.signal);
|
|
35
|
-
if (!this.isCodexContext(ctx)) return;
|
|
36
|
-
|
|
37
|
-
ctx.ui.setStatus(STATUS_KEY, renderUsage(ctx.ui.theme, usage));
|
|
38
|
-
} catch (error) {
|
|
39
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
-
ctx.ui.setStatus(STATUS_KEY, renderError(ctx.ui.theme, message));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private isCodexContext(ctx: ExtensionContext): boolean {
|
|
45
|
-
return ctx.model?.provider === CODEX_PROVIDER;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
import type { CodexUsage } from "./client";
|
|
4
|
-
|
|
5
|
-
const LABEL = "Codex";
|
|
6
|
-
|
|
7
|
-
/** Render a footer status string for normalized Codex usage. */
|
|
8
|
-
export function renderUsage(theme: Theme, usage: CodexUsage): string {
|
|
9
|
-
const label = theme.fg("dim", LABEL);
|
|
10
|
-
|
|
11
|
-
if (usage.unlimited || (usage.primaryPercent === undefined && usage.secondaryPercent === undefined)) {
|
|
12
|
-
return `${label} ${theme.fg("success", "unlimited")}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const fiveHour = renderLane(theme, "5h", usage.primaryPercent);
|
|
16
|
-
const sevenDay = renderLane(theme, "7d", usage.secondaryPercent);
|
|
17
|
-
return `${label} ${fiveHour} ${sevenDay}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Render a footer status string for a usage fetch error. */
|
|
21
|
-
export function renderError(theme: Theme, message: string): string {
|
|
22
|
-
return theme.fg("error", `${LABEL} usage unavailable: ${message}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function renderLane(theme: Theme, label: string, percent?: number): string {
|
|
26
|
-
const text = `${label} ${percent === undefined ? "?" : percent.toFixed(0)}%`;
|
|
27
|
-
|
|
28
|
-
if (percent && percent > 90) return theme.fg("error", text);
|
|
29
|
-
if (percent && percent > 70) return theme.fg("warning", text);
|
|
30
|
-
return theme.fg("dim", text);
|
|
31
|
-
}
|