claude-remote-approver 0.1.0 → 0.2.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.
- package/README.md +33 -0
- package/bin/cli.mjs +62 -7
- package/package.json +2 -2
- package/src/setup.mjs +37 -1
package/README.md
CHANGED
|
@@ -108,6 +108,39 @@ claude-remote-approver status
|
|
|
108
108
|
# Timeout: 120s
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
### `enable`
|
|
112
|
+
|
|
113
|
+
Re-enable the hook after it has been disabled.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
claude-remote-approver enable
|
|
117
|
+
# Hook enabled.
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This reads the existing configuration and re-registers the hook in Claude Code's settings. You must have run `setup` at least once before using this command.
|
|
121
|
+
|
|
122
|
+
### `disable`
|
|
123
|
+
|
|
124
|
+
Temporarily disable the hook without removing your configuration.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
claude-remote-approver disable
|
|
128
|
+
# Hook disabled. Run 'claude-remote-approver enable' to re-enable.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Your topic and settings are preserved. Use `enable` to re-activate.
|
|
132
|
+
|
|
133
|
+
### `uninstall`
|
|
134
|
+
|
|
135
|
+
Remove the hook and delete all configuration.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
claude-remote-approver uninstall
|
|
139
|
+
# Uninstalled. Hook removed and configuration deleted.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This removes the hook entry from Claude Code's settings.json and deletes `~/.claude-remote-approver.json`. To use the tool again, run `setup`.
|
|
143
|
+
|
|
111
144
|
### `hook`
|
|
112
145
|
|
|
113
146
|
Internal command. Claude Code calls this automatically via the registered hook. It reads a JSON payload from stdin and writes a decision to stdout. You do not need to run this manually.
|
package/bin/cli.mjs
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* CLI entry point for claude-remote-approver.
|
|
5
5
|
*
|
|
6
|
-
* Subcommands: setup | test | status | hook
|
|
6
|
+
* Subcommands: setup | test | status | enable | disable | uninstall | hook
|
|
7
7
|
* All I/O goes through the injected `deps` object so the module is fully testable.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import os from "node:os";
|
|
12
14
|
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
14
16
|
// main
|
|
@@ -16,11 +18,11 @@ import { realpathSync } from "node:fs";
|
|
|
16
18
|
|
|
17
19
|
export async function main(args, deps) {
|
|
18
20
|
if (args.includes("--help") || args.includes("-h")) {
|
|
19
|
-
deps.stdout.write("Usage: claude-remote-approver <command>\n\nCommands:\n setup
|
|
21
|
+
deps.stdout.write("Usage: claude-remote-approver <command>\n\nCommands:\n setup Set up remote approval\n test Send a test notification\n status Show current configuration\n enable Re-enable the hook\n disable Temporarily disable the hook\n uninstall Remove hook and delete configuration\n hook Process a Claude Code hook (internal)\n");
|
|
20
22
|
return;
|
|
21
23
|
}
|
|
22
24
|
if (args.includes("--version") || args.includes("-v")) {
|
|
23
|
-
deps.stdout.write("0.
|
|
25
|
+
deps.stdout.write("0.2.0\n");
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -86,9 +88,56 @@ export async function main(args, deps) {
|
|
|
86
88
|
break;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
case "uninstall": {
|
|
92
|
+
try {
|
|
93
|
+
deps.unregisterHook(deps.settingsPath);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
deps.stderr.write(`Error: Failed to remove hook: ${err.message}\n`);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
deps.unlinkSync(deps.configPath);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err.code !== "ENOENT") {
|
|
102
|
+
deps.stderr.write(`Error: Failed to delete config: ${err.message}\n`);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
deps.stdout.write("Uninstalled. Hook removed and configuration deleted.\n");
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case "disable": {
|
|
111
|
+
try {
|
|
112
|
+
deps.unregisterHook(deps.settingsPath);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
deps.stderr.write(`Error: Failed to disable hook: ${err.message}\n`);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
deps.stdout.write("Hook disabled. Run 'claude-remote-approver enable' to re-enable.\n");
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "enable": {
|
|
122
|
+
const config = deps.loadConfig();
|
|
123
|
+
if (!config.topic) {
|
|
124
|
+
deps.stderr.write("Error: No topic configured. Run 'claude-remote-approver setup' first.\n");
|
|
125
|
+
deps.exit(1);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
deps.registerHook(deps.settingsPath, deps.getHookCommand());
|
|
130
|
+
} catch (err) {
|
|
131
|
+
deps.stderr.write(`Error: Failed to enable hook: ${err.message}\n`);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
deps.stdout.write("Hook enabled.\n");
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
89
138
|
default: {
|
|
90
139
|
deps.stderr.write(
|
|
91
|
-
"Usage: claude-remote-approver <command>\n\nCommands:\n setup
|
|
140
|
+
"Usage: claude-remote-approver <command>\n\nCommands:\n setup Set up remote approval\n test Send a test notification\n status Show current configuration\n enable Re-enable the hook\n disable Temporarily disable the hook\n uninstall Remove hook and delete configuration\n hook Process a Claude Code hook (internal)\n",
|
|
92
141
|
);
|
|
93
142
|
deps.exit(1);
|
|
94
143
|
break;
|
|
@@ -105,7 +154,7 @@ const isMain =
|
|
|
105
154
|
process.argv[1] &&
|
|
106
155
|
(() => {
|
|
107
156
|
try {
|
|
108
|
-
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
157
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(process.argv[1]);
|
|
109
158
|
} catch {
|
|
110
159
|
return false;
|
|
111
160
|
}
|
|
@@ -119,7 +168,7 @@ if (isMain) {
|
|
|
119
168
|
"../src/ntfy.mjs"
|
|
120
169
|
);
|
|
121
170
|
const { processHook } = await import("../src/hook.mjs");
|
|
122
|
-
const { runSetup } = await import("../src/setup.mjs");
|
|
171
|
+
const { runSetup, registerHook, getHookCommand, unregisterHook } = await import("../src/setup.mjs");
|
|
123
172
|
|
|
124
173
|
const args = process.argv.slice(2);
|
|
125
174
|
|
|
@@ -141,6 +190,12 @@ if (isMain) {
|
|
|
141
190
|
formatToolInfo,
|
|
142
191
|
processHook,
|
|
143
192
|
runSetup,
|
|
193
|
+
registerHook,
|
|
194
|
+
getHookCommand,
|
|
195
|
+
unregisterHook,
|
|
196
|
+
unlinkSync: fs.unlinkSync,
|
|
197
|
+
configPath: (await import("../src/config.mjs")).CONFIG_PATH,
|
|
198
|
+
settingsPath: path.join(os.homedir(), ".claude", "settings.json"),
|
|
144
199
|
stdout: process.stdout,
|
|
145
200
|
stderr: process.stderr,
|
|
146
201
|
stdin: stdinData,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-remote-approver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Approve or deny Claude Code permission prompts remotely from your phone via ntfy.sh",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "node --test test
|
|
16
|
+
"test": "node --test test/*.test.mjs"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"claude",
|
package/src/setup.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Returns the hook command string: `node <absolute_path_to_src/hook.mjs>`
|
|
6
7
|
*/
|
|
7
8
|
export function getHookCommand() {
|
|
8
|
-
const hookPath = path.resolve(import.meta.
|
|
9
|
+
const hookPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "hook.mjs");
|
|
9
10
|
return `node "${hookPath}"`;
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -48,6 +49,41 @@ export function registerHook(settingsPath, hookCommand) {
|
|
|
48
49
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Removes the claude-remote-approver hook entry from Claude's settings.json.
|
|
54
|
+
* If the file does not exist, does nothing.
|
|
55
|
+
*/
|
|
56
|
+
export function unregisterHook(settingsPath) {
|
|
57
|
+
let settings;
|
|
58
|
+
try {
|
|
59
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err.code === "ENOENT") return;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!settings.hooks?.PermissionRequest) return;
|
|
66
|
+
|
|
67
|
+
const original = settings.hooks.PermissionRequest;
|
|
68
|
+
const filtered = original.filter(
|
|
69
|
+
(h) => !(h.command && h.command.includes("claude-remote-approver"))
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (filtered.length === original.length) return;
|
|
73
|
+
|
|
74
|
+
if (filtered.length === 0) {
|
|
75
|
+
delete settings.hooks.PermissionRequest;
|
|
76
|
+
} else {
|
|
77
|
+
settings.hooks.PermissionRequest = filtered;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
81
|
+
delete settings.hooks;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
51
87
|
/**
|
|
52
88
|
* Runs the full setup flow:
|
|
53
89
|
* 1. Generate a topic
|