ntfyx 0.1.4 → 0.1.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/README.md +31 -25
- package/installer.mjs +38 -18
- package/ntfyx-runtime-entry.mjs +263 -102
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,37 +1,43 @@
|
|
|
1
1
|
# Ntfyx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Send task completion notifications and answer agent questions from your phone.
|
|
4
|
+
Agent and CLI messages are end-to-end encrypted. A Ntfyx phone app and an approved
|
|
5
|
+
phone pairing are required before sending messages or using the MCP tools.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```sh
|
|
10
|
-
npm install -g ntfyx
|
|
11
|
-
ntfyx --help
|
|
12
|
-
ntfyx version --json
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
The npm package downloads the matching official binary from `https://ntfyx.me/releases/0.1.4/` during installation. It verifies the release manifest signature, download size, SHA-256 digest, and binary version before installing. An internet connection and permission to run the package's installation script are required. If installation scripts are disabled, follow the [standalone installation instructions](https://ntfyx.me/install).
|
|
16
|
-
|
|
17
|
-
## Get started
|
|
18
|
-
|
|
19
|
-
Install the Ntfyx phone app, then pair this computer by scanning the QR code and approving it on your phone:
|
|
7
|
+
Requires Node.js 24 or later and macOS (Apple silicon or Intel) or Linux glibc
|
|
8
|
+
(x64 or arm64). Windows and musl Linux are not supported. Installation downloads
|
|
9
|
+
the version-pinned binary from ntfyx.me and verifies its release signature and hash.
|
|
20
10
|
|
|
21
11
|
```sh
|
|
12
|
+
npm install -g ntfyx@0.1.6
|
|
22
13
|
ntfyx connect
|
|
23
|
-
ntfyx send "
|
|
24
|
-
ntfyx ask "Continue?" --allow-deny
|
|
14
|
+
ntfyx send "Task finished"
|
|
15
|
+
ntfyx ask "Continue?" --allow-deny
|
|
25
16
|
```
|
|
26
17
|
|
|
27
|
-
|
|
18
|
+
Scan the connection QR code in Ntfyx and approve the connection on your phone.
|
|
19
|
+
Keep the resulting local credentials on that machine.
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
ntfyx result REQUEST_ID --json
|
|
31
|
-
```
|
|
21
|
+
For an MCP client, first pair using `npx ntfyx@0.1.6 connect`, then configure:
|
|
32
22
|
|
|
33
|
-
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"ntfyx": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "ntfyx@0.1.6", "mcp"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
The MCP server uses local stdio and the local paired identity. It does not offer
|
|
35
|
+
a hosted MCP endpoint. Native tool approval is separate from MCP questions:
|
|
36
|
+
Apple silicon Mac validation covers Claude Code 2.1.303 and Codex 0.163.0 with
|
|
37
|
+
CLI 0.1.5 or later; Intel Mac native approval remains unverified and returns to
|
|
38
|
+
the terminal. Claude permission hooks require explicit `--permissions on` during
|
|
39
|
+
setup. Codex requires interactive TUI and `/hooks` trust; `codex exec` does not
|
|
40
|
+
trigger permission hooks.
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
[Installation and signed downloads](https://ntfyx.me/install) ·
|
|
43
|
+
[Agent setup and limits](https://ntfyx.me/docs/agents)
|
package/installer.mjs
CHANGED
|
@@ -208,7 +208,6 @@ var schemas = {
|
|
|
208
208
|
}
|
|
209
209
|
})
|
|
210
210
|
};
|
|
211
|
-
|
|
212
211
|
// packages/protocol/src/index.ts
|
|
213
212
|
var LIMITS = Object.freeze({
|
|
214
213
|
free_topics: 1,
|
|
@@ -218,6 +217,12 @@ var LIMITS = Object.freeze({
|
|
|
218
217
|
sources_per_inbox: 100,
|
|
219
218
|
browsers: 2,
|
|
220
219
|
messages_per_day: 1000,
|
|
220
|
+
monitor_free_messages_per_day: 100,
|
|
221
|
+
monitor_ten_messages_per_day: 480,
|
|
222
|
+
monitor_unlimited_messages_per_day: 4800,
|
|
223
|
+
monitor_free_bytes: 1 * 1024 * 1024,
|
|
224
|
+
monitor_ten_bytes: 8 * 1024 * 1024,
|
|
225
|
+
monitor_unlimited_bytes: 64 * 1024 * 1024,
|
|
221
226
|
requests_per_day: 200,
|
|
222
227
|
pending: 50,
|
|
223
228
|
payload_bytes: 16384,
|
|
@@ -292,24 +297,39 @@ async function installVerified(options) {
|
|
|
292
297
|
await mkdir(dirname(options.target), { recursive: true });
|
|
293
298
|
const directory = await mkdtemp(join(dirname(options.target), ".ntfyx-install-"));
|
|
294
299
|
try {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
300
|
+
const stalled = new AbortController, chunks = [];
|
|
301
|
+
let length = 0, timer;
|
|
302
|
+
const arm = () => {
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
timer = setTimeout(() => stalled.abort(), 60000);
|
|
305
|
+
};
|
|
306
|
+
try {
|
|
307
|
+
arm();
|
|
308
|
+
const response = await (options.fetcher ?? fetch)(artifact.url, {
|
|
309
|
+
redirect: "error",
|
|
310
|
+
signal: AbortSignal.any([stalled.signal, AbortSignal.timeout(1800000)])
|
|
311
|
+
});
|
|
312
|
+
if (!response.ok)
|
|
313
|
+
throw new NtfyxError("network_error", 503);
|
|
314
|
+
const reader = response.body.getReader();
|
|
315
|
+
while (true) {
|
|
316
|
+
const { done, value } = await reader.read();
|
|
317
|
+
if (done)
|
|
318
|
+
break;
|
|
319
|
+
arm();
|
|
320
|
+
length += value.length;
|
|
321
|
+
if (length > artifact.bytes) {
|
|
322
|
+
await reader.cancel();
|
|
323
|
+
throw new NtfyxError("invalid_signature");
|
|
324
|
+
}
|
|
325
|
+
chunks.push(value);
|
|
311
326
|
}
|
|
312
|
-
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (error instanceof NtfyxError)
|
|
329
|
+
throw error;
|
|
330
|
+
throw new NtfyxError("network_error", 503);
|
|
331
|
+
} finally {
|
|
332
|
+
clearTimeout(timer);
|
|
313
333
|
}
|
|
314
334
|
const binary = Buffer.concat(chunks);
|
|
315
335
|
if (length !== artifact.bytes || createHash("sha256").update(binary).digest("hex") !== artifact.sha256)
|
package/ntfyx-runtime-entry.mjs
CHANGED
|
@@ -3285,6 +3285,11 @@ var init_schemas = __esm(() => {
|
|
|
3285
3285
|
};
|
|
3286
3286
|
});
|
|
3287
3287
|
|
|
3288
|
+
// packages/protocol/src/http-send-url.ts
|
|
3289
|
+
var init_http_send_url = __esm(() => {
|
|
3290
|
+
init_src();
|
|
3291
|
+
});
|
|
3292
|
+
|
|
3288
3293
|
// packages/protocol/src/index.ts
|
|
3289
3294
|
function validate(name, value) {
|
|
3290
3295
|
if (!exports_validators[name]?.(value))
|
|
@@ -3333,6 +3338,7 @@ var LIMITS, NtfyxError, EXIT_CODES;
|
|
|
3333
3338
|
var init_src = __esm(() => {
|
|
3334
3339
|
init_validators();
|
|
3335
3340
|
init_schemas();
|
|
3341
|
+
init_http_send_url();
|
|
3336
3342
|
LIMITS = Object.freeze({
|
|
3337
3343
|
free_topics: 1,
|
|
3338
3344
|
plus_topics: 20,
|
|
@@ -3341,6 +3347,12 @@ var init_src = __esm(() => {
|
|
|
3341
3347
|
sources_per_inbox: 100,
|
|
3342
3348
|
browsers: 2,
|
|
3343
3349
|
messages_per_day: 1000,
|
|
3350
|
+
monitor_free_messages_per_day: 100,
|
|
3351
|
+
monitor_ten_messages_per_day: 480,
|
|
3352
|
+
monitor_unlimited_messages_per_day: 4800,
|
|
3353
|
+
monitor_free_bytes: 1 * 1024 * 1024,
|
|
3354
|
+
monitor_ten_bytes: 8 * 1024 * 1024,
|
|
3355
|
+
monitor_unlimited_bytes: 64 * 1024 * 1024,
|
|
3344
3356
|
requests_per_day: 200,
|
|
3345
3357
|
pending: 50,
|
|
3346
3358
|
payload_bytes: 16384,
|
|
@@ -14239,24 +14251,39 @@ async function installVerified(options) {
|
|
|
14239
14251
|
await mkdir6(dirname4(options.target), { recursive: true });
|
|
14240
14252
|
const directory = await mkdtemp2(join8(dirname4(options.target), ".ntfyx-install-"));
|
|
14241
14253
|
try {
|
|
14242
|
-
const
|
|
14243
|
-
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
|
|
14249
|
-
|
|
14250
|
-
|
|
14251
|
-
|
|
14252
|
-
|
|
14253
|
-
|
|
14254
|
-
|
|
14255
|
-
|
|
14256
|
-
|
|
14257
|
-
|
|
14254
|
+
const stalled = new AbortController, chunks = [];
|
|
14255
|
+
let length = 0, timer;
|
|
14256
|
+
const arm = () => {
|
|
14257
|
+
clearTimeout(timer);
|
|
14258
|
+
timer = setTimeout(() => stalled.abort(), 60000);
|
|
14259
|
+
};
|
|
14260
|
+
try {
|
|
14261
|
+
arm();
|
|
14262
|
+
const response = await (options.fetcher ?? fetch)(artifact.url, {
|
|
14263
|
+
redirect: "error",
|
|
14264
|
+
signal: AbortSignal.any([stalled.signal, AbortSignal.timeout(1800000)])
|
|
14265
|
+
});
|
|
14266
|
+
if (!response.ok)
|
|
14267
|
+
throw new NtfyxError("network_error", 503);
|
|
14268
|
+
const reader = response.body.getReader();
|
|
14269
|
+
while (true) {
|
|
14270
|
+
const { done, value } = await reader.read();
|
|
14271
|
+
if (done)
|
|
14272
|
+
break;
|
|
14273
|
+
arm();
|
|
14274
|
+
length += value.length;
|
|
14275
|
+
if (length > artifact.bytes) {
|
|
14276
|
+
await reader.cancel();
|
|
14277
|
+
throw new NtfyxError("invalid_signature");
|
|
14278
|
+
}
|
|
14279
|
+
chunks.push(value);
|
|
14258
14280
|
}
|
|
14259
|
-
|
|
14281
|
+
} catch (error) {
|
|
14282
|
+
if (error instanceof NtfyxError)
|
|
14283
|
+
throw error;
|
|
14284
|
+
throw new NtfyxError("network_error", 503);
|
|
14285
|
+
} finally {
|
|
14286
|
+
clearTimeout(timer);
|
|
14260
14287
|
}
|
|
14261
14288
|
const binary = Buffer.concat(chunks);
|
|
14262
14289
|
if (length !== artifact.bytes || createHash("sha256").update(binary).digest("hex") !== artifact.sha256)
|
|
@@ -14315,15 +14342,23 @@ async function upgrade() {
|
|
|
14315
14342
|
if (!RELEASE_PUBLIC_KEY)
|
|
14316
14343
|
throw new NtfyxError("release_unavailable", 503, "Production release signing key is not configured");
|
|
14317
14344
|
const origin = "https://ntfyx.me/releases/stable";
|
|
14318
|
-
|
|
14319
|
-
|
|
14320
|
-
|
|
14321
|
-
|
|
14322
|
-
|
|
14345
|
+
let raw, signature;
|
|
14346
|
+
try {
|
|
14347
|
+
const [m, s] = await Promise.all([
|
|
14348
|
+
fetch(origin + "/manifest.json", { redirect: "error", signal: AbortSignal.timeout(30000) }),
|
|
14349
|
+
fetch(origin + "/manifest.sig", { redirect: "error", signal: AbortSignal.timeout(30000) })
|
|
14350
|
+
]);
|
|
14351
|
+
if (!m.ok || !s.ok)
|
|
14352
|
+
throw new NtfyxError("network_error", 503);
|
|
14353
|
+
[raw, signature] = [await m.text(), (await s.text()).trim()];
|
|
14354
|
+
} catch (error) {
|
|
14355
|
+
if (error instanceof NtfyxError)
|
|
14356
|
+
throw error;
|
|
14323
14357
|
throw new NtfyxError("network_error", 503);
|
|
14358
|
+
}
|
|
14324
14359
|
return installVerified({
|
|
14325
|
-
raw
|
|
14326
|
-
signature
|
|
14360
|
+
raw,
|
|
14361
|
+
signature,
|
|
14327
14362
|
publicKey: RELEASE_PUBLIC_KEY,
|
|
14328
14363
|
target: process.execPath,
|
|
14329
14364
|
platform: process.platform + "-" + process.arch
|
|
@@ -14339,6 +14374,34 @@ var init_upgrade = __esm(() => {
|
|
|
14339
14374
|
init_installer();
|
|
14340
14375
|
});
|
|
14341
14376
|
|
|
14377
|
+
// packages/adapters/src/lifecycle.ts
|
|
14378
|
+
function hookTransaction(db, operation) {
|
|
14379
|
+
db.exec("BEGIN IMMEDIATE");
|
|
14380
|
+
try {
|
|
14381
|
+
const result = operation();
|
|
14382
|
+
db.exec("COMMIT");
|
|
14383
|
+
return result;
|
|
14384
|
+
} catch (error) {
|
|
14385
|
+
db.exec("ROLLBACK");
|
|
14386
|
+
throw error;
|
|
14387
|
+
}
|
|
14388
|
+
}
|
|
14389
|
+
function ensureHookEpoch(db) {
|
|
14390
|
+
hookTransaction(db, () => {
|
|
14391
|
+
const columns = db.prepare("PRAGMA table_info(hook_sessions)").all();
|
|
14392
|
+
if (!columns.some((column) => column.name === "epoch"))
|
|
14393
|
+
db.exec("ALTER TABLE hook_sessions ADD COLUMN epoch INTEGER NOT NULL DEFAULT 0");
|
|
14394
|
+
});
|
|
14395
|
+
}
|
|
14396
|
+
function hookEpoch(db, session) {
|
|
14397
|
+
const row = db.prepare("SELECT epoch FROM hook_sessions WHERE session_hash=?").get(session);
|
|
14398
|
+
return row.epoch;
|
|
14399
|
+
}
|
|
14400
|
+
function sameHookEpoch(db, session, epoch) {
|
|
14401
|
+
const row = db.prepare("SELECT closed,epoch FROM hook_sessions WHERE session_hash=?").get(session);
|
|
14402
|
+
return !!row && !row.closed && row.epoch === epoch;
|
|
14403
|
+
}
|
|
14404
|
+
|
|
14342
14405
|
// packages/client-core/src/stream.ts
|
|
14343
14406
|
class SyncHints {
|
|
14344
14407
|
socket;
|
|
@@ -16807,9 +16870,18 @@ async function api(origin, path, token, body, method, signal, inbox) {
|
|
|
16807
16870
|
}
|
|
16808
16871
|
async function createPairing(origin, role, intent, options = {}) {
|
|
16809
16872
|
assertOrigin(origin);
|
|
16873
|
+
if (options.identity) {
|
|
16874
|
+
if (role !== "browser" || intent !== "web-connect" || options.extractable)
|
|
16875
|
+
throw new NtfyxError("invalid_binding");
|
|
16876
|
+
const { signing, hpke, principal_id, key_id } = options.identity;
|
|
16877
|
+
if (signing?.privateKey?.extractable || hpke?.privateKey?.extractable)
|
|
16878
|
+
throw new NtfyxError("private_key_exportable");
|
|
16879
|
+
if (!/^[A-Za-z0-9_-]{22,80}$/.test(principal_id) || !/^[A-Za-z0-9_-]{22,80}$/.test(key_id) || signing?.privateKey?.type !== "private" || hpke?.privateKey?.type !== "private" || signing?.publicKey?.type !== "public" || hpke?.publicKey?.type !== "public" || signing.privateKey.algorithm.name !== "ECDSA" || hpke.privateKey.algorithm.name !== "ECDH" || signing.privateKey.algorithm.namedCurve !== "P-256" || hpke.privateKey.algorithm.namedCurve !== "P-256" || !signing.privateKey.usages.includes("sign") || !hpke.privateKey.usages.includes("deriveBits"))
|
|
16880
|
+
throw new NtfyxError("invalid_identity");
|
|
16881
|
+
}
|
|
16810
16882
|
const config = await api(origin, "/service-config");
|
|
16811
16883
|
const server_offset = config.server_time - Date.now();
|
|
16812
|
-
const identity = await newIdentity(options.extractable ?? false);
|
|
16884
|
+
const identity = options.identity ?? await newIdentity(options.extractable ?? false);
|
|
16813
16885
|
const pub = await publicIdentity(identity);
|
|
16814
16886
|
const ephemeral = await suite.kem.generateKeyPair();
|
|
16815
16887
|
const qr = {
|
|
@@ -17454,7 +17526,7 @@ function hostAnswer(q, answer) {
|
|
|
17454
17526
|
}
|
|
17455
17527
|
return value;
|
|
17456
17528
|
}
|
|
17457
|
-
async function handleClaudeQuestion(store, raw) {
|
|
17529
|
+
async function handleClaudeQuestion(store, raw, topic) {
|
|
17458
17530
|
let ctx;
|
|
17459
17531
|
let pending;
|
|
17460
17532
|
let invocation;
|
|
@@ -17487,14 +17559,16 @@ async function handleClaudeQuestion(store, raw) {
|
|
|
17487
17559
|
if (Buffer.byteLength(JSON.stringify(candidate)) > LIMITS.payload_bytes)
|
|
17488
17560
|
return;
|
|
17489
17561
|
}
|
|
17490
|
-
ctx = await openClient(store);
|
|
17562
|
+
ctx = await openClient(store, topic);
|
|
17491
17563
|
const db = ctx.ledger.db, session = await hash(data.session_id);
|
|
17492
17564
|
db.exec("CREATE TABLE IF NOT EXISTS hook_sessions(session_hash TEXT PRIMARY KEY,closed INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS hook_invocations(invocation_id TEXT PRIMARY KEY,session_hash TEXT NOT NULL,request_id TEXT NOT NULL,pid INTEGER NOT NULL,parent_pid INTEGER NOT NULL,state TEXT NOT NULL); CREATE TABLE IF NOT EXISTS question_batches(invocation_id TEXT PRIMARY KEY);");
|
|
17565
|
+
ensureHookEpoch(db);
|
|
17493
17566
|
db.prepare("INSERT OR IGNORE INTO hook_sessions(session_hash,closed) VALUES(?,0)").run(session);
|
|
17494
|
-
const
|
|
17567
|
+
const epoch = hookEpoch(db, session);
|
|
17568
|
+
const batch = await hash(data.session_id + "\x00" + data.tool_use_id + "\x00" + epoch);
|
|
17495
17569
|
if (!db.prepare("INSERT OR IGNORE INTO question_batches VALUES(?)").run(batch).changes)
|
|
17496
17570
|
return;
|
|
17497
|
-
const active = () => alive() &&
|
|
17571
|
+
const active = () => alive() && sameHookEpoch(db, session, epoch) && (!invocation || db.prepare("SELECT state FROM hook_invocations WHERE invocation_id=?").get(invocation)?.state === "waiting");
|
|
17498
17572
|
monitor = setInterval(() => {
|
|
17499
17573
|
if (!active())
|
|
17500
17574
|
stop();
|
|
@@ -17517,7 +17591,9 @@ async function handleClaudeQuestion(store, raw) {
|
|
|
17517
17591
|
if (value === null)
|
|
17518
17592
|
return;
|
|
17519
17593
|
answers[questions[i].question] = value;
|
|
17520
|
-
db.prepare("UPDATE hook_invocations SET state='delivered' WHERE invocation_id=?").run(invocation);
|
|
17594
|
+
const delivered = db.prepare("UPDATE hook_invocations SET state='delivered' WHERE invocation_id=? AND state='waiting'").run(invocation);
|
|
17595
|
+
if (!delivered.changes || !sameHookEpoch(db, session, epoch))
|
|
17596
|
+
return;
|
|
17521
17597
|
pending = undefined;
|
|
17522
17598
|
invocation = undefined;
|
|
17523
17599
|
}
|
|
@@ -17900,7 +17976,7 @@ var MCP_TOOLS = [
|
|
|
17900
17976
|
}));
|
|
17901
17977
|
var inputValidator = new import_ajv.default({ strict: false });
|
|
17902
17978
|
var inputSchemas = Object.fromEntries(MCP_TOOLS.map((t) => [t.name, inputValidator.compile(t.inputSchema)]));
|
|
17903
|
-
async function runMcp(store) {
|
|
17979
|
+
async function runMcp(store, defaultTopic) {
|
|
17904
17980
|
const active = new Map;
|
|
17905
17981
|
const output = (v) => process.stdout.write(JSON.stringify(v) + `
|
|
17906
17982
|
`);
|
|
@@ -17934,7 +18010,7 @@ async function runMcp(store) {
|
|
|
17934
18010
|
protocolVersion: "2025-11-25",
|
|
17935
18011
|
capabilities: { tools: {} },
|
|
17936
18012
|
instructions: "Use ntfyx_ask for user questions, choices and missing information when phone replies are enabled. Read pending requests with ntfyx_result(consume=true); continue only after an answered result with claim_acquired. Keep one session_id for the current host session. Respect the user\u2019s channel choice and existing authorization; do not ask again for an already authorized step.",
|
|
17937
|
-
serverInfo: { name: "ntfyx", version: "0.1.
|
|
18013
|
+
serverInfo: { name: "ntfyx", version: "0.1.6" }
|
|
17938
18014
|
});
|
|
17939
18015
|
return;
|
|
17940
18016
|
}
|
|
@@ -17965,7 +18041,7 @@ async function runMcp(store) {
|
|
|
17965
18041
|
const seconds = a.wait_seconds ?? 30;
|
|
17966
18042
|
if (!Number.isInteger(seconds) || seconds < 0 || seconds > 45)
|
|
17967
18043
|
throw new NtfyxError("invalid_duration");
|
|
17968
|
-
context = await openClient(store, a.topic);
|
|
18044
|
+
context = await openClient(store, a.topic ?? defaultTopic);
|
|
17969
18045
|
const { client, ledger } = context;
|
|
17970
18046
|
const sessionId = a.session_id ?? process.env.NTFYX_SESSION_ID;
|
|
17971
18047
|
const task = ["ntfyx_send", "ntfyx_ask"].includes(name) ? await sessionTask(sessionId, a.session_name ?? process.env.NTFYX_SESSION_NAME, a.task) : undefined;
|
|
@@ -18148,10 +18224,12 @@ var compatibility_default = {
|
|
|
18148
18224
|
min_version: "2.1.283",
|
|
18149
18225
|
verified_versions: [
|
|
18150
18226
|
"2.1.283",
|
|
18151
|
-
"2.1.284"
|
|
18227
|
+
"2.1.284",
|
|
18228
|
+
"2.1.303"
|
|
18152
18229
|
],
|
|
18153
18230
|
os: [
|
|
18154
|
-
"linux-x64"
|
|
18231
|
+
"linux-x64",
|
|
18232
|
+
"darwin-arm64"
|
|
18155
18233
|
],
|
|
18156
18234
|
tools: [
|
|
18157
18235
|
"Bash",
|
|
@@ -18167,21 +18245,24 @@ var compatibility_default = {
|
|
|
18167
18245
|
unsupported: [
|
|
18168
18246
|
"Interactive terminal-choice precedence and terminal UI takeover require a separate real interactive acceptance run."
|
|
18169
18247
|
],
|
|
18170
|
-
tested_at: "2026-
|
|
18248
|
+
tested_at: "2026-10-19",
|
|
18171
18249
|
evidence: [
|
|
18172
18250
|
"docs/native-agent-evidence.json",
|
|
18173
18251
|
"docs/native-agent-regression-evidence.json",
|
|
18174
|
-
"docs/evidence/native-hosts/claude-2.1.284-basic.json"
|
|
18252
|
+
"docs/evidence/native-hosts/claude-2.1.284-basic.json",
|
|
18253
|
+
"docs/evidence/native-hosts/claude-2.1.303-darwin-arm64.json"
|
|
18175
18254
|
]
|
|
18176
18255
|
},
|
|
18177
18256
|
{
|
|
18178
18257
|
host: "codex",
|
|
18179
18258
|
min_version: "0.158.0",
|
|
18180
18259
|
verified_versions: [
|
|
18181
|
-
"0.158.0"
|
|
18260
|
+
"0.158.0",
|
|
18261
|
+
"0.163.0"
|
|
18182
18262
|
],
|
|
18183
18263
|
os: [
|
|
18184
|
-
"linux-x64"
|
|
18264
|
+
"linux-x64",
|
|
18265
|
+
"darwin-arm64"
|
|
18185
18266
|
],
|
|
18186
18267
|
tools: [
|
|
18187
18268
|
"Bash",
|
|
@@ -18198,9 +18279,10 @@ var compatibility_default = {
|
|
|
18198
18279
|
"codex exec disables approvals, so PermissionRequest hooks never run there.",
|
|
18199
18280
|
"Codex requires the user to review and trust hooks with /hooks before they run."
|
|
18200
18281
|
],
|
|
18201
|
-
tested_at: "2026-
|
|
18282
|
+
tested_at: "2026-10-19",
|
|
18202
18283
|
evidence: [
|
|
18203
|
-
"docs/evidence/native-hosts/codex-0.158.0-basic.json"
|
|
18284
|
+
"docs/evidence/native-hosts/codex-0.158.0-basic.json",
|
|
18285
|
+
"docs/evidence/native-hosts/codex-0.163.0-darwin-arm64.json"
|
|
18204
18286
|
]
|
|
18205
18287
|
}
|
|
18206
18288
|
],
|
|
@@ -18290,6 +18372,7 @@ function permissionContext(host, data) {
|
|
|
18290
18372
|
// packages/adapters/src/claude.ts
|
|
18291
18373
|
var quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
18292
18374
|
var CLAUDE_HOOKS = [
|
|
18375
|
+
["SessionStart", "session-start", ""],
|
|
18293
18376
|
["PreToolUse", "question", "AskUserQuestion"],
|
|
18294
18377
|
["PermissionRequest", "permission", "Bash|Edit|Write"],
|
|
18295
18378
|
["Notification", "notification", "permission_prompt|elicitation_dialog"],
|
|
@@ -18306,7 +18389,12 @@ function mergeHooks(settings, executable, remove = false, spec = CLAUDE_HOOKS, s
|
|
|
18306
18389
|
const entries = updated.hooks[event] ?? [];
|
|
18307
18390
|
if (!Array.isArray(entries))
|
|
18308
18391
|
throw new NtfyxError("local_store");
|
|
18309
|
-
updated.hooks[event] = entries.
|
|
18392
|
+
updated.hooks[event] = entries.map((entry) => {
|
|
18393
|
+
if (!entry || !Array.isArray(entry.hooks))
|
|
18394
|
+
return entry;
|
|
18395
|
+
const hooks = entry.hooks.filter((hook) => !commands.includes(hook?.command));
|
|
18396
|
+
return hooks.length ? { ...entry, hooks } : undefined;
|
|
18397
|
+
}).filter((entry) => entry !== undefined);
|
|
18310
18398
|
if (!remove)
|
|
18311
18399
|
updated.hooks[event].push({
|
|
18312
18400
|
...matcher ? { matcher } : {},
|
|
@@ -18349,8 +18437,29 @@ async function setupClaude(options) {
|
|
|
18349
18437
|
if (e.code !== "ENOENT")
|
|
18350
18438
|
throw new NtfyxError("local_store", 400, "Cannot parse Claude settings");
|
|
18351
18439
|
}
|
|
18352
|
-
const suffix = options.config ? " --config " + quote(options.config) : "";
|
|
18353
|
-
const
|
|
18440
|
+
const suffix = (options.config ? " --config " + quote(options.config) : "") + (options.topic ? " --topic " + quote(options.topic) : "");
|
|
18441
|
+
const permissions = options.permissions ?? false;
|
|
18442
|
+
const questions = options.questions ?? true;
|
|
18443
|
+
const notifications = options.notifications ?? true;
|
|
18444
|
+
const lifecycle = permissions || questions;
|
|
18445
|
+
const selected = CLAUDE_HOOKS.filter(([event]) => {
|
|
18446
|
+
if (event === "PermissionRequest")
|
|
18447
|
+
return permissions;
|
|
18448
|
+
if (event === "PreToolUse")
|
|
18449
|
+
return questions;
|
|
18450
|
+
if (event === "SessionStart" || event === "SessionEnd")
|
|
18451
|
+
return lifecycle;
|
|
18452
|
+
if (event === "Stop")
|
|
18453
|
+
return notifications || lifecycle;
|
|
18454
|
+
return notifications;
|
|
18455
|
+
}).map(([event, hook, matcher]) => [
|
|
18456
|
+
event,
|
|
18457
|
+
event === "Stop" && !notifications ? "stop-cleanup" : hook,
|
|
18458
|
+
matcher
|
|
18459
|
+
]);
|
|
18460
|
+
const managed = [...CLAUDE_HOOKS, ["Stop", "stop-cleanup", ""]];
|
|
18461
|
+
const cleaned = mergeHooks(settings, options.executable, true, managed, suffix);
|
|
18462
|
+
const updated = options.remove ? cleaned : mergeHooks(cleaned, options.executable, false, selected, suffix);
|
|
18354
18463
|
const mcpPath = options.directory ? join5(directory, "mcp-test.json") : join5(homedir2(), ".claude.json");
|
|
18355
18464
|
let mcp = {};
|
|
18356
18465
|
try {
|
|
@@ -18360,7 +18469,7 @@ async function setupClaude(options) {
|
|
|
18360
18469
|
throw new NtfyxError("local_store");
|
|
18361
18470
|
}
|
|
18362
18471
|
mcp.mcpServers ??= {};
|
|
18363
|
-
const owned = { command: options.executable, args: ["mcp", ...options.config ? ["--config", options.config] : []] };
|
|
18472
|
+
const owned = { command: options.executable, args: ["mcp", ...options.config ? ["--config", options.config] : [], ...options.topic ? ["--topic", options.topic] : []] };
|
|
18364
18473
|
if (mcp.mcpServers.ntfyx && JSON.stringify(mcp.mcpServers.ntfyx) !== JSON.stringify(owned))
|
|
18365
18474
|
throw new NtfyxError("local_store", 409, "Existing ntfyx MCP entry is not managed by this executable");
|
|
18366
18475
|
if (options.remove)
|
|
@@ -18372,8 +18481,9 @@ async function setupClaude(options) {
|
|
|
18372
18481
|
return {
|
|
18373
18482
|
status: "dry_run",
|
|
18374
18483
|
action: options.remove ? "remove" : "merge",
|
|
18375
|
-
managed_hooks: mergeHooks({}, options.executable, false,
|
|
18484
|
+
managed_hooks: mergeHooks({}, options.executable, false, selected, suffix).hooks,
|
|
18376
18485
|
managed_mcp: { ntfyx: owned },
|
|
18486
|
+
components: { permissions, questions, notifications },
|
|
18377
18487
|
preserves_existing_permissions: true,
|
|
18378
18488
|
preserves_unrelated_hooks: true,
|
|
18379
18489
|
preserves_unrelated_mcp: true
|
|
@@ -18387,10 +18497,11 @@ async function setupClaude(options) {
|
|
|
18387
18497
|
await installGuidance(directory, options.remove);
|
|
18388
18498
|
return {
|
|
18389
18499
|
status: options.remove ? "removed" : "configured",
|
|
18390
|
-
|
|
18500
|
+
components: { permissions, questions, notifications },
|
|
18501
|
+
native_approval: permissions ? "version_and_platform_gated" : "disabled"
|
|
18391
18502
|
};
|
|
18392
18503
|
}
|
|
18393
|
-
async function handleHook(store, event, input, host = "claude-code") {
|
|
18504
|
+
async function handleHook(store, event, input, host = "claude-code", topic) {
|
|
18394
18505
|
const label = HOST_LABEL[host];
|
|
18395
18506
|
let ctx;
|
|
18396
18507
|
let request_id;
|
|
@@ -18418,13 +18529,30 @@ async function handleHook(store, event, input, host = "claude-code") {
|
|
|
18418
18529
|
if (typeof data.session_id !== "string" || !data.session_id)
|
|
18419
18530
|
return;
|
|
18420
18531
|
const task = await sessionTask(data.session_id, label + " \xB7 " + data.session_id.slice(0, 8));
|
|
18421
|
-
ctx = await openClient(store);
|
|
18532
|
+
ctx = await openClient(store, topic);
|
|
18422
18533
|
const db = ctx.ledger.db, session_hash = await hash(data.session_id);
|
|
18423
18534
|
db.exec("CREATE TABLE IF NOT EXISTS hook_sessions(session_hash TEXT PRIMARY KEY,closed INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS hook_invocations(invocation_id TEXT PRIMARY KEY,session_hash TEXT NOT NULL,request_id TEXT NOT NULL,pid INTEGER NOT NULL,parent_pid INTEGER NOT NULL,state TEXT NOT NULL);");
|
|
18535
|
+
ensureHookEpoch(db);
|
|
18424
18536
|
db.prepare("INSERT OR IGNORE INTO hook_sessions(session_hash,closed) VALUES(?,0)").run(session_hash);
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18537
|
+
const epoch = hookEpoch(db, session_hash);
|
|
18538
|
+
if (event === "session-start") {
|
|
18539
|
+
const prior = hookTransaction(db, () => {
|
|
18540
|
+
const waits = db.prepare("SELECT request_id FROM hook_invocations WHERE session_hash=? AND state='waiting'").all(session_hash);
|
|
18541
|
+
db.prepare("UPDATE hook_invocations SET state='abandoned' WHERE session_hash=? AND state='waiting'").run(session_hash);
|
|
18542
|
+
db.prepare("UPDATE hook_sessions SET closed=0,epoch=epoch+1 WHERE session_hash=?").run(session_hash);
|
|
18543
|
+
return waits;
|
|
18544
|
+
});
|
|
18545
|
+
for (const old of prior) {
|
|
18546
|
+
ctx.ledger.abandon(String(old.request_id));
|
|
18547
|
+
await Promise.race([
|
|
18548
|
+
ctx.client.cancel(String(old.request_id), "terminal_takeover").catch(() => {}),
|
|
18549
|
+
new Promise((resolve) => setTimeout(resolve, 1000))
|
|
18550
|
+
]);
|
|
18551
|
+
}
|
|
18552
|
+
return;
|
|
18553
|
+
}
|
|
18554
|
+
if (event === "session-end" || event === "stop" || event === "stop-cleanup") {
|
|
18555
|
+
db.prepare(event === "session-end" ? "UPDATE hook_sessions SET epoch=epoch+1,closed=1 WHERE session_hash=?" : "UPDATE hook_sessions SET epoch=epoch+1 WHERE session_hash=?").run(session_hash);
|
|
18428
18556
|
const pending = db.prepare("SELECT request_id FROM hook_invocations WHERE session_hash=? AND state='waiting'").all(session_hash);
|
|
18429
18557
|
db.prepare("UPDATE hook_invocations SET state='abandoned' WHERE session_hash=? AND state='waiting'").run(session_hash);
|
|
18430
18558
|
for (const p of pending) {
|
|
@@ -18434,7 +18562,7 @@ async function handleHook(store, event, input, host = "claude-code") {
|
|
|
18434
18562
|
new Promise((r) => setTimeout(r, 1000))
|
|
18435
18563
|
]);
|
|
18436
18564
|
}
|
|
18437
|
-
if (event === "session-end")
|
|
18565
|
+
if (event === "session-end" || event === "stop-cleanup")
|
|
18438
18566
|
return;
|
|
18439
18567
|
}
|
|
18440
18568
|
if (event !== "permission") {
|
|
@@ -18490,7 +18618,7 @@ async function handleHook(store, event, input, host = "claude-code") {
|
|
|
18490
18618
|
}
|
|
18491
18619
|
const q = validateQuestion(question);
|
|
18492
18620
|
db.prepare("INSERT INTO hook_invocations VALUES(?,?,?,?,?,'waiting')").run(invocation_id, session_hash, request_id, process.pid, initialParent);
|
|
18493
|
-
const active = () => parentAlive() && !abort.signal.aborted &&
|
|
18621
|
+
const active = () => parentAlive() && !abort.signal.aborted && sameHookEpoch(db, session_hash, epoch) && db.prepare("SELECT state FROM hook_invocations WHERE invocation_id=?").get(invocation_id)?.state === "waiting";
|
|
18494
18622
|
monitor = setInterval(() => {
|
|
18495
18623
|
if (!active())
|
|
18496
18624
|
interrupted();
|
|
@@ -18502,8 +18630,8 @@ async function handleHook(store, event, input, host = "claude-code") {
|
|
|
18502
18630
|
const decision = answer.response.value;
|
|
18503
18631
|
if (decision !== "allow" && decision !== "deny")
|
|
18504
18632
|
return;
|
|
18505
|
-
db.prepare("UPDATE hook_invocations SET state='delivered' WHERE invocation_id=? AND state='waiting'").run(invocation_id);
|
|
18506
|
-
if (!parentAlive() || abort.signal.aborted)
|
|
18633
|
+
const deliveredRow = db.prepare("UPDATE hook_invocations SET state='delivered' WHERE invocation_id=? AND state='waiting'").run(invocation_id);
|
|
18634
|
+
if (!deliveredRow.changes || !parentAlive() || abort.signal.aborted || !sameHookEpoch(db, session_hash, epoch))
|
|
18507
18635
|
return;
|
|
18508
18636
|
process.stdout.write(JSON.stringify({
|
|
18509
18637
|
hookSpecificOutput: {
|
|
@@ -18564,8 +18692,9 @@ async function setupCodex(options) {
|
|
|
18564
18692
|
throw new NtfyxError("local_store", 400, "Cannot parse Codex hooks.json");
|
|
18565
18693
|
}
|
|
18566
18694
|
const suffix = CODEX_SUFFIX + (options.config ? " --config " + "'" + options.config.replace(/'/g, "'\\''") + "'" : "");
|
|
18567
|
-
const
|
|
18568
|
-
const
|
|
18695
|
+
const topicSuffix = options.topic ? " --topic " + "'" + options.topic.replace(/'/g, "'\\''") + "'" : "";
|
|
18696
|
+
const updated = mergeHooks(hooks, options.executable, options.remove, CODEX_HOOKS, suffix + topicSuffix);
|
|
18697
|
+
const owned = { command: options.executable, args: ["mcp", ...options.config ? ["--config", options.config] : [], ...options.topic ? ["--topic", options.topic] : []] };
|
|
18569
18698
|
let existing;
|
|
18570
18699
|
try {
|
|
18571
18700
|
existing = JSON.parse(codex(["mcp", "get", "ntfyx", "--json"]));
|
|
@@ -18582,7 +18711,7 @@ async function setupCodex(options) {
|
|
|
18582
18711
|
return {
|
|
18583
18712
|
status: "dry_run",
|
|
18584
18713
|
action: options.remove ? "remove" : "merge",
|
|
18585
|
-
managed_hooks: mergeHooks({}, options.executable, false, CODEX_HOOKS, suffix).hooks,
|
|
18714
|
+
managed_hooks: mergeHooks({}, options.executable, false, CODEX_HOOKS, suffix + topicSuffix).hooks,
|
|
18586
18715
|
managed_mcp: { ntfyx: owned },
|
|
18587
18716
|
preserves_unrelated_hooks: true,
|
|
18588
18717
|
hook_trust: "Review and trust the ntfyx hooks in Codex with /hooks before they run."
|
|
@@ -18947,46 +19076,62 @@ function codexPath() {
|
|
|
18947
19076
|
}
|
|
18948
19077
|
}
|
|
18949
19078
|
async function main(args = process.argv.slice(2)) {
|
|
18950
|
-
const
|
|
19079
|
+
const options = {
|
|
19080
|
+
json: bool,
|
|
19081
|
+
"no-input": bool,
|
|
19082
|
+
new: bool,
|
|
19083
|
+
stdin: bool,
|
|
19084
|
+
"allow-deny": bool,
|
|
19085
|
+
text: bool,
|
|
19086
|
+
"allow-text": bool,
|
|
19087
|
+
async: bool,
|
|
19088
|
+
"keep-pending": bool,
|
|
19089
|
+
claim: bool,
|
|
19090
|
+
"dry-run": bool,
|
|
19091
|
+
remove: bool,
|
|
19092
|
+
permissions: string,
|
|
19093
|
+
questions: string,
|
|
19094
|
+
notifications: string,
|
|
19095
|
+
roundtrip: bool,
|
|
19096
|
+
topic: string,
|
|
19097
|
+
title: string,
|
|
19098
|
+
task: string,
|
|
19099
|
+
"session-id": string,
|
|
19100
|
+
sessionid: string,
|
|
19101
|
+
"session-name": string,
|
|
19102
|
+
priority: string,
|
|
19103
|
+
"dedupe-key": string,
|
|
19104
|
+
expires: string,
|
|
19105
|
+
wait: string,
|
|
19106
|
+
config: string,
|
|
19107
|
+
resume: string,
|
|
19108
|
+
qr: string,
|
|
19109
|
+
server: string,
|
|
19110
|
+
host: string,
|
|
19111
|
+
codex: string,
|
|
19112
|
+
interval: string,
|
|
19113
|
+
once: bool,
|
|
19114
|
+
choice: { type: "string", multiple: true },
|
|
19115
|
+
help: bool
|
|
19116
|
+
};
|
|
19117
|
+
let commandIndex = 0;
|
|
19118
|
+
while (commandIndex < args.length && args[commandIndex].startsWith("--")) {
|
|
19119
|
+
const token = args[commandIndex];
|
|
19120
|
+
if (token === "--")
|
|
19121
|
+
break;
|
|
19122
|
+
const name = token.slice(2).split("=", 1)[0];
|
|
19123
|
+
const option = options[name];
|
|
19124
|
+
if (!option)
|
|
19125
|
+
break;
|
|
19126
|
+
commandIndex += option.type === "string" && !token.includes("=") ? 2 : 1;
|
|
19127
|
+
}
|
|
19128
|
+
const requestIndex = commandIndex + 1;
|
|
19129
|
+
const leadingRequestId = ["result", "cancel"].includes(args[commandIndex] ?? "") && /^-[A-Za-z0-9_-]{21,79}$/.test(args[requestIndex] ?? "") ? args[requestIndex] : undefined;
|
|
18951
19130
|
const parsed = parseArgs({
|
|
18952
|
-
args: leadingRequestId ?
|
|
19131
|
+
args: leadingRequestId ? args.filter((_, index) => index !== requestIndex) : args,
|
|
18953
19132
|
allowPositionals: true,
|
|
18954
19133
|
strict: true,
|
|
18955
|
-
options
|
|
18956
|
-
json: bool,
|
|
18957
|
-
"no-input": bool,
|
|
18958
|
-
new: bool,
|
|
18959
|
-
stdin: bool,
|
|
18960
|
-
"allow-deny": bool,
|
|
18961
|
-
text: bool,
|
|
18962
|
-
"allow-text": bool,
|
|
18963
|
-
async: bool,
|
|
18964
|
-
"keep-pending": bool,
|
|
18965
|
-
claim: bool,
|
|
18966
|
-
"dry-run": bool,
|
|
18967
|
-
remove: bool,
|
|
18968
|
-
roundtrip: bool,
|
|
18969
|
-
topic: string,
|
|
18970
|
-
title: string,
|
|
18971
|
-
task: string,
|
|
18972
|
-
"session-id": string,
|
|
18973
|
-
sessionid: string,
|
|
18974
|
-
"session-name": string,
|
|
18975
|
-
priority: string,
|
|
18976
|
-
"dedupe-key": string,
|
|
18977
|
-
expires: string,
|
|
18978
|
-
wait: string,
|
|
18979
|
-
config: string,
|
|
18980
|
-
resume: string,
|
|
18981
|
-
qr: string,
|
|
18982
|
-
server: string,
|
|
18983
|
-
host: string,
|
|
18984
|
-
codex: string,
|
|
18985
|
-
interval: string,
|
|
18986
|
-
once: bool,
|
|
18987
|
-
choice: { type: "string", multiple: true },
|
|
18988
|
-
help: bool
|
|
18989
|
-
}
|
|
19134
|
+
options
|
|
18990
19135
|
});
|
|
18991
19136
|
if (leadingRequestId)
|
|
18992
19137
|
parsed.positionals.splice(1, 0, leadingRequestId);
|
|
@@ -18997,7 +19142,8 @@ async function main(args = process.argv.slice(2)) {
|
|
|
18997
19142
|
if (f.help || !command || command === "help") {
|
|
18998
19143
|
process.stdout.write(`ntfyx connect | create | send BODY | ask QUESTION --allow-deny|--choice id=label|--text
|
|
18999
19144
|
ntfyx result ID [--claim] | cancel ID | topics | use ALIAS | sources | disconnect ALIAS
|
|
19000
|
-
ntfyx doctor | setup claude-code|codex|codex-usage [--dry-run|--remove]
|
|
19145
|
+
ntfyx doctor | setup claude-code|codex|codex-usage [--dry-run|--remove]
|
|
19146
|
+
setup claude-code: --permissions on|off (default off) --questions on|off --notifications on|off; --config FILE --topic ALIAS | mcp | version | upgrade
|
|
19001
19147
|
send/ask: --session-id ID [--session-name NAME] (or NTFYX_SESSION_ID)
|
|
19002
19148
|
ntfyx watch codex-usage [--once] [--interval 15m] [--dry-run] [--topic ALIAS]
|
|
19003
19149
|
ntfyx connect --new --topic NEW_ALIAS | connect --resume ID --topic NEW_ALIAS
|
|
@@ -19008,7 +19154,7 @@ connect/create: --qr auto|terminal|file (default: auto; compact QR or private lo
|
|
|
19008
19154
|
if (command === "version") {
|
|
19009
19155
|
output({
|
|
19010
19156
|
application: "ntfyx",
|
|
19011
|
-
version: "0.1.
|
|
19157
|
+
version: "0.1.6",
|
|
19012
19158
|
protocol: 1,
|
|
19013
19159
|
build: "release",
|
|
19014
19160
|
installation: process.env.NTFYX_INSTALL_METHOD === "npm" ? "npm" : process.versions.bun ? "binary" : "source"
|
|
@@ -19016,7 +19162,7 @@ connect/create: --qr auto|terminal|file (default: auto; compact QR or private lo
|
|
|
19016
19162
|
return 0;
|
|
19017
19163
|
}
|
|
19018
19164
|
if (command === "mcp") {
|
|
19019
|
-
await runMcp(store);
|
|
19165
|
+
await runMcp(store, f.topic);
|
|
19020
19166
|
return 0;
|
|
19021
19167
|
}
|
|
19022
19168
|
if (command === "hook") {
|
|
@@ -19026,9 +19172,9 @@ connect/create: --qr auto|terminal|file (default: auto; compact QR or private lo
|
|
|
19026
19172
|
try {
|
|
19027
19173
|
const input = await stdin(128 * 1024);
|
|
19028
19174
|
if (rest[0] === "question" && host === "claude-code")
|
|
19029
|
-
await handleClaudeQuestion(store, input);
|
|
19175
|
+
await handleClaudeQuestion(store, input, f.topic);
|
|
19030
19176
|
else
|
|
19031
|
-
await handleHook(store, rest[0], input, host);
|
|
19177
|
+
await handleHook(store, rest[0], input, host, f.topic);
|
|
19032
19178
|
} catch {}
|
|
19033
19179
|
return 0;
|
|
19034
19180
|
}
|
|
@@ -19052,11 +19198,26 @@ connect/create: --qr auto|terminal|file (default: auto; compact QR or private lo
|
|
|
19052
19198
|
const setup = rest[0] === "claude-code" ? setupClaude : rest[0] === "codex" ? setupCodex : undefined;
|
|
19053
19199
|
if (!setup)
|
|
19054
19200
|
throw new NtfyxError("invalid_schema");
|
|
19201
|
+
const toggle = (value, fallback) => {
|
|
19202
|
+
if (value === undefined)
|
|
19203
|
+
return fallback;
|
|
19204
|
+
if (value !== "on" && value !== "off")
|
|
19205
|
+
throw new NtfyxError("invalid_schema");
|
|
19206
|
+
return value === "on";
|
|
19207
|
+
};
|
|
19208
|
+
if (rest[0] !== "claude-code" && [f.permissions, f.questions, f.notifications].some((v) => v !== undefined))
|
|
19209
|
+
throw new NtfyxError("invalid_schema");
|
|
19055
19210
|
output(await setup({
|
|
19056
19211
|
executable: resolve(process.execPath),
|
|
19057
19212
|
dryRun: !!f["dry-run"],
|
|
19058
19213
|
remove: !!f.remove,
|
|
19059
|
-
...f.config ? { config: store.path } : {}
|
|
19214
|
+
...f.config ? { config: store.path } : {},
|
|
19215
|
+
...f.topic ? { topic: f.topic } : {},
|
|
19216
|
+
...rest[0] === "claude-code" ? {
|
|
19217
|
+
permissions: toggle(f.permissions, false),
|
|
19218
|
+
questions: toggle(f.questions, true),
|
|
19219
|
+
notifications: toggle(f.notifications, true)
|
|
19220
|
+
} : {}
|
|
19060
19221
|
}));
|
|
19061
19222
|
return 0;
|
|
19062
19223
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntfyx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "End-to-end encrypted agent notifications and one-time replies",
|
|
7
|
+
"homepage": "https://ntfyx.me/docs/agents",
|
|
8
|
+
"mcpName": "me.ntfyx/ntfyx",
|
|
7
9
|
"engines": {
|
|
8
10
|
"node": ">=24"
|
|
9
11
|
},
|
|
@@ -19,6 +21,7 @@
|
|
|
19
21
|
"installer.mjs",
|
|
20
22
|
"release-public-key.pem",
|
|
21
23
|
"package.json",
|
|
24
|
+
"README.md",
|
|
22
25
|
"LICENSE",
|
|
23
26
|
"THIRD_PARTY_NOTICES.txt",
|
|
24
27
|
"RELINKING.md",
|