@pushary/agent-hooks 0.4.5 → 0.4.6
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.
- package/data/SKILL.md +293 -0
- package/dist/bin/pushary-clean.js +10 -50
- package/dist/bin/pushary-codex.js +2 -1
- package/dist/bin/pushary-doctor.js +46 -121
- package/dist/bin/pushary-hook.js +3 -2
- package/dist/bin/pushary-setup.js +58 -80
- package/dist/{chunk-KINE5LNQ.js → chunk-4TQW4K6T.js} +1 -1
- package/dist/chunk-AC4UYAGX.js +136 -0
- package/dist/chunk-M5SRSBLS.js +23 -0
- package/dist/chunk-RJKW6LLC.js +78 -0
- package/dist/src/index.js +3 -2
- package/package.json +5 -3
- package/dist/chunk-4TWRLEOX.js +0 -49
- package/dist/chunk-5ZMTG7GF.js +0 -184
- package/dist/chunk-I546R6K2.js +0 -165
- package/dist/chunk-YKZWCOVP.js +0 -165
- package/dist/pushary-clean-M5RW2DG6.js +0 -154
- package/dist/pushary-clean-RM6TBJ3H.js +0 -147
- package/dist/pushary-doctor-EHLTPBD3.js +0 -252
- package/dist/pushary-doctor-LYMEFIZN.js +0 -254
|
@@ -1,147 +0,0 @@
|
|
|
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();
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// bin/pushary-doctor.ts
|
|
4
|
-
import { existsSync, readFileSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { homedir } from "os";
|
|
7
|
-
import { confirm } from "@inquirer/prompts";
|
|
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 red = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
12
|
-
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
13
|
-
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
14
|
-
var pass = green("\u2713");
|
|
15
|
-
var fail = red("\u2717");
|
|
16
|
-
var warn = yellow("!");
|
|
17
|
-
var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
|
|
18
|
-
var SKILL_PATH = join(homedir(), ".claude", "skills", "pushary", "SKILL.md");
|
|
19
|
-
var MCP_URL = "https://pushary.com/api/mcp/mcp";
|
|
20
|
-
var readJson = (path) => {
|
|
21
|
-
try {
|
|
22
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
23
|
-
} catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
var results = [];
|
|
28
|
-
var check = (passed, label, detail) => {
|
|
29
|
-
results.push({ passed, label, detail });
|
|
30
|
-
const icon = passed ? pass : fail;
|
|
31
|
-
const suffix = detail ? ` ${dim(`(${detail})`)}` : "";
|
|
32
|
-
console.log(` ${icon} ${label}${suffix}`);
|
|
33
|
-
};
|
|
34
|
-
var main = async () => {
|
|
35
|
-
console.log();
|
|
36
|
-
console.log(` ${bold("Pushary Doctor")}`);
|
|
37
|
-
console.log();
|
|
38
|
-
console.log(` ${dim("Configuration")}`);
|
|
39
|
-
const apiKey = process.env.PUSHARY_API_KEY;
|
|
40
|
-
check(!!apiKey, "API key in environment", apiKey ? `pk_${apiKey.split(".")[0]?.slice(3, 7)}...` : "PUSHARY_API_KEY not set");
|
|
41
|
-
const settings = readJson(CLAUDE_SETTINGS);
|
|
42
|
-
if (settings) {
|
|
43
|
-
const mcpServers = settings.mcpServers;
|
|
44
|
-
const pusharyServer = mcpServers?.pushary;
|
|
45
|
-
check(!!pusharyServer, "Claude Code: MCP server configured");
|
|
46
|
-
if (pusharyServer) {
|
|
47
|
-
check(pusharyServer.type === "http", "Claude Code: MCP server type", pusharyServer.type ? String(pusharyServer.type) : 'missing \u2014 add type: "http"');
|
|
48
|
-
}
|
|
49
|
-
const hooks = settings.hooks;
|
|
50
|
-
const hasPreHook = JSON.stringify(hooks?.PreToolUse ?? []).includes("pushary-hook");
|
|
51
|
-
const hasPostHook = JSON.stringify(hooks?.PostToolUse ?? []).includes("pushary-post-hook");
|
|
52
|
-
const hasStopHook = JSON.stringify(hooks?.Stop ?? []).includes("pushary-stop-hook");
|
|
53
|
-
check(hasPreHook, "Claude Code: PreToolUse hook");
|
|
54
|
-
check(hasPostHook, "Claude Code: PostToolUse hook");
|
|
55
|
-
check(hasStopHook, "Claude Code: Stop hook");
|
|
56
|
-
const permissions = settings.permissions;
|
|
57
|
-
const hasWildcard = permissions?.allow?.some((r) => r === "MCP(pushary:*)") ?? false;
|
|
58
|
-
check(hasWildcard, "Claude Code: Pushary tools auto-allowed", hasWildcard ? "MCP(pushary:*)" : "missing");
|
|
59
|
-
const hasLegacyPerms = permissions?.allow?.some((r) => r.startsWith("mcp__pushary__")) ?? false;
|
|
60
|
-
if (hasLegacyPerms) {
|
|
61
|
-
console.log(` ${warn} Legacy individual permissions detected ${dim("(run pushary clean, then setup again)")}`);
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
check(false, "Claude Code: settings.json", "not found");
|
|
65
|
-
}
|
|
66
|
-
check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
|
|
67
|
-
let globalVersion = "";
|
|
68
|
-
try {
|
|
69
|
-
const { execSync } = await import("child_process");
|
|
70
|
-
globalVersion = execSync("npm list -g @pushary/agent-hooks --depth=0 2>/dev/null", { encoding: "utf-8" }).match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
|
|
71
|
-
} catch {
|
|
72
|
-
}
|
|
73
|
-
check(!!globalVersion, "Global package installed", globalVersion || "not found");
|
|
74
|
-
console.log();
|
|
75
|
-
console.log(` ${dim("Connectivity")}`);
|
|
76
|
-
if (!apiKey) {
|
|
77
|
-
check(false, "MCP server reachable", "skipped \u2014 no API key");
|
|
78
|
-
check(false, "API key valid", "skipped");
|
|
79
|
-
check(false, "MCP handshake", "skipped");
|
|
80
|
-
} else {
|
|
81
|
-
let sessionId = "";
|
|
82
|
-
let toolCount = 0;
|
|
83
|
-
try {
|
|
84
|
-
const initRes = await fetch(MCP_URL, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
headers: {
|
|
87
|
-
"Content-Type": "application/json",
|
|
88
|
-
"Accept": "application/json, text/event-stream",
|
|
89
|
-
"Authorization": `Bearer ${apiKey}`
|
|
90
|
-
},
|
|
91
|
-
body: JSON.stringify({
|
|
92
|
-
jsonrpc: "2.0",
|
|
93
|
-
id: 1,
|
|
94
|
-
method: "initialize",
|
|
95
|
-
params: {
|
|
96
|
-
protocolVersion: "2025-03-26",
|
|
97
|
-
capabilities: {},
|
|
98
|
-
clientInfo: { name: "pushary-doctor", version: "1.0" }
|
|
99
|
-
}
|
|
100
|
-
}),
|
|
101
|
-
signal: AbortSignal.timeout(1e4)
|
|
102
|
-
});
|
|
103
|
-
check(initRes.ok, "MCP server reachable", `${initRes.status} ${initRes.statusText}`);
|
|
104
|
-
sessionId = initRes.headers.get("mcp-session-id") ?? "";
|
|
105
|
-
check(!!sessionId, "Session ID returned", sessionId ? `${sessionId.slice(0, 8)}...` : "missing \u2014 update MCP SDK");
|
|
106
|
-
const initBody = await initRes.text();
|
|
107
|
-
const initMatch = initBody.match(/data: (.+)/);
|
|
108
|
-
if (initMatch) {
|
|
109
|
-
const initData = JSON.parse(initMatch[1]);
|
|
110
|
-
check(!!initData.result?.serverInfo, "API key valid", initData.result?.serverInfo?.name ?? "unknown server");
|
|
111
|
-
}
|
|
112
|
-
const toolsRes = await fetch(MCP_URL, {
|
|
113
|
-
method: "POST",
|
|
114
|
-
headers: {
|
|
115
|
-
"Content-Type": "application/json",
|
|
116
|
-
"Accept": "application/json, text/event-stream",
|
|
117
|
-
"Authorization": `Bearer ${apiKey}`,
|
|
118
|
-
...sessionId ? { "Mcp-Session-Id": sessionId } : {}
|
|
119
|
-
},
|
|
120
|
-
body: JSON.stringify({
|
|
121
|
-
jsonrpc: "2.0",
|
|
122
|
-
id: 2,
|
|
123
|
-
method: "tools/list",
|
|
124
|
-
params: {}
|
|
125
|
-
}),
|
|
126
|
-
signal: AbortSignal.timeout(1e4)
|
|
127
|
-
});
|
|
128
|
-
const toolsBody = await toolsRes.text();
|
|
129
|
-
const toolsMatch = toolsBody.match(/data: (.+)/);
|
|
130
|
-
if (toolsMatch) {
|
|
131
|
-
const toolsData = JSON.parse(toolsMatch[1]);
|
|
132
|
-
toolCount = toolsData.result?.tools?.length ?? 0;
|
|
133
|
-
}
|
|
134
|
-
check(toolCount > 0, "MCP tools discovered", `${toolCount} tools`);
|
|
135
|
-
} catch (err) {
|
|
136
|
-
const msg = err instanceof Error ? err.message : "unknown error";
|
|
137
|
-
check(false, "MCP server reachable", msg);
|
|
138
|
-
}
|
|
139
|
-
console.log();
|
|
140
|
-
console.log(` ${dim("Notification Delivery")}`);
|
|
141
|
-
try {
|
|
142
|
-
const notifRes = await fetch("https://pushary.com/api/v1/server/send", {
|
|
143
|
-
method: "POST",
|
|
144
|
-
headers: {
|
|
145
|
-
"Content-Type": "application/json",
|
|
146
|
-
"Authorization": `Bearer ${apiKey}`
|
|
147
|
-
},
|
|
148
|
-
body: JSON.stringify({
|
|
149
|
-
title: "Pushary Doctor",
|
|
150
|
-
body: "If you see this, push notifications are working."
|
|
151
|
-
}),
|
|
152
|
-
signal: AbortSignal.timeout(1e4)
|
|
153
|
-
});
|
|
154
|
-
check(notifRes.ok, "Push notification sent", notifRes.ok ? "check your phone" : `${notifRes.status}`);
|
|
155
|
-
} catch (err) {
|
|
156
|
-
const msg = err instanceof Error ? err.message : "network error";
|
|
157
|
-
check(false, "Push notification sent", msg);
|
|
158
|
-
}
|
|
159
|
-
const testQuestion = await confirm({ message: "Test question roundtrip? (sends a push notification)", default: false });
|
|
160
|
-
if (testQuestion) {
|
|
161
|
-
console.log();
|
|
162
|
-
console.log(` ${dim("Question Roundtrip")}`);
|
|
163
|
-
try {
|
|
164
|
-
const mcpHeaders = {
|
|
165
|
-
"Content-Type": "application/json",
|
|
166
|
-
"Accept": "application/json, text/event-stream",
|
|
167
|
-
"Authorization": `Bearer ${apiKey}`,
|
|
168
|
-
...sessionId ? { "Mcp-Session-Id": sessionId } : {}
|
|
169
|
-
};
|
|
170
|
-
const askRes = await fetch(MCP_URL, {
|
|
171
|
-
method: "POST",
|
|
172
|
-
headers: mcpHeaders,
|
|
173
|
-
body: JSON.stringify({
|
|
174
|
-
jsonrpc: "2.0",
|
|
175
|
-
id: 3,
|
|
176
|
-
method: "tools/call",
|
|
177
|
-
params: {
|
|
178
|
-
name: "ask_user",
|
|
179
|
-
arguments: {
|
|
180
|
-
question: "Pushary Doctor: tap Yes to verify the roundtrip works.",
|
|
181
|
-
type: "confirm",
|
|
182
|
-
agentName: "Pushary Doctor"
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}),
|
|
186
|
-
signal: AbortSignal.timeout(15e3)
|
|
187
|
-
});
|
|
188
|
-
const askBody = await askRes.text();
|
|
189
|
-
const askMatch = askBody.match(/data: (.+)/);
|
|
190
|
-
let correlationId = "";
|
|
191
|
-
if (askMatch) {
|
|
192
|
-
const askData = JSON.parse(askMatch[1]);
|
|
193
|
-
const content = askData.result?.content?.[0]?.text;
|
|
194
|
-
if (content) {
|
|
195
|
-
const parsed = JSON.parse(content);
|
|
196
|
-
correlationId = parsed.correlationId;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
if (correlationId) {
|
|
200
|
-
console.log(` ${dim("\u2192")} Question sent, waiting for your answer...`);
|
|
201
|
-
const start = Date.now();
|
|
202
|
-
const waitRes = await fetch(MCP_URL, {
|
|
203
|
-
method: "POST",
|
|
204
|
-
headers: mcpHeaders,
|
|
205
|
-
body: JSON.stringify({
|
|
206
|
-
jsonrpc: "2.0",
|
|
207
|
-
id: 4,
|
|
208
|
-
method: "tools/call",
|
|
209
|
-
params: {
|
|
210
|
-
name: "wait_for_answer",
|
|
211
|
-
arguments: { correlationId, timeoutMs: 55e3 }
|
|
212
|
-
}
|
|
213
|
-
}),
|
|
214
|
-
signal: AbortSignal.timeout(6e4)
|
|
215
|
-
});
|
|
216
|
-
const waitBody = await waitRes.text();
|
|
217
|
-
const waitMatch = waitBody.match(/data: (.+)/);
|
|
218
|
-
if (waitMatch) {
|
|
219
|
-
const waitData = JSON.parse(waitMatch[1]);
|
|
220
|
-
const content = waitData.result?.content?.[0]?.text;
|
|
221
|
-
if (content) {
|
|
222
|
-
const parsed = JSON.parse(content);
|
|
223
|
-
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
224
|
-
check(parsed.answered === true, "Answer received", `"${parsed.value}" (${elapsed}s roundtrip)`);
|
|
225
|
-
} else {
|
|
226
|
-
check(false, "Answer received", "no response within timeout");
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
check(false, "Question sent", "failed to get correlationId");
|
|
231
|
-
}
|
|
232
|
-
} catch (err) {
|
|
233
|
-
const msg = err instanceof Error ? err.message : "unknown error";
|
|
234
|
-
check(false, "Question roundtrip", msg);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
console.log();
|
|
239
|
-
const failed = results.filter((r) => !r.passed);
|
|
240
|
-
if (failed.length === 0) {
|
|
241
|
-
console.log(` ${green(bold("All checks passed."))}`);
|
|
242
|
-
} else {
|
|
243
|
-
console.log(` ${red(bold(`${failed.length} check${failed.length === 1 ? "" : "s"} failed:`))}`);
|
|
244
|
-
for (const f of failed) {
|
|
245
|
-
console.log(` ${fail} ${f.label}${f.detail ? ` \u2014 ${f.detail}` : ""}`);
|
|
246
|
-
}
|
|
247
|
-
console.log();
|
|
248
|
-
console.log(` ${dim("Run")} ${cyan("npx @pushary/agent-hooks@latest clean")} ${dim("then")} ${cyan("setup")} ${dim("to fix.")}`);
|
|
249
|
-
}
|
|
250
|
-
console.log();
|
|
251
|
-
};
|
|
252
|
-
main();
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// bin/pushary-doctor.ts
|
|
4
|
-
import { existsSync, readFileSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { homedir } from "os";
|
|
7
|
-
import { confirm } from "@inquirer/prompts";
|
|
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 red = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
12
|
-
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
13
|
-
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
14
|
-
var pass = green("\u2713");
|
|
15
|
-
var fail = red("\u2717");
|
|
16
|
-
var warn = yellow("!");
|
|
17
|
-
var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
|
|
18
|
-
var SKILL_PATH = join(homedir(), ".claude", "skills", "pushary", "SKILL.md");
|
|
19
|
-
var MCP_URL = "https://pushary.com/api/mcp/mcp";
|
|
20
|
-
var readJson = (path) => {
|
|
21
|
-
try {
|
|
22
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
23
|
-
} catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
var results = [];
|
|
28
|
-
var check = (passed, label, detail) => {
|
|
29
|
-
results.push({ passed, label, detail });
|
|
30
|
-
const icon = passed ? pass : fail;
|
|
31
|
-
const suffix = detail ? ` ${dim(`(${detail})`)}` : "";
|
|
32
|
-
console.log(` ${icon} ${label}${suffix}`);
|
|
33
|
-
};
|
|
34
|
-
var main = async () => {
|
|
35
|
-
console.log();
|
|
36
|
-
console.log(` ${bold("Pushary Doctor")}`);
|
|
37
|
-
console.log();
|
|
38
|
-
console.log(` ${dim("Configuration")}`);
|
|
39
|
-
const apiKey = process.env.PUSHARY_API_KEY;
|
|
40
|
-
check(!!apiKey, "API key in environment", apiKey ? `pk_${apiKey.split(".")[0]?.slice(3, 7)}...` : "PUSHARY_API_KEY not set");
|
|
41
|
-
const settings = readJson(CLAUDE_SETTINGS);
|
|
42
|
-
if (settings) {
|
|
43
|
-
const mcpServers = settings.mcpServers;
|
|
44
|
-
const pusharyServer = mcpServers?.pushary;
|
|
45
|
-
check(!!pusharyServer, "Claude Code: MCP server configured");
|
|
46
|
-
if (pusharyServer) {
|
|
47
|
-
check(pusharyServer.type === "http", "Claude Code: MCP server type", pusharyServer.type ? String(pusharyServer.type) : 'missing \u2014 add type: "http"');
|
|
48
|
-
}
|
|
49
|
-
const hooks = settings.hooks;
|
|
50
|
-
const hasPreHook = JSON.stringify(hooks?.PreToolUse ?? []).includes("pushary-hook");
|
|
51
|
-
const hasPostHook = JSON.stringify(hooks?.PostToolUse ?? []).includes("pushary-post-hook");
|
|
52
|
-
const hasStopHook = JSON.stringify(hooks?.Stop ?? []).includes("pushary-stop-hook");
|
|
53
|
-
check(hasPreHook, "Claude Code: PreToolUse hook");
|
|
54
|
-
check(hasPostHook, "Claude Code: PostToolUse hook");
|
|
55
|
-
check(hasStopHook, "Claude Code: Stop hook");
|
|
56
|
-
const permissions = settings.permissions;
|
|
57
|
-
const hasWildcard = permissions?.allow?.some((r) => r === "MCP(pushary:*)") ?? false;
|
|
58
|
-
check(hasWildcard, "Claude Code: Pushary tools auto-allowed", hasWildcard ? "MCP(pushary:*)" : "missing");
|
|
59
|
-
const hasLegacyPerms = permissions?.allow?.some((r) => r.startsWith("mcp__pushary__")) ?? false;
|
|
60
|
-
if (hasLegacyPerms) {
|
|
61
|
-
console.log(` ${warn} Legacy individual permissions detected ${dim("(run pushary clean, then setup again)")}`);
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
check(false, "Claude Code: settings.json", "not found");
|
|
65
|
-
}
|
|
66
|
-
check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
|
|
67
|
-
let globalVersion = "";
|
|
68
|
-
try {
|
|
69
|
-
const { execSync } = await import("child_process");
|
|
70
|
-
globalVersion = execSync("npm list -g @pushary/agent-hooks --depth=0 2>/dev/null", { encoding: "utf-8" }).match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
|
|
71
|
-
} catch {
|
|
72
|
-
}
|
|
73
|
-
check(!!globalVersion, "Global package installed", globalVersion || "not found");
|
|
74
|
-
console.log();
|
|
75
|
-
console.log(` ${dim("Connectivity")}`);
|
|
76
|
-
if (!apiKey) {
|
|
77
|
-
check(false, "MCP server reachable", "skipped \u2014 no API key");
|
|
78
|
-
check(false, "API key valid", "skipped");
|
|
79
|
-
check(false, "MCP handshake", "skipped");
|
|
80
|
-
} else {
|
|
81
|
-
let sessionId = "";
|
|
82
|
-
let toolCount = 0;
|
|
83
|
-
try {
|
|
84
|
-
const initRes = await fetch(MCP_URL, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
headers: {
|
|
87
|
-
"Content-Type": "application/json",
|
|
88
|
-
"Accept": "application/json, text/event-stream",
|
|
89
|
-
"Authorization": `Bearer ${apiKey}`
|
|
90
|
-
},
|
|
91
|
-
body: JSON.stringify({
|
|
92
|
-
jsonrpc: "2.0",
|
|
93
|
-
id: 1,
|
|
94
|
-
method: "initialize",
|
|
95
|
-
params: {
|
|
96
|
-
protocolVersion: "2025-03-26",
|
|
97
|
-
capabilities: {},
|
|
98
|
-
clientInfo: { name: "pushary-doctor", version: "1.0" }
|
|
99
|
-
}
|
|
100
|
-
}),
|
|
101
|
-
signal: AbortSignal.timeout(1e4)
|
|
102
|
-
});
|
|
103
|
-
check(initRes.ok, "MCP server reachable", `${initRes.status} ${initRes.statusText}`);
|
|
104
|
-
sessionId = initRes.headers.get("mcp-session-id") ?? "";
|
|
105
|
-
check(!!sessionId, "Session ID returned", sessionId ? `${sessionId.slice(0, 8)}...` : "missing \u2014 update MCP SDK");
|
|
106
|
-
const initBody = await initRes.text();
|
|
107
|
-
const initMatch = initBody.match(/data: (.+)/);
|
|
108
|
-
if (initMatch) {
|
|
109
|
-
const initData = JSON.parse(initMatch[1]);
|
|
110
|
-
check(!!initData.result?.serverInfo, "API key valid", initData.result?.serverInfo?.name ?? "unknown server");
|
|
111
|
-
}
|
|
112
|
-
const toolsRes = await fetch(MCP_URL, {
|
|
113
|
-
method: "POST",
|
|
114
|
-
headers: {
|
|
115
|
-
"Content-Type": "application/json",
|
|
116
|
-
"Accept": "application/json, text/event-stream",
|
|
117
|
-
"Authorization": `Bearer ${apiKey}`,
|
|
118
|
-
...sessionId ? { "Mcp-Session-Id": sessionId } : {}
|
|
119
|
-
},
|
|
120
|
-
body: JSON.stringify({
|
|
121
|
-
jsonrpc: "2.0",
|
|
122
|
-
id: 2,
|
|
123
|
-
method: "tools/list",
|
|
124
|
-
params: {}
|
|
125
|
-
}),
|
|
126
|
-
signal: AbortSignal.timeout(1e4)
|
|
127
|
-
});
|
|
128
|
-
const toolsBody = await toolsRes.text();
|
|
129
|
-
const toolsMatch = toolsBody.match(/data: (.+)/);
|
|
130
|
-
if (toolsMatch) {
|
|
131
|
-
const toolsData = JSON.parse(toolsMatch[1]);
|
|
132
|
-
toolCount = toolsData.result?.tools?.length ?? 0;
|
|
133
|
-
}
|
|
134
|
-
check(toolCount > 0, "MCP tools discovered", `${toolCount} tools`);
|
|
135
|
-
} catch (err) {
|
|
136
|
-
const msg = err instanceof Error ? err.message : "unknown error";
|
|
137
|
-
check(false, "MCP server reachable", msg);
|
|
138
|
-
}
|
|
139
|
-
console.log();
|
|
140
|
-
console.log(` ${dim("Notification Delivery")}`);
|
|
141
|
-
try {
|
|
142
|
-
const notifRes = await fetch("https://pushary.com/api/v1/server/send", {
|
|
143
|
-
method: "POST",
|
|
144
|
-
headers: {
|
|
145
|
-
"Content-Type": "application/json",
|
|
146
|
-
"Authorization": `Bearer ${apiKey}`
|
|
147
|
-
},
|
|
148
|
-
body: JSON.stringify({
|
|
149
|
-
title: "Pushary Doctor",
|
|
150
|
-
body: "If you see this, push notifications are working."
|
|
151
|
-
}),
|
|
152
|
-
signal: AbortSignal.timeout(1e4)
|
|
153
|
-
});
|
|
154
|
-
check(notifRes.ok, "Push notification sent", notifRes.ok ? "check your phone" : `${notifRes.status}`);
|
|
155
|
-
} catch (err) {
|
|
156
|
-
const msg = err instanceof Error ? err.message : "network error";
|
|
157
|
-
check(false, "Push notification sent", msg);
|
|
158
|
-
}
|
|
159
|
-
const testQuestion = await confirm({ message: "Test question roundtrip? (sends a push notification)", default: false });
|
|
160
|
-
if (testQuestion) {
|
|
161
|
-
console.log();
|
|
162
|
-
console.log(` ${dim("Question Roundtrip")}`);
|
|
163
|
-
try {
|
|
164
|
-
const askRes = await fetch(MCP_URL, {
|
|
165
|
-
method: "POST",
|
|
166
|
-
headers: {
|
|
167
|
-
"Content-Type": "application/json",
|
|
168
|
-
"Accept": "application/json, text/event-stream",
|
|
169
|
-
"Authorization": `Bearer ${apiKey}`
|
|
170
|
-
},
|
|
171
|
-
body: JSON.stringify({
|
|
172
|
-
jsonrpc: "2.0",
|
|
173
|
-
id: 3,
|
|
174
|
-
method: "tools/call",
|
|
175
|
-
params: {
|
|
176
|
-
name: "ask_user",
|
|
177
|
-
arguments: {
|
|
178
|
-
question: "Pushary Doctor: tap Yes to verify the roundtrip works.",
|
|
179
|
-
type: "confirm",
|
|
180
|
-
agentName: "Pushary Doctor"
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}),
|
|
184
|
-
signal: AbortSignal.timeout(15e3)
|
|
185
|
-
});
|
|
186
|
-
const askBody = await askRes.text();
|
|
187
|
-
const askMatch = askBody.match(/data: (.+)/);
|
|
188
|
-
let correlationId = "";
|
|
189
|
-
if (askMatch) {
|
|
190
|
-
const askData = JSON.parse(askMatch[1]);
|
|
191
|
-
const content = askData.result?.content?.[0]?.text;
|
|
192
|
-
if (content) {
|
|
193
|
-
const parsed = JSON.parse(content);
|
|
194
|
-
correlationId = parsed.correlationId;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (correlationId) {
|
|
198
|
-
console.log(` ${dim("\u2192")} Question sent, waiting for your answer...`);
|
|
199
|
-
const start = Date.now();
|
|
200
|
-
const waitRes = await fetch(MCP_URL, {
|
|
201
|
-
method: "POST",
|
|
202
|
-
headers: {
|
|
203
|
-
"Content-Type": "application/json",
|
|
204
|
-
"Accept": "application/json, text/event-stream",
|
|
205
|
-
"Authorization": `Bearer ${apiKey}`
|
|
206
|
-
},
|
|
207
|
-
body: JSON.stringify({
|
|
208
|
-
jsonrpc: "2.0",
|
|
209
|
-
id: 4,
|
|
210
|
-
method: "tools/call",
|
|
211
|
-
params: {
|
|
212
|
-
name: "wait_for_answer",
|
|
213
|
-
arguments: { correlationId, timeoutMs: 55e3 }
|
|
214
|
-
}
|
|
215
|
-
}),
|
|
216
|
-
signal: AbortSignal.timeout(6e4)
|
|
217
|
-
});
|
|
218
|
-
const waitBody = await waitRes.text();
|
|
219
|
-
const waitMatch = waitBody.match(/data: (.+)/);
|
|
220
|
-
if (waitMatch) {
|
|
221
|
-
const waitData = JSON.parse(waitMatch[1]);
|
|
222
|
-
const content = waitData.result?.content?.[0]?.text;
|
|
223
|
-
if (content) {
|
|
224
|
-
const parsed = JSON.parse(content);
|
|
225
|
-
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
226
|
-
check(parsed.answered === true, "Answer received", `"${parsed.value}" (${elapsed}s roundtrip)`);
|
|
227
|
-
} else {
|
|
228
|
-
check(false, "Answer received", "no response within timeout");
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
check(false, "Question sent", "failed to get correlationId");
|
|
233
|
-
}
|
|
234
|
-
} catch (err) {
|
|
235
|
-
const msg = err instanceof Error ? err.message : "unknown error";
|
|
236
|
-
check(false, "Question roundtrip", msg);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
console.log();
|
|
241
|
-
const failed = results.filter((r) => !r.passed);
|
|
242
|
-
if (failed.length === 0) {
|
|
243
|
-
console.log(` ${green(bold("All checks passed."))}`);
|
|
244
|
-
} else {
|
|
245
|
-
console.log(` ${red(bold(`${failed.length} check${failed.length === 1 ? "" : "s"} failed:`))}`);
|
|
246
|
-
for (const f of failed) {
|
|
247
|
-
console.log(` ${fail} ${f.label}${f.detail ? ` \u2014 ${f.detail}` : ""}`);
|
|
248
|
-
}
|
|
249
|
-
console.log();
|
|
250
|
-
console.log(` ${dim("Run")} ${cyan("npx @pushary/agent-hooks@latest clean")} ${dim("then")} ${cyan("setup")} ${dim("to fix.")}`);
|
|
251
|
-
}
|
|
252
|
-
console.log();
|
|
253
|
-
};
|
|
254
|
-
main();
|