tau-all-agent 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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/extensions/answer.ts +601 -0
  4. package/extensions/branch-term.ts +405 -0
  5. package/extensions/btw.ts +444 -0
  6. package/extensions/ghostty.ts +301 -0
  7. package/extensions/git-diff-stats.ts +277 -0
  8. package/extensions/git-pr-status.ts +286 -0
  9. package/extensions/insights.ts +2367 -0
  10. package/extensions/interlude.ts +144 -0
  11. package/extensions/loop.ts +529 -0
  12. package/extensions/memory.ts +1889 -0
  13. package/extensions/notify.ts +161 -0
  14. package/extensions/openai-fast.ts +227 -0
  15. package/extensions/openai-verbosity.ts +223 -0
  16. package/extensions/review.ts +4347 -0
  17. package/extensions/sandbox/index.ts +2578 -0
  18. package/extensions/telegram/README.md +81 -0
  19. package/extensions/telegram/daemon.mjs +1942 -0
  20. package/extensions/telegram/index.ts +1073 -0
  21. package/extensions/telegram/message-text.d.mts +1 -0
  22. package/extensions/telegram/message-text.mjs +16 -0
  23. package/extensions/usage/anthropic.ts +198 -0
  24. package/extensions/usage/github-copilot.ts +204 -0
  25. package/extensions/usage/google-gemini-cli.ts +232 -0
  26. package/extensions/usage/index.ts +2310 -0
  27. package/extensions/usage/minimax.ts +208 -0
  28. package/extensions/usage/openai-codex.ts +180 -0
  29. package/extensions/usage/openrouter.ts +168 -0
  30. package/extensions/usage/providers.ts +26 -0
  31. package/extensions/usage/shared.ts +156 -0
  32. package/extensions/usage/types.ts +53 -0
  33. package/extensions/usage/zai.ts +186 -0
  34. package/extensions/websearch/README.md +68 -0
  35. package/extensions/websearch/browser/chromium.ts +330 -0
  36. package/extensions/websearch/browser/discovery.ts +75 -0
  37. package/extensions/websearch/browser/firefox.ts +150 -0
  38. package/extensions/websearch/browser/sqlite.ts +38 -0
  39. package/extensions/websearch/config.ts +76 -0
  40. package/extensions/websearch/index.ts +312 -0
  41. package/extensions/websearch/normalize.ts +155 -0
  42. package/extensions/websearch/providers/anthropic.pi.ts +139 -0
  43. package/extensions/websearch/providers/gemini.browser.ts +185 -0
  44. package/extensions/websearch/providers/gemini.pi.ts +108 -0
  45. package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
  46. package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
  47. package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
  48. package/extensions/websearch/providers/pi-model.shared.ts +86 -0
  49. package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
  50. package/extensions/websearch/providers/shared.ts +76 -0
  51. package/extensions/websearch/types.ts +52 -0
  52. package/extensions/worktree.ts +2223 -0
  53. package/package.json +76 -0
  54. package/skills/browser-tools/SKILL.md +253 -0
  55. package/skills/browser-tools/scripts/browser-content.js +100 -0
  56. package/skills/browser-tools/scripts/browser-cookies.js +33 -0
  57. package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
  58. package/skills/browser-tools/scripts/browser-eval.js +75 -0
  59. package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
  60. package/skills/browser-tools/scripts/browser-nav.js +28 -0
  61. package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
  62. package/skills/browser-tools/scripts/browser-pick.js +133 -0
  63. package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
  64. package/skills/browser-tools/scripts/browser-start.js +281 -0
  65. package/skills/browser-tools/scripts/browser-watch.js +321 -0
  66. package/skills/browser-tools/scripts/utils.js +60 -0
  67. package/skills/git-clean-history/SKILL.md +45 -0
  68. package/skills/git-commit/SKILL.md +55 -0
  69. package/skills/homeassistant-ops/SKILL.md +184 -0
  70. package/skills/homeassistant-ops/references/api.md +64 -0
  71. package/skills/homeassistant-ops/references/id_conventions.md +71 -0
  72. package/skills/homeassistant-ops/references/playbook.md +150 -0
  73. package/skills/homeassistant-ops/scripts/ha_ops.js +2942 -0
  74. package/skills/openscad/SKILL.md +149 -0
  75. package/skills/openscad/examples/parametric_box.scad +92 -0
  76. package/skills/openscad/examples/phone_stand.scad +95 -0
  77. package/skills/openscad/scripts/common.sh +49 -0
  78. package/skills/openscad/scripts/export-stl.sh +56 -0
  79. package/skills/openscad/scripts/extract-params.sh +147 -0
  80. package/skills/openscad/scripts/multi-preview.sh +68 -0
  81. package/skills/openscad/scripts/preview.sh +74 -0
  82. package/skills/openscad/scripts/render-with-params.sh +91 -0
  83. package/skills/openscad/scripts/validate.sh +46 -0
  84. package/skills/oracle/SKILL.md +64 -0
  85. package/skills/oracle/scripts/oracle +249 -0
  86. package/skills/oracle/scripts/oracle-bundle +227 -0
  87. package/skills/sentry/SKILL.md +198 -0
  88. package/skills/sentry/lib/auth.js +99 -0
  89. package/skills/sentry/scripts/fetch-event.js +325 -0
  90. package/skills/sentry/scripts/fetch-issue.js +368 -0
  91. package/skills/sentry/scripts/list-issues.js +245 -0
  92. package/skills/sentry/scripts/search-events.js +291 -0
  93. package/skills/sentry/scripts/search-logs.js +234 -0
  94. package/skills/update-changelog/SKILL.md +135 -0
  95. package/skills/web-design/SKILL.md +117 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Pi Notify Extension
3
+ *
4
+ * Sends native terminal notifications when Pi is waiting for input.
5
+ * - Agent finished and ready for the next prompt
6
+ * - Extension prompt is waiting for a question/confirmation answer
7
+ * Supports multiple terminal protocols:
8
+ * - OSC 777: Ghostty, iTerm2, WezTerm, rxvt-unicode
9
+ * - OSC 99: Kitty
10
+ * - Windows toast: Windows Terminal (WSL)
11
+ */
12
+
13
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
14
+
15
+ const REVIEW_EVENT_START = "review:start";
16
+ const REVIEW_EVENT_END = "review:end";
17
+
18
+ function windowsToastScript(title: string, body: string): string {
19
+ const type = "Windows.UI.Notifications";
20
+ const mgr = `[${type}.ToastNotificationManager, ${type}, ContentType = WindowsRuntime]`;
21
+ const template = `[${type}.ToastTemplateType]::ToastText01`;
22
+ const toast = `[${type}.ToastNotification]::new($xml)`;
23
+ return [
24
+ `${mgr} > $null`,
25
+ `$xml = [${type}.ToastNotificationManager]::GetTemplateContent(${template})`,
26
+ `$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${body}')) > $null`,
27
+ `[${type}.ToastNotificationManager]::CreateToastNotifier('${title}').Show(${toast})`,
28
+ ].join("; ");
29
+ }
30
+
31
+ function notifyOSC777(title: string, body: string): void {
32
+ process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
33
+ }
34
+
35
+ function notifyOSC99(title: string, body: string): void {
36
+ // Kitty OSC 99: i=notification id, d=0 means not done yet, p=body for second part
37
+ process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`);
38
+ process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`);
39
+ }
40
+
41
+ function notifyWindows(title: string, body: string): void {
42
+ const { execFile } = require("child_process");
43
+ execFile("powershell.exe", ["-NoProfile", "-Command", windowsToastScript(title, body)]);
44
+ }
45
+
46
+ function notify(title: string, body: string): void {
47
+ if (!process.stdout.isTTY || !process.stdin.isTTY) return;
48
+
49
+ if (process.env.WT_SESSION) {
50
+ notifyWindows(title, body);
51
+ } else if (process.env.KITTY_WINDOW_ID) {
52
+ notifyOSC99(title, body);
53
+ } else {
54
+ notifyOSC777(title, body);
55
+ }
56
+ }
57
+
58
+ function getPromptSource(event: unknown): string | undefined {
59
+ if (!event || typeof event !== "object") return undefined;
60
+ const source = (event as Record<string, unknown>).source;
61
+ if (typeof source !== "string") return undefined;
62
+ const trimmed = source.trim();
63
+ return trimmed.length > 0 ? trimmed : undefined;
64
+ }
65
+
66
+ function getSessionKey(ctx: ExtensionContext): string {
67
+ return ctx.sessionManager.getSessionFile() ?? `session:${ctx.sessionManager.getSessionId()}`;
68
+ }
69
+
70
+ function extractReviewSessionKey(data: unknown): string | undefined {
71
+ if (!data || typeof data !== "object") return undefined;
72
+ const payload = data as { sessionKey?: unknown };
73
+ if (typeof payload.sessionKey !== "string") return undefined;
74
+ const sessionKey = payload.sessionKey.trim();
75
+ return sessionKey.length > 0 ? sessionKey : undefined;
76
+ }
77
+
78
+ function extractReviewOutcome(data: unknown): "success" | "failed" | "cancelled" | undefined {
79
+ if (!data || typeof data !== "object") return undefined;
80
+ const payload = data as { outcome?: unknown };
81
+ if (
82
+ payload.outcome === "success" ||
83
+ payload.outcome === "failed" ||
84
+ payload.outcome === "cancelled"
85
+ ) {
86
+ return payload.outcome;
87
+ }
88
+ return undefined;
89
+ }
90
+
91
+ export default function (pi: ExtensionAPI) {
92
+ let pendingPromptCount = 0;
93
+ const activeReviewSessions = new Set<string>();
94
+ let currentSessionKey: string | undefined;
95
+
96
+ const hasCurrentSessionReviewRun = () => {
97
+ if (!currentSessionKey) return false;
98
+ return activeReviewSessions.has(currentSessionKey);
99
+ };
100
+
101
+ pi.on("session_start", async (_event, ctx) => {
102
+ currentSessionKey = getSessionKey(ctx);
103
+ });
104
+
105
+ pi.events.on("ui:prompt_start", (data) => {
106
+ const wasIdle = pendingPromptCount === 0;
107
+ pendingPromptCount += 1;
108
+
109
+ if (!wasIdle) return;
110
+ const source = getPromptSource(data);
111
+ notify("Pi", source ? `Question pending (${source})` : "Question pending");
112
+ });
113
+
114
+ pi.events.on("ui:prompt_end", () => {
115
+ if (pendingPromptCount === 0) return;
116
+ pendingPromptCount -= 1;
117
+ });
118
+
119
+ pi.events.on(REVIEW_EVENT_START, (data) => {
120
+ const sessionKey = extractReviewSessionKey(data);
121
+ if (!sessionKey) return;
122
+ activeReviewSessions.add(sessionKey);
123
+ });
124
+
125
+ pi.events.on(REVIEW_EVENT_END, (data) => {
126
+ const sessionKey = extractReviewSessionKey(data);
127
+ if (!sessionKey) return;
128
+
129
+ activeReviewSessions.delete(sessionKey);
130
+
131
+ if (pendingPromptCount > 0) return;
132
+ if (!currentSessionKey || sessionKey !== currentSessionKey) return;
133
+
134
+ const outcome = extractReviewOutcome(data);
135
+ if (outcome === "success") {
136
+ notify("Pi", "Review completed");
137
+ return;
138
+ }
139
+ if (outcome === "cancelled") {
140
+ notify("Pi", "Review cancelled");
141
+ return;
142
+ }
143
+ notify("Pi", "Review failed");
144
+ });
145
+
146
+ pi.on("agent_end", async (_event, ctx) => {
147
+ currentSessionKey = getSessionKey(ctx);
148
+ if (pendingPromptCount > 0) return;
149
+ if (hasCurrentSessionReviewRun()) return;
150
+ notify("Pi", "Ready for input");
151
+ });
152
+
153
+ pi.on("session_shutdown", async (_event, ctx) => {
154
+ pendingPromptCount = 0;
155
+ const sessionKey = getSessionKey(ctx);
156
+ activeReviewSessions.delete(sessionKey);
157
+ if (currentSessionKey === sessionKey) {
158
+ currentSessionKey = undefined;
159
+ }
160
+ });
161
+ }
@@ -0,0 +1,227 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
5
+
6
+ const STATUS_KEY = "openai-fast";
7
+ const SUPPORTED_APIS = new Set([
8
+ "openai-responses",
9
+ "openai-codex-responses",
10
+ "azure-openai-responses",
11
+ ]);
12
+
13
+ type FastMode = "fast";
14
+ type FastSetting = FastMode | "auto";
15
+ type JsonObject = Record<string, unknown>;
16
+ type ModelInfo = NonNullable<ExtensionContext["model"]>;
17
+ type FastConfig = {
18
+ models: Record<string, FastMode>;
19
+ };
20
+
21
+ function emptyConfig(): FastConfig {
22
+ return { models: {} };
23
+ }
24
+
25
+ function getConfigPath(): string {
26
+ const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
27
+ return path.join(agentDir, "openai-fast.json");
28
+ }
29
+
30
+ function isObject(value: unknown): value is JsonObject {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
+ }
33
+
34
+ function normalizeFastMode(value: string): FastMode | undefined {
35
+ return value.trim().toLowerCase() === "fast" ? "fast" : undefined;
36
+ }
37
+
38
+ function parseFastSetting(value: string): FastSetting | undefined {
39
+ const normalized = value.trim().toLowerCase();
40
+
41
+ if (normalized === "on" || normalized === "enabled") return "fast";
42
+ if (normalized === "off" || normalized === "disabled") return "auto";
43
+
44
+ return undefined;
45
+ }
46
+
47
+ function getFastArgumentCompletions(
48
+ prefix: string,
49
+ ): Array<{ value: string; label: string }> | null {
50
+ const trimmed = prefix.trim().toLowerCase();
51
+ if (trimmed.includes(" ")) return null;
52
+
53
+ const options = ["on", "off", "enabled", "disabled"];
54
+ const matches = options.filter((option) => option.startsWith(trimmed));
55
+ if (!matches.length) return null;
56
+
57
+ return matches.map((option) => ({ value: option, label: option }));
58
+ }
59
+
60
+ function parseConfig(value: unknown): FastConfig {
61
+ if (!isObject(value) || !isObject(value.models)) return emptyConfig();
62
+
63
+ const models: Record<string, FastMode> = {};
64
+ for (const [key, rawMode] of Object.entries(value.models)) {
65
+ const normalizedKey = key.trim();
66
+ if (!normalizedKey || typeof rawMode !== "string") continue;
67
+
68
+ const mode = normalizeFastMode(rawMode);
69
+ if (!mode) continue;
70
+
71
+ models[normalizedKey] = mode;
72
+ }
73
+
74
+ return { models };
75
+ }
76
+
77
+ async function loadConfig(): Promise<FastConfig> {
78
+ try {
79
+ const raw = await readFile(getConfigPath(), "utf8");
80
+ return parseConfig(JSON.parse(raw) as unknown);
81
+ } catch (error) {
82
+ const code = (error as NodeJS.ErrnoException).code;
83
+ if (code !== "ENOENT") {
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ console.warn(`[openai-fast] Failed to load config: ${message}`);
86
+ }
87
+
88
+ return emptyConfig();
89
+ }
90
+ }
91
+
92
+ async function saveConfig(config: FastConfig): Promise<void> {
93
+ const configPath = getConfigPath();
94
+ await mkdir(path.dirname(configPath), { recursive: true });
95
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
96
+ }
97
+
98
+ function isSupportedModel(model: ExtensionContext["model"]): model is ModelInfo {
99
+ return !!model && SUPPORTED_APIS.has(model.api);
100
+ }
101
+
102
+ function getExactModelKey(model: Pick<ModelInfo, "provider" | "id">): string {
103
+ return `${model.provider}/${model.id}`;
104
+ }
105
+
106
+ function resolveFastMode(
107
+ config: FastConfig,
108
+ model: Pick<ModelInfo, "provider" | "id">,
109
+ ): { key?: string; mode?: FastMode } {
110
+ const exactKey = getExactModelKey(model);
111
+ const exactMode = config.models[exactKey];
112
+ if (exactMode) return { key: exactKey, mode: exactMode };
113
+
114
+ const sharedMode = config.models[model.id];
115
+ if (sharedMode) return { key: model.id, mode: sharedMode };
116
+
117
+ return {};
118
+ }
119
+
120
+ function setFastMode(config: FastConfig, key: string, setting: FastSetting): FastConfig {
121
+ const models = { ...config.models };
122
+
123
+ if (setting === "auto") delete models[key];
124
+ else models[key] = setting;
125
+
126
+ return { models };
127
+ }
128
+
129
+ function updateStatus(ctx: ExtensionContext, config: FastConfig): void {
130
+ if (!ctx.hasUI) return;
131
+ if (!isSupportedModel(ctx.model)) {
132
+ ctx.ui.setStatus(STATUS_KEY, undefined);
133
+ return;
134
+ }
135
+
136
+ const { mode } = resolveFastMode(config, ctx.model);
137
+ ctx.ui.setStatus(STATUS_KEY, mode ? ctx.ui.theme.fg("dim", mode) : undefined);
138
+ }
139
+
140
+ export default function openaiFastExtension(pi: ExtensionAPI): void {
141
+ let config = emptyConfig();
142
+
143
+ async function applySetting(setting: FastSetting, ctx: ExtensionContext): Promise<void> {
144
+ const model = ctx.model;
145
+ if (!model) {
146
+ ctx.ui.notify("No active model.", "warning");
147
+ return;
148
+ }
149
+
150
+ if (!isSupportedModel(model)) {
151
+ ctx.ui.notify(
152
+ "Current model does not support fast mode. Supported APIs: openai-responses, openai-codex-responses, azure-openai-responses.",
153
+ "warning",
154
+ );
155
+ updateStatus(ctx, config);
156
+ return;
157
+ }
158
+
159
+ const resolved = resolveFastMode(config, model);
160
+ const configKey = resolved.key ?? getExactModelKey(model);
161
+ const nextConfig = setFastMode(config, configKey, setting);
162
+
163
+ try {
164
+ await saveConfig(nextConfig);
165
+ } catch (error) {
166
+ const message = error instanceof Error ? error.message : String(error);
167
+ ctx.ui.notify(`Failed to save ${getConfigPath()}: ${message}`, "error");
168
+ return;
169
+ }
170
+
171
+ config = nextConfig;
172
+ updateStatus(ctx, config);
173
+
174
+ const modelLabel = getExactModelKey(model);
175
+ if (setting === "auto") {
176
+ ctx.ui.notify(`Fast mode reset to auto for ${modelLabel}`, "info");
177
+ return;
178
+ }
179
+
180
+ ctx.ui.notify(`Fast mode enabled for ${modelLabel}`, "info");
181
+ }
182
+
183
+ pi.registerCommand("fast", {
184
+ description: "Toggle priority service tier for the current OpenAI model",
185
+ getArgumentCompletions: getFastArgumentCompletions,
186
+ handler: async (args, ctx) => {
187
+ const arg = args.trim();
188
+ if (arg === "") {
189
+ const nextSetting: FastSetting =
190
+ isSupportedModel(ctx.model) && resolveFastMode(config, ctx.model).mode ? "auto" : "fast";
191
+ await applySetting(nextSetting, ctx);
192
+ return;
193
+ }
194
+
195
+ const setting = parseFastSetting(arg);
196
+ if (!setting) {
197
+ ctx.ui.notify("Usage: /fast [on|off|enabled|disabled]", "error");
198
+ return;
199
+ }
200
+
201
+ await applySetting(setting, ctx);
202
+ },
203
+ });
204
+
205
+ pi.on("session_start", async (_event, ctx) => {
206
+ config = await loadConfig();
207
+ updateStatus(ctx, config);
208
+ });
209
+
210
+ pi.on("model_select", async (_event, ctx) => {
211
+ updateStatus(ctx, config);
212
+ });
213
+
214
+ pi.on("before_provider_request", (event, ctx) => {
215
+ const model = ctx.model;
216
+ if (!isSupportedModel(model)) return;
217
+ if (!resolveFastMode(config, model).mode) return;
218
+
219
+ const payload = event.payload;
220
+ if (!isObject(payload)) return;
221
+
222
+ return {
223
+ ...payload,
224
+ service_tier: "priority",
225
+ };
226
+ });
227
+ }
@@ -0,0 +1,223 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
5
+
6
+ const STATUS_KEY = "openai-verbosity";
7
+ const SUPPORTED_APIS = new Set([
8
+ "openai-responses",
9
+ "openai-codex-responses",
10
+ "azure-openai-responses",
11
+ ]);
12
+
13
+ type Verbosity = "low" | "medium" | "high";
14
+ type VerbositySetting = Verbosity | "auto";
15
+ type JsonObject = Record<string, unknown>;
16
+ type ModelInfo = NonNullable<ExtensionContext["model"]>;
17
+ type VerbosityConfig = {
18
+ models: Record<string, Verbosity>;
19
+ };
20
+
21
+ function emptyConfig(): VerbosityConfig {
22
+ return { models: {} };
23
+ }
24
+
25
+ function getConfigPath(): string {
26
+ const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
27
+ return path.join(agentDir, "openai-verbosity.json");
28
+ }
29
+
30
+ function isObject(value: unknown): value is JsonObject {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
+ }
33
+
34
+ function normalizeVerbosity(value: string): Verbosity | undefined {
35
+ const normalized = value.trim().toLowerCase();
36
+ if (normalized === "low" || normalized === "medium" || normalized === "high") return normalized;
37
+ return undefined;
38
+ }
39
+
40
+ function parseVerbositySetting(value: string): VerbositySetting | undefined {
41
+ const normalized = value.trim().toLowerCase();
42
+ if (normalized === "auto") return normalized;
43
+ return normalizeVerbosity(normalized);
44
+ }
45
+
46
+ function getVerbosityArgumentCompletions(
47
+ prefix: string,
48
+ ): Array<{ value: string; label: string }> | null {
49
+ const trimmed = prefix.trim().toLowerCase();
50
+ if (trimmed.includes(" ")) return null;
51
+
52
+ const options = ["low", "medium", "high", "auto"];
53
+ const matches = options.filter((option) => option.startsWith(trimmed));
54
+ if (!matches.length) return null;
55
+
56
+ return matches.map((option) => ({ value: option, label: option }));
57
+ }
58
+
59
+ function parseConfig(value: unknown): VerbosityConfig {
60
+ if (!isObject(value) || !isObject(value.models)) return emptyConfig();
61
+
62
+ const models: Record<string, Verbosity> = {};
63
+ for (const [key, rawVerbosity] of Object.entries(value.models)) {
64
+ const normalizedKey = key.trim();
65
+ if (!normalizedKey || typeof rawVerbosity !== "string") continue;
66
+
67
+ const verbosity = normalizeVerbosity(rawVerbosity);
68
+ if (!verbosity) continue;
69
+
70
+ models[normalizedKey] = verbosity;
71
+ }
72
+
73
+ return { models };
74
+ }
75
+
76
+ async function loadConfig(): Promise<VerbosityConfig> {
77
+ try {
78
+ const raw = await readFile(getConfigPath(), "utf8");
79
+ return parseConfig(JSON.parse(raw) as unknown);
80
+ } catch (error) {
81
+ const code = (error as NodeJS.ErrnoException).code;
82
+ if (code !== "ENOENT") {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ console.warn(`[openai-verbosity] Failed to load config: ${message}`);
85
+ }
86
+
87
+ return emptyConfig();
88
+ }
89
+ }
90
+
91
+ async function saveConfig(config: VerbosityConfig): Promise<void> {
92
+ const configPath = getConfigPath();
93
+ await mkdir(path.dirname(configPath), { recursive: true });
94
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
95
+ }
96
+
97
+ function isSupportedModel(model: ExtensionContext["model"]): model is ModelInfo {
98
+ return !!model && SUPPORTED_APIS.has(model.api);
99
+ }
100
+
101
+ function getExactModelKey(model: Pick<ModelInfo, "provider" | "id">): string {
102
+ return `${model.provider}/${model.id}`;
103
+ }
104
+
105
+ function resolveVerbosity(
106
+ config: VerbosityConfig,
107
+ model: Pick<ModelInfo, "provider" | "id">,
108
+ ): { key?: string; verbosity?: Verbosity } {
109
+ const exactKey = getExactModelKey(model);
110
+ const exactVerbosity = config.models[exactKey];
111
+ if (exactVerbosity) return { key: exactKey, verbosity: exactVerbosity };
112
+
113
+ const sharedVerbosity = config.models[model.id];
114
+ if (sharedVerbosity) return { key: model.id, verbosity: sharedVerbosity };
115
+
116
+ return {};
117
+ }
118
+
119
+ function setVerbosity(
120
+ config: VerbosityConfig,
121
+ key: string,
122
+ verbosity: VerbositySetting,
123
+ ): VerbosityConfig {
124
+ const models = { ...config.models };
125
+
126
+ if (verbosity === "auto") delete models[key];
127
+ else models[key] = verbosity;
128
+
129
+ return { models };
130
+ }
131
+
132
+ function updateStatus(ctx: ExtensionContext, config: VerbosityConfig): void {
133
+ if (!ctx.hasUI) return;
134
+ if (!isSupportedModel(ctx.model)) {
135
+ ctx.ui.setStatus(STATUS_KEY, undefined);
136
+ return;
137
+ }
138
+
139
+ const { verbosity } = resolveVerbosity(config, ctx.model);
140
+ ctx.ui.setStatus(STATUS_KEY, verbosity ? ctx.ui.theme.fg("dim", verbosity) : undefined);
141
+ }
142
+
143
+ export default function openaiVerbosityExtension(pi: ExtensionAPI): void {
144
+ let config = emptyConfig();
145
+
146
+ pi.registerCommand("verbosity", {
147
+ description: "Set OpenAI response verbosity for the current model",
148
+ getArgumentCompletions: getVerbosityArgumentCompletions,
149
+ handler: async (args, ctx) => {
150
+ const verbosity = parseVerbositySetting(args);
151
+ if (!verbosity) {
152
+ ctx.ui.notify("Usage: /verbosity <low|medium|high|auto>", "error");
153
+ return;
154
+ }
155
+
156
+ const model = ctx.model;
157
+ if (!model) {
158
+ ctx.ui.notify("No active model.", "warning");
159
+ return;
160
+ }
161
+
162
+ if (!isSupportedModel(model)) {
163
+ ctx.ui.notify(
164
+ "Current model does not support verbosity control. Supported APIs: openai-responses, openai-codex-responses, azure-openai-responses.",
165
+ "warning",
166
+ );
167
+ updateStatus(ctx, config);
168
+ return;
169
+ }
170
+
171
+ const resolved = resolveVerbosity(config, model);
172
+ const configKey = resolved.key ?? getExactModelKey(model);
173
+ const nextConfig = setVerbosity(config, configKey, verbosity);
174
+
175
+ try {
176
+ await saveConfig(nextConfig);
177
+ } catch (error) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ ctx.ui.notify(`Failed to save ${getConfigPath()}: ${message}`, "error");
180
+ return;
181
+ }
182
+
183
+ config = nextConfig;
184
+ updateStatus(ctx, config);
185
+
186
+ if (verbosity === "auto") {
187
+ ctx.ui.notify("Verbosity reset to auto", "info");
188
+ return;
189
+ }
190
+
191
+ ctx.ui.notify(`Verbosity set to ${verbosity}`, "info");
192
+ },
193
+ });
194
+
195
+ pi.on("session_start", async (_event, ctx) => {
196
+ config = await loadConfig();
197
+ updateStatus(ctx, config);
198
+ });
199
+
200
+ pi.on("model_select", async (_event, ctx) => {
201
+ updateStatus(ctx, config);
202
+ });
203
+
204
+ pi.on("before_provider_request", (event, ctx) => {
205
+ const model = ctx.model;
206
+ if (!isSupportedModel(model)) return;
207
+
208
+ const { verbosity } = resolveVerbosity(config, model);
209
+ if (!verbosity) return;
210
+
211
+ const payload = event.payload;
212
+ if (!isObject(payload)) return;
213
+
214
+ const text = isObject(payload.text) ? payload.text : {};
215
+ return {
216
+ ...payload,
217
+ text: {
218
+ ...text,
219
+ verbosity,
220
+ },
221
+ };
222
+ });
223
+ }