-
Notifications
You must be signed in to change notification settings - Fork 896
Set up hooks for notifications #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| @AGENTS.md | ||
| @AGENTS.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { observable } from "@trpc/server/observable"; | ||
| import { | ||
| notificationsEmitter, | ||
| type AgentCompleteEvent, | ||
| } from "main/lib/notifications/server"; | ||
| import { publicProcedure, router } from ".."; | ||
|
|
||
| type NotificationEvent = | ||
| | { type: "agent-complete"; data: AgentCompleteEvent } | ||
| | { type: "focus-tab"; data: { tabId: string; workspaceId: string } }; | ||
|
|
||
| export const createNotificationsRouter = () => { | ||
| return router({ | ||
| /** | ||
| * Subscribe to notification events (completions and focus requests). | ||
| */ | ||
| subscribe: publicProcedure.subscription(() => { | ||
| return observable<NotificationEvent>((emit) => { | ||
| const onComplete = (event: AgentCompleteEvent) => { | ||
| emit.next({ type: "agent-complete", data: event }); | ||
| }; | ||
|
|
||
| const onFocusTab = (data: { tabId: string; workspaceId: string }) => { | ||
| emit.next({ type: "focus-tab", data }); | ||
| }; | ||
|
|
||
| notificationsEmitter.on("agent-complete", onComplete); | ||
| notificationsEmitter.on("focus-tab", onFocusTab); | ||
|
|
||
| return () => { | ||
| notificationsEmitter.off("agent-complete", onComplete); | ||
| notificationsEmitter.off("focus-tab", onFocusTab); | ||
| }; | ||
| }); | ||
| }), | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| import fs from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { execSync } from "node:child_process"; | ||
| import { NOTIFICATIONS_PORT } from "shared/constants"; | ||
|
|
||
| const SUPERSET_DIR = path.join(os.homedir(), ".superset"); | ||
| const BIN_DIR = path.join(SUPERSET_DIR, "bin"); | ||
| const HOOKS_DIR = path.join(SUPERSET_DIR, "hooks"); | ||
|
|
||
| /** | ||
| * Finds the real path of a binary, skipping our wrapper scripts | ||
| */ | ||
| function findRealBinary(name: string): string | null { | ||
| try { | ||
| // Get all paths, filter out our bin dir | ||
| const result = execSync(`which -a ${name} 2>/dev/null || true`, { | ||
| encoding: "utf-8", | ||
| }); | ||
| const paths = result | ||
| .trim() | ||
| .split("\n") | ||
| .filter((p) => p && !p.startsWith(BIN_DIR)); | ||
| return paths[0] || null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates the notify.sh script | ||
| */ | ||
| function createNotifyScript(): void { | ||
| const notifyPath = path.join(HOOKS_DIR, "notify.sh"); | ||
| const script = `#!/bin/bash | ||
| # Superset agent notification hook | ||
| # Called by CLI agents (Claude Code, Codex, etc.) when they complete | ||
|
|
||
| # Only run if inside a Superset terminal | ||
| [ -z "$SUPERSET_TAB_ID" ] && exit 0 | ||
|
|
||
| curl -sG "http://127.0.0.1:\${SUPERSET_PORT:-${NOTIFICATIONS_PORT}}/hook/complete" \\ | ||
| --data-urlencode "tabId=$SUPERSET_TAB_ID" \\ | ||
| --data-urlencode "tabTitle=$SUPERSET_TAB_TITLE" \\ | ||
| --data-urlencode "workspaceName=$SUPERSET_WORKSPACE_NAME" \\ | ||
| --data-urlencode "workspaceId=$SUPERSET_WORKSPACE_ID" \\ | ||
| > /dev/null 2>&1 | ||
| `; | ||
| fs.writeFileSync(notifyPath, script, { mode: 0o755 }); | ||
| } | ||
|
Comment on lines
+33
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use NOTIFICATIONS_PORT constant for consistency. The port is hardcoded as Consider passing the NOTIFICATIONS_PORT value into the script generation: -function createNotifyScript(): void {
+function createNotifyScript(port: number): void {
const notifyPath = path.join(HOOKS_DIR, "notify.sh");
const script = `#!/bin/bash
# Superset agent notification hook
# Called by CLI agents (Claude Code, Codex, etc.) when they complete
# Only run if inside a Superset terminal
[ -z "$SUPERSET_TAB_ID" ] && exit 0
-curl -sG "http://127.0.0.1:\${SUPERSET_PORT:-31415}/hook/complete" \\
+curl -sG "http://127.0.0.1:\${SUPERSET_PORT:-${port}}/hook/complete" \\
--data-urlencode "tabId=$SUPERSET_TAB_ID" \\
--data-urlencode "tabTitle=$SUPERSET_TAB_TITLE" \\
--data-urlencode "workspaceName=$SUPERSET_WORKSPACE_NAME" \\
--data-urlencode "workspaceId=$SUPERSET_WORKSPACE_ID" \\
> /dev/null 2>&1
`;
fs.writeFileSync(notifyPath, script, { mode: 0o755 });
}Then import and pass NOTIFICATIONS_PORT when calling this function.
🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Creates wrapper script for Claude Code | ||
| */ | ||
| function createClaudeWrapper(): void { | ||
| const wrapperPath = path.join(BIN_DIR, "claude"); | ||
| const realClaude = findRealBinary("claude"); | ||
|
|
||
| if (!realClaude) { | ||
| console.log("[agent-setup] Claude not found, skipping wrapper"); | ||
| return; | ||
| } | ||
|
|
||
| const script = `#!/bin/bash | ||
| # Superset wrapper for Claude Code | ||
| # Injects notification hook settings | ||
|
|
||
| SUPERSET_CLAUDE_SETTINGS='{"hooks":{"Stop":[{"matcher":"","hooks":[{"type":"command","command":"~/.superset/hooks/notify.sh"}]}]}}' | ||
|
|
||
| exec "${realClaude}" --settings "$SUPERSET_CLAUDE_SETTINGS" "$@" | ||
| `; | ||
| fs.writeFileSync(wrapperPath, script, { mode: 0o755 }); | ||
| console.log(`[agent-setup] Created Claude wrapper -> ${realClaude}`); | ||
| } | ||
|
Comment on lines
+55
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Use proper path construction instead of hardcoding ~/.superset. The hook path is hardcoded as Apply this diff to use the actual resolved path: function createClaudeWrapper(): void {
const wrapperPath = path.join(BIN_DIR, "claude");
const realClaude = findRealBinary("claude");
+ const notifyPath = path.join(HOOKS_DIR, "notify.sh");
if (!realClaude) {
console.log("[agent-setup] Claude not found, skipping wrapper");
return;
}
const script = `#!/bin/bash
# Superset wrapper for Claude Code
# Injects notification hook settings
-SUPERSET_CLAUDE_SETTINGS='{"hooks":{"Stop":[{"matcher":"","hooks":[{"type":"command","command":"~/.superset/hooks/notify.sh"}]}]}}'
+SUPERSET_CLAUDE_SETTINGS='{"hooks":{"Stop":[{"matcher":"","hooks":[{"type":"command","command":"${notifyPath}"}]}]}}'
exec "${realClaude}" --settings "$SUPERSET_CLAUDE_SETTINGS" "$@"
`;
fs.writeFileSync(wrapperPath, script, { mode: 0o755 });
console.log(`[agent-setup] Created Claude wrapper -> ${realClaude}`);
}
🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Creates wrapper script for Codex | ||
| */ | ||
| function createCodexWrapper(): void { | ||
| const wrapperPath = path.join(BIN_DIR, "codex"); | ||
| const realCodex = findRealBinary("codex"); | ||
|
|
||
| if (!realCodex) { | ||
| console.log("[agent-setup] Codex not found, skipping wrapper"); | ||
| return; | ||
| } | ||
|
|
||
| const notifyPath = path.join(HOOKS_DIR, "notify.sh"); | ||
| const script = `#!/bin/bash | ||
| # Superset wrapper for Codex | ||
| # Injects notification hook settings | ||
|
|
||
| exec "${realCodex}" -c 'notify=["bash","${notifyPath}"]' "$@" | ||
| `; | ||
| fs.writeFileSync(wrapperPath, script, { mode: 0o755 }); | ||
| console.log(`[agent-setup] Created Codex wrapper -> ${realCodex}`); | ||
| } | ||
|
|
||
| /** | ||
| * Sets up the ~/.superset directory structure and agent wrappers | ||
| * Called on app startup | ||
| */ | ||
| export function setupAgentHooks(): void { | ||
| console.log("[agent-setup] Initializing agent hooks..."); | ||
|
|
||
| // Create directories | ||
| fs.mkdirSync(BIN_DIR, { recursive: true }); | ||
| fs.mkdirSync(HOOKS_DIR, { recursive: true }); | ||
|
|
||
| // Create scripts | ||
| createNotifyScript(); | ||
| createClaudeWrapper(); | ||
| createCodexWrapper(); | ||
|
|
||
| console.log("[agent-setup] Agent hooks initialized"); | ||
| } | ||
|
Comment on lines
+103
to
+116
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for filesystem operations. The function performs multiple filesystem operations without error handling. If directory creation or script writing fails (e.g., due to permissions), the app will crash on startup. Apply this diff to add basic error handling: export function setupAgentHooks(): void {
console.log("[agent-setup] Initializing agent hooks...");
- // Create directories
- fs.mkdirSync(BIN_DIR, { recursive: true });
- fs.mkdirSync(HOOKS_DIR, { recursive: true });
-
- // Create scripts
- createNotifyScript();
- createClaudeWrapper();
- createCodexWrapper();
-
- console.log("[agent-setup] Agent hooks initialized");
+ try {
+ // Create directories
+ fs.mkdirSync(BIN_DIR, { recursive: true });
+ fs.mkdirSync(HOOKS_DIR, { recursive: true });
+
+ // Create scripts
+ createNotifyScript();
+ createClaudeWrapper();
+ createCodexWrapper();
+
+ console.log("[agent-setup] Agent hooks initialized");
+ } catch (error) {
+ console.error("[agent-setup] Failed to initialize agent hooks:", error);
+ // Optionally: decide if this should be a fatal error or just log and continue
+ }
}🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Returns the PATH with our bin directory prepended | ||
| */ | ||
| export function getSupersetPath(): string { | ||
| return `${BIN_DIR}:${process.env.PATH || ""}`; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the bin directory path | ||
| */ | ||
| export function getSupersetBinDir(): string { | ||
| return BIN_DIR; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { EventEmitter } from "node:events"; | ||
| import express from "express"; | ||
| import { NOTIFICATIONS_PORT } from "shared/constants"; | ||
|
|
||
| export interface AgentCompleteEvent { | ||
| tabId: string; | ||
| tabTitle: string; | ||
| workspaceName: string; | ||
| workspaceId: string; | ||
| } | ||
|
|
||
| export const notificationsEmitter = new EventEmitter(); | ||
|
|
||
| const app = express(); | ||
|
|
||
| // CORS | ||
| app.use((req, res, next) => { | ||
| res.setHeader("Access-Control-Allow-Origin", "*"); | ||
| res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); | ||
| if (req.method === "OPTIONS") { | ||
| return res.status(200).end(); | ||
| } | ||
| next(); | ||
| }); | ||
|
|
||
| // Agent completion hook | ||
| app.get("/hook/complete", (req, res) => { | ||
| const { tabId, tabTitle, workspaceName, workspaceId } = req.query; | ||
|
|
||
| if (!tabId || typeof tabId !== "string") { | ||
| return res.status(400).json({ error: "Missing tabId parameter" }); | ||
| } | ||
|
|
||
| const event: AgentCompleteEvent = { | ||
| tabId, | ||
| tabTitle: (tabTitle as string) || "Terminal", | ||
| workspaceName: (workspaceName as string) || "Workspace", | ||
| workspaceId: (workspaceId as string) || "", | ||
| }; | ||
|
|
||
| notificationsEmitter.emit("agent-complete", event); | ||
|
|
||
| res.json({ success: true, tabId }); | ||
| }); | ||
|
|
||
| // Health check | ||
| app.get("/health", (req, res) => { | ||
| res.json({ status: "ok" }); | ||
| }); | ||
|
|
||
| // 404 | ||
| app.use((req, res) => { | ||
| res.status(404).json({ error: "Not found" }); | ||
| }); | ||
|
|
||
| export const notificationsApp = app; | ||
| export { NOTIFICATIONS_PORT }; |
Uh oh!
There was an error while loading. Please reload this page.