f343923014aa829e2922a21032b142fe27b0592d

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Integrate Herdr notifications

Diff

This diff is truncated to protect this page.

  1diff --git a/AGENTS.md b/AGENTS.md
  2index ba8f5e1ee05cb821153a06cac1cfc87cbe5d7d88..0831b5a8f8f035e0cbd5a5ae4dfa120afcd957b4 100644
  3--- a/AGENTS.md
  4+++ b/AGENTS.md
  5@@ -15,7 +15,6 @@ Security policy engine for Pi. Evaluates tool calls, primarily Bash commands, ag
  6 - `src/normalizer.ts` — Request normalization and cache-key generation
  7 - `src/cache.ts` — JSONL decision cache
  8 - `src/logger.ts` — JSONL decision logging
  9-- `src/notify.ts` — Native Linux and macOS desktop notifications
 10 - `src/types.ts` — Shared decision types
 11 
 12 ## Commands
 13diff --git a/README.md b/README.md
 14index bee627fd1a1be73b1303f2ac073290f929671a35..73ba64a10206f89813a2989d34401da7eecc4b6f 100644
 15--- a/README.md
 16+++ b/README.md
 17@@ -8,8 +8,6 @@ A [Pi](https://pi.dev) extension that evaluates tool calls through a three-stage
 18 
 19 An `allow` decision lets Pi run the tool. A `deny` decision blocks it. An `ask` decision opens a confirmation dialog in interactive Pi and blocks the call when no UI is available.
 20 
 21-The extension also sends a desktop notification when a Pi session settles.
 22-
 23 ## Install
 24 
 25 Install the checked-out package globally:
 26@@ -64,9 +62,9 @@ The extension handles Pi's `tool_call` event and evaluates each tool call before
 27 
 28diff --git a/opencode-logo.png b/opencode-logo.png
 29deleted file mode 100644
 30index cf868c8e8711cf9e79638e17fedb2ae483047b74..0000000000000000000000000000000000000000
 31Binary files a/opencode-logo.png and /dev/null differ
 32diff --git a/package.json b/package.json
 33index dfbd20a12ac26ddcfbbf25c00cadf672ed05e3a0..806761936dbf709fb4816c435df41c50c7b474e6 100644
 34--- a/package.json
 35+++ b/package.json
 36@@ -3,15 +3,19 @@
 37   "version": "3.0.0",
 38   "type": "module",
 39   "main": "src/index.ts",
 40-  "keywords": ["pi-package"],
 41+  "keywords": [
 42+    "pi-package"
 43+  ],
 44   "pi": {
 45-    "extensions": ["./src/index.ts"]
 46+    "extensions": [
 47+      "./src/index.ts"
 48+    ]
 49   },
 50   "peerDependencies": {
 51     "@earendil-works/pi-coding-agent": "*"
 52   },
 53   "devDependencies": {
 54-    "@earendil-works/pi-coding-agent": "^0.82.1",
 55+    "@earendil-works/pi-coding-agent": "^0.84.2",
 56     "@eslint/js": "^10.0.1",
 57     "bun-types": "latest",
 58     "eslint": "^10.1.0",
 59diff --git a/src/index.ts b/src/index.ts
 60index aeb09d709dde1023bab23c6b2be1eccf40d1b9d9..ea587b919a641c955f8ab9bab4c18b477548a479 100644
 61--- a/src/index.ts
 62+++ b/src/index.ts
 63@@ -1,5 +1,4 @@
 64 import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
 65-import { basename } from "node:path"
 66 import { captureProviderCredentials } from "./api"
 67 import { lookupCache, writeCache } from "./cache"
 68 import { checkDeterministic } from "./deterministic"
 69@@ -7,7 +6,6 @@ import { evaluateWithLLM } from "./llm"
 70 import { checkStaticPermission } from "./static-permissions"
 71 import { logDecision } from "./logger"
 72 import { cacheKey, normalizeRequest } from "./normalizer"
 73-import { notify } from "./notify"
 74 import type { Decision } from "./types"
 75 
 76 type ToolInput = Record<string, unknown>
 77@@ -140,16 +138,17 @@ export default function (pi: ExtensionAPI) {
 78     }
 79 
 80     const summary = inputSummary(event.toolName, input)
 81-    const approved = await ctx.ui.confirm(
 82-      "Policy approval required",
 83-      `${event.toolName}: ${summary}\n\n${decision.reason}`,
 84-    )
 85+    pi.events.emit("herdr:blocked", { active: true, label: "Policy approval required" })
 86+    let approved: boolean
 87+    try {
 88+      approved = await ctx.ui.confirm(
 89+        "Policy approval required",
 90+        `${event.toolName}: ${summary}\n\n${decision.reason}`,
 91+      )
 92+    } finally {
 93+      pi.events.emit("herdr:blocked", { active: false })
 94+    }
 95     if (approved) return undefined
 96     return { block: true, reason: `Blocked by user: ${decision.reason}` }
 97   })
 98-
 99-  pi.on("agent_settled", async (_event, ctx) => {
100-    const projectName = basename(ctx.cwd)
101-    notify(projectName ? `Pi (${projectName})` : "Pi", "Session complete")
102-  })
103 }
104diff --git a/src/notify.ts b/src/notify.ts
105deleted file mode 100644
106index 81e8c861714d1b6d5007b588d0dff03e5fbedbc9..0000000000000000000000000000000000000000
107--- a/src/notify.ts
108+++ /dev/null
109@@ -1,23 +0,0 @@
110-import { execFile } from "child_process"
111-const IS_MAC = process.platform === "darwin"
112-
113-function notifyLinux(title: string, message: string, timeout: number): void {
114-  const args = ["--app-name", "pi", "--expire-time", String(timeout * 1000), "--", title, message]
115-
116-  execFile("notify-send", args, () => {})
117-}
118-
119-function escapeAppleScript(s: string): string {
120-  return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
121-}
122-
123-function notifyMac(title: string, message: string): void {
124-  const t = escapeAppleScript(title)
125-  const m = escapeAppleScript(message)
126-  execFile("osascript", ["-e", `display notification "${m}" with title "${t}"`], () => {})
127-}
128-
129-export function notify(title: string, message: string, timeout = 10): void {
130-  if (IS_MAC) notifyMac(title, message)
131-  else notifyLinux(title, message, timeout)
132-}
133diff --git a/test/extension.test.ts b/test/extension.test.ts
134index b2b22d9c707e5d60e11a6260cb92c23ea4bd7ef2..e84da230cbb4822950af4a1a48183ccd7f76a579 100644
135--- a/test/extension.test.ts
136+++ b/test/extension.test.ts
137@@ -20,13 +20,19 @@ afterEach(() => {
138 })
139 
140 type ToolCallHandler = (event: { toolName: string; input: Record<string, unknown> }, ctx: any) => Promise<unknown>
141+type EmittedEvent = { name: string; data: unknown }
142 
143-function loadToolCallHandler(): ToolCallHandler {
144+function loadToolCallHandler(emittedEvents: EmittedEvent[] = []): ToolCallHandler {
145   let handler: ToolCallHandler | undefined
146   PolicyEngine({
147     on(event: string, callback: ToolCallHandler) {
148       if (event === "tool_call") handler = callback
149     },
150+    events: {
151+      emit(name: string, data: unknown) {
152+        emittedEvents.push({ name, data })
153+      },
154+    },
155   } as any)
156   if (!handler) throw new Error("tool_call handler was not registered")
157   return handler
158@@ -65,7 +71,8 @@ describe("Pi policy extension", () => {
159   })
160 
161   test("allows a user-approved command", async () => {
162-    const handler = loadToolCallHandler()
163+    const emittedEvents: EmittedEvent[] = []
164+    const handler = loadToolCallHandler(emittedEvents)
165     let prompted = false
166     const confirm = async () => {
167       prompted = true
168@@ -74,5 +81,9 @@ describe("Pi policy extension", () => {
169     await expect(handler({ toolName: "bash", input: { command: "sudo apt install foo" } }, context(true, confirm)))
170       .resolves.toBeUndefined()
171     expect(prompted).toBe(true)
172+    expect(emittedEvents).toEqual([
173+      { name: "herdr:blocked", data: { active: true, label: "Policy approval required" } },
174+      { name: "herdr:blocked", data: { active: false } },
175+    ])
176   })
177 })