pi-agent-extensions 0.3.0 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * /context
2
+ * /context-simple
3
3
  *
4
4
  * Small TUI view showing what's loaded/available:
5
5
  * - extensions (best-effort from registered extension slash commands)
@@ -470,8 +470,8 @@ export default function contextExtension(pi: ExtensionAPI) {
470
470
  }
471
471
  });
472
472
 
473
- pi.registerCommand("context", {
474
- description: "Show loaded context overview",
473
+ pi.registerCommand("context-simple", {
474
+ description: "Show loaded context overview",
475
475
  handler: async (_args, ctx: ExtensionCommandContext) => {
476
476
  const commands = pi.getCommands();
477
477
  const extensionCmds = commands.filter((c) => c.source === "extension");
@@ -1,12 +1,12 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import type { OAuthCredentials, OAuthLoginCallbacks } from "@mariozechner/pi-ai/dist/utils/oauth/types.js";
2
3
  import * as fs from "node:fs/promises";
3
4
  import * as fsSync from "node:fs";
4
5
  import * as path from "node:path";
5
6
  import * as os from "node:os";
6
7
 
7
- /** Persisted config shape */
8
- export interface NvidiaNimConfig {
9
- apiKey: string;
8
+ /** Persisted model config (API key is now managed via OAuth in ~/.pi/agent/auth.json) */
9
+ export interface NvidiaModelsConfig {
10
10
  models: NvidiaModelEntry[];
11
11
  }
12
12
 
@@ -36,12 +36,14 @@ export function getConfigPath(): string {
36
36
  return path.join(os.homedir(), ".pi", "nvidia-nim.json");
37
37
  }
38
38
 
39
- export async function loadConfig(): Promise<NvidiaNimConfig | null> {
39
+ /** Load models config from ~/.pi/nvidia-nim.json (async).
40
+ * Handles both new format (models only) and legacy format (with apiKey). */
41
+ export async function loadModelsConfig(): Promise<NvidiaModelsConfig | null> {
40
42
  try {
41
43
  const content = await fs.readFile(getConfigPath(), "utf-8");
42
44
  const parsed = JSON.parse(content);
43
- if (parsed.apiKey && Array.isArray(parsed.models)) {
44
- return parsed as NvidiaNimConfig;
45
+ if (Array.isArray(parsed.models) && parsed.models.length > 0) {
46
+ return { models: parsed.models };
45
47
  }
46
48
  return null;
47
49
  } catch {
@@ -49,13 +51,14 @@ export async function loadConfig(): Promise<NvidiaNimConfig | null> {
49
51
  }
50
52
  }
51
53
 
52
- /** Synchronous version for use during extension init (before Pi finishes loading) */
53
- export function loadConfigSync(): NvidiaNimConfig | null {
54
+ /** Synchronous version for use during extension init (before Pi finishes loading).
55
+ * Handles both new format (models only) and legacy format (with apiKey). */
56
+ export function loadModelsConfigSync(): NvidiaModelsConfig | null {
54
57
  try {
55
58
  const content = fsSync.readFileSync(getConfigPath(), "utf-8");
56
59
  const parsed = JSON.parse(content);
57
- if (parsed.apiKey && Array.isArray(parsed.models)) {
58
- return parsed as NvidiaNimConfig;
60
+ if (Array.isArray(parsed.models) && parsed.models.length > 0) {
61
+ return { models: parsed.models };
59
62
  }
60
63
  return null;
61
64
  } catch {
@@ -63,7 +66,7 @@ export function loadConfigSync(): NvidiaNimConfig | null {
63
66
  }
64
67
  }
65
68
 
66
- export async function saveConfig(config: NvidiaNimConfig): Promise<void> {
69
+ export async function saveModelsConfig(config: NvidiaModelsConfig): Promise<void> {
67
70
  const configPath = getConfigPath();
68
71
  await fs.mkdir(path.dirname(configPath), { recursive: true });
69
72
  await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
@@ -100,42 +103,6 @@ function formatModelName(id: string): string {
100
103
  .replace(/\b\w/g, (c) => c.toUpperCase());
101
104
  }
102
105
 
103
- /** Get the path to ~/.pi/agent/settings.json */
104
- export function getAgentSettingsPath(): string {
105
- return path.join(os.homedir(), ".pi", "agent", "settings.json");
106
- }
107
-
108
- /**
109
- * Add nvidia model IDs to enabledModels in ~/.pi/agent/settings.json
110
- * so they show up in the scoped /model view and Ctrl+P cycling.
111
- * Removes any stale nvidia/ entries first, then appends the new ones.
112
- */
113
- export async function updateEnabledModels(models: NvidiaModelEntry[]): Promise<void> {
114
- const settingsPath = getAgentSettingsPath();
115
-
116
- let settings: Record<string, any> = {};
117
- try {
118
- const content = await fs.readFile(settingsPath, "utf-8");
119
- if (content.trim()) {
120
- settings = JSON.parse(content);
121
- }
122
- } catch (err: any) {
123
- if (err.code !== "ENOENT") throw err;
124
- }
125
-
126
- const existing: string[] = Array.isArray(settings.enabledModels) ? settings.enabledModels : [];
127
-
128
- // Remove old nvidia/ entries
129
- const filtered = existing.filter((id: string) => !id.startsWith(`${NVIDIA_PROVIDER_NAME}/`));
130
-
131
- // Add new nvidia models with provider prefix
132
- const nvidiaIds = models.map((m) => `${NVIDIA_PROVIDER_NAME}/${m.id}`);
133
- settings.enabledModels = [...filtered, ...nvidiaIds];
134
-
135
- await fs.mkdir(path.dirname(settingsPath), { recursive: true });
136
- await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
137
- }
138
-
139
106
  /**
140
107
  * Fetch available model IDs from the Nvidia NIM API.
141
108
  * Returns the set of valid model IDs, or null on failure.
@@ -154,122 +121,91 @@ export async function fetchAvailableModels(apiKey: string): Promise<Set<string>
154
121
  }
155
122
  }
156
123
 
157
- /** Register the nvidia provider with pi so models appear in /model */
158
- export function registerProvider(pi: ExtensionAPI, config: NvidiaNimConfig): void {
159
- pi.registerProvider("nvidia", {
160
- baseUrl: NVIDIA_BASE_URL,
161
- apiKey: config.apiKey,
162
- api: "openai-completions",
163
- models: config.models.map((m) => ({
164
- id: m.id,
165
- name: m.name,
166
- reasoning: m.reasoning,
167
- input: ["text"] as ("text" | "image")[],
168
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
169
- contextWindow: 128000,
170
- maxTokens: 16384,
171
- })),
172
- });
124
+ /** Build the provider model descriptors from our model entries. */
125
+ function buildModelDescriptors(models: NvidiaModelEntry[]) {
126
+ return models.map((m) => ({
127
+ id: m.id,
128
+ name: m.name,
129
+ reasoning: m.reasoning,
130
+ input: ["text"] as ("text" | "image")[],
131
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
132
+ contextWindow: 128000,
133
+ maxTokens: 16384,
134
+ }));
173
135
  }
174
136
 
175
- export default function nvidiaNimExtension(pi: ExtensionAPI) {
176
- // Register synchronously at init so models are available before Pi finishes loading
177
- const savedConfig = loadConfigSync();
178
- if (savedConfig && savedConfig.models.length > 0) {
179
- registerProvider(pi, savedConfig);
180
- }
181
-
182
- // --- /nvidia-nim-auth: full setup (API key + models) ---
183
- const authHandler = async (args: string | undefined, ctx: ExtensionCommandContext) => {
184
- if (!ctx.hasUI) {
185
- console.log("This command requires interactive mode.");
186
- return;
187
- }
188
-
189
- const existing = await loadConfig();
190
-
191
- // Step 1: API Key
192
- let apiKey = await ctx.ui.input(
193
- "Nvidia NIM — Enter API Key",
194
- existing ? "(current key saved — paste new key or press Enter to keep)" : "Paste your nvapi-... key from build.nvidia.com",
195
- );
196
-
197
- if (!apiKey?.trim() && existing?.apiKey) {
198
- apiKey = existing.apiKey;
199
- } else if (!apiKey?.trim()) {
200
- ctx.ui.notify("Setup cancelled — API key is required.", "error");
201
- return;
202
- }
203
-
204
- apiKey = apiKey.trim();
205
- if (!apiKey.startsWith("nvapi-")) {
206
- const proceed = await ctx.ui.confirm(
207
- "Nvidia NIM — API Key Warning",
208
- `Key doesn't start with "nvapi-". Nvidia NIM keys usually do.\n\nContinue anyway?`,
209
- );
210
- if (!proceed) return;
211
- }
212
-
213
- // Step 2: Models via multi-line editor
214
- const existingModelIds = existing?.models.map((m) => m.id).join("\n") ?? "";
215
- const prefill = MODEL_EDITOR_TEMPLATE + "\n" + (existingModelIds || "meta/llama-3.1-405b-instruct") + "\n";
137
+ /** Build the OAuth config for Nvidia NIM API key authentication. */
138
+ function buildOAuthConfig() {
139
+ return {
140
+ name: "Nvidia NIM",
216
141
 
217
- const modelText = await ctx.ui.editor("Nvidia NIM Edit Models (one per line)", prefill);
142
+ async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
143
+ const apiKey = await callbacks.onPrompt({
144
+ message: "Enter your Nvidia NIM API key (nvapi-... from build.nvidia.com):",
145
+ placeholder: "nvapi-...",
146
+ });
218
147
 
219
- if (!modelText?.trim()) {
220
- ctx.ui.notify("Setup cancelled at least one model is required.", "error");
221
- return;
222
- }
148
+ if (!apiKey?.trim()) {
149
+ throw new Error("API key is required. Get one at https://build.nvidia.com");
150
+ }
223
151
 
224
- let models = parseModelLines(modelText);
225
- if (models.length === 0) {
226
- ctx.ui.notify("No valid model IDs found. Add at least one non-comment line.", "error");
227
- return;
228
- }
152
+ const key = apiKey.trim();
229
153
 
230
- // Validate model IDs against the Nvidia API
231
- ctx.ui.notify("Validating model IDs against Nvidia NIM API...", "info");
232
- const available = await fetchAvailableModels(apiKey);
233
- if (available) {
234
- const invalid = models.filter((m) => !available.has(m.id));
235
- if (invalid.length > 0) {
236
- const names = invalid.map((m) => m.id).join(", ");
237
- const proceed = await ctx.ui.confirm(
238
- "Nvidia NIM — Invalid Model IDs",
239
- `These model IDs were not found on the API:\n\n ${names}\n\nSave anyway (they'll 404), or cancel to fix them?`,
154
+ // Validate by hitting the models endpoint
155
+ const res = await fetch(`${NVIDIA_BASE_URL}/models`, {
156
+ headers: { Authorization: `Bearer ${key}` },
157
+ });
158
+ if (!res.ok) {
159
+ throw new Error(
160
+ `API key validation failed (HTTP ${res.status}). Check your key at build.nvidia.com`,
240
161
  );
241
- if (!proceed) return;
242
162
  }
243
- }
244
163
 
245
- // Save + register + add to scoped models
246
- const config: NvidiaNimConfig = { apiKey, models };
247
- try {
248
- await saveConfig(config);
249
- registerProvider(pi, config);
250
- await updateEnabledModels(models);
251
- const names = models.map((m) => m.id).join(", ");
252
- ctx.ui.notify(`Nvidia NIM configured — ${models.length} model(s): ${names}. Available in /model.`, "info");
253
- } catch (error) {
254
- ctx.ui.notify(`Failed to save: ${error instanceof Error ? error.message : String(error)}`, "error");
255
- }
164
+ return {
165
+ access: key,
166
+ refresh: key, // API keys don't have refresh tokens
167
+ expires: Date.now() + 365 * 24 * 60 * 60 * 1000, // effectively never
168
+ };
169
+ },
170
+
171
+ async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
172
+ // Nvidia API keys don't expire — return as-is with extended expiry
173
+ return {
174
+ ...credentials,
175
+ expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
176
+ };
177
+ },
178
+
179
+ getApiKey(credentials: OAuthCredentials): string {
180
+ return credentials.access;
181
+ },
256
182
  };
183
+ }
257
184
 
258
- // --- /nvidia-nim-models: quick add/edit models without re-entering key ---
185
+ export default function nvidiaNimExtension(pi: ExtensionAPI) {
186
+ // Load saved models synchronously at init so they're available before Pi finishes loading.
187
+ // The provider is always registered (even without models) so `/login nvidia` works.
188
+ const savedModels = loadModelsConfigSync();
189
+ const models = savedModels?.models ?? [];
190
+
191
+ pi.registerProvider(NVIDIA_PROVIDER_NAME, {
192
+ baseUrl: NVIDIA_BASE_URL,
193
+ api: "openai-completions",
194
+ models: buildModelDescriptors(models),
195
+ oauth: buildOAuthConfig(),
196
+ });
197
+
198
+ // --- /nvidia-nim-models: add/edit models ---
259
199
  const modelsHandler = async (args: string | undefined, ctx: ExtensionCommandContext) => {
260
200
  if (!ctx.hasUI) {
261
201
  console.log("This command requires interactive mode.");
262
202
  return;
263
203
  }
264
204
 
265
- const existing = await loadConfig();
266
- if (!existing) {
267
- ctx.ui.notify("Run /nvidia-nim-auth first to set up your API key.", "error");
268
- return;
269
- }
270
-
271
- const existingModelIds = existing.models.map((m) => m.id).join("\n");
272
- const prefill = MODEL_EDITOR_TEMPLATE + "\n" + existingModelIds + "\n";
205
+ const existing = await loadModelsConfig();
206
+ const existingModelIds = existing?.models.map((m) => m.id).join("\n") ?? "";
207
+ const prefill =
208
+ MODEL_EDITOR_TEMPLATE + "\n" + (existingModelIds || "meta/llama-3.1-405b-instruct") + "\n";
273
209
 
274
210
  const modelText = await ctx.ui.editor("Nvidia NIM — Edit Models (one per line)", prefill);
275
211
  if (!modelText?.trim()) {
@@ -277,33 +213,36 @@ export default function nvidiaNimExtension(pi: ExtensionAPI) {
277
213
  return;
278
214
  }
279
215
 
280
- const models = parseModelLines(modelText);
281
- if (models.length === 0) {
216
+ const parsedModels = parseModelLines(modelText);
217
+ if (parsedModels.length === 0) {
282
218
  ctx.ui.notify("No valid model IDs found. Models unchanged.", "error");
283
219
  return;
284
220
  }
285
221
 
286
- const config: NvidiaNimConfig = { apiKey: existing.apiKey, models };
287
222
  try {
288
- await saveConfig(config);
289
- registerProvider(pi, config);
290
- await updateEnabledModels(models);
291
- ctx.ui.notify(`Nvidia NIM models updated — ${models.length} model(s). Available in /model.`, "info");
223
+ await saveModelsConfig({ models: parsedModels });
224
+
225
+ // Re-register provider with updated models (preserving OAuth config)
226
+ pi.registerProvider(NVIDIA_PROVIDER_NAME, {
227
+ baseUrl: NVIDIA_BASE_URL,
228
+ api: "openai-completions",
229
+ models: buildModelDescriptors(parsedModels),
230
+ oauth: buildOAuthConfig(),
231
+ });
232
+
233
+ const names = parsedModels.map((m) => m.id).join(", ");
234
+ ctx.ui.notify(
235
+ `Nvidia NIM models updated — ${parsedModels.length} model(s): ${names}. Use /login nvidia to authenticate.`,
236
+ "info",
237
+ );
292
238
  } catch (error) {
293
- ctx.ui.notify(`Failed to save: ${error instanceof Error ? error.message : String(error)}`, "error");
239
+ ctx.ui.notify(
240
+ `Failed to save: ${error instanceof Error ? error.message : String(error)}`,
241
+ "error",
242
+ );
294
243
  }
295
244
  };
296
245
 
297
- pi.registerCommand("nvidia-nim-auth", {
298
- description: "Configure Nvidia NIM API key and models",
299
- handler: authHandler,
300
- });
301
-
302
- pi.registerCommand("nvidia-auth", {
303
- description: "Configure Nvidia NIM (alias for /nvidia-nim-auth)",
304
- handler: authHandler,
305
- });
306
-
307
246
  pi.registerCommand("nvidia-nim-models", {
308
247
  description: "Add or edit Nvidia NIM models",
309
248
  handler: modelsHandler,
@@ -1,59 +1,80 @@
1
- # Whimsical Extension
1
+ # Whimsical Extension (Chaos Mixer)
2
2
 
3
- A personality engine for the [pi coding agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent). Adds delightful, context-aware, and humorous loading messages to make waiting for the LLM less boring.
3
+ A stupid, nerdy, fun, Bollywood-infused loading-message extension for Pi.
4
4
 
5
- ## Features
5
+ ## What changed
6
6
 
7
- - **Context-Aware Loading Messages**: Messages change based on time of day (Morning/Night) and how long the task is taking.
8
- - **Multiple Personality Modes**: Choose your vibe, from Bollywood drama to Geek humor.
9
- - **Smart Exit**: Adds `/exit` and `/bye` commands that perform a graceful shutdown with a whimsical goodbye message (and ensure your terminal is left clean!).
10
- - **Helpful Tips**: Occasionally shows useful Pi usage tips while you wait.
7
+ Older separate modes (`classic`, `bollywood`, `geek`) are replaced by one chaos mixer with 7 weighted buckets:
11
8
 
12
- ## Usage
9
+ - **A**: Absurd Nerd Lines — grepping the void, refactoring by vibes
10
+ - **B**: Boss Progression — phase-based messages by wait duration
11
+ - **C**: Fake Compiler Panic — chaotic fake diagnostics
12
+ - **D**: Terminal Meme Lines — CLI one-liners and git jokes
13
+ - **E**: Bollywood & Hinglish — classic dialogues, movie vibes, desi dev humor
14
+ - **F**: Whimsical Verbs — Combobulating... Skedaddling... Noodling...
15
+ - **G**: Pi Tips — helpful tips for using Pi effectively
13
16
 
14
- The extension activates automatically. You can configure it using the `/whimsy` command.
17
+ Default split is `A=10 / B=10 / C=10 / D=10 / E=30 / F=15 / G=15` (Bollywood-heavy by default).
15
18
 
16
- ### Commands
19
+ Context-aware overrides still apply: morning messages (5-11 AM), late night messages (12-4 AM), and long-wait reassurance (>5s).
20
+
21
+ ## Commands
17
22
 
18
23
  | Command | Description |
19
- |---------|-------------|
20
- | `/whimsy` | Check current status and usage. |
21
- | `/whimsy <mode>` | Switch mode (chaos, bollywood, geek, classic). |
22
- | `/whimsy on` | Enable whimsical messages. |
23
- | `/whimsy off` | Disable (revert to standard "Thinking..." messages). |
24
- | `/exit` | Exit Pi gracefully with a random goodbye message. |
25
- | `/bye` | Alias for `/exit`. |
26
-
27
- ### Modes
28
-
29
- - **`chaos` (Default)**: A curated mix of everything!
30
- - 50% Bollywood/Hinglish
31
- - 30% Helpful Tips
32
- - 20% Sci-Fi/Classic Gerunds
33
- - **`bollywood`**: 100% Bollywood dialogues and Hinglish developer memes.
34
- - *"Picture abhi baaki hai mere dost..."*
35
- - *"Tareekh pe tareekh..."*
36
- - **`geek`**: Sci-Fi, Cyberpunk, and Developer humor.
37
- - *"Reticulating splines..."*
38
- - *"Downloading more RAM..."*
39
- - **`classic`**: Simple, whimsical verbs (Claude-style).
40
- - *"Schlepping..."*, *"Combobulating..."*
41
-
42
- ## Examples
43
-
44
- **Long Wait (Story Mode):**
45
- If a task takes longer than 5 seconds, the message changes to reassure you:
46
- > "Abhi hum zinda hain!" (We are still alive!)
47
- > "Sabar ka phal meetha hota hai..." (Patience pays off...)
48
-
49
- **Late Night (12 AM - 4 AM):**
50
- > "Soja beta, varna Gabbar aa jayega..."
51
- > "Burning the midnight oil..."
52
-
53
- ## Installation
54
-
55
- Included in `pi-agent-extensions`.
56
-
57
- ```bash
58
- pi install npm:pi-agent-extensions
24
+ |---|---|
25
+ | `/whimsy` | Open interactive percentage tuner window |
26
+ | `/whimsy on` | Enable whimsical messages |
27
+ | `/whimsy off` | Disable whimsical messages |
28
+ | `/whimsy status` | Show enabled state + current percentages + spinner preset |
29
+ | `/whimsy reset` | Reset to default weights + default spinner preset |
30
+ | `/exit` | Exit Pi with a weighted goodbye (uses current bucket percentages) |
31
+ | `/bye` | Alias for `/exit` |
32
+
33
+ ## Interactive tuner behavior
34
+
35
+ When you run `/whimsy`, you get a window where:
36
+
37
+ - a live spinner preview is shown using the current weights
38
+ - preview spinner animates at real-turn cadence and preview message rotates every 10 seconds
39
+
40
+ - `↑ / ↓` moves between A-G rows and the spinner row at the bottom
41
+ - `← / →` adjusts the selected bucket by **5** (or switches spinner preset when spinner row is selected)
42
+ - spinner row shows a small frame sample next to each preset name
43
+ - `Enter` saves **only if total = 100**
44
+ - `Esc` cancels
45
+
46
+ Totals are allowed to go below/above 100 while tuning, but save is blocked until total is exactly 100. A warning is shown at the bottom when invalid.
47
+
48
+ Spinner presets included: Sleek Orbit, Neon Pulse, Scanline, Chevron Flow, Matrix Glyph.
49
+
50
+ Selected spinner preset is applied to Pi's live loader spinner (not just preview).
51
+
52
+ Goodbye messages also use A-G bucketed pools and follow the same weighted split.
53
+
54
+ ## Persistence
55
+
56
+ Settings persist globally in:
57
+
58
+ - `~/.pi/agent/settings.json` under the `whimsical` key
59
+
60
+ No local project settings are used for whimsy anymore.
61
+
62
+ Example:
63
+
64
+ ```json
65
+ {
66
+ "whimsical": {
67
+ "enabled": true,
68
+ "weights": {
69
+ "A": 10,
70
+ "B": 10,
71
+ "C": 10,
72
+ "D": 10,
73
+ "E": 30,
74
+ "F": 15,
75
+ "G": 15
76
+ },
77
+ "spinnerPreset": "sleekOrbit"
78
+ }
79
+ }
59
80
  ```