bermudis-pi-goodies 0.1.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 ADDED
@@ -0,0 +1,112 @@
1
+ # bermudis-pi-goodies
2
+
3
+ A bundle of small, frequently-used [Pi](https://github.com/earendil-works/pi)
4
+ extensions. One entry point, eight independent features.
5
+
6
+ | Feature | Command / hook | What it does |
7
+ |---------|----------------|--------------|
8
+ | `copy-with-model` | `/copy-with-model` | Copy last assistant message to the clipboard in a code fence tagged with the model name. |
9
+ | `name-with-ai` | `/name-with-ai [name]` | Generate a short session name from the first user message (or set one manually). |
10
+ | `zed` | `/z` | Open Zed editor on the current working directory. |
11
+ | `prefer-tools` | hook (no command) | Nudge toward modern CLIs: `rg` over `grep`, `fd` over `find`, `uv` over bare `python`/`pip`/`pytest`/`mypy`. |
12
+ | `model-thinking` | hook + `/model-thinking` | Apply provider/model thinking defaults and explicitly save per-model defaults. |
13
+ | `fixed-defaults` | hook + `/fixed-defaults` | Keep the global startup provider, model, and thinking defaults fixed while allowing in-session model changes; `/fixed-defaults set` pins the current model and thinking level. |
14
+ | `kilo` | provider | Access Kilo Gateway models via `/login kilo` or `KILO_API_KEY`. |
15
+ | `provider-balance` | footer (no command) | Show remaining Kilo or OpenRouter credits, z.ai token-plan quota, or OpenAI Codex quota on the right side of the working-directory footer line. |
16
+
17
+ ## Install
18
+
19
+ After publishing the package to npm:
20
+
21
+ ```bash
22
+ pi install npm:bermudis-pi-goodies@0.1.0
23
+ ```
24
+
25
+ Remove any old `bermudis-pi-goodies.ts` symlink before reloading Pi. Each
26
+ feature is independent — disabling one is a one-line edit in `index.ts`.
27
+ Kilo's provider and its balance footer are bundled here.
28
+
29
+ ## Model-specific thinking
30
+
31
+ This feature is deliberately opt-in: models not covered by the config retain
32
+ Pi's native thinking-level behavior. Create `~/.pi/agent/model-thinking.json`
33
+ with provider defaults, exact model defaults, or both:
34
+
35
+ ```json
36
+ {
37
+ "providers": {
38
+ "anthropic": "high",
39
+ "openai-codex": "xhigh"
40
+ },
41
+ "models": {
42
+ "anthropic/claude-haiku-4-5": "low"
43
+ }
44
+ }
45
+ ```
46
+
47
+ Exact `provider/model-id` entries take precedence over provider defaults. Use
48
+ `/model-thinking set` to explicitly save the current model's current thinking
49
+ level as an exact-model entry; it also bootstraps models not yet covered by the
50
+ config and overwrites an existing entry. Manual thinking-level changes in Pi do
51
+ not modify this file. All current Pi levels are accepted:
52
+ `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`.
53
+ `/model-thinking` shows the active resolution and config path;
54
+ `/model-thinking reset` deletes the whole config.
55
+
56
+ ## Fixed startup defaults
57
+
58
+ Pi normally saves the last model and thinking level selected in the global
59
+ settings file. The bundle pins those cross-session defaults to:
60
+
61
+ ```json
62
+ {
63
+ "defaultProvider": "openai-codex",
64
+ "defaultModel": "gpt-5.6-luna",
65
+ "defaultThinkingLevel": "max"
66
+ }
67
+ ```
68
+
69
+ Selecting a different model still changes the active session and its transcript;
70
+ `fixed-defaults` only restores the startup values after Pi persists a selection.
71
+
72
+ The pinned values are the built-ins above, optionally overridden per field by
73
+ `~/.pi/agent/fixed-defaults.json` — omit a field to keep the built-in value for
74
+ it, and hand edits are picked up without a reload. Manage it from Pi:
75
+
76
+ - `/fixed-defaults set` — pin the currently active model and thinking level as
77
+ the new startup defaults (written to the override file and applied to
78
+ `settings.json` immediately).
79
+ - `/fixed-defaults` — show the effective pin, the active model, and the
80
+ override file path.
81
+ - `/fixed-defaults reset` — delete the override file and fall back to the
82
+ built-in values.
83
+
84
+ ## Provider and balance details
85
+
86
+ Kilo registration is network-free: it starts with `kilo-auto/free`, restores
87
+ an authenticated catalog from Pi's model store, and normally revalidates that
88
+ catalog no more than every four hours. Balance and quota requests run in the background so they
89
+ never delay session startup, model selection, or post-run input readiness.
90
+ The footer also reads OpenRouter remaining credits for `openrouter`, z.ai GLM
91
+ Coding Plan token quota for `zai` (Global) and `zai-coding-cn` (BigModel China),
92
+ and OpenAI Codex's ChatGPT subscription quota when using `openai-codex` OAuth;
93
+ it skips platform API-key auth because that has no ChatGPT subscription quota.
94
+ OpenRouter uses `GET /api/v1/credits`; z.ai uses
95
+ `GET /api/monitor/usage/quota/limit`; Codex uses `GET /wham/usage`. z.ai and
96
+ Codex show both each quota's window length and its `nextResetTime`/`reset_at`
97
+ countdown (when supplied), and `CODEX_API_URL` or `CHATGPT_BASE_URL` can
98
+ override the Codex base URL.
99
+
100
+ All providers share one renderer: every balance is projected to a list of
101
+ segments and formatted the same way. Credits render as `$1.5k`; each usage
102
+ window renders as `[label ]<window> <remaining>%[ ↻<countdown>]`, e.g.
103
+ `7d 72% ↻4d4h` (`↻` = resets in). Multiple windows are joined with `·`, and
104
+ named extra limits like Codex Spark get a label: `7d 72% ↻4d4h · Spark 7d 74% ↻5d4h`.
105
+ The footer refreshes on session start, model
106
+ switch, and after each completed run (`agent_settled`), so it tracks both
107
+ consumption and external tier changes for whatever provider is active —
108
+ providers without a balance adapter are skipped, so this costs nothing for
109
+ unrelated sessions. Do not also load the standalone `kilo.ts` or
110
+ `provider-balance.ts` entries once this bundle is installed. If you previously
111
+ symlinked the standalone `provider-balance.ts`, remove that link — the feature
112
+ now ships in this bundle.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * /copy-with-model — Copy last assistant message wrapped in a code block
3
+ * tagged with the model name. Escapes backticks if needed.
4
+ *
5
+ * Example output for claude-sonnet-4:
6
+ * ```claude-sonnet-4
7
+ * Ok
8
+ * ```
9
+ */
10
+
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import { execSync, spawn } from "node:child_process";
13
+ import { platform } from "node:os";
14
+
15
+ // ── Clipboard ─────────────────────────────────────────────────────────
16
+
17
+ function copyToClipboard(text: string): void {
18
+ const p = platform();
19
+ try {
20
+ if (p === "darwin") {
21
+ execSync("pbcopy", { input: text, timeout: 5000 });
22
+ return;
23
+ }
24
+ if (p === "win32") {
25
+ execSync("clip", { input: text, timeout: 5000 });
26
+ return;
27
+ }
28
+ // Linux — Wayland
29
+ if (process.env.WAYLAND_DISPLAY) {
30
+ const proc = spawn("wl-copy", [], {
31
+ stdio: ["pipe", "ignore", "ignore"],
32
+ });
33
+ proc.stdin.on("error", () => {});
34
+ proc.stdin.write(text);
35
+ proc.stdin.end();
36
+ proc.unref();
37
+ return;
38
+ }
39
+ // Linux — X11
40
+ if (process.env.DISPLAY) {
41
+ try {
42
+ execSync("xclip -selection clipboard", { input: text, timeout: 5000 });
43
+ } catch {
44
+ execSync("xsel --clipboard --input", { input: text, timeout: 5000 });
45
+ }
46
+ return;
47
+ }
48
+ } catch {
49
+ // OSC 52 fallback
50
+ const encoded = Buffer.from(text).toString("base64");
51
+ if (encoded.length <= 100_000) {
52
+ process.stdout.write(`\x1b]52;c;${encoded}\x07`);
53
+ return;
54
+ }
55
+ }
56
+ throw new Error("No clipboard available");
57
+ }
58
+
59
+ // ── Helpers ───────────────────────────────────────────────────────────
60
+
61
+ /** Get text content of the last assistant entry from session entries. */
62
+ function getLastAssistantText(entries: any[]): string | undefined {
63
+ for (let i = entries.length - 1; i >= 0; i--) {
64
+ const entry = entries[i];
65
+ if (entry.type !== "message") continue;
66
+ const msg = entry.message;
67
+ if (!msg || msg.role !== "assistant") continue;
68
+ // Skip aborted messages with no content
69
+ if (
70
+ msg.stopReason === "aborted" &&
71
+ (!msg.content || msg.content.length === 0)
72
+ )
73
+ continue;
74
+ let text = "";
75
+ for (const block of msg.content ?? []) {
76
+ if (block.type === "text") text += block.text;
77
+ }
78
+ return text.trim() || undefined;
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ /** Derive a short model tag from the full model id. */
84
+ function modelTag(model: { provider: string; id: string }): string {
85
+ // e.g. "anthropic/claude-sonnet-4" → "claude-sonnet-4"
86
+ // "openai/gpt-4o" → "gpt-4o"
87
+ return model.id;
88
+ }
89
+
90
+ /**
91
+ * Wrap text in a code fence, escalating fence level if the content
92
+ * already contains backtick sequences.
93
+ *
94
+ * ``` → ````
95
+ * ```` → `````
96
+ * etc.
97
+ */
98
+ function wrapInCodeBlock(tag: string, text: string): string {
99
+ // Find the longest run of consecutive backticks in the content
100
+ let maxRun = 0;
101
+ for (const line of text.split("\n")) {
102
+ let run = 0;
103
+ for (const ch of line) {
104
+ if (ch === "`") {
105
+ run++;
106
+ maxRun = Math.max(maxRun, run);
107
+ } else {
108
+ run = 0;
109
+ }
110
+ }
111
+ }
112
+
113
+ // Fence needs at least 3 backticks and one more than the longest run
114
+ const fenceLen = Math.max(3, maxRun + 1);
115
+ const fence = "`".repeat(fenceLen);
116
+
117
+ return `${fence}${tag}\n${text}\n${fence}`;
118
+ }
119
+
120
+ // ── Extension ─────────────────────────────────────────────────────────
121
+
122
+ export default function (pi: ExtensionAPI) {
123
+ pi.registerCommand("copy-with-model", {
124
+ description:
125
+ "Copy last assistant message to clipboard in a code block tagged with the model name",
126
+ handler: async (_args, ctx) => {
127
+ await ctx.waitForIdle();
128
+
129
+ const entries = ctx.sessionManager.getEntries();
130
+ const text = getLastAssistantText(entries);
131
+
132
+ if (!text) {
133
+ ctx.ui.notify("No assistant messages to copy", "error");
134
+ return;
135
+ }
136
+
137
+ const model = ctx.model;
138
+ if (!model) {
139
+ ctx.ui.notify("No model selected", "error");
140
+ return;
141
+ }
142
+
143
+ const tag = modelTag(model);
144
+ const wrapped = wrapInCodeBlock(tag, text);
145
+
146
+ try {
147
+ copyToClipboard(wrapped);
148
+ ctx.ui.notify(`Copied with model \`${tag}\``, "info");
149
+ } catch (err) {
150
+ ctx.ui.notify(
151
+ `Failed to copy: ${err instanceof Error ? err.message : err}`,
152
+ "error",
153
+ );
154
+ }
155
+ },
156
+ });
157
+ }
@@ -0,0 +1,362 @@
1
+ import {
2
+ getAgentDir,
3
+ SettingsManager,
4
+ type ExtensionAPI,
5
+ type ExtensionContext,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ mkdirSync,
9
+ readFileSync,
10
+ renameSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+
16
+ const CONFIG_FILENAME = "fixed-defaults.json";
17
+ const ALL_LEVELS = [
18
+ "off",
19
+ "minimal",
20
+ "low",
21
+ "medium",
22
+ "high",
23
+ "xhigh",
24
+ "max",
25
+ ] as const;
26
+
27
+ type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
28
+
29
+ /**
30
+ * Values read from the override file. `thinkingLevel` may layer independently;
31
+ * `provider` and `model` are a coupled pair — a model id is meaningless without
32
+ * its provider, so both must be present if either is. Missing fields fall back
33
+ * to the built-in defaults.
34
+ */
35
+ interface FixedDefaultsOverride {
36
+ provider?: string;
37
+ model?: string;
38
+ thinkingLevel?: ThinkingLevel;
39
+ }
40
+
41
+ interface FixedDefaultsOptions {
42
+ /** Internal seam used by tests; normal callers use Pi's global agent dir. */
43
+ agentDir?: string;
44
+ /** Internal seam used by tests; defaults to <agentDir>/fixed-defaults.json. */
45
+ configPath?: string;
46
+ }
47
+
48
+ function describeError(error: unknown): string {
49
+ return error instanceof Error ? error.message : String(error);
50
+ }
51
+
52
+ function isThinkingLevel(value: unknown): value is ThinkingLevel {
53
+ return (
54
+ typeof value === "string" &&
55
+ (ALL_LEVELS as readonly string[]).includes(value)
56
+ );
57
+ }
58
+
59
+ function parseOverride(value: unknown): FixedDefaultsOverride {
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
61
+ throw new Error("the top level must be an object");
62
+ }
63
+
64
+ const input = value as Record<string, unknown>;
65
+ const unknownKeys = Object.keys(input).filter(
66
+ (key) => key !== "provider" && key !== "model" && key !== "thinkingLevel",
67
+ );
68
+ if (unknownKeys.length > 0) {
69
+ throw new Error(`unknown field(s): ${unknownKeys.join(", ")}`);
70
+ }
71
+
72
+ const override: FixedDefaultsOverride = {};
73
+ if (input.provider !== undefined) {
74
+ if (typeof input.provider !== "string" || input.provider.length === 0) {
75
+ throw new Error("`provider` must be a non-empty string");
76
+ }
77
+ override.provider = input.provider;
78
+ }
79
+ if (input.model !== undefined) {
80
+ if (typeof input.model !== "string" || input.model.length === 0) {
81
+ throw new Error("`model` must be a non-empty string");
82
+ }
83
+ override.model = input.model;
84
+ }
85
+ if (input.thinkingLevel !== undefined) {
86
+ if (!isThinkingLevel(input.thinkingLevel)) {
87
+ throw new Error(
88
+ `\`thinkingLevel\` must be one of: ${ALL_LEVELS.join(", ")}`,
89
+ );
90
+ }
91
+ override.thinkingLevel = input.thinkingLevel;
92
+ }
93
+
94
+ // provider and model are a coupled pair — a model id is meaningless without
95
+ // its provider. Require both if either is present so a hand-edited partial
96
+ // file can't pin a broken default. thinkingLevel may still layer alone.
97
+ if (
98
+ (override.provider !== undefined && override.model === undefined) ||
99
+ (override.model !== undefined && override.provider === undefined)
100
+ ) {
101
+ throw new Error(
102
+ "`provider` and `model` must be specified together — a model id is meaningless without its provider",
103
+ );
104
+ }
105
+
106
+ return override;
107
+ }
108
+
109
+ /**
110
+ * Read/write the override file. Reads are uncached (the file is tiny and the
111
+ * events are rare), so hand edits are picked up without a reload. Writes are
112
+ * atomic via temp file + rename.
113
+ */
114
+ class DefaultsStore {
115
+ readonly path: string;
116
+
117
+ constructor(path: string) {
118
+ this.path = path;
119
+ }
120
+
121
+ /** Load the override; returns { override: {}, error: null } when missing. */
122
+ load(): LoadResult {
123
+ let raw: string;
124
+ try {
125
+ raw = readFileSync(this.path, "utf8");
126
+ } catch (error) {
127
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
128
+ return { override: {}, error: null };
129
+ }
130
+ throw error;
131
+ }
132
+
133
+ try {
134
+ return {
135
+ override: parseOverride(JSON.parse(raw) as unknown),
136
+ error: null,
137
+ };
138
+ } catch (error) {
139
+ const message = describeError(error);
140
+ console.error(
141
+ `[fixed-defaults] invalid config at ${this.path}:`,
142
+ message,
143
+ );
144
+ return { override: {}, error: message };
145
+ }
146
+ }
147
+
148
+ save(override: FixedDefaultsOverride): void {
149
+ mkdirSync(dirname(this.path), { recursive: true });
150
+ const temporaryPath = `${this.path}.${process.pid}.${Date.now()}.tmp`;
151
+
152
+ try {
153
+ writeFileSync(temporaryPath, `${JSON.stringify(override, null, 2)}\n`, {
154
+ encoding: "utf8",
155
+ mode: 0o600,
156
+ });
157
+ renameSync(temporaryPath, this.path);
158
+ } catch (error) {
159
+ try {
160
+ unlinkSync(temporaryPath);
161
+ } catch {
162
+ // The temporary file usually does not exist when the initial write failed.
163
+ }
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ reset(): boolean {
169
+ try {
170
+ unlinkSync(this.path);
171
+ return true;
172
+ } catch (error) {
173
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
174
+ throw error;
175
+ }
176
+ }
177
+ }
178
+
179
+ /** Result of reading the override file: the parsed values plus any error. */
180
+ interface LoadResult {
181
+ override: FixedDefaultsOverride;
182
+ /** Non-null when the file existed but failed to parse or validate. */
183
+ error: string | null;
184
+ }
185
+
186
+ /**
187
+ * Keep Pi's cross-session defaults stable while still allowing model changes in
188
+ * the current session. Pi intentionally saves the last selected model and
189
+ * thinking level, so this runs after those notifications and restores the
190
+ * configured startup defaults in the global settings file.
191
+ *
192
+ * The pin lives in `<agentDir>/fixed-defaults.json` and is the sole source of
193
+ * truth — there are no built-in defaults, so with no override file the
194
+ * extension is dormant and Pi's native last-selection behavior is preserved.
195
+ * `thinkingLevel` may layer independently; `provider` and `model` are a
196
+ * coupled pair. `/fixed-defaults set` pins the currently active model and
197
+ * thinking level; `/fixed-defaults reset` stops pinning; `/fixed-defaults`
198
+ * shows the active pin.
199
+ */
200
+ export default function fixedDefaults(
201
+ pi: ExtensionAPI,
202
+ options: FixedDefaultsOptions = {},
203
+ ): void {
204
+ const agentDir = options.agentDir ?? getAgentDir();
205
+ const store = new DefaultsStore(
206
+ options.configPath ?? join(agentDir, CONFIG_FILENAME),
207
+ );
208
+ let pending = Promise.resolve();
209
+
210
+ async function restore(ctx: ExtensionContext): Promise<void> {
211
+ const { override, error } = store.load();
212
+ // A broken or absent override means no pin: leave settings untouched so
213
+ // Pi's native last-selection behavior is preserved rather than guessed at.
214
+ if (error) return;
215
+ const hasModelPin =
216
+ override.provider !== undefined && override.model !== undefined;
217
+ const hasThinkingPin = override.thinkingLevel !== undefined;
218
+ if (!hasModelPin && !hasThinkingPin) return;
219
+
220
+ const settings = SettingsManager.create(ctx.cwd, agentDir, {
221
+ projectTrusted: ctx.isProjectTrusted(),
222
+ });
223
+ if (hasModelPin) {
224
+ settings.setDefaultModelAndProvider(override.provider!, override.model!);
225
+ }
226
+ if (hasThinkingPin) {
227
+ settings.setDefaultThinkingLevel(override.thinkingLevel!);
228
+ }
229
+ await settings.flush();
230
+
231
+ const errors = settings.drainErrors();
232
+ if (errors.length > 0) {
233
+ const details = errors
234
+ .map(({ scope, error }) => `${scope}: ${describeError(error)}`)
235
+ .join("; ");
236
+ throw new Error(`failed to persist fixed defaults (${details})`);
237
+ }
238
+ }
239
+
240
+ function schedule(ctx: ExtensionContext): Promise<void> {
241
+ const operation = pending.then(() => restore(ctx));
242
+ // Keep later notifications serviceable after one failed write. The
243
+ // original operation is still returned so Pi can report that failure.
244
+ pending = operation.catch((error: unknown) => {
245
+ console.error("[fixed-defaults] failed to restore defaults:", error);
246
+ });
247
+ return operation;
248
+ }
249
+
250
+ pi.on("session_start", (_event, ctx) => schedule(ctx));
251
+ pi.on("model_select", (_event, ctx) => schedule(ctx));
252
+ pi.on("thinking_level_select", (_event, ctx) => schedule(ctx));
253
+
254
+ pi.registerCommand("fixed-defaults", {
255
+ description:
256
+ "Show, set, or reset the pinned startup model/thinking defaults",
257
+ handler: async (args, ctx) => {
258
+ const command = args.trim();
259
+
260
+ if (command === "set") {
261
+ const model = ctx.model;
262
+ if (!model) {
263
+ ctx.ui.notify(
264
+ "Cannot pin defaults without an active model.",
265
+ "warning",
266
+ );
267
+ return;
268
+ }
269
+
270
+ const level = pi.getThinkingLevel();
271
+ const override: FixedDefaultsOverride = {
272
+ provider: model.provider,
273
+ model: model.id,
274
+ thinkingLevel: level,
275
+ };
276
+ try {
277
+ store.save(override);
278
+ } catch (error) {
279
+ console.error("[fixed-defaults] failed to save override:", error);
280
+ ctx.ui.notify("Failed to save the new fixed defaults.", "error");
281
+ return;
282
+ }
283
+
284
+ try {
285
+ // Bring settings.json in line with the new pin right away instead
286
+ // of waiting for the next model/thinking/session event.
287
+ await schedule(ctx);
288
+ } catch (error) {
289
+ console.error("[fixed-defaults] failed to apply override:", error);
290
+ ctx.ui.notify(
291
+ "Defaults saved, but applying them to settings.json failed.",
292
+ "warning",
293
+ );
294
+ return;
295
+ }
296
+
297
+ ctx.ui.notify(
298
+ `Pinned ${model.provider}/${model.id} · thinking ${level} as startup defaults`,
299
+ "info",
300
+ );
301
+ return;
302
+ }
303
+
304
+ if (command === "reset") {
305
+ let removed: boolean;
306
+ try {
307
+ removed = store.reset();
308
+ } catch (error) {
309
+ console.error("[fixed-defaults] failed to reset override:", error);
310
+ ctx.ui.notify("Failed to reset fixed defaults.", "error");
311
+ return;
312
+ }
313
+
314
+ // Removing the override makes future events no-ops; there is nothing
315
+ // to apply to settings.json now. Existing defaults are left as-is so
316
+ // reset means "stop pinning," not "revert to an earlier state."
317
+ ctx.ui.notify(
318
+ removed
319
+ ? "Fixed-defaults pin removed. Pi will use your last selection."
320
+ : "No fixed-defaults override file to reset.",
321
+ "info",
322
+ );
323
+ return;
324
+ }
325
+
326
+ if (command !== "") {
327
+ ctx.ui.notify("Usage: /fixed-defaults [set|reset]", "warning");
328
+ return;
329
+ }
330
+
331
+ const { override, error } = store.load();
332
+ const model = ctx.model;
333
+ const lines = [
334
+ `model: ${model ? `${model.provider}/${model.id}` : "none"}`,
335
+ `thinking: ${pi.getThinkingLevel()}`,
336
+ "",
337
+ ];
338
+ if (error) {
339
+ lines.push(`⚠ override file invalid — no pin active: ${error}`);
340
+ } else if (Object.keys(override).length === 0) {
341
+ lines.push("No pin active. Pi will use your last selection.");
342
+ } else {
343
+ if (override.provider !== undefined && override.model !== undefined) {
344
+ lines.push(`pinned provider: ${override.provider}`);
345
+ lines.push(`pinned model: ${override.model}`);
346
+ }
347
+ if (override.thinkingLevel !== undefined) {
348
+ lines.push(`pinned thinking: ${override.thinkingLevel}`);
349
+ }
350
+ }
351
+ lines.push("", `override file: ${store.path}`);
352
+ lines.push(
353
+ "",
354
+ "run `/fixed-defaults set` to pin the current model and thinking level; `/fixed-defaults reset` to stop pinning",
355
+ );
356
+ const message = lines.join("\n");
357
+
358
+ if (ctx.hasUI) ctx.ui.notify(message, "info");
359
+ else console.log(message);
360
+ },
361
+ });
362
+ }
package/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * bermudis-pi-goodies — a bundle of small, frequently-used Pi extensions.
3
+ *
4
+ * Composes independent modules, each registering its own commands/hooks
5
+ * against the shared ExtensionAPI:
6
+ * - copy-with-model /copy-with-model copy last reply tagged with the model
7
+ * - name-with-ai /name-with-ai generate a session name via the model
8
+ * - zed /z open Zed on cwd
9
+ * - prefer-tools hook block legacy tools (use trash/rg/fd/uv)
10
+ * - model-thinking hook + command remember thinking levels by provider/model
11
+ * - fixed-defaults hook + command keep startup defaults stable; /fixed-defaults set pins the current model
12
+ * - kilo provider access Kilo Gateway models
13
+ * - provider-balance footer show Kilo credits or Codex quota in the footer
14
+ */
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import copyWithModel from "./copy-with-model.ts";
17
+ import fixedDefaults from "./fixed-defaults.ts";
18
+ import kilo from "./kilo.ts";
19
+ import modelThinking from "./model-thinking.ts";
20
+ import providerBalance from "./provider-balance.ts";
21
+ import nameWithAi from "./name-with-ai.ts";
22
+ import zed from "./zed.ts";
23
+ import preferTools from "./prefer-tools.ts";
24
+
25
+ export default function bermudisPiGoodies(pi: ExtensionAPI): void {
26
+ copyWithModel(pi);
27
+ nameWithAi(pi);
28
+ zed(pi);
29
+ preferTools(pi);
30
+ modelThinking(pi);
31
+ fixedDefaults(pi);
32
+ providerBalance(pi);
33
+ kilo(pi);
34
+ }