tau-coding-agent 0.1.6 → 0.2.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 (80) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -79
  3. package/extensions/branch-term/README.md +7 -0
  4. package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
  5. package/extensions/btw.ts +17 -2
  6. package/extensions/caffeinate/README.md +5 -0
  7. package/extensions/caffeinate/index.ts +144 -0
  8. package/extensions/fast.ts +292 -0
  9. package/extensions/ghostty.ts +214 -211
  10. package/extensions/git-diff-stats.ts +124 -84
  11. package/extensions/git-pr-status.ts +274 -208
  12. package/extensions/insights.ts +109 -191
  13. package/extensions/loop.ts +149 -145
  14. package/extensions/memory.ts +138 -86
  15. package/extensions/notify.ts +14 -26
  16. package/extensions/openai-verbosity.ts +108 -42
  17. package/extensions/review/fix.ts +15 -6
  18. package/extensions/review/git.ts +93 -103
  19. package/extensions/review/index.ts +60 -62
  20. package/extensions/review/interrupt.ts +117 -0
  21. package/extensions/review/message-queue.ts +45 -12
  22. package/extensions/review/models.ts +79 -3
  23. package/extensions/review/prompts.ts +41 -38
  24. package/extensions/review/review.ts +161 -166
  25. package/extensions/review/runner.ts +139 -147
  26. package/extensions/review/runtime.ts +173 -120
  27. package/extensions/review/triage.ts +41 -44
  28. package/extensions/sandbox/bash.ts +775 -0
  29. package/extensions/sandbox/command.ts +605 -0
  30. package/extensions/sandbox/config.ts +764 -0
  31. package/extensions/sandbox/index.ts +132 -3338
  32. package/extensions/sandbox/permissions/dialog.ts +118 -0
  33. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  34. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  35. package/extensions/sandbox/permissions/network.ts +164 -0
  36. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  37. package/extensions/sandbox/runtime.ts +616 -0
  38. package/extensions/stash.ts +28 -15
  39. package/extensions/subagent/README.md +73 -0
  40. package/extensions/subagent/index.ts +822 -0
  41. package/extensions/subagent/interrupt.ts +117 -0
  42. package/extensions/subagent/permissions.ts +101 -0
  43. package/extensions/subagent/rpc.ts +177 -0
  44. package/extensions/tool-display-mode.ts +267 -64
  45. package/extensions/usage/index.ts +249 -356
  46. package/extensions/usage/openrouter.ts +10 -2
  47. package/extensions/websearch/README.md +12 -47
  48. package/extensions/websearch/config.ts +5 -2
  49. package/extensions/websearch/index.ts +94 -109
  50. package/extensions/websearch/output.ts +51 -0
  51. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  52. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  53. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  54. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  55. package/extensions/websearch/providers/pi-model.shared.ts +33 -39
  56. package/extensions/websearch/providers/shared.ts +87 -2
  57. package/extensions/websearch/types.ts +0 -3
  58. package/extensions/worktree.ts +132 -172
  59. package/package.json +10 -5
  60. package/skills/browser-tools/SKILL.md +29 -234
  61. package/skills/browser-tools/references/cookies.md +36 -0
  62. package/skills/browser-tools/references/interaction.md +90 -0
  63. package/skills/browser-tools/references/logging.md +34 -0
  64. package/skills/git-clean-history/SKILL.md +6 -6
  65. package/skills/git-commit/SKILL.md +5 -3
  66. package/skills/github-pull-request/SKILL.md +60 -0
  67. package/skills/github-pull-request/references/create.md +40 -0
  68. package/skills/github-pull-request/references/stewardship.md +60 -0
  69. package/skills/oracle/SKILL.md +3 -3
  70. package/skills/oracle/scripts/oracle +10 -1
  71. package/skills/sentry/SKILL.md +15 -185
  72. package/skills/sentry/references/events.md +79 -0
  73. package/skills/sentry/references/issues.md +62 -0
  74. package/skills/sentry/references/logs.md +46 -0
  75. package/skills/update-changelog/SKILL.md +27 -121
  76. package/skills/web-design/SKILL.md +16 -105
  77. package/themes/tau-dark.json +4 -0
  78. package/extensions/openai-fast.ts +0 -229
  79. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  80. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -0,0 +1,292 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, open, readFile, realpath, rename, unlink } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { lock } from "proper-lockfile";
5
+ import {
6
+ getAgentDir,
7
+ type ExtensionAPI,
8
+ type ExtensionContext,
9
+ } from "@earendil-works/pi-coding-agent";
10
+
11
+ const STATUS_KEY = "fast";
12
+ const SUPPORTED_APIS = new Set([
13
+ "openai-completions",
14
+ "openai-responses",
15
+ "openai-codex-responses",
16
+ "azure-openai-responses",
17
+ ]);
18
+
19
+ type FastMode = "fast";
20
+ type FastSetting = FastMode | "auto";
21
+ type JsonObject = Record<string, unknown>;
22
+ type ModelInfo = NonNullable<ExtensionContext["model"]>;
23
+ type FastConfig = {
24
+ models: Record<string, FastSetting>;
25
+ };
26
+
27
+ function emptyConfig(): FastConfig {
28
+ return { models: Object.create(null) };
29
+ }
30
+
31
+ function getConfigPath(): string {
32
+ return path.join(getAgentDir(), "fast.json");
33
+ }
34
+
35
+ function isObject(value: unknown): value is JsonObject {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+
39
+ function normalizeFastMode(value: string): FastSetting | undefined {
40
+ const normalized = value.trim().toLowerCase();
41
+ return normalized === "fast" || normalized === "auto" ? normalized : undefined;
42
+ }
43
+
44
+ function parseFastSetting(value: string): FastSetting | undefined {
45
+ const normalized = value.trim().toLowerCase();
46
+
47
+ if (normalized === "on" || normalized === "enabled") return "fast";
48
+ if (normalized === "off" || normalized === "disabled") return "auto";
49
+
50
+ return undefined;
51
+ }
52
+
53
+ function getFastArgumentCompletions(
54
+ prefix: string,
55
+ ): Array<{ value: string; label: string }> | null {
56
+ const trimmed = prefix.trim().toLowerCase();
57
+ if (trimmed.includes(" ")) return null;
58
+
59
+ const options = ["on", "off", "enabled", "disabled"];
60
+ const matches = options.filter((option) => option.startsWith(trimmed));
61
+ if (!matches.length) return null;
62
+
63
+ return matches.map((option) => ({ value: option, label: option }));
64
+ }
65
+
66
+ function parseConfig(value: unknown): FastConfig {
67
+ if (!isObject(value) || !isObject(value.models)) return emptyConfig();
68
+
69
+ const models: Record<string, FastSetting> = Object.create(null);
70
+ for (const [key, rawMode] of Object.entries(value.models)) {
71
+ const normalizedKey = key.trim();
72
+ if (!normalizedKey || typeof rawMode !== "string") continue;
73
+ if (key !== normalizedKey && Object.hasOwn(value.models, normalizedKey)) continue;
74
+
75
+ const mode = normalizeFastMode(rawMode);
76
+ if (!mode) continue;
77
+
78
+ models[normalizedKey] = mode;
79
+ }
80
+
81
+ return { models };
82
+ }
83
+
84
+ async function loadConfig(): Promise<FastConfig> {
85
+ try {
86
+ const raw = await readFile(getConfigPath(), "utf8");
87
+ return parseConfig(JSON.parse(raw) as unknown);
88
+ } catch (error) {
89
+ const code = (error as NodeJS.ErrnoException).code;
90
+ if (code !== "ENOENT") {
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ console.warn(`[fast] Failed to load config: ${message}`);
93
+ }
94
+
95
+ return emptyConfig();
96
+ }
97
+ }
98
+
99
+ // Resolve the target before locking and renaming: aliases must share a lock, not lose their symlink.
100
+ async function resolveConfigPath(): Promise<string> {
101
+ const configPath = getConfigPath();
102
+ await mkdir(path.dirname(configPath), { recursive: true });
103
+ try {
104
+ return await realpath(configPath);
105
+ } catch (error) {
106
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
107
+ const entry = await lstat(configPath).catch((error: NodeJS.ErrnoException) => {
108
+ if (error.code !== "ENOENT") throw error;
109
+ return undefined;
110
+ });
111
+ if (entry) throw new Error("Cannot save through an unresolved configuration symlink");
112
+ return path.join(await realpath(path.dirname(configPath)), path.basename(configPath));
113
+ }
114
+ }
115
+
116
+ async function saveConfig(
117
+ model: Pick<ModelInfo, "provider" | "id">,
118
+ setting: FastSetting | undefined,
119
+ onSaved: (config: FastConfig) => void,
120
+ ): Promise<void> {
121
+ const configPath = await resolveConfigPath();
122
+ let compromised: Error | undefined;
123
+ const release = await lock(configPath, {
124
+ realpath: false,
125
+ retries: { retries: 10, factor: 1, minTimeout: 100, maxTimeout: 100 },
126
+ // The library's default callback throws asynchronously and can terminate Pi.
127
+ onCompromised: (error) => {
128
+ compromised = error;
129
+ },
130
+ });
131
+ let temporary: string | undefined;
132
+ try {
133
+ const raw = await readFile(configPath, "utf8").catch((error: NodeJS.ErrnoException) => {
134
+ if (error.code !== "ENOENT") throw error;
135
+ return '{"models":{}}';
136
+ });
137
+ const current: unknown = JSON.parse(raw);
138
+ if (!isObject(current) || !isObject(current.models)) {
139
+ throw new Error("Configuration must be an object with a models object");
140
+ }
141
+ const selected =
142
+ setting ?? (resolveFastMode(parseConfig(current), model).mode ? "auto" : "fast");
143
+ const next = { ...current, models: { ...current.models, [getExactModelKey(model)]: selected } };
144
+ const nextConfig = parseConfig(next);
145
+ if (compromised) throw compromised;
146
+ const candidate = `${configPath}.${randomUUID()}.tmp`;
147
+ const file = await open(candidate, "wx", 0o600);
148
+ temporary = candidate;
149
+ try {
150
+ await file.writeFile(`${JSON.stringify(next, null, 2)}\n`, "utf8");
151
+ } finally {
152
+ await file.close();
153
+ }
154
+ // This is a cooperative mtime lease, not fencing against arbitrary OS pauses or lock removal.
155
+ if (compromised) throw compromised;
156
+ await rename(temporary, configPath);
157
+ temporary = undefined;
158
+ // Publication succeeded even if releasing the lock later fails.
159
+ onSaved(nextConfig);
160
+ } finally {
161
+ try {
162
+ if (temporary) await unlink(temporary);
163
+ } finally {
164
+ await release().catch((error: unknown) => {
165
+ throw compromised ?? error;
166
+ });
167
+ }
168
+ }
169
+ }
170
+
171
+ function isSupportedModel(model: ExtensionContext["model"]): model is ModelInfo {
172
+ return !!model && SUPPORTED_APIS.has(model.api);
173
+ }
174
+
175
+ function getExactModelKey(model: Pick<ModelInfo, "provider" | "id">): string {
176
+ return `${model.provider}/${model.id}`;
177
+ }
178
+
179
+ function resolveFastMode(
180
+ config: FastConfig,
181
+ model: Pick<ModelInfo, "provider" | "id">,
182
+ ): { mode?: FastMode } {
183
+ const setting = config.models[getExactModelKey(model)] ?? config.models[model.id];
184
+ return { mode: setting === "fast" ? "fast" : undefined };
185
+ }
186
+
187
+ function updateStatus(ctx: ExtensionContext, config: FastConfig): void {
188
+ if (!ctx.hasUI) return;
189
+ if (!isSupportedModel(ctx.model)) {
190
+ ctx.ui.setStatus(STATUS_KEY, undefined);
191
+ return;
192
+ }
193
+
194
+ const { mode } = resolveFastMode(config, ctx.model);
195
+ ctx.ui.setStatus(STATUS_KEY, mode ? ctx.ui.theme.fg("dim", mode) : undefined);
196
+ }
197
+
198
+ export default function fastExtension(pi: ExtensionAPI): void {
199
+ let config = emptyConfig();
200
+ const pending = new Set<Promise<void>>();
201
+
202
+ async function applySetting(
203
+ setting: FastSetting | undefined,
204
+ ctx: ExtensionContext,
205
+ ): Promise<void> {
206
+ const model = ctx.model;
207
+ if (!model) {
208
+ ctx.ui.notify("No active model.", "warning");
209
+ return;
210
+ }
211
+
212
+ if (!isSupportedModel(model)) {
213
+ ctx.ui.notify("Current model does not support fast processing.", "warning");
214
+ updateStatus(ctx, config);
215
+ return;
216
+ }
217
+
218
+ let saved = false;
219
+ try {
220
+ await saveConfig(model, setting, (nextConfig) => {
221
+ config = nextConfig;
222
+ saved = true;
223
+ });
224
+ } catch (error) {
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ if (saved) updateStatus(ctx, config);
227
+ ctx.ui.notify(
228
+ `${saved ? "Saved, but cleanup failed for" : "Failed to save"} ${getConfigPath()}: ${message}`,
229
+ saved ? "warning" : "error",
230
+ );
231
+ return;
232
+ }
233
+
234
+ updateStatus(ctx, config);
235
+
236
+ const modelLabel = getExactModelKey(model);
237
+ if (!resolveFastMode(config, model).mode) {
238
+ ctx.ui.notify(`Fast mode reset to auto for ${modelLabel}`, "info");
239
+ return;
240
+ }
241
+
242
+ ctx.ui.notify(`Fast mode enabled for ${modelLabel}`, "info");
243
+ }
244
+
245
+ pi.registerCommand("fast", {
246
+ description: "Toggle fast processing for the current model",
247
+ getArgumentCompletions: getFastArgumentCompletions,
248
+ handler: async (args, ctx) => {
249
+ const arg = args.trim();
250
+ const setting = parseFastSetting(arg);
251
+ if (arg && !setting) {
252
+ ctx.ui.notify("Usage: /fast [on|off|enabled|disabled]", "error");
253
+ return;
254
+ }
255
+
256
+ const operation = applySetting(setting, ctx);
257
+ pending.add(operation);
258
+ try {
259
+ await operation;
260
+ } finally {
261
+ pending.delete(operation);
262
+ }
263
+ },
264
+ });
265
+
266
+ pi.on("session_shutdown", async () => {
267
+ await Promise.allSettled(pending);
268
+ });
269
+
270
+ pi.on("session_start", async (_event, ctx) => {
271
+ config = await loadConfig();
272
+ updateStatus(ctx, config);
273
+ });
274
+
275
+ pi.on("model_select", async (_event, ctx) => {
276
+ updateStatus(ctx, config);
277
+ });
278
+
279
+ pi.on("before_provider_request", (event, ctx) => {
280
+ const model = ctx.model;
281
+ if (!isSupportedModel(model)) return;
282
+ if (!resolveFastMode(config, model).mode) return;
283
+
284
+ const payload = event.payload;
285
+ if (!isObject(payload)) return;
286
+
287
+ return {
288
+ ...payload,
289
+ service_tier: "priority",
290
+ };
291
+ });
292
+ }