claude-remote-approver 0.6.1 → 0.7.2
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 +36 -0
- package/bin/cli.mjs +45 -2
- package/package.json +1 -1
- package/src/config.mjs +11 -0
- package/src/hook.mjs +20 -5
- package/src/ntfy.mjs +33 -31
- package/src/setup.mjs +12 -0
package/README.md
CHANGED
|
@@ -128,6 +128,7 @@ claude-remote-approver status
|
|
|
128
128
|
# Topic: cra-a1b2c3d4...
|
|
129
129
|
# Server: https://ntfy.sh
|
|
130
130
|
# Timeout: 120s
|
|
131
|
+
# Auth: not configured
|
|
131
132
|
```
|
|
132
133
|
|
|
133
134
|
### `enable`
|
|
@@ -197,6 +198,8 @@ Config file location: `~/.claude-remote-approver.json`
|
|
|
197
198
|
| `planTimeout` | `number` | `300` | Seconds to wait for ExitPlanMode (plan approval) responses. Plan reviews need more reading time. |
|
|
198
199
|
| `autoApprove` | `string[]` | `[]` | Reserved for future use. |
|
|
199
200
|
| `autoDeny` | `string[]` | `[]` | Reserved for future use. |
|
|
201
|
+
| `ntfyUsername` | `string` | `""` | Username for ntfy Basic Auth. Set this if your ntfy server requires authentication. |
|
|
202
|
+
| `ntfyPassword` | `string` | `""` | Password for ntfy Basic Auth. Set this if your ntfy server requires authentication. |
|
|
200
203
|
|
|
201
204
|
### Using a self-hosted ntfy server
|
|
202
205
|
|
|
@@ -210,6 +213,39 @@ Edit `~/.claude-remote-approver.json` and set `ntfyServer` to your server URL:
|
|
|
210
213
|
|
|
211
214
|
Then subscribe to the topic on your self-hosted server in the ntfy app.
|
|
212
215
|
|
|
216
|
+
### Using authenticated topics
|
|
217
|
+
|
|
218
|
+
If your ntfy server requires authentication, you can configure Basic Auth credentials.
|
|
219
|
+
|
|
220
|
+
**Option 1: Interactive setup**
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
claude-remote-approver setup
|
|
224
|
+
# ... after topic generation, you will be asked:
|
|
225
|
+
# Use authenticated topics? (y/n): y
|
|
226
|
+
# Username: myuser
|
|
227
|
+
# Password: mypassword
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Option 2: Edit `~/.claude-remote-approver.json`**
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"ntfyServer": "https://ntfy.example.com",
|
|
235
|
+
"ntfyUsername": "myuser",
|
|
236
|
+
"ntfyPassword": "mypassword"
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Option 3: Environment variables**
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
export NTFY_USERNAME=myuser
|
|
244
|
+
export NTFY_PASSWORD=mypassword
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Environment variables take priority over settings.json values. Credentials are included as `Authorization: Basic <base64>` headers in all requests to the ntfy server, including action button callbacks.
|
|
248
|
+
|
|
213
249
|
## How ntfy.sh works
|
|
214
250
|
|
|
215
251
|
[ntfy.sh](https://ntfy.sh) is a simple HTTP-based pub-sub notification service. Any client can publish a message to a topic by sending a POST request, and any client subscribed to that topic receives the message as a push notification.
|
package/bin/cli.mjs
CHANGED
|
@@ -40,7 +40,7 @@ export async function main(args, deps) {
|
|
|
40
40
|
const isHttps = serverUrl.protocol === "https:";
|
|
41
41
|
const ntfyUrl = isHttps
|
|
42
42
|
? `ntfy://${serverUrl.host}/${result.topic}`
|
|
43
|
-
:
|
|
43
|
+
: `ntfy://${serverUrl.host}/${result.topic}?secure=false`;
|
|
44
44
|
const subscribeUrl = `${result.ntfyServer.replace(/\/+$/, "")}/${result.topic}`;
|
|
45
45
|
|
|
46
46
|
deps.stdout.write("Scan this QR code in the ntfy app to subscribe:\n\n");
|
|
@@ -62,6 +62,7 @@ export async function main(args, deps) {
|
|
|
62
62
|
deps.stderr.write("Error: No topic configured. Run 'claude-remote-approver setup' first.\n");
|
|
63
63
|
break;
|
|
64
64
|
}
|
|
65
|
+
const auth = deps.resolveAuth(config);
|
|
65
66
|
try {
|
|
66
67
|
await deps.sendNotification({
|
|
67
68
|
server: config.ntfyServer,
|
|
@@ -70,6 +71,7 @@ export async function main(args, deps) {
|
|
|
70
71
|
message: "Test notification - if you see this, setup is working!",
|
|
71
72
|
actions: [],
|
|
72
73
|
requestId: "test",
|
|
74
|
+
auth,
|
|
73
75
|
});
|
|
74
76
|
deps.stdout.write("Test notification sent successfully.\n");
|
|
75
77
|
} catch (err) {
|
|
@@ -83,6 +85,12 @@ export async function main(args, deps) {
|
|
|
83
85
|
deps.stdout.write(`Topic: ${config.topic}\n`);
|
|
84
86
|
deps.stdout.write(`Server: ${config.ntfyServer}\n`);
|
|
85
87
|
deps.stdout.write(`Timeout: ${config.timeout}s\n`);
|
|
88
|
+
const auth = deps.resolveAuth(config);
|
|
89
|
+
if (auth) {
|
|
90
|
+
deps.stdout.write(`Auth: configured (username: ${auth.username})\n`);
|
|
91
|
+
} else {
|
|
92
|
+
deps.stdout.write(`Auth: not configured\n`);
|
|
93
|
+
}
|
|
86
94
|
break;
|
|
87
95
|
}
|
|
88
96
|
|
|
@@ -184,7 +192,7 @@ const isMain =
|
|
|
184
192
|
if (isMain) {
|
|
185
193
|
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
186
194
|
|
|
187
|
-
const { loadConfig, saveConfig, generateTopic } = await import(
|
|
195
|
+
const { loadConfig, saveConfig, generateTopic, resolveAuth } = await import(
|
|
188
196
|
"../src/config.mjs"
|
|
189
197
|
);
|
|
190
198
|
const { sendNotification, waitForResponse, formatToolInfo } = await import(
|
|
@@ -208,6 +216,7 @@ if (isMain) {
|
|
|
208
216
|
loadConfig,
|
|
209
217
|
saveConfig,
|
|
210
218
|
generateTopic,
|
|
219
|
+
resolveAuth,
|
|
211
220
|
sendNotification,
|
|
212
221
|
waitForResponse,
|
|
213
222
|
formatToolInfo,
|
|
@@ -225,6 +234,40 @@ if (isMain) {
|
|
|
225
234
|
stderr: process.stderr,
|
|
226
235
|
stdin: stdinData,
|
|
227
236
|
exit: process.exit,
|
|
237
|
+
prompt: async (question) => {
|
|
238
|
+
const { createInterface } = await import("node:readline");
|
|
239
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
240
|
+
return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer); }));
|
|
241
|
+
},
|
|
242
|
+
promptSecret: async (question) => {
|
|
243
|
+
process.stdout.write(question);
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
let input = '';
|
|
246
|
+
process.stdin.setRawMode(true);
|
|
247
|
+
process.stdin.resume();
|
|
248
|
+
process.stdin.setEncoding('utf8');
|
|
249
|
+
const onData = (ch) => {
|
|
250
|
+
if (ch === '\r' || ch === '\n') {
|
|
251
|
+
process.stdin.setRawMode(false);
|
|
252
|
+
process.stdin.pause();
|
|
253
|
+
process.stdin.removeListener('data', onData);
|
|
254
|
+
process.stdout.write('\n');
|
|
255
|
+
resolve(input);
|
|
256
|
+
} else if (ch === '\u007f' || ch === '\b') {
|
|
257
|
+
if (input.length > 0) {
|
|
258
|
+
input = input.slice(0, -1);
|
|
259
|
+
process.stdout.write('\b \b');
|
|
260
|
+
}
|
|
261
|
+
} else if (ch === '\u0003') {
|
|
262
|
+
process.exit(0);
|
|
263
|
+
} else {
|
|
264
|
+
input += ch;
|
|
265
|
+
process.stdout.write('*');
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
process.stdin.on('data', onData);
|
|
269
|
+
});
|
|
270
|
+
},
|
|
228
271
|
};
|
|
229
272
|
|
|
230
273
|
await main(args, deps);
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -13,6 +13,8 @@ export const DEFAULT_CONFIG = {
|
|
|
13
13
|
// autoApprove/autoDeny are reserved for future use and not yet implemented
|
|
14
14
|
autoApprove: [],
|
|
15
15
|
autoDeny: [],
|
|
16
|
+
ntfyUsername: "",
|
|
17
|
+
ntfyPassword: "",
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
export function loadConfig(configPath = CONFIG_PATH) {
|
|
@@ -26,6 +28,8 @@ export function loadConfig(configPath = CONFIG_PATH) {
|
|
|
26
28
|
if (!Number.isFinite(config.planTimeout) || config.planTimeout <= 0) config.planTimeout = DEFAULT_CONFIG.planTimeout;
|
|
27
29
|
if (!Array.isArray(config.autoApprove)) config.autoApprove = DEFAULT_CONFIG.autoApprove;
|
|
28
30
|
if (!Array.isArray(config.autoDeny)) config.autoDeny = DEFAULT_CONFIG.autoDeny;
|
|
31
|
+
if (typeof config.ntfyUsername !== "string") config.ntfyUsername = DEFAULT_CONFIG.ntfyUsername;
|
|
32
|
+
if (typeof config.ntfyPassword !== "string") config.ntfyPassword = DEFAULT_CONFIG.ntfyPassword;
|
|
29
33
|
return config;
|
|
30
34
|
} catch (err) {
|
|
31
35
|
if (err.code === "ENOENT") {
|
|
@@ -42,3 +46,10 @@ export function saveConfig(config, configPath = CONFIG_PATH) {
|
|
|
42
46
|
export function generateTopic() {
|
|
43
47
|
return `cra-${crypto.randomBytes(16).toString("hex")}`;
|
|
44
48
|
}
|
|
49
|
+
|
|
50
|
+
export function resolveAuth(config, env = process.env) {
|
|
51
|
+
const username = env.NTFY_USERNAME || config.ntfyUsername || "";
|
|
52
|
+
const password = env.NTFY_PASSWORD || config.ntfyPassword || "";
|
|
53
|
+
if (!username || !password) return null;
|
|
54
|
+
return { username, password };
|
|
55
|
+
}
|
package/src/hook.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { DEFAULT_CONFIG } from "./config.mjs";
|
|
5
|
+
import { buildAuthHeader } from "./ntfy.mjs";
|
|
5
6
|
|
|
6
7
|
export const ASK = Object.freeze({ hookSpecificOutput: Object.freeze({ hookEventName: "PermissionRequest", decision: Object.freeze({ behavior: "ask" }) }) });
|
|
7
8
|
const DENY = Object.freeze({ hookSpecificOutput: Object.freeze({ hookEventName: "PermissionRequest", decision: Object.freeze({ behavior: "deny" }) }) });
|
|
@@ -20,8 +21,9 @@ export const _internal = { delay: ms => new Promise(r => setTimeout(r, ms)) };
|
|
|
20
21
|
* @param {string[]} [options.permissionSuggestions] - When non-empty, adds an "Always Approve" button
|
|
21
22
|
* @returns {Array<object>} Array of action objects
|
|
22
23
|
*/
|
|
23
|
-
export function buildActions(server, topic, requestId, { permissionSuggestions } = {}) {
|
|
24
|
+
export function buildActions(server, topic, requestId, { permissionSuggestions, auth } = {}) {
|
|
24
25
|
const url = `${server}/${topic}-response`;
|
|
26
|
+
const authHeaders = auth ? buildAuthHeader(auth) : undefined;
|
|
25
27
|
const actions = [
|
|
26
28
|
{
|
|
27
29
|
action: "http",
|
|
@@ -29,6 +31,7 @@ export function buildActions(server, topic, requestId, { permissionSuggestions }
|
|
|
29
31
|
url,
|
|
30
32
|
body: JSON.stringify({ requestId, approved: true }),
|
|
31
33
|
method: "POST",
|
|
34
|
+
...(authHeaders && { headers: authHeaders }),
|
|
32
35
|
},
|
|
33
36
|
{
|
|
34
37
|
action: "http",
|
|
@@ -36,6 +39,7 @@ export function buildActions(server, topic, requestId, { permissionSuggestions }
|
|
|
36
39
|
url,
|
|
37
40
|
body: JSON.stringify({ requestId, approved: false }),
|
|
38
41
|
method: "POST",
|
|
42
|
+
...(authHeaders && { headers: authHeaders }),
|
|
39
43
|
},
|
|
40
44
|
];
|
|
41
45
|
if (permissionSuggestions?.length > 0) {
|
|
@@ -45,6 +49,7 @@ export function buildActions(server, topic, requestId, { permissionSuggestions }
|
|
|
45
49
|
url,
|
|
46
50
|
body: JSON.stringify({ requestId, approved: true, alwaysAllow: true }),
|
|
47
51
|
method: "POST",
|
|
52
|
+
...(authHeaders && { headers: authHeaders }),
|
|
48
53
|
});
|
|
49
54
|
}
|
|
50
55
|
return actions;
|
|
@@ -83,14 +88,16 @@ export function isAskUserQuestion(input) {
|
|
|
83
88
|
/**
|
|
84
89
|
* Build ntfy action buttons for question options.
|
|
85
90
|
*/
|
|
86
|
-
export function buildQuestionActions(server, topic, requestId, options) {
|
|
91
|
+
export function buildQuestionActions(server, topic, requestId, options, { auth } = {}) {
|
|
87
92
|
const url = `${server}/${topic}-response`;
|
|
93
|
+
const authHeaders = auth ? buildAuthHeader(auth) : undefined;
|
|
88
94
|
return options.map((opt) => ({
|
|
89
95
|
action: "http",
|
|
90
96
|
label: opt.label,
|
|
91
97
|
url,
|
|
92
98
|
body: JSON.stringify({ requestId, answer: opt.label }),
|
|
93
99
|
method: "POST",
|
|
100
|
+
...(authHeaders && { headers: authHeaders }),
|
|
94
101
|
}));
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -116,6 +123,7 @@ export async function processAskUserQuestion(input, deps) {
|
|
|
116
123
|
const config = deps.loadConfig();
|
|
117
124
|
if (!config.topic) return ASK;
|
|
118
125
|
|
|
126
|
+
const auth = deps.resolveAuth ? deps.resolveAuth(config) : null;
|
|
119
127
|
const questions = input.tool_input.questions;
|
|
120
128
|
const answers = {};
|
|
121
129
|
|
|
@@ -132,7 +140,7 @@ export async function processAskUserQuestion(input, deps) {
|
|
|
132
140
|
for (let i = 0; i < batches.length; i++) {
|
|
133
141
|
const batch = batches[i];
|
|
134
142
|
const batchInfo = batches.length > 1 ? `(${i + 1}/${batches.length})` : undefined;
|
|
135
|
-
const actions = buildQuestionActions(config.ntfyServer, config.topic, requestId, batch);
|
|
143
|
+
const actions = buildQuestionActions(config.ntfyServer, config.topic, requestId, batch, { ...(auth && { auth }) });
|
|
136
144
|
const message = buildQuestionMessage(q.question, batch, { multiSelect: q.multiSelect, batchInfo });
|
|
137
145
|
|
|
138
146
|
const sent = await sendWithRetry(deps.sendNotification, {
|
|
@@ -142,6 +150,7 @@ export async function processAskUserQuestion(input, deps) {
|
|
|
142
150
|
message,
|
|
143
151
|
actions,
|
|
144
152
|
requestId,
|
|
153
|
+
...(auth && { auth }),
|
|
145
154
|
});
|
|
146
155
|
if (!sent) return ASK;
|
|
147
156
|
}
|
|
@@ -154,6 +163,7 @@ export async function processAskUserQuestion(input, deps) {
|
|
|
154
163
|
topic: config.topic,
|
|
155
164
|
requestId,
|
|
156
165
|
timeout: config.timeout * 1000,
|
|
166
|
+
...(auth && { auth }),
|
|
157
167
|
});
|
|
158
168
|
} catch (err) {
|
|
159
169
|
console.error("[claude-remote-approver] Response listener failed:", err.message, "— Falling back to CLI.");
|
|
@@ -193,21 +203,24 @@ export async function processAskUserQuestion(input, deps) {
|
|
|
193
203
|
* @param {Function} deps.formatToolInfo
|
|
194
204
|
* @returns {Promise<object>} Decision JSON
|
|
195
205
|
*/
|
|
196
|
-
export async function processHook(input, { loadConfig, sendNotification, waitForResponse, formatToolInfo }) {
|
|
206
|
+
export async function processHook(input, { loadConfig, sendNotification, waitForResponse, formatToolInfo, resolveAuth }) {
|
|
197
207
|
const config = loadConfig();
|
|
198
208
|
|
|
199
209
|
if (!config.topic) {
|
|
200
210
|
return ASK;
|
|
201
211
|
}
|
|
202
212
|
|
|
213
|
+
const auth = resolveAuth ? resolveAuth(config) : null;
|
|
214
|
+
|
|
203
215
|
if (isAskUserQuestion(input)) {
|
|
204
|
-
return processAskUserQuestion(input, { loadConfig, sendNotification, waitForResponse });
|
|
216
|
+
return processAskUserQuestion(input, { loadConfig, sendNotification, waitForResponse, resolveAuth });
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
const requestId = crypto.randomUUID();
|
|
208
220
|
const { title, message } = formatToolInfo(input);
|
|
209
221
|
const actions = buildActions(config.ntfyServer, config.topic, requestId, {
|
|
210
222
|
permissionSuggestions: input.permission_suggestions,
|
|
223
|
+
...(auth && { auth }),
|
|
211
224
|
});
|
|
212
225
|
|
|
213
226
|
const sent = await sendWithRetry(sendNotification, {
|
|
@@ -217,6 +230,7 @@ export async function processHook(input, { loadConfig, sendNotification, waitFor
|
|
|
217
230
|
message,
|
|
218
231
|
actions,
|
|
219
232
|
requestId,
|
|
233
|
+
...(auth && { auth }),
|
|
220
234
|
});
|
|
221
235
|
if (!sent) return ASK;
|
|
222
236
|
|
|
@@ -229,6 +243,7 @@ export async function processHook(input, { loadConfig, sendNotification, waitFor
|
|
|
229
243
|
topic: config.topic,
|
|
230
244
|
requestId,
|
|
231
245
|
timeout,
|
|
246
|
+
...(auth && { auth }),
|
|
232
247
|
});
|
|
233
248
|
} catch (err) {
|
|
234
249
|
console.error("[claude-remote-approver] Response listener failed:", err.message, "— Falling back to CLI.");
|
package/src/ntfy.mjs
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
// src/ntfy.mjs
|
|
2
2
|
|
|
3
|
+
export function buildAuthHeader(auth) {
|
|
4
|
+
if (!auth) return {};
|
|
5
|
+
return { Authorization: `Basic ${Buffer.from(auth.username + ':' + auth.password).toString('base64')}` };
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* Send a push notification via ntfy.
|
|
5
10
|
*
|
|
6
11
|
* @param {{ server: string, topic: string, title: string, message: string, actions: unknown[], requestId: string }} params
|
|
7
12
|
* @returns {Promise<Response>}
|
|
8
13
|
*/
|
|
9
|
-
export async function sendNotification({ server, topic, title, message, actions, requestId }) {
|
|
14
|
+
export async function sendNotification({ server, topic, title, message, actions, requestId, auth }) {
|
|
10
15
|
const baseUrl = server.replace(/\/+$/, '');
|
|
11
16
|
const url = baseUrl;
|
|
12
17
|
|
|
13
18
|
const response = await fetch(url, {
|
|
14
19
|
method: 'POST',
|
|
15
|
-
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
headers: { 'Content-Type': 'application/json', ...buildAuthHeader(auth) },
|
|
16
21
|
body: JSON.stringify({ topic, title, message, actions }),
|
|
17
22
|
});
|
|
18
23
|
|
|
@@ -29,7 +34,7 @@ export async function sendNotification({ server, topic, title, message, actions,
|
|
|
29
34
|
* @param {{ server: string, topic: string, requestId: string, timeout: number }} params
|
|
30
35
|
* @returns {Promise<{ approved: boolean } | { timeout: true } | { error: Error } | { answer: string }>}
|
|
31
36
|
*/
|
|
32
|
-
export async function waitForResponse({ server, topic, requestId, timeout }) {
|
|
37
|
+
export async function waitForResponse({ server, topic, requestId, timeout, auth }) {
|
|
33
38
|
const baseUrl = server.replace(/\/+$/, '');
|
|
34
39
|
const url = `${baseUrl}/${topic}-response/json`;
|
|
35
40
|
|
|
@@ -39,7 +44,8 @@ export async function waitForResponse({ server, topic, requestId, timeout }) {
|
|
|
39
44
|
let timer;
|
|
40
45
|
|
|
41
46
|
try {
|
|
42
|
-
const
|
|
47
|
+
const authHeaders = auth ? buildAuthHeader(auth) : undefined;
|
|
48
|
+
const response = await fetch(url, { signal: controller.signal, ...(authHeaders && { headers: authHeaders }) });
|
|
43
49
|
const reader = response.body.getReader();
|
|
44
50
|
const decoder = new TextDecoder();
|
|
45
51
|
let buffer = '';
|
|
@@ -127,6 +133,7 @@ export function stripMarkdown(text) {
|
|
|
127
133
|
// ---------------------------------------------------------------------------
|
|
128
134
|
|
|
129
135
|
const MAX_INPUT = 10000;
|
|
136
|
+
const MESSAGE_MAX_LENGTH = 1000;
|
|
130
137
|
|
|
131
138
|
/**
|
|
132
139
|
* Count consecutive runs of character ch starting at pos.
|
|
@@ -442,37 +449,32 @@ function stripInline(text) {
|
|
|
442
449
|
* @returns {{ title: string, message: string }}
|
|
443
450
|
*/
|
|
444
451
|
export function formatToolInfo({ hook_event_name, tool_name, tool_input }) {
|
|
445
|
-
|
|
452
|
+
let title;
|
|
453
|
+
let message;
|
|
454
|
+
|
|
446
455
|
if (tool_name === 'ExitPlanMode' && typeof tool_input?.plan === 'string') {
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
456
|
+
title = 'Claude Code: Plan Review';
|
|
457
|
+
const plain = tool_input.plan.trim() ? stripMarkdown(tool_input.plan) : '';
|
|
458
|
+
message = plain || '(empty plan)';
|
|
459
|
+
} else {
|
|
460
|
+
title = `Claude Code: ${tool_name}`;
|
|
461
|
+
switch (tool_name) {
|
|
462
|
+
case 'Bash':
|
|
463
|
+
message = tool_input?.command ?? JSON.stringify(tool_input);
|
|
464
|
+
break;
|
|
465
|
+
case 'Read':
|
|
466
|
+
case 'Write':
|
|
467
|
+
case 'Edit':
|
|
468
|
+
message = tool_input?.file_path ?? JSON.stringify(tool_input);
|
|
469
|
+
break;
|
|
470
|
+
default:
|
|
471
|
+
message = JSON.stringify(tool_input);
|
|
472
|
+
break;
|
|
451
473
|
}
|
|
452
|
-
const raw = tool_input.plan;
|
|
453
|
-
const plain = stripMarkdown(raw);
|
|
454
|
-
const message = plain
|
|
455
|
-
? (plain.length > PLAN_MESSAGE_MAX_LENGTH ? plain.slice(0, PLAN_MESSAGE_MAX_LENGTH) + '...' : plain)
|
|
456
|
-
: '(empty plan)';
|
|
457
|
-
return { title, message };
|
|
458
474
|
}
|
|
459
475
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
switch (tool_name) {
|
|
464
|
-
case 'Bash':
|
|
465
|
-
message = tool_input?.command ?? JSON.stringify(tool_input);
|
|
466
|
-
break;
|
|
467
|
-
case 'Read':
|
|
468
|
-
case 'Write':
|
|
469
|
-
case 'Edit':
|
|
470
|
-
message = tool_input?.file_path ?? JSON.stringify(tool_input);
|
|
471
|
-
break;
|
|
472
|
-
default:
|
|
473
|
-
message = JSON.stringify(tool_input);
|
|
474
|
-
break;
|
|
476
|
+
if (message.length > MESSAGE_MAX_LENGTH) {
|
|
477
|
+
message = message.slice(0, MESSAGE_MAX_LENGTH) + '...';
|
|
475
478
|
}
|
|
476
|
-
|
|
477
479
|
return { title, message };
|
|
478
480
|
}
|
package/src/setup.mjs
CHANGED
|
@@ -105,11 +105,23 @@ export async function runSetup({
|
|
|
105
105
|
generateTopic,
|
|
106
106
|
saveConfig,
|
|
107
107
|
loadConfig,
|
|
108
|
+
prompt,
|
|
109
|
+
promptSecret,
|
|
108
110
|
}) {
|
|
109
111
|
const topic = generateTopic();
|
|
110
112
|
|
|
111
113
|
const config = loadConfig(configPath);
|
|
112
114
|
config.topic = topic;
|
|
115
|
+
|
|
116
|
+
if (prompt) {
|
|
117
|
+
const useAuth = await prompt("Use authenticated topics? (y/n): ");
|
|
118
|
+
if (useAuth?.toLowerCase() === "y") {
|
|
119
|
+
config.ntfyUsername = await prompt("Username: ");
|
|
120
|
+
const promptSecretFn = promptSecret || prompt;
|
|
121
|
+
config.ntfyPassword = await promptSecretFn("Password: ");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
113
125
|
saveConfig(config, configPath);
|
|
114
126
|
|
|
115
127
|
const hookCommand = getHookCommand();
|