Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions agents/entire-agent-pi/internal/pi/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,32 +131,39 @@ func (a *Agent) AreHooksInstalled() bool {

func generateExtension() string {
return `import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { execFileSync } from "node:child_process";
import { execFile } from "node:child_process";

export default function (pi: ExtensionAPI) {
function fireHook(hookName: string, data: Record<string, unknown>) {
try {
const json = JSON.stringify(data);
execFileSync("entire", ["hooks", "pi", hookName], {
input: json,
timeout: 10000,
stdio: ["pipe", "pipe", "pipe"],
});
} catch {
// best effort — don't block the agent
}
function fireHook(hookName: string, data: Record<string, unknown>): Promise<void> {
return new Promise((resolve) => {
try {
const child = execFile(
"entire",
["hooks", "pi", hookName],
{
timeout: 10000,
windowsHide: true,
},
() => resolve(),
);
child.stdin?.end(JSON.stringify(data));
} catch {
// best effort — don't block the agent
resolve();
}
});
}

pi.on("session_start", async (_event, ctx) => {
fireHook("session_start", {
await fireHook("session_start", {
type: "session_start",
cwd: ctx.cwd,
session_file: ctx.sessionManager.getSessionFile(),
});
});

pi.on("before_agent_start", async (event, ctx) => {
fireHook("before_agent_start", {
await fireHook("before_agent_start", {
type: "before_agent_start",
cwd: ctx.cwd,
session_file: ctx.sessionManager.getSessionFile(),
Expand All @@ -165,15 +172,15 @@ export default function (pi: ExtensionAPI) {
});

pi.on("agent_end", async (_event, ctx) => {
fireHook("agent_end", {
await fireHook("agent_end", {
type: "agent_end",
cwd: ctx.cwd,
session_file: ctx.sessionManager.getSessionFile(),
});
});

pi.on("session_shutdown", async () => {
fireHook("session_shutdown", {
await fireHook("session_shutdown", {
type: "session_shutdown",
});
});
Expand Down
68 changes: 65 additions & 3 deletions agents/entire-agent-pi/internal/pi/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,69 @@ func TestParseHook_UnknownHook(t *testing.T) {
}
}

func TestGenerateExtension(t *testing.T) {
const want = `import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { execFile } from "node:child_process";

export default function (pi: ExtensionAPI) {
function fireHook(hookName: string, data: Record<string, unknown>): Promise<void> {
return new Promise((resolve) => {
try {
const child = execFile(
"entire",
["hooks", "pi", hookName],
{
timeout: 10000,
windowsHide: true,
},
() => resolve(),
);
child.stdin?.end(JSON.stringify(data));
} catch {
// best effort — don't block the agent
resolve();
}
});
}

pi.on("session_start", async (_event, ctx) => {
await fireHook("session_start", {
type: "session_start",
cwd: ctx.cwd,
session_file: ctx.sessionManager.getSessionFile(),
});
});

pi.on("before_agent_start", async (event, ctx) => {
await fireHook("before_agent_start", {
type: "before_agent_start",
cwd: ctx.cwd,
session_file: ctx.sessionManager.getSessionFile(),
prompt: event.prompt,
});
});

pi.on("agent_end", async (_event, ctx) => {
await fireHook("agent_end", {
type: "agent_end",
cwd: ctx.cwd,
session_file: ctx.sessionManager.getSessionFile(),
});
});

pi.on("session_shutdown", async () => {
await fireHook("session_shutdown", {
type: "session_shutdown",
});
});
}
`

if got := generateExtension(); got != want {
t.Fatalf("generateExtension() mismatch\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
}

func TestInstallAndUninstallHooks(t *testing.T) {
tmp := t.TempDir()
t.Setenv("ENTIRE_REPO_ROOT", tmp)
Expand All @@ -151,14 +214,13 @@ func TestInstallAndUninstallHooks(t *testing.T) {
t.Error("hooks should be installed after InstallHooks")
}

// Verify the extension file exists and contains expected content.
extPath := filepath.Join(tmp, extensionFile)
data, err := os.ReadFile(extPath)
if err != nil {
t.Fatal(err)
}
if len(data) == 0 {
t.Error("extension file is empty")
if string(data) != generateExtension() {
t.Fatal("installed extension does not match generated extension")
}

// Idempotent install should return 0.
Expand Down
Loading