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}