pi-better-openai 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # pi-better-openai
2
+
3
+ A pi extension for OpenAI subscription workflows: fast mode, usage visibility, footer polish, and image generation through `openai-codex` auth.
4
+
5
+ ## Screenshots
6
+
7
+ <!-- Add screenshots here. -->
8
+
9
+ ## Install
10
+
11
+ Install from a local checkout:
12
+
13
+ ```bash
14
+ pi install /path/to/pi-better-openai
15
+ ```
16
+
17
+ For a project-local install instead of a global install, add `-l`:
18
+
19
+ ```bash
20
+ pi install -l /path/to/pi-better-openai
21
+ ```
22
+
23
+ Reload pi after installing:
24
+
25
+ ```text
26
+ /reload
27
+ ```
28
+
29
+ Sign in to OpenAI Codex subscription auth if you want usage stats or image generation:
30
+
31
+ ```text
32
+ /login openai-codex
33
+ ```
34
+
35
+ Start pi with fast mode enabled:
36
+
37
+ ```bash
38
+ pi --fast
39
+ ```
40
+
41
+ ## Features
42
+
43
+ - Fast mode for supported OpenAI models, toggled with `/fast` or in `/openai-settings`.
44
+ - OpenAI subscription usage display via `/openai-usage` and the footer.
45
+ - Interactive settings picker via `/openai-settings`.
46
+ - Footer customization for model, thinking, fast mode, usage, and token/cost context.
47
+ - OpenAI image generation/editing through the `openai_image` tool.
48
+ - Commands:
49
+ - `/fast` toggles fast mode.
50
+ - `/openai-usage` shows current OpenAI subscription usage.
51
+ - `/openai-settings` opens settings, diagnostics, and config details.
@@ -0,0 +1,232 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { CONFIG_BASENAME, logPrefix } from "./identity.ts";
5
+
6
+ export const FOOTER_MODES = ["replace", "status", "off"] as const;
7
+ export const IMAGE_SAVE_MODES = ["none", "project", "global", "custom"] as const;
8
+ export const IMAGE_OUTPUT_FORMATS = ["png", "jpeg", "webp"] as const;
9
+
10
+ export const DEFAULT_SUPPORTED_MODELS = [
11
+ "openai/gpt-5.4",
12
+ "openai/gpt-5.5",
13
+ "openai-codex/gpt-5.4",
14
+ "openai-codex/gpt-5.5"
15
+ ] as const;
16
+
17
+ export type FooterMode = typeof FOOTER_MODES[number];
18
+ export type ImageSaveMode = typeof IMAGE_SAVE_MODES[number];
19
+ export type ImageOutputFormat = typeof IMAGE_OUTPUT_FORMATS[number];
20
+
21
+ export type UsageConfig = {
22
+ enabled?: boolean;
23
+ refreshIntervalMs?: number;
24
+ showOnlyOnSubscriptionModels?: boolean;
25
+ showResetTimes?: boolean;
26
+ };
27
+
28
+ export type FooterConfig = {
29
+ mode?: FooterMode;
30
+ };
31
+
32
+ export type ImageConfig = {
33
+ enabled?: boolean;
34
+ defaultModel?: string;
35
+ defaultSave?: ImageSaveMode;
36
+ outputFormat?: ImageOutputFormat;
37
+ timeoutMs?: number;
38
+ };
39
+
40
+ export interface ConfigFile {
41
+ persistState?: boolean;
42
+ active?: boolean;
43
+ desiredActive?: boolean;
44
+ supportedModels?: string[];
45
+ usage?: UsageConfig;
46
+ footer?: FooterConfig;
47
+ image?: ImageConfig;
48
+ }
49
+
50
+ export interface SupportedModel {
51
+ provider: string;
52
+ id: string;
53
+ }
54
+
55
+ export interface ResolvedConfig {
56
+ configPath: string;
57
+ projectConfigPath: string;
58
+ globalConfigPath: string;
59
+ projectConfigExists: boolean;
60
+ globalConfigExists: boolean;
61
+ persistState: boolean;
62
+ active: boolean;
63
+ desiredActive: boolean;
64
+ supportedModels: SupportedModel[];
65
+ usage: Required<UsageConfig>;
66
+ footer: Required<FooterConfig>;
67
+ image: Required<ImageConfig>;
68
+ }
69
+
70
+ export const DEFAULT_USAGE_CONFIG: Required<UsageConfig> = {
71
+ enabled: true,
72
+ refreshIntervalMs: 60_000,
73
+ showOnlyOnSubscriptionModels: true,
74
+ showResetTimes: true
75
+ };
76
+
77
+ export const DEFAULT_FOOTER_CONFIG: Required<FooterConfig> = {
78
+ mode: "replace"
79
+ };
80
+
81
+ export const DEFAULT_IMAGE_CONFIG: Required<ImageConfig> = {
82
+ enabled: true,
83
+ defaultModel: "gpt-5.5",
84
+ defaultSave: "project",
85
+ outputFormat: "png",
86
+ timeoutMs: 180_000
87
+ };
88
+
89
+ export const DEFAULT_CONFIG: ConfigFile = {
90
+ persistState: true,
91
+ active: false,
92
+ desiredActive: false,
93
+ supportedModels: [...DEFAULT_SUPPORTED_MODELS],
94
+ usage: DEFAULT_USAGE_CONFIG,
95
+ footer: DEFAULT_FOOTER_CONFIG,
96
+ image: DEFAULT_IMAGE_CONFIG
97
+ };
98
+
99
+ export function isRecord(value: unknown): value is Record<string, unknown> {
100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
101
+ }
102
+
103
+ export function configPaths(cwd: string, home = homedir()) {
104
+ return {
105
+ project: join(cwd, ".pi", "extensions", CONFIG_BASENAME),
106
+ global: join(home, ".pi", "agent", "extensions", CONFIG_BASENAME)
107
+ };
108
+ }
109
+
110
+ export function parseModelKey(value: string): SupportedModel | undefined {
111
+ const key = value.trim();
112
+ const slash = key.indexOf("/");
113
+ if (slash <= 0 || slash === key.length - 1) return undefined;
114
+ const provider = key.slice(0, slash).trim();
115
+ const id = key.slice(slash + 1).trim();
116
+ return provider && id ? { provider, id } : undefined;
117
+ }
118
+
119
+ export function normalizeModelKeys(value: unknown): string[] | undefined {
120
+ if (value === undefined) return undefined;
121
+ if (!Array.isArray(value)) return undefined;
122
+ return value
123
+ .filter((entry): entry is string => typeof entry === "string")
124
+ .map((entry) => parseModelKey(entry))
125
+ .filter((entry): entry is SupportedModel => entry !== undefined)
126
+ .map((entry) => `${entry.provider}/${entry.id}`);
127
+ }
128
+
129
+ export function parseModels(value: unknown): SupportedModel[] | undefined {
130
+ const keys = normalizeModelKeys(value);
131
+ if (keys === undefined) return undefined;
132
+ return keys.map((key) => parseModelKey(key)).filter((entry): entry is SupportedModel => entry !== undefined);
133
+ }
134
+
135
+ export function readRawConfig(path: string): Record<string, unknown> {
136
+ if (!existsSync(path)) return {};
137
+ try {
138
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
139
+ return isRecord(parsed) ? parsed : {};
140
+ } catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ console.warn(`${logPrefix()} Failed to read ${path}: ${message}`);
143
+ return {};
144
+ }
145
+ }
146
+
147
+ export function readConfig(path: string): ConfigFile | undefined {
148
+ if (!existsSync(path)) return undefined;
149
+ const parsed = readRawConfig(path);
150
+ const config: ConfigFile = {};
151
+ if (typeof parsed.persistState === "boolean") config.persistState = parsed.persistState;
152
+ if (typeof parsed.active === "boolean") config.active = parsed.active;
153
+ if (typeof parsed.desiredActive === "boolean") config.desiredActive = parsed.desiredActive;
154
+ const supportedModels = normalizeModelKeys(parsed.supportedModels);
155
+ if (supportedModels !== undefined) config.supportedModels = supportedModels;
156
+ if (isRecord(parsed.usage)) {
157
+ config.usage = {};
158
+ if (typeof parsed.usage.enabled === "boolean") config.usage.enabled = parsed.usage.enabled;
159
+ if (typeof parsed.usage.refreshIntervalMs === "number") config.usage.refreshIntervalMs = parsed.usage.refreshIntervalMs;
160
+ if (typeof parsed.usage.showOnlyOnSubscriptionModels === "boolean") config.usage.showOnlyOnSubscriptionModels = parsed.usage.showOnlyOnSubscriptionModels;
161
+ if (typeof parsed.usage.showResetTimes === "boolean") config.usage.showResetTimes = parsed.usage.showResetTimes;
162
+ }
163
+ if (isRecord(parsed.footer) && typeof parsed.footer.mode === "string" && (FOOTER_MODES as readonly string[]).includes(parsed.footer.mode)) {
164
+ config.footer = { mode: parsed.footer.mode as FooterMode };
165
+ }
166
+ if (isRecord(parsed.image)) {
167
+ config.image = {};
168
+ if (typeof parsed.image.enabled === "boolean") config.image.enabled = parsed.image.enabled;
169
+ if (typeof parsed.image.defaultModel === "string" && parsed.image.defaultModel.trim()) config.image.defaultModel = parsed.image.defaultModel.trim();
170
+ if (typeof parsed.image.defaultSave === "string" && (IMAGE_SAVE_MODES as readonly string[]).includes(parsed.image.defaultSave)) config.image.defaultSave = parsed.image.defaultSave as ImageSaveMode;
171
+ if (typeof parsed.image.outputFormat === "string" && (IMAGE_OUTPUT_FORMATS as readonly string[]).includes(parsed.image.outputFormat)) config.image.outputFormat = parsed.image.outputFormat as ImageOutputFormat;
172
+ if (typeof parsed.image.timeoutMs === "number") config.image.timeoutMs = parsed.image.timeoutMs;
173
+ }
174
+ return config;
175
+ }
176
+
177
+ export function writeConfig(path: string, config: ConfigFile | Record<string, unknown>): void {
178
+ try {
179
+ mkdirSync(dirname(path), { recursive: true });
180
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
181
+ } catch (error) {
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ console.warn(`${logPrefix()} Failed to write ${path}: ${message}`);
184
+ }
185
+ }
186
+
187
+ function ensureConfigFile(projectConfigPath: string, globalConfigPath: string): void {
188
+ if (existsSync(projectConfigPath) || existsSync(globalConfigPath)) return;
189
+ writeConfig(globalConfigPath, DEFAULT_CONFIG);
190
+ }
191
+
192
+ export function resolveConfig(cwd: string): ResolvedConfig {
193
+ const paths = configPaths(cwd);
194
+ ensureConfigFile(paths.project, paths.global);
195
+
196
+ const projectConfigExists = existsSync(paths.project);
197
+ const globalConfigExists = existsSync(paths.global);
198
+ const globalConfig = readConfig(paths.global) ?? {};
199
+ const projectConfig = readConfig(paths.project) ?? {};
200
+ const merged = { ...DEFAULT_CONFIG, ...globalConfig, ...projectConfig };
201
+ const selectedPath = projectConfigExists ? paths.project : paths.global;
202
+ const desiredActive = merged.desiredActive ?? merged.active ?? false;
203
+
204
+ return {
205
+ configPath: selectedPath,
206
+ projectConfigPath: paths.project,
207
+ globalConfigPath: paths.global,
208
+ projectConfigExists,
209
+ globalConfigExists,
210
+ persistState: merged.persistState ?? true,
211
+ active: merged.active ?? desiredActive,
212
+ desiredActive,
213
+ supportedModels: parseModels(merged.supportedModels) ?? parseModels(DEFAULT_SUPPORTED_MODELS) ?? [],
214
+ usage: {
215
+ ...DEFAULT_USAGE_CONFIG,
216
+ ...(globalConfig.usage ?? {}),
217
+ ...(projectConfig.usage ?? {}),
218
+ refreshIntervalMs: Math.max(15_000, Math.min(10 * 60_000, projectConfig.usage?.refreshIntervalMs ?? globalConfig.usage?.refreshIntervalMs ?? DEFAULT_USAGE_CONFIG.refreshIntervalMs))
219
+ },
220
+ footer: {
221
+ ...DEFAULT_FOOTER_CONFIG,
222
+ ...(globalConfig.footer ?? {}),
223
+ ...(projectConfig.footer ?? {})
224
+ },
225
+ image: {
226
+ ...DEFAULT_IMAGE_CONFIG,
227
+ ...(globalConfig.image ?? {}),
228
+ ...(projectConfig.image ?? {}),
229
+ timeoutMs: Math.max(30_000, Math.min(5 * 60_000, projectConfig.image?.timeoutMs ?? globalConfig.image?.timeoutMs ?? DEFAULT_IMAGE_CONFIG.timeoutMs))
230
+ }
231
+ };
232
+ }
@@ -0,0 +1,35 @@
1
+ export function stripAnsi(value: string): string {
2
+ return value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "");
3
+ }
4
+
5
+ export function visibleWidth(value: string): number {
6
+ return stripAnsi(value).length;
7
+ }
8
+
9
+ function leadingAnsi(value: string): string {
10
+ return value.match(/^(?:\u001b\[[0-?]*[ -/]*[@-~])+/)?.[0] ?? "";
11
+ }
12
+
13
+ function trailingAnsi(value: string): string {
14
+ return value.match(/(?:\u001b\[[0-?]*[ -/]*[@-~])+$/)?.[0] ?? "";
15
+ }
16
+
17
+ export function truncateToWidth(value: string, width: number, ellipsis = "..."): string {
18
+ if (visibleWidth(value) <= width) return value;
19
+ if (width <= 0) return "";
20
+ const plain = stripAnsi(value);
21
+ if (width <= ellipsis.length) return ellipsis.slice(0, width);
22
+ return `${leadingAnsi(value)}${plain.slice(0, Math.max(0, width - ellipsis.length))}${ellipsis}${trailingAnsi(value)}`;
23
+ }
24
+
25
+ export function formatTokens(count: number): string {
26
+ if (count < 1000) return count.toString();
27
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
28
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
29
+ if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
30
+ return `${Math.round(count / 1000000)}M`;
31
+ }
32
+
33
+ export function sanitizeStatusText(text: string): string {
34
+ return text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
35
+ }
@@ -0,0 +1,7 @@
1
+ export const EXTENSION_NAME = "pi-better-openai";
2
+ export const CONFIG_BASENAME = "pi-better-openai.json";
3
+ export const STATUS_KEY = "better-openai";
4
+
5
+ export function logPrefix(): string {
6
+ return `[${EXTENSION_NAME}]`;
7
+ }