main
  1/**
  2 * Defaults Extension for Pi
  3 *
  4 * Sensible quality-of-life improvements:
  5 * - Directory-aware read (returns listing instead of EISDIR error)
  6 * - get_current_time tool (structured date/time for the LLM)
  7 * - Git rebase helper (injects editor env vars to prevent hangs)
  8 *
  9 * Adapted from: aliou/pi-extensions defaults
 10 */
 11
 12import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
 13import { Type } from "@sinclair/typebox";
 14import { lstat } from "node:fs/promises";
 15import { resolve } from "node:path";
 16import { createReadTool, createLsTool } from "@mariozechner/pi-coding-agent";
 17
 18export default function (pi: ExtensionAPI) {
 19	// ── Directory-aware read ──────────────────────────────────
 20
 21	const cwd = process.cwd();
 22	const nativeRead = createReadTool(cwd);
 23	const nativeLs = createLsTool(cwd);
 24
 25	pi.registerTool({
 26		...nativeRead,
 27		async execute(toolCallId, params, signal, onUpdate, ctx) {
 28			const { path } = params as { path: string; offset?: number; limit?: number };
 29			const absolutePath = resolve(ctx.cwd, path);
 30
 31			try {
 32				const stat = await lstat(absolutePath);
 33				if (stat.isDirectory()) {
 34					return nativeLs.execute(toolCallId, { path }, signal, onUpdate);
 35				}
 36			} catch {
 37				// Let nativeRead handle the error
 38			}
 39
 40			return nativeRead.execute(
 41				toolCallId,
 42				params as { path: string; offset?: number; limit?: number },
 43				signal,
 44				onUpdate,
 45			);
 46		},
 47	});
 48
 49	// ── get_current_time tool ─────────────────────────────────
 50
 51	const GetCurrentTimeParams = Type.Object({
 52		format: Type.Optional(
 53			Type.String({
 54				description: "Output format: 'iso8601' (default), 'unix', 'date', 'time'",
 55			}),
 56		),
 57	});
 58
 59	pi.registerTool({
 60		name: "get_current_time",
 61		label: "Get Current Time",
 62		description:
 63			"Get the current date and time. Returns formatted time with date, time, timezone, and day of week.",
 64		parameters: GetCurrentTimeParams,
 65
 66		async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
 67			const now = new Date();
 68			const format = (params as { format?: string }).format || "iso8601";
 69
 70			let formatted: string;
 71			switch (format.toLowerCase()) {
 72				case "unix":
 73					formatted = Math.floor(now.getTime() / 1000).toString();
 74					break;
 75				case "date":
 76					formatted = now.toLocaleDateString();
 77					break;
 78				case "time":
 79					formatted = now.toLocaleTimeString();
 80					break;
 81				default:
 82					formatted = now.toISOString();
 83			}
 84
 85			const timezoneOffset = -now.getTimezoneOffset();
 86			const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60);
 87			const offsetMinutes = Math.abs(timezoneOffset) % 60;
 88			const offsetSign = timezoneOffset >= 0 ? "+" : "-";
 89			const timezone = `UTC${offsetSign}${String(offsetHours).padStart(2, "0")}:${String(offsetMinutes).padStart(2, "0")}`;
 90
 91			const text = [
 92				`Formatted: ${formatted}`,
 93				`Date: ${now.toLocaleDateString("en-CA")}`,
 94				`Time: ${now.toLocaleTimeString("en-GB", { hour12: false })}`,
 95				`Timezone: ${timezone} (${Intl.DateTimeFormat().resolvedOptions().timeZone})`,
 96				`Day: ${now.toLocaleDateString("en-US", { weekday: "long" })}`,
 97				`Unix: ${Math.floor(now.getTime() / 1000)}`,
 98			].join("\n");
 99
100			return {
101				content: [{ type: "text", text }],
102				details: {},
103			};
104		},
105	});
106
107	// ── Git rebase helper ─────────────────────────────────────
108
109	pi.on("tool_call", async (event, ctx) => {
110		if (event.toolName !== "bash") return undefined;
111
112		const command = (event.input.command as string) || "";
113
114		// Only intercept git rebase commands
115		if (!/\bgit\s+rebase\b/.test(command)) return undefined;
116
117		// Skip if already has editor config
118		if (/GIT_SEQUENCE_EDITOR|GIT_EDITOR|core\.editor/.test(command)) return undefined;
119
120		// Check if it's an interactive rebase
121		const isInteractive = /\brebase\s+(-[^\s]*i|--interactive)/.test(command);
122		const isContinue = /\brebase\s+--continue/.test(command);
123
124		if (isInteractive || isContinue) {
125			// Prepend editor env vars to prevent hanging
126			const prefix = isInteractive
127				? 'GIT_SEQUENCE_EDITOR=":" GIT_EDITOR="true"'
128				: 'GIT_EDITOR="true"';
129
130			ctx.ui.notify(`Injected ${prefix} to prevent editor hang`, "info");
131
132			return {
133				input: {
134					...event.input,
135					command: `${prefix} ${command}`,
136				},
137			};
138		}
139
140		return undefined;
141	});
142}