@pushary/agent-hooks 0.4.0 → 0.4.1
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/dist/bin/pushary-clean.js +7 -0
- package/dist/bin/pushary-doctor.js +8 -10
- package/dist/bin/pushary-setup.js +20 -19
- package/dist/pushary-clean-M5RW2DG6.js +154 -0
- package/dist/pushary-doctor-EHLTPBD3.js +252 -0
- package/package.json +3 -4
- package/data/SKILL.md +0 -293
|
@@ -5,6 +5,7 @@ import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { execSync } from "child_process";
|
|
8
|
+
import { confirm } from "@inquirer/prompts";
|
|
8
9
|
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
9
10
|
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
10
11
|
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
@@ -88,6 +89,12 @@ var main = async () => {
|
|
|
88
89
|
console.log(` ${bold("Pushary Clean")}`);
|
|
89
90
|
console.log(` ${dim("Removes all Pushary configuration")}`);
|
|
90
91
|
console.log();
|
|
92
|
+
const proceed = await confirm({ message: "Remove all Pushary configuration?", default: false });
|
|
93
|
+
if (!proceed) {
|
|
94
|
+
console.log(` ${dim("Cancelled.")}`);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
console.log();
|
|
91
98
|
cleanSettingsFile(CLAUDE_SETTINGS, "Claude Code settings");
|
|
92
99
|
cleanSettingsFile(CLAUDE_SETTINGS_LOCAL, "Claude Code settings.local");
|
|
93
100
|
const cursorData = readJson(CURSOR_MCP);
|
|
@@ -161,13 +161,15 @@ var main = async () => {
|
|
|
161
161
|
console.log();
|
|
162
162
|
console.log(` ${dim("Question Roundtrip")}`);
|
|
163
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
|
+
};
|
|
164
170
|
const askRes = await fetch(MCP_URL, {
|
|
165
171
|
method: "POST",
|
|
166
|
-
headers:
|
|
167
|
-
"Content-Type": "application/json",
|
|
168
|
-
"Accept": "application/json, text/event-stream",
|
|
169
|
-
"Authorization": `Bearer ${apiKey}`
|
|
170
|
-
},
|
|
172
|
+
headers: mcpHeaders,
|
|
171
173
|
body: JSON.stringify({
|
|
172
174
|
jsonrpc: "2.0",
|
|
173
175
|
id: 3,
|
|
@@ -199,11 +201,7 @@ var main = async () => {
|
|
|
199
201
|
const start = Date.now();
|
|
200
202
|
const waitRes = await fetch(MCP_URL, {
|
|
201
203
|
method: "POST",
|
|
202
|
-
headers:
|
|
203
|
-
"Content-Type": "application/json",
|
|
204
|
-
"Accept": "application/json, text/event-stream",
|
|
205
|
-
"Authorization": `Bearer ${apiKey}`
|
|
206
|
-
},
|
|
204
|
+
headers: mcpHeaders,
|
|
207
205
|
body: JSON.stringify({
|
|
208
206
|
jsonrpc: "2.0",
|
|
209
207
|
id: 4,
|
|
@@ -46,15 +46,23 @@ var spinner = async (label, fn) => {
|
|
|
46
46
|
`);
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
-
var
|
|
49
|
+
var getPackageVersion = () => {
|
|
50
|
+
try {
|
|
51
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
52
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
53
|
+
return pkg.version ?? "0.0.0";
|
|
54
|
+
} catch {
|
|
55
|
+
return "0.0.0";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var checkForUpdates = async (current) => {
|
|
50
59
|
try {
|
|
51
60
|
const res = await fetch("https://registry.npmjs.org/@pushary/agent-hooks/latest", {
|
|
52
61
|
signal: AbortSignal.timeout(3e3)
|
|
53
62
|
});
|
|
54
63
|
const data = await res.json();
|
|
55
64
|
const latest = data.version;
|
|
56
|
-
|
|
57
|
-
if (latest && current && latest !== current) {
|
|
65
|
+
if (latest && latest !== current) {
|
|
58
66
|
console.log(` ${yellow("!")} Update available: ${dim(current)} \u2192 ${green(latest)}`);
|
|
59
67
|
console.log(` ${dim("Run:")} npx @pushary/agent-hooks@${latest} setup`);
|
|
60
68
|
console.log();
|
|
@@ -126,18 +134,11 @@ var addToolPermissions = (settings) => {
|
|
|
126
134
|
};
|
|
127
135
|
var installSkill = async () => {
|
|
128
136
|
await spinner("Installing Pushary skill", async () => {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
} else {
|
|
135
|
-
const res = await fetch("https://raw.githubusercontent.com/pushary/pushary-skill/main/skills/pushary/SKILL.md", {
|
|
136
|
-
signal: AbortSignal.timeout(5e3)
|
|
137
|
-
});
|
|
138
|
-
if (!res.ok) throw new Error("Failed to fetch skill");
|
|
139
|
-
content = await res.text();
|
|
140
|
-
}
|
|
137
|
+
const res = await fetch("https://raw.githubusercontent.com/pushary/pushary-skill/main/skills/pushary/SKILL.md", {
|
|
138
|
+
signal: AbortSignal.timeout(1e4)
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok) throw new Error(`Failed to fetch skill (${res.status})`);
|
|
141
|
+
const content = await res.text();
|
|
141
142
|
if (!existsSync(SKILL_DIR)) mkdirSync(SKILL_DIR, { recursive: true });
|
|
142
143
|
writeFileSync(join(SKILL_DIR, "SKILL.md"), content, "utf-8");
|
|
143
144
|
});
|
|
@@ -262,7 +263,7 @@ var setupCursor = async (apiKey) => {
|
|
|
262
263
|
var saveApiKey = async (apiKey) => {
|
|
263
264
|
await spinner("Saving API key to shell profile", async () => {
|
|
264
265
|
const exportLine = `
|
|
265
|
-
export PUSHARY_API_KEY=
|
|
266
|
+
export PUSHARY_API_KEY='${apiKey}'
|
|
266
267
|
`;
|
|
267
268
|
const shellFile = SHELL_FILES.find((f) => existsSync(f));
|
|
268
269
|
if (shellFile) {
|
|
@@ -318,16 +319,16 @@ var AGENT_SETUP = {
|
|
|
318
319
|
cursor: setupCursor
|
|
319
320
|
};
|
|
320
321
|
var main = async () => {
|
|
321
|
-
const version =
|
|
322
|
+
const version = getPackageVersion();
|
|
322
323
|
console.log();
|
|
323
324
|
console.log(` ${bold("Pushary")} ${dim("v" + version)}`);
|
|
324
325
|
console.log(` ${dim("Push notifications for AI coding agents")}`);
|
|
325
326
|
console.log();
|
|
326
|
-
await checkForUpdates();
|
|
327
|
+
await checkForUpdates(version);
|
|
327
328
|
console.log(` ${dim("Get your API key at")} ${cyan("pushary.com/sign-up")}`);
|
|
328
329
|
console.log();
|
|
329
330
|
const apiKey = await input({ message: "API key:" });
|
|
330
|
-
if (!apiKey.trim() ||
|
|
331
|
+
if (!apiKey.trim() || !/^pk_[a-f0-9]+\.[a-f0-9]+$/.test(apiKey.trim())) {
|
|
331
332
|
console.log(`
|
|
332
333
|
${yellow("!")} Invalid key format. Expected: pk_xxx.sk_xxx`);
|
|
333
334
|
console.log(` ${dim("Get yours at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
|
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
import { confirm } from "@inquirer/prompts";
|
|
9
|
+
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
10
|
+
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
11
|
+
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
12
|
+
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
13
|
+
var check = green("\u2713");
|
|
14
|
+
var skip = yellow("\u2013");
|
|
15
|
+
var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
|
|
16
|
+
var CLAUDE_SETTINGS_LOCAL = join(homedir(), ".claude", "settings.local.json");
|
|
17
|
+
var SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
|
|
18
|
+
var CURSOR_MCP = join(".cursor", "mcp.json");
|
|
19
|
+
var SHELL_FILES = [".zshrc", ".bashrc"].map((f) => join(homedir(), f));
|
|
20
|
+
var readJson = (path) => {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var writeJson = (path, data) => {
|
|
28
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
29
|
+
};
|
|
30
|
+
var isPusharyPermission = (rule) => rule.includes("pushary") || rule.includes("MCP(pushary");
|
|
31
|
+
var isPusharyHook = (entry) => {
|
|
32
|
+
const hooks = entry.hooks;
|
|
33
|
+
if (!hooks) return false;
|
|
34
|
+
return hooks.some((h) => {
|
|
35
|
+
const cmd = String(h.command ?? "");
|
|
36
|
+
return cmd.includes("pushary-hook") || cmd.includes("pushary-post-hook") || cmd.includes("pushary-stop-hook");
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
var cleanSettingsFile = (path, label) => {
|
|
40
|
+
const data = readJson(path);
|
|
41
|
+
if (!data) {
|
|
42
|
+
console.log(` ${skip} ${label} ${dim("(not found)")}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let changed = false;
|
|
46
|
+
const mcpServers = data.mcpServers;
|
|
47
|
+
if (mcpServers?.pushary) {
|
|
48
|
+
delete mcpServers.pushary;
|
|
49
|
+
if (Object.keys(mcpServers).length === 0) delete data.mcpServers;
|
|
50
|
+
changed = true;
|
|
51
|
+
}
|
|
52
|
+
const permissions = data.permissions;
|
|
53
|
+
if (permissions?.allow) {
|
|
54
|
+
const allow = permissions.allow;
|
|
55
|
+
const filtered = allow.filter((r) => !isPusharyPermission(r));
|
|
56
|
+
if (filtered.length !== allow.length) {
|
|
57
|
+
permissions.allow = filtered;
|
|
58
|
+
if (filtered.length === 0) delete permissions.allow;
|
|
59
|
+
if (Object.keys(permissions).length === 0) delete data.permissions;
|
|
60
|
+
changed = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const hooks = data.hooks;
|
|
64
|
+
if (hooks) {
|
|
65
|
+
for (const key of ["PreToolUse", "PostToolUse", "Stop"]) {
|
|
66
|
+
const entries = hooks[key];
|
|
67
|
+
if (!entries) continue;
|
|
68
|
+
const filtered = entries.filter((e) => !isPusharyHook(e));
|
|
69
|
+
if (filtered.length !== entries.length) {
|
|
70
|
+
if (filtered.length === 0) {
|
|
71
|
+
delete hooks[key];
|
|
72
|
+
} else {
|
|
73
|
+
hooks[key] = filtered;
|
|
74
|
+
}
|
|
75
|
+
changed = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (Object.keys(hooks).length === 0) delete data.hooks;
|
|
79
|
+
}
|
|
80
|
+
if (changed) {
|
|
81
|
+
writeJson(path, data);
|
|
82
|
+
console.log(` ${check} ${label} ${dim("(cleaned)")}`);
|
|
83
|
+
} else {
|
|
84
|
+
console.log(` ${skip} ${label} ${dim("(no pushary entries)")}`);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var main = async () => {
|
|
88
|
+
console.log();
|
|
89
|
+
console.log(` ${bold("Pushary Clean")}`);
|
|
90
|
+
console.log(` ${dim("Removes all Pushary configuration")}`);
|
|
91
|
+
console.log();
|
|
92
|
+
const proceed = await confirm({ message: "Remove all Pushary configuration?", default: false });
|
|
93
|
+
if (!proceed) {
|
|
94
|
+
console.log(` ${dim("Cancelled.")}`);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
console.log();
|
|
98
|
+
cleanSettingsFile(CLAUDE_SETTINGS, "Claude Code settings");
|
|
99
|
+
cleanSettingsFile(CLAUDE_SETTINGS_LOCAL, "Claude Code settings.local");
|
|
100
|
+
const cursorData = readJson(CURSOR_MCP);
|
|
101
|
+
if (cursorData) {
|
|
102
|
+
const mcpServers = cursorData.mcpServers;
|
|
103
|
+
if (mcpServers?.pushary) {
|
|
104
|
+
delete mcpServers.pushary;
|
|
105
|
+
writeJson(CURSOR_MCP, cursorData);
|
|
106
|
+
console.log(` ${check} Cursor MCP config ${dim("(cleaned)")}`);
|
|
107
|
+
} else {
|
|
108
|
+
console.log(` ${skip} Cursor MCP config ${dim("(no pushary entries)")}`);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
console.log(` ${skip} Cursor MCP config ${dim("(not found)")}`);
|
|
112
|
+
}
|
|
113
|
+
if (existsSync(SKILL_DIR)) {
|
|
114
|
+
rmSync(SKILL_DIR, { recursive: true });
|
|
115
|
+
console.log(` ${check} Skill directory ${dim("(removed)")}`);
|
|
116
|
+
} else {
|
|
117
|
+
console.log(` ${skip} Skill directory ${dim("(not found)")}`);
|
|
118
|
+
}
|
|
119
|
+
const codexConfig = join(homedir(), ".codex", "config.toml");
|
|
120
|
+
try {
|
|
121
|
+
let config = readFileSync(codexConfig, "utf-8");
|
|
122
|
+
if (config.includes("pushary-codex")) {
|
|
123
|
+
config = config.split("\n").filter((l) => !l.includes("pushary-codex")).join("\n");
|
|
124
|
+
writeFileSync(codexConfig, config, "utf-8");
|
|
125
|
+
console.log(` ${check} Codex config ${dim("(cleaned)")}`);
|
|
126
|
+
} else {
|
|
127
|
+
console.log(` ${skip} Codex config ${dim("(no pushary entries)")}`);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
console.log(` ${skip} Codex config ${dim("(not found)")}`);
|
|
131
|
+
}
|
|
132
|
+
for (const shellFile of SHELL_FILES) {
|
|
133
|
+
try {
|
|
134
|
+
const content = readFileSync(shellFile, "utf-8");
|
|
135
|
+
if (content.includes("PUSHARY_API_KEY")) {
|
|
136
|
+
const cleaned = content.split("\n").filter((l) => !l.includes("PUSHARY_API_KEY")).join("\n");
|
|
137
|
+
writeFileSync(shellFile, cleaned, "utf-8");
|
|
138
|
+
console.log(` ${check} ${shellFile.split("/").pop()} ${dim("(removed API key)")}`);
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
execSync("npm uninstall -g @pushary/agent-hooks", { stdio: "ignore" });
|
|
145
|
+
console.log(` ${check} Global package ${dim("(uninstalled)")}`);
|
|
146
|
+
} catch {
|
|
147
|
+
console.log(` ${skip} Global package ${dim("(not installed)")}`);
|
|
148
|
+
}
|
|
149
|
+
console.log();
|
|
150
|
+
console.log(` ${green(bold("Clean complete."))}`);
|
|
151
|
+
console.log(` ${dim("Run")} npx @pushary/agent-hooks@latest setup ${dim("to reinstall.")}`);
|
|
152
|
+
console.log();
|
|
153
|
+
};
|
|
154
|
+
main();
|
|
@@ -0,0 +1,252 @@
|
|
|
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();
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
|
|
5
5
|
"author": "Pushary <business@pushary.com>",
|
|
6
6
|
"homepage": "https://pushary.com",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/pushary/pushary.git",
|
|
9
|
+
"url": "git+https://github.com/pushary/pushary.git",
|
|
10
10
|
"directory": "packages/agent-hooks"
|
|
11
11
|
},
|
|
12
12
|
"license": "MIT",
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
"pushary-doctor": "./dist/bin/pushary-doctor.js"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist"
|
|
29
|
-
"data"
|
|
28
|
+
"dist"
|
|
30
29
|
],
|
|
31
30
|
"scripts": {
|
|
32
31
|
"build": "tsup src/index.ts bin/pushary.ts bin/pushary-hook.ts bin/pushary-post-hook.ts bin/pushary-stop-hook.ts bin/pushary-codex.ts bin/pushary-setup.ts bin/pushary-clean.ts bin/pushary-doctor.ts --format esm --dts --outDir dist",
|
package/data/SKILL.md
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pushary
|
|
3
|
-
version: 0.1.0
|
|
4
|
-
description: Push notifications and human-in-the-loop for AI coding agents. Send alerts when tasks finish, ask questions (yes/no, multiple choice, or free text) via push, and get answers from the user's lock screen. Use these tools proactively — do not wait for the user to ask for notifications. Works with Cursor, Claude Code, Windsurf, Hermes, and any MCP client.
|
|
5
|
-
metadata:
|
|
6
|
-
hermes:
|
|
7
|
-
tags: [notifications, push, human-in-the-loop, alerts, permissions, mcp]
|
|
8
|
-
category: productivity
|
|
9
|
-
requires_toolsets: []
|
|
10
|
-
tags: notifications, push, mcp, human-in-the-loop, cursor, claude, windsurf, hermes, alerts, permissions
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# Pushary — Push Notifications for AI Agents
|
|
14
|
-
|
|
15
|
-
Pushary is an MCP server that gives you three capabilities:
|
|
16
|
-
|
|
17
|
-
1. **Send push notifications** to the user's phone or desktop when a task finishes or an error occurs.
|
|
18
|
-
2. **Ask questions** via push — yes/no, multiple choice, or free text — and wait for the user's answer.
|
|
19
|
-
3. **Send rich context notifications** with file changes, error details, next steps, and embedded questions.
|
|
20
|
-
|
|
21
|
-
Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
22
|
-
|
|
23
|
-
## When to Use
|
|
24
|
-
|
|
25
|
-
**Send a notification when:**
|
|
26
|
-
- You finish a task that took 3 or more steps — use `context.type = "task_complete"`
|
|
27
|
-
- A build, test suite, or deployment fails — use `context.type = "error"` with `errorMessage`
|
|
28
|
-
- A long-running process completes (migration, refactor, generation)
|
|
29
|
-
- A status update is worth sharing — use `context.type = "info"`
|
|
30
|
-
|
|
31
|
-
**Ask with type "confirm" when:**
|
|
32
|
-
- You need confirmation before a destructive or irreversible action
|
|
33
|
-
- Binary decision: proceed or abort
|
|
34
|
-
|
|
35
|
-
**Ask with type "select" when:**
|
|
36
|
-
- Multiple implementation approaches exist (2-6 options)
|
|
37
|
-
- The user needs to pick from a known set
|
|
38
|
-
|
|
39
|
-
**Ask with type "input" when:**
|
|
40
|
-
- You need a name, path, value, or free-text decision
|
|
41
|
-
- The options cannot be enumerated in advance
|
|
42
|
-
|
|
43
|
-
**Do NOT notify when:**
|
|
44
|
-
- The task is trivial or single-step
|
|
45
|
-
- The question can be answered from context without user input
|
|
46
|
-
- You already sent 3 notifications for the current task (unless the user explicitly asked for more)
|
|
47
|
-
|
|
48
|
-
## Setup
|
|
49
|
-
|
|
50
|
-
Add Pushary to your MCP configuration:
|
|
51
|
-
|
|
52
|
-
```json
|
|
53
|
-
{
|
|
54
|
-
"mcpServers": {
|
|
55
|
-
"pushary": {
|
|
56
|
-
"url": "https://pushary.com/api/mcp/mcp"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Sign up at https://pushary.com/sign-up?from=ai-coding to get your API key.
|
|
63
|
-
|
|
64
|
-
## Tools
|
|
65
|
-
|
|
66
|
-
### send_notification
|
|
67
|
-
|
|
68
|
-
Send a one-way push notification to the user. Optionally include structured context for a rich detail page.
|
|
69
|
-
|
|
70
|
-
**Parameters:**
|
|
71
|
-
|
|
72
|
-
| Name | Type | Required | Description |
|
|
73
|
-
|------|------|----------|-------------|
|
|
74
|
-
| title | string | Yes | Notification title (max 100 chars, aim for under 60) |
|
|
75
|
-
| body | string | Yes | Notification body (max 500 chars, aim for under 200) |
|
|
76
|
-
| url | string | No | URL opened when tapped. Ignored if context is provided. |
|
|
77
|
-
| agentName | string | No | Identifies which agent sent this (e.g., "Claude Code - myproject") |
|
|
78
|
-
| iconUrl | string | No | Custom notification icon URL |
|
|
79
|
-
| imageUrl | string | No | Large image shown in the notification |
|
|
80
|
-
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
81
|
-
| externalIds | string[] | No | Target by external IDs |
|
|
82
|
-
| tags | string[] | No | Target by subscriber tags |
|
|
83
|
-
| context | object | No | Structured context for a rich detail page (see below) |
|
|
84
|
-
|
|
85
|
-
**Context object:**
|
|
86
|
-
|
|
87
|
-
| Name | Type | Description |
|
|
88
|
-
|------|------|-------------|
|
|
89
|
-
| type | "task_complete" / "error" / "info" | The kind of notification |
|
|
90
|
-
| summary | string | Short summary of what happened |
|
|
91
|
-
| details | string[] | Bullet-point details |
|
|
92
|
-
| filesChanged | string[] | List of files that were changed |
|
|
93
|
-
| errorMessage | string | Error message (for error type) |
|
|
94
|
-
| errorFile | string | File path where the error occurred |
|
|
95
|
-
| nextSteps | string | Suggested next steps for the user |
|
|
96
|
-
| askQuestion | object | Embed a decision prompt in the notification (see below) |
|
|
97
|
-
|
|
98
|
-
**Embedded askQuestion:**
|
|
99
|
-
|
|
100
|
-
| Name | Type | Description |
|
|
101
|
-
|------|------|-------------|
|
|
102
|
-
| question | string | A follow-up question shown below the context |
|
|
103
|
-
| type | "confirm" / "select" / "input" | Question type (default: confirm) |
|
|
104
|
-
| options | string[] | Options for select type (2-6 items) |
|
|
105
|
-
|
|
106
|
-
When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
|
|
107
|
-
|
|
108
|
-
**Example — task completed with context:**
|
|
109
|
-
|
|
110
|
-
```json
|
|
111
|
-
{
|
|
112
|
-
"title": "Refactoring complete",
|
|
113
|
-
"body": "Extracted 3 shared components across 12 files",
|
|
114
|
-
"agentName": "Claude Code - pushary repo",
|
|
115
|
-
"context": {
|
|
116
|
-
"type": "task_complete",
|
|
117
|
-
"summary": "Extracted shared Button, Modal, and Card components from 12 files",
|
|
118
|
-
"filesChanged": ["src/components/Button.tsx", "src/components/Modal.tsx", "src/components/Card.tsx"],
|
|
119
|
-
"nextSteps": "Run the test suite to verify no regressions"
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**Example — error with embedded question:**
|
|
125
|
-
|
|
126
|
-
```json
|
|
127
|
-
{
|
|
128
|
-
"title": "Build failed",
|
|
129
|
-
"body": "TypeScript error in auth.ts:42",
|
|
130
|
-
"agentName": "Claude Code - api-server",
|
|
131
|
-
"context": {
|
|
132
|
-
"type": "error",
|
|
133
|
-
"errorMessage": "Type 'string' is not assignable to type 'AuthToken'",
|
|
134
|
-
"errorFile": "src/auth.ts:42",
|
|
135
|
-
"summary": "The auth token type changed upstream and this file needs updating",
|
|
136
|
-
"askQuestion": {
|
|
137
|
-
"question": "Should I update the type or revert the upstream change?",
|
|
138
|
-
"type": "select",
|
|
139
|
-
"options": ["Update the type in auth.ts", "Revert the upstream change", "Skip for now"]
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### ask_user
|
|
146
|
-
|
|
147
|
-
Send a question to the user via push notification. Supports three question types. Returns a `correlationId` that you pass to `wait_for_answer` to get the response.
|
|
148
|
-
|
|
149
|
-
**Parameters:**
|
|
150
|
-
|
|
151
|
-
| Name | Type | Required | Description |
|
|
152
|
-
|------|------|----------|-------------|
|
|
153
|
-
| question | string | Yes | The question to ask (max 500 chars) |
|
|
154
|
-
| type | "confirm" / "select" / "input" | No | Question type (default: confirm) |
|
|
155
|
-
| options | string[] | No | Choices for select type (2-6 options). Required when type is select. |
|
|
156
|
-
| placeholder | string | No | Placeholder text for input type (max 200 chars) |
|
|
157
|
-
| context | string | No | What the agent is working on, shown above the question (max 500 chars) |
|
|
158
|
-
| agentName | string | No | Identifies which agent is asking (e.g., "Claude Code - myproject") |
|
|
159
|
-
| callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
|
|
160
|
-
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
161
|
-
| externalIds | string[] | No | Target by external IDs |
|
|
162
|
-
| tags | string[] | No | Target by subscriber tags |
|
|
163
|
-
|
|
164
|
-
**Returns:** `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }`
|
|
165
|
-
|
|
166
|
-
**Example — confirm (yes/no):**
|
|
167
|
-
|
|
168
|
-
```json
|
|
169
|
-
{
|
|
170
|
-
"question": "Delete the 3 unused migration files?",
|
|
171
|
-
"type": "confirm",
|
|
172
|
-
"context": "Cleaning up old database migrations in db/migrate/",
|
|
173
|
-
"agentName": "Claude Code - myproject"
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
**Example — select (multiple choice):**
|
|
178
|
-
|
|
179
|
-
```json
|
|
180
|
-
{
|
|
181
|
-
"question": "Which auth strategy should I use?",
|
|
182
|
-
"type": "select",
|
|
183
|
-
"options": ["JWT tokens", "Session cookies", "OAuth2 + PKCE"],
|
|
184
|
-
"context": "Setting up authentication for the new API endpoints",
|
|
185
|
-
"agentName": "Claude Code - api-server"
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
**Example — input (free text):**
|
|
190
|
-
|
|
191
|
-
```json
|
|
192
|
-
{
|
|
193
|
-
"question": "What should the new API endpoint path be?",
|
|
194
|
-
"type": "input",
|
|
195
|
-
"placeholder": "/api/v2/...",
|
|
196
|
-
"context": "Creating a new REST endpoint for user preferences",
|
|
197
|
-
"agentName": "Cursor - frontend"
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### wait_for_answer
|
|
202
|
-
|
|
203
|
-
Long-poll for the user's response to a question sent via `ask_user`. Blocks until the user responds or the timeout is reached.
|
|
204
|
-
|
|
205
|
-
**Parameters:**
|
|
206
|
-
|
|
207
|
-
| Name | Type | Required | Description |
|
|
208
|
-
|------|------|----------|-------------|
|
|
209
|
-
| correlationId | string (uuid) | Yes | The correlationId from ask_user |
|
|
210
|
-
| timeoutMs | integer | No | How long to wait (default 30000, max 55000) |
|
|
211
|
-
|
|
212
|
-
**Returns:**
|
|
213
|
-
- `{ "answered": true, "value": "yes" }` — user responded
|
|
214
|
-
- `{ "answered": false }` — timeout reached, no answer yet
|
|
215
|
-
|
|
216
|
-
### cancel_question
|
|
217
|
-
|
|
218
|
-
Cancel a pending question so it can no longer be answered. Use when the question becomes irrelevant (e.g., you found the answer another way or the user responded in chat).
|
|
219
|
-
|
|
220
|
-
**Parameters:**
|
|
221
|
-
|
|
222
|
-
| Name | Type | Required | Description |
|
|
223
|
-
|------|------|----------|-------------|
|
|
224
|
-
| correlationId | string (uuid) | Yes | The correlationId of the question to cancel |
|
|
225
|
-
|
|
226
|
-
## Permission Gating (REQUIRED)
|
|
227
|
-
|
|
228
|
-
Before executing any of the following, you MUST call `ask_user` with type "confirm" and wait for approval. Do NOT proceed without an explicit "yes" from the user:
|
|
229
|
-
|
|
230
|
-
- File deletion (`rm`, `unlink`, any destructive file operation)
|
|
231
|
-
- Database mutations (`DROP`, `DELETE`, `TRUNCATE`, migrations)
|
|
232
|
-
- Deployment commands (`deploy`, `push`, `publish`, `release`)
|
|
233
|
-
- System administration (`systemctl`, `service`, package install/remove)
|
|
234
|
-
- Git operations that rewrite history (`reset --hard`, `push --force`, `rebase`)
|
|
235
|
-
- Network configuration changes (firewall, DNS, proxy)
|
|
236
|
-
- Any command the user has flagged as dangerous
|
|
237
|
-
|
|
238
|
-
If `wait_for_answer` returns `answered: false` after 3 attempts, do NOT execute the command. Notify the user that the operation was skipped due to no response.
|
|
239
|
-
|
|
240
|
-
This is not optional. Treat it as a hard constraint, not a suggestion.
|
|
241
|
-
|
|
242
|
-
## Human-in-the-Loop Flow
|
|
243
|
-
|
|
244
|
-
Follow this exact sequence when you need a decision from the user:
|
|
245
|
-
|
|
246
|
-
1. Call `ask_user` with a clear, concise question and the appropriate type.
|
|
247
|
-
2. Immediately call `wait_for_answer` with the returned `correlationId` and `timeoutMs: 55000`.
|
|
248
|
-
3. If `wait_for_answer` returns `{ "answered": false }`, retry the same `wait_for_answer` call up to 3 times. The answer persists in Redis for 10 minutes, so it will be there when the user responds.
|
|
249
|
-
4. Once you receive `{ "answered": true, "value": "..." }`, act on the decision.
|
|
250
|
-
5. If the user answers in chat before the push response arrives, continue normally and call `cancel_question` to clean up.
|
|
251
|
-
|
|
252
|
-
**Pseudocode:**
|
|
253
|
-
|
|
254
|
-
```
|
|
255
|
-
result = ask_user({
|
|
256
|
-
question: "Which auth strategy should I use?",
|
|
257
|
-
type: "select",
|
|
258
|
-
options: ["JWT tokens", "Session cookies", "OAuth2 + PKCE"],
|
|
259
|
-
context: "Setting up authentication for the new API",
|
|
260
|
-
agentName: "Claude Code - myproject"
|
|
261
|
-
})
|
|
262
|
-
correlationId = result.correlationId
|
|
263
|
-
|
|
264
|
-
for attempt in 1..3:
|
|
265
|
-
answer = wait_for_answer({ correlationId, timeoutMs: 55000 })
|
|
266
|
-
if answer.answered:
|
|
267
|
-
// answer.value = "JWT tokens" (the selected option)
|
|
268
|
-
// proceed with the chosen approach
|
|
269
|
-
break
|
|
270
|
-
|
|
271
|
-
if not answer.answered after 3 attempts:
|
|
272
|
-
// user did not respond — pick the safe default or ask in chat
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
## Identifying Your Agent
|
|
276
|
-
|
|
277
|
-
Always pass `agentName` when you are one of multiple possible agents the user may be running. The user sees this in the notification title to know which agent is asking.
|
|
278
|
-
|
|
279
|
-
**Format:** `{Agent Type} - {project or context}`
|
|
280
|
-
|
|
281
|
-
**Examples:**
|
|
282
|
-
- `"Claude Code - pushary repo"`
|
|
283
|
-
- `"Hermes - daily-briefing"`
|
|
284
|
-
- `"Cursor - frontend refactor"`
|
|
285
|
-
|
|
286
|
-
## Notification Etiquette
|
|
287
|
-
|
|
288
|
-
- **Titles under 60 characters.** They get truncated on phone lock screens.
|
|
289
|
-
- **Bodies under 200 characters.** Concise summaries, not full explanations.
|
|
290
|
-
- **Max 3 notifications per task** unless the user explicitly requests more.
|
|
291
|
-
- **Use context for detail.** Put file lists, error traces, and next steps in the context object — not the notification body.
|
|
292
|
-
- **Write questions as if talking to a busy person.** The user is on their phone, possibly away from their computer. Be specific: "Delete the 3 unused migration files?" is better than "Should I clean up?"
|
|
293
|
-
- **Pick the right question type.** Use confirm for binary decisions, select when options are known, input when they are not.
|