Commit 012ac0ac1a06

Vincent Demeester <vincent@sbr.pm>
2026-02-15 22:47:42
feat(pi): add defaults extension
Quality-of-life improvements: directory-aware read (returns listing instead of EISDIR), get_current_time tool, and git rebase helper (injects editor env vars to prevent hangs). Adapted from aliou/pi-extensions defaults, keeping only the features not already covered by existing extensions.
1 parent 5fd0265
Changed files (2)
dots
pi
agent
extensions
dots/pi/agent/extensions/defaults/index.ts
@@ -0,0 +1,142 @@
+/**
+ * Defaults Extension for Pi
+ *
+ * Sensible quality-of-life improvements:
+ * - Directory-aware read (returns listing instead of EISDIR error)
+ * - get_current_time tool (structured date/time for the LLM)
+ * - Git rebase helper (injects editor env vars to prevent hangs)
+ *
+ * Adapted from: aliou/pi-extensions defaults
+ */
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import { Type } from "@sinclair/typebox";
+import { lstat } from "node:fs/promises";
+import { resolve } from "node:path";
+import { createReadTool, createLsTool } from "@mariozechner/pi-coding-agent";
+
+export default function (pi: ExtensionAPI) {
+	// ── Directory-aware read ──────────────────────────────────
+
+	const cwd = process.cwd();
+	const nativeRead = createReadTool(cwd);
+	const nativeLs = createLsTool(cwd);
+
+	pi.registerTool({
+		...nativeRead,
+		async execute(toolCallId, params, signal, onUpdate, ctx) {
+			const { path } = params as { path: string; offset?: number; limit?: number };
+			const absolutePath = resolve(ctx.cwd, path);
+
+			try {
+				const stat = await lstat(absolutePath);
+				if (stat.isDirectory()) {
+					return nativeLs.execute(toolCallId, { path }, signal, onUpdate);
+				}
+			} catch {
+				// Let nativeRead handle the error
+			}
+
+			return nativeRead.execute(
+				toolCallId,
+				params as { path: string; offset?: number; limit?: number },
+				signal,
+				onUpdate,
+			);
+		},
+	});
+
+	// ── get_current_time tool ─────────────────────────────────
+
+	const GetCurrentTimeParams = Type.Object({
+		format: Type.Optional(
+			Type.String({
+				description: "Output format: 'iso8601' (default), 'unix', 'date', 'time'",
+			}),
+		),
+	});
+
+	pi.registerTool({
+		name: "get_current_time",
+		label: "Get Current Time",
+		description:
+			"Get the current date and time. Returns formatted time with date, time, timezone, and day of week.",
+		parameters: GetCurrentTimeParams,
+
+		async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
+			const now = new Date();
+			const format = (params as { format?: string }).format || "iso8601";
+
+			let formatted: string;
+			switch (format.toLowerCase()) {
+				case "unix":
+					formatted = Math.floor(now.getTime() / 1000).toString();
+					break;
+				case "date":
+					formatted = now.toLocaleDateString();
+					break;
+				case "time":
+					formatted = now.toLocaleTimeString();
+					break;
+				default:
+					formatted = now.toISOString();
+			}
+
+			const timezoneOffset = -now.getTimezoneOffset();
+			const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60);
+			const offsetMinutes = Math.abs(timezoneOffset) % 60;
+			const offsetSign = timezoneOffset >= 0 ? "+" : "-";
+			const timezone = `UTC${offsetSign}${String(offsetHours).padStart(2, "0")}:${String(offsetMinutes).padStart(2, "0")}`;
+
+			const text = [
+				`Formatted: ${formatted}`,
+				`Date: ${now.toLocaleDateString("en-CA")}`,
+				`Time: ${now.toLocaleTimeString("en-GB", { hour12: false })}`,
+				`Timezone: ${timezone} (${Intl.DateTimeFormat().resolvedOptions().timeZone})`,
+				`Day: ${now.toLocaleDateString("en-US", { weekday: "long" })}`,
+				`Unix: ${Math.floor(now.getTime() / 1000)}`,
+			].join("\n");
+
+			return {
+				content: [{ type: "text", text }],
+				details: {},
+			};
+		},
+	});
+
+	// ── Git rebase helper ─────────────────────────────────────
+
+	pi.on("tool_call", async (event, ctx) => {
+		if (event.toolName !== "bash") return undefined;
+
+		const command = (event.input.command as string) || "";
+
+		// Only intercept git rebase commands
+		if (!/\bgit\s+rebase\b/.test(command)) return undefined;
+
+		// Skip if already has editor config
+		if (/GIT_SEQUENCE_EDITOR|GIT_EDITOR|core\.editor/.test(command)) return undefined;
+
+		// Check if it's an interactive rebase
+		const isInteractive = /\brebase\s+(-[^\s]*i|--interactive)/.test(command);
+		const isContinue = /\brebase\s+--continue/.test(command);
+
+		if (isInteractive || isContinue) {
+			// Prepend editor env vars to prevent hanging
+			const prefix = isInteractive
+				? 'GIT_SEQUENCE_EDITOR=":" GIT_EDITOR="true"'
+				: 'GIT_EDITOR="true"';
+
+			ctx.ui.notify(`Injected ${prefix} to prevent editor hang`, "info");
+
+			return {
+				input: {
+					...event.input,
+					command: `${prefix} ${command}`,
+				},
+			};
+		}
+
+		return undefined;
+	});
+}
dots/pi/agent/extensions/defaults/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "defaults",
+  "version": "1.0.0",
+  "type": "module",
+  "main": "index.ts",
+  "dependencies": {
+    "@mariozechner/pi-coding-agent": "*"
+  }
+}