@pushary/agent-hooks 0.2.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,16 +6,23 @@ if (command === "setup") {
6
6
  await import("./pushary-setup.js");
7
7
  } else if (command === "hook") {
8
8
  await import("./pushary-hook.js");
9
+ } else if (command === "clean") {
10
+ await import("./pushary-clean.js");
11
+ } else if (command === "doctor") {
12
+ await import("./pushary-doctor.js");
9
13
  } else {
10
14
  console.log(`
11
15
  Pushary Agent Hooks
12
16
 
13
17
  Commands:
14
- setup Configure Claude Code or Cursor with Pushary
18
+ setup Configure Claude Code, Codex, Hermes, or Cursor with Pushary
19
+ doctor Verify your Pushary installation is working
20
+ clean Remove all Pushary configuration
15
21
  hook Run as a PreToolUse hook (reads stdin, writes stdout)
16
22
 
17
23
  Usage:
18
- npx pushary setup
19
- npx pushary hook
24
+ npx @pushary/agent-hooks@latest setup
25
+ npx @pushary/agent-hooks@latest doctor
26
+ npx @pushary/agent-hooks@latest clean
20
27
  `);
21
28
  }
@@ -0,0 +1,49 @@
1
+ import {
2
+ getBaseUrl
3
+ } from "./chunk-VUNL35KE.js";
4
+
5
+ // src/api.ts
6
+ var mcpToolCall = async (apiKey, toolName, params) => {
7
+ const baseUrl = getBaseUrl();
8
+ const response = await fetch(`${baseUrl}/api/mcp/mcp`, {
9
+ method: "POST",
10
+ headers: {
11
+ "Content-Type": "application/json",
12
+ "Authorization": `Bearer ${apiKey}`
13
+ },
14
+ body: JSON.stringify({
15
+ jsonrpc: "2.0",
16
+ id: Date.now(),
17
+ method: "tools/call",
18
+ params: { name: toolName, arguments: params }
19
+ })
20
+ });
21
+ if (!response.ok) {
22
+ throw new Error(`Pushary API error: ${response.status} ${response.statusText}`);
23
+ }
24
+ const json = await response.json();
25
+ if (json.error) throw new Error(json.error.message);
26
+ const text = json.result?.content?.[0]?.text;
27
+ if (!text) throw new Error("Empty response from Pushary");
28
+ return JSON.parse(text);
29
+ };
30
+ var askUser = async (apiKey, params) => {
31
+ const result = await mcpToolCall(apiKey, "ask_user", { ...params });
32
+ return result;
33
+ };
34
+ var waitForAnswer = async (apiKey, correlationId, timeoutMs = 3e4) => {
35
+ const result = await mcpToolCall(apiKey, "wait_for_answer", {
36
+ correlationId,
37
+ timeoutMs
38
+ });
39
+ return result;
40
+ };
41
+ var cancelQuestion = async (apiKey, correlationId) => {
42
+ await mcpToolCall(apiKey, "cancel_question", { correlationId });
43
+ };
44
+
45
+ export {
46
+ askUser,
47
+ waitForAnswer,
48
+ cancelQuestion
49
+ };
@@ -0,0 +1,77 @@
1
+ import {
2
+ getApiKey,
3
+ getBaseUrl
4
+ } from "./chunk-VUNL35KE.js";
5
+
6
+ // src/events.ts
7
+ import { hostname } from "os";
8
+ import { basename } from "path";
9
+ var reportEvent = async (event) => {
10
+ const apiKey = getApiKey();
11
+ const baseUrl = getBaseUrl();
12
+ await fetch(`${baseUrl}/api/agent/event`, {
13
+ method: "POST",
14
+ headers: {
15
+ "Content-Type": "application/json",
16
+ "Authorization": `Bearer ${apiKey}`
17
+ },
18
+ body: JSON.stringify({
19
+ ...event,
20
+ machineId: event.machineId ?? hostname()
21
+ })
22
+ });
23
+ };
24
+ var handlePostToolUse = async (input) => {
25
+ const projectName = basename(input.cwd ?? process.cwd());
26
+ let action;
27
+ switch (input.tool_name) {
28
+ case "Bash":
29
+ action = `ran: ${String(input.tool_input.command ?? "").slice(0, 120)}`;
30
+ break;
31
+ case "Write":
32
+ action = `wrote: ${input.tool_input.file_path ?? "unknown"}`;
33
+ break;
34
+ case "Edit":
35
+ action = `edited: ${input.tool_input.file_path ?? "unknown"}`;
36
+ break;
37
+ case "Read":
38
+ action = `read: ${input.tool_input.file_path ?? "unknown"}`;
39
+ break;
40
+ default:
41
+ action = `${input.tool_name}: done`;
42
+ }
43
+ const isError = input.tool_result && ("error" in input.tool_result || "is_error" in input.tool_result);
44
+ await reportEvent({
45
+ event: isError ? "tool_error" : "tool_complete",
46
+ agentType: "claude_code",
47
+ agentName: `Claude Code - ${projectName}`,
48
+ action,
49
+ error: isError ? String(input.tool_result?.error ?? input.tool_result?.stderr ?? "").slice(0, 500) : void 0
50
+ });
51
+ };
52
+ var handleStop = async (input) => {
53
+ const projectName = basename(input.cwd ?? process.cwd());
54
+ await reportEvent({
55
+ event: "session_end",
56
+ agentType: "claude_code",
57
+ agentName: `Claude Code - ${projectName}`,
58
+ action: "Session ended"
59
+ });
60
+ };
61
+ var handleNotification = async (input) => {
62
+ const projectName = basename(input.cwd ?? process.cwd());
63
+ await reportEvent({
64
+ event: input.type === "error" ? "error" : "notification",
65
+ agentType: "claude_code",
66
+ agentName: `Claude Code - ${projectName}`,
67
+ action: input.title ?? input.message ?? "Notification",
68
+ error: input.type === "error" ? input.message : void 0
69
+ });
70
+ };
71
+
72
+ export {
73
+ reportEvent,
74
+ handlePostToolUse,
75
+ handleStop,
76
+ handleNotification
77
+ };
@@ -0,0 +1,136 @@
1
+ import {
2
+ askUser,
3
+ waitForAnswer
4
+ } from "./chunk-4TWRLEOX.js";
5
+ import {
6
+ getApiKey,
7
+ getBaseUrl
8
+ } from "./chunk-VUNL35KE.js";
9
+
10
+ // src/policy.ts
11
+ import { createHash } from "crypto";
12
+ import { existsSync, readFileSync, writeFileSync } from "fs";
13
+ import { join } from "path";
14
+ import { tmpdir } from "os";
15
+ var cacheFile = (apiKey) => {
16
+ const hash = createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
17
+ return join(tmpdir(), `pushary-policy-${hash}.json`);
18
+ };
19
+ var fetchPolicy = async (apiKey) => {
20
+ const baseUrl = getBaseUrl();
21
+ const response = await fetch(`${baseUrl}/api/mcp/policy`, {
22
+ headers: { "Authorization": `Bearer ${apiKey}` },
23
+ signal: AbortSignal.timeout(1e4)
24
+ });
25
+ if (!response.ok) {
26
+ throw new Error(`Failed to fetch policy: ${response.status}`);
27
+ }
28
+ return response.json();
29
+ };
30
+ var getPolicy = async (apiKey) => {
31
+ const path = cacheFile(apiKey);
32
+ if (existsSync(path)) {
33
+ try {
34
+ return JSON.parse(readFileSync(path, "utf-8"));
35
+ } catch {
36
+ }
37
+ }
38
+ const policy = await fetchPolicy(apiKey);
39
+ writeFileSync(path, JSON.stringify(policy), "utf-8");
40
+ return policy;
41
+ };
42
+ var resolvePolicy = (config, toolName) => {
43
+ const exact = config.policies.find((p) => p.tool === toolName);
44
+ if (exact) return exact;
45
+ const wildcard = config.policies.find((p) => p.tool === "*");
46
+ if (wildcard) return wildcard;
47
+ return {
48
+ tool: toolName,
49
+ timeoutSeconds: config.defaultTimeoutSeconds,
50
+ timeoutAction: config.defaultTimeoutAction
51
+ };
52
+ };
53
+
54
+ // src/hook.ts
55
+ import { basename } from "path";
56
+ var describeToolCall = (input) => {
57
+ const { tool_name, tool_input } = input;
58
+ switch (tool_name) {
59
+ case "Bash":
60
+ return `bash: ${tool_input.command ?? "(no command)"}`;
61
+ case "Write":
62
+ return `write file: ${tool_input.file_path ?? "(unknown path)"}`;
63
+ case "Edit":
64
+ return `edit file: ${tool_input.file_path ?? "(unknown path)"}`;
65
+ case "Read":
66
+ return `read file: ${tool_input.file_path ?? "(unknown path)"}`;
67
+ default:
68
+ return `${tool_name}: ${JSON.stringify(tool_input).slice(0, 200)}`;
69
+ }
70
+ };
71
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
72
+ var allow = () => ({
73
+ hookSpecificOutput: {
74
+ hookEventName: "PreToolUse",
75
+ permissionDecision: "allow"
76
+ }
77
+ });
78
+ var deny = (reason) => ({
79
+ hookSpecificOutput: {
80
+ hookEventName: "PreToolUse",
81
+ permissionDecision: "deny",
82
+ permissionDecisionReason: reason
83
+ }
84
+ });
85
+ var ask = (reason) => ({
86
+ hookSpecificOutput: {
87
+ hookEventName: "PreToolUse",
88
+ permissionDecision: "ask",
89
+ permissionDecisionReason: reason
90
+ }
91
+ });
92
+ var handlePreToolUse = async (input) => {
93
+ const apiKey = getApiKey();
94
+ const policy = await getPolicy(apiKey);
95
+ const toolPolicy = resolvePolicy(policy, input.tool_name);
96
+ if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") {
97
+ return allow();
98
+ }
99
+ const description = describeToolCall(input);
100
+ const projectName = basename(input.cwd ?? process.cwd());
101
+ const result = await askUser(apiKey, {
102
+ question: `Allow ${description}?`,
103
+ type: "confirm",
104
+ context: `Agent wants to run this in ${projectName}`,
105
+ agentName: `Claude Code - ${projectName}`
106
+ });
107
+ const deadline = Date.now() + toolPolicy.timeoutSeconds * 1e3;
108
+ const pollInterval = 2e3;
109
+ while (Date.now() < deadline) {
110
+ const remaining = Math.min(
111
+ Math.max(deadline - Date.now(), 1e3),
112
+ 3e4
113
+ );
114
+ const answer = await waitForAnswer(apiKey, result.correlationId, remaining);
115
+ if (answer.answered) {
116
+ return answer.value === "yes" ? allow() : deny("Denied via Pushary push notification");
117
+ }
118
+ if (Date.now() + pollInterval >= deadline) break;
119
+ await sleep(pollInterval);
120
+ }
121
+ switch (toolPolicy.timeoutAction) {
122
+ case "approve":
123
+ return allow();
124
+ case "deny":
125
+ return deny("No response within timeout");
126
+ case "escalate":
127
+ default:
128
+ return ask("Pushary: no response, asking in terminal");
129
+ }
130
+ };
131
+
132
+ export {
133
+ getPolicy,
134
+ resolvePolicy,
135
+ handlePreToolUse
136
+ };
@@ -0,0 +1,16 @@
1
+ // src/config.ts
2
+ var getApiKey = () => {
3
+ const key = process.env.PUSHARY_API_KEY;
4
+ if (!key) {
5
+ throw new Error(
6
+ "PUSHARY_API_KEY environment variable is not set. Get your API key at https://pushary.com/sign-up?from=ai-coding"
7
+ );
8
+ }
9
+ return key;
10
+ };
11
+ var getBaseUrl = () => process.env.PUSHARY_BASE_URL ?? "https://pushary.com";
12
+
13
+ export {
14
+ getApiKey,
15
+ getBaseUrl
16
+ };
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/pushary-clean.ts
4
+ import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { execSync } from "child_process";
8
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
9
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
10
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
11
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
12
+ var check = green("\u2713");
13
+ var skip = yellow("\u2013");
14
+ var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
15
+ var CLAUDE_SETTINGS_LOCAL = join(homedir(), ".claude", "settings.local.json");
16
+ var SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
17
+ var CURSOR_MCP = join(".cursor", "mcp.json");
18
+ var SHELL_FILES = [".zshrc", ".bashrc"].map((f) => join(homedir(), f));
19
+ var readJson = (path) => {
20
+ try {
21
+ return JSON.parse(readFileSync(path, "utf-8"));
22
+ } catch {
23
+ return null;
24
+ }
25
+ };
26
+ var writeJson = (path, data) => {
27
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
28
+ };
29
+ var isPusharyPermission = (rule) => rule.includes("pushary") || rule.includes("MCP(pushary");
30
+ var isPusharyHook = (entry) => {
31
+ const hooks = entry.hooks;
32
+ if (!hooks) return false;
33
+ return hooks.some((h) => {
34
+ const cmd = String(h.command ?? "");
35
+ return cmd.includes("pushary-hook") || cmd.includes("pushary-post-hook") || cmd.includes("pushary-stop-hook");
36
+ });
37
+ };
38
+ var cleanSettingsFile = (path, label) => {
39
+ const data = readJson(path);
40
+ if (!data) {
41
+ console.log(` ${skip} ${label} ${dim("(not found)")}`);
42
+ return;
43
+ }
44
+ let changed = false;
45
+ const mcpServers = data.mcpServers;
46
+ if (mcpServers?.pushary) {
47
+ delete mcpServers.pushary;
48
+ if (Object.keys(mcpServers).length === 0) delete data.mcpServers;
49
+ changed = true;
50
+ }
51
+ const permissions = data.permissions;
52
+ if (permissions?.allow) {
53
+ const allow = permissions.allow;
54
+ const filtered = allow.filter((r) => !isPusharyPermission(r));
55
+ if (filtered.length !== allow.length) {
56
+ permissions.allow = filtered;
57
+ if (filtered.length === 0) delete permissions.allow;
58
+ if (Object.keys(permissions).length === 0) delete data.permissions;
59
+ changed = true;
60
+ }
61
+ }
62
+ const hooks = data.hooks;
63
+ if (hooks) {
64
+ for (const key of ["PreToolUse", "PostToolUse", "Stop"]) {
65
+ const entries = hooks[key];
66
+ if (!entries) continue;
67
+ const filtered = entries.filter((e) => !isPusharyHook(e));
68
+ if (filtered.length !== entries.length) {
69
+ if (filtered.length === 0) {
70
+ delete hooks[key];
71
+ } else {
72
+ hooks[key] = filtered;
73
+ }
74
+ changed = true;
75
+ }
76
+ }
77
+ if (Object.keys(hooks).length === 0) delete data.hooks;
78
+ }
79
+ if (changed) {
80
+ writeJson(path, data);
81
+ console.log(` ${check} ${label} ${dim("(cleaned)")}`);
82
+ } else {
83
+ console.log(` ${skip} ${label} ${dim("(no pushary entries)")}`);
84
+ }
85
+ };
86
+ var main = async () => {
87
+ console.log();
88
+ console.log(` ${bold("Pushary Clean")}`);
89
+ console.log(` ${dim("Removes all Pushary configuration")}`);
90
+ console.log();
91
+ cleanSettingsFile(CLAUDE_SETTINGS, "Claude Code settings");
92
+ cleanSettingsFile(CLAUDE_SETTINGS_LOCAL, "Claude Code settings.local");
93
+ const cursorData = readJson(CURSOR_MCP);
94
+ if (cursorData) {
95
+ const mcpServers = cursorData.mcpServers;
96
+ if (mcpServers?.pushary) {
97
+ delete mcpServers.pushary;
98
+ writeJson(CURSOR_MCP, cursorData);
99
+ console.log(` ${check} Cursor MCP config ${dim("(cleaned)")}`);
100
+ } else {
101
+ console.log(` ${skip} Cursor MCP config ${dim("(no pushary entries)")}`);
102
+ }
103
+ } else {
104
+ console.log(` ${skip} Cursor MCP config ${dim("(not found)")}`);
105
+ }
106
+ if (existsSync(SKILL_DIR)) {
107
+ rmSync(SKILL_DIR, { recursive: true });
108
+ console.log(` ${check} Skill directory ${dim("(removed)")}`);
109
+ } else {
110
+ console.log(` ${skip} Skill directory ${dim("(not found)")}`);
111
+ }
112
+ const codexConfig = join(homedir(), ".codex", "config.toml");
113
+ try {
114
+ let config = readFileSync(codexConfig, "utf-8");
115
+ if (config.includes("pushary-codex")) {
116
+ config = config.split("\n").filter((l) => !l.includes("pushary-codex")).join("\n");
117
+ writeFileSync(codexConfig, config, "utf-8");
118
+ console.log(` ${check} Codex config ${dim("(cleaned)")}`);
119
+ } else {
120
+ console.log(` ${skip} Codex config ${dim("(no pushary entries)")}`);
121
+ }
122
+ } catch {
123
+ console.log(` ${skip} Codex config ${dim("(not found)")}`);
124
+ }
125
+ for (const shellFile of SHELL_FILES) {
126
+ try {
127
+ const content = readFileSync(shellFile, "utf-8");
128
+ if (content.includes("PUSHARY_API_KEY")) {
129
+ const cleaned = content.split("\n").filter((l) => !l.includes("PUSHARY_API_KEY")).join("\n");
130
+ writeFileSync(shellFile, cleaned, "utf-8");
131
+ console.log(` ${check} ${shellFile.split("/").pop()} ${dim("(removed API key)")}`);
132
+ }
133
+ } catch {
134
+ }
135
+ }
136
+ try {
137
+ execSync("npm uninstall -g @pushary/agent-hooks", { stdio: "ignore" });
138
+ console.log(` ${check} Global package ${dim("(uninstalled)")}`);
139
+ } catch {
140
+ console.log(` ${skip} Global package ${dim("(not installed)")}`);
141
+ }
142
+ console.log();
143
+ console.log(` ${green(bold("Clean complete."))}`);
144
+ console.log(` ${dim("Run")} npx @pushary/agent-hooks@latest setup ${dim("to reinstall.")}`);
145
+ console.log();
146
+ };
147
+ main();