pi-spark 0.6.3 → 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 CHANGED
@@ -6,14 +6,12 @@ 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.
13
12
  - **Name:** exposes a `name` tool so the agent can give the current session a concise, recognizable name in the session selector.
14
13
  - **Presets:** switches named model presets with `/preset`, `--preset`, and quick cycle shortcuts.
15
14
  - **Recap:** generates a short idle-session recap and exposes a `/recap` command for manual generation, inspired by [Claude Code's session recap](https://code.claude.com/docs/en/interactive-mode#session-recap).
16
- - **Trust all:** bypasses pi's project trust dialog, added in 0.79.0, and removes saved trust decisions so startup stays minimal.
17
15
 
18
16
  ![Screenshot](./assets/screenshot.png)
19
17
 
@@ -47,29 +45,25 @@ Example:
47
45
  "footer": false,
48
46
  "presets": {
49
47
  "claude-opus": {
50
- "model": "claude-opus-4-8",
51
48
  "provider": "anthropic",
49
+ "model": "claude-opus-4-8",
52
50
  "thinkingLevel": "high"
53
51
  },
54
52
  "gpt": {
55
- "model": "gpt-5.5",
56
53
  "provider": "openai-codex",
54
+ "model": "gpt-5.5",
57
55
  "thinkingLevel": "medium"
58
56
  }
59
57
  },
60
58
  "recap": {
61
- "idle": 180000,
62
- "model": "gpt-5.4-mini",
59
+ "idle": "5m",
63
60
  "provider": "openai-codex",
61
+ "model": "gpt-5.4-mini",
64
62
  "thinkingLevel": "off"
65
63
  }
66
64
  }
67
65
  ```
68
66
 
69
- ### Codex usage
70
-
71
- - 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.
72
-
73
67
  ### Editor
74
68
 
75
69
  - `editor.spinner` controls the working indicator style and can be `dots`, `lights`, `tildes`, or `pulse`.
@@ -99,9 +93,21 @@ Use presets in these ways:
99
93
  ### Recap
100
94
 
101
95
  - pi-spark can generate a short recap after the session has been idle or when you run `/recap` manually.
102
- - The `recap.idle` value is in milliseconds and must be at least `5000`. The recap model can be customized with `provider`, `model`, and `thinkingLevel`.
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`.
98
+
99
+ ## Recommended pi settings
100
+
101
+ [Pi 0.79.0](https://pi.dev/news/releases/0.79.0) added a project trust dialog that asks before loading project-local resources ([earendil-works/pi#5514](https://github.com/earendil-works/pi/issues/5514)), and [pi 0.79.1](https://pi.dev/news/releases/0.79.1) made the default behavior configurable via `defaultProjectTrust`. To keep startup minimal, set it to `"always"` in `~/.pi/agent/settings.json` (or change it with `/settings`):
102
+
103
+ ```json
104
+ {
105
+ "defaultProjectTrust": "always"
106
+ }
107
+ ```
108
+
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.
103
110
 
104
- ### Trust all
111
+ ## Other pi packages
105
112
 
106
- - pi-spark always answers pi's project trust check with `yes` and removes `~/.pi/agent/trust.json` on startup.
107
- - This keeps the experience minimal after the project trust dialog added in pi 0.79.0. Follow [earendil-works/pi#5514](https://github.com/earendil-works/pi/issues/5514) for discussion. If pi ships a better default experience in the future, this extension may be deleted.
113
+ - [pi-credits](https://github.com/zlliang/pi-credits): shows the active provider's credit balance or rate-limit usage as a footer status.
Binary file
@@ -1,6 +1,32 @@
1
+ import { ms } from "ms";
1
2
  import * as z from "zod";
2
3
 
3
- export const idleTimeoutSchema = z.number().min(5000);
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.6.3",
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,5 +0,0 @@
1
- import * as z from "zod";
2
-
3
- export const codexUsageConfigSchema = z.object({});
4
-
5
- export type CodexUsageConfig = z.infer<typeof codexUsageConfigSchema>;
@@ -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
- }
@@ -1,19 +0,0 @@
1
- import { rmSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
-
5
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
-
7
- /**
8
- * Keep the experience minimal after the project trust dialog added in pi 0.79.0.
9
- *
10
- * Follow [earendil-works/pi#5514](https://github.com/earendil-works/pi/issues/5514) for discussion.
11
- * If pi ships a better default experience in the future, this extension may be deleted.
12
- */
13
- export default function (pi: ExtensionAPI) {
14
- rmSync(join(getAgentDir(), "trust.json"), { force: true });
15
-
16
- pi.on("project_trust", () => {
17
- return { trusted: "yes" };
18
- });
19
- }