nonotify 0.1.15 → 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/CHANGELOG.md +6 -0
- package/dist/cli.js +170 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/notifier.d.ts +19 -0
- package/dist/notifier.js +94 -1
- package/dist/telegram.d.ts +24 -1
- package/dist/telegram.js +293 -31
- package/package.json +1 -1
- package/src/cli.ts +224 -4
- package/src/index.ts +4 -0
- package/src/notifier.ts +155 -1
- package/src/telegram.ts +451 -48
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# nonotify
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5384093: Add interactive `nnt ask` Telegram prompts, expose `Notifier.ask()`, and let the OpenCode plugin answer pending permissions and questions through Telegram buttons.
|
|
8
|
+
|
|
3
9
|
## 0.1.15
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { askConfirm, askRequired, askRequiredWithInitial, askSelect, } from "./p
|
|
|
4
4
|
import { getConfigPath, loadConfig, saveConfig } from "./config.js";
|
|
5
5
|
import { printKeyValueTable, printProfilesTable } from "./display.js";
|
|
6
6
|
import { getLatestUpdateOffset, sendTelegramMessage, waitForChatId, } from "./telegram.js";
|
|
7
|
-
import { Notifier } from "./notifier.js";
|
|
7
|
+
import { AskAbortedError, AskTimeoutError, Notifier, NotifierError, } from "./notifier.js";
|
|
8
8
|
const profileCli = Cli.create("profile", {
|
|
9
9
|
description: "Manage notification profiles",
|
|
10
10
|
});
|
|
@@ -348,7 +348,7 @@ function routeDefaultCommand(argv) {
|
|
|
348
348
|
if (argv.length === 0) {
|
|
349
349
|
return argv;
|
|
350
350
|
}
|
|
351
|
-
const topLevelCommands = new Set(["profile", "send", "skills", "mcp"]);
|
|
351
|
+
const topLevelCommands = new Set(["ask", "profile", "send", "skills", "mcp"]);
|
|
352
352
|
const bareGlobalFlags = new Set([
|
|
353
353
|
"--help",
|
|
354
354
|
"-h",
|
|
@@ -390,6 +390,76 @@ function normalizeFormatFlag(argv) {
|
|
|
390
390
|
}
|
|
391
391
|
return normalized;
|
|
392
392
|
}
|
|
393
|
+
function getCommandIndex(argv) {
|
|
394
|
+
const bareGlobalFlags = new Set([
|
|
395
|
+
"--help",
|
|
396
|
+
"-h",
|
|
397
|
+
"--version",
|
|
398
|
+
"--llms",
|
|
399
|
+
"--mcp",
|
|
400
|
+
"--json",
|
|
401
|
+
"--verbose",
|
|
402
|
+
]);
|
|
403
|
+
let index = 0;
|
|
404
|
+
while (index < argv.length) {
|
|
405
|
+
const token = argv[index];
|
|
406
|
+
if (token === "--format") {
|
|
407
|
+
index += 2;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (bareGlobalFlags.has(token)) {
|
|
411
|
+
index += 1;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
return index;
|
|
417
|
+
}
|
|
418
|
+
async function serveAskCommandIfRequested(argv) {
|
|
419
|
+
const commandIndex = getCommandIndex(argv);
|
|
420
|
+
if (commandIndex >= argv.length || argv[commandIndex] !== "ask") {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
const parsed = parseAskArgs(argv.slice(commandIndex + 1));
|
|
425
|
+
if (parsed.helpRequested) {
|
|
426
|
+
process.stderr.write(getAskUsage());
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
const notifier = new Notifier();
|
|
430
|
+
const abortController = new AbortController();
|
|
431
|
+
let exitCode = 0;
|
|
432
|
+
const onAbortSignal = () => abortController.abort();
|
|
433
|
+
process.once("SIGINT", onAbortSignal);
|
|
434
|
+
process.once("SIGTERM", onAbortSignal);
|
|
435
|
+
try {
|
|
436
|
+
const result = await notifier.ask({
|
|
437
|
+
message: parsed.message,
|
|
438
|
+
options: parsed.options,
|
|
439
|
+
profile: parsed.profile,
|
|
440
|
+
timeoutMs: parsed.timeoutMs,
|
|
441
|
+
signal: abortController.signal,
|
|
442
|
+
});
|
|
443
|
+
process.stdout.write(`${result.selected}\n`);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
exitCode = renderAskError(error);
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
process.removeListener("SIGINT", onAbortSignal);
|
|
450
|
+
process.removeListener("SIGTERM", onAbortSignal);
|
|
451
|
+
}
|
|
452
|
+
if (exitCode !== 0) {
|
|
453
|
+
process.exitCode = exitCode;
|
|
454
|
+
}
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
process.stderr.write(`${formatAskError(error)}\n`);
|
|
459
|
+
process.exitCode = 1;
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
393
463
|
function isStrictOutputRequested(argv) {
|
|
394
464
|
for (const token of argv) {
|
|
395
465
|
if (token === "--json" || token === "--verbose" || token === "--format") {
|
|
@@ -434,6 +504,102 @@ const argvWithAgentDefaults = withAgentDefaultFormat(normalizedArgv, strictOutpu
|
|
|
434
504
|
function shouldRenderPretty(agent) {
|
|
435
505
|
return !agent && !strictOutputRequested && !isAgentEnvironment();
|
|
436
506
|
}
|
|
437
|
-
|
|
438
|
-
|
|
507
|
+
if (!(await serveAskCommandIfRequested(normalizedArgv))) {
|
|
508
|
+
const routedArgv = routeDefaultCommand(argvWithAgentDefaults);
|
|
509
|
+
await cli.serve(routedArgv);
|
|
510
|
+
}
|
|
439
511
|
export default cli;
|
|
512
|
+
function parseAskArgs(argv) {
|
|
513
|
+
let profile;
|
|
514
|
+
let timeoutMs;
|
|
515
|
+
let helpRequested = false;
|
|
516
|
+
const positionals = [];
|
|
517
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
518
|
+
const token = argv[index];
|
|
519
|
+
if (token === "--help" || token === "-h") {
|
|
520
|
+
helpRequested = true;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (token === "--profile" || token === "-p") {
|
|
524
|
+
const value = argv[index + 1];
|
|
525
|
+
if (!value) {
|
|
526
|
+
throw new Error("Missing value for --profile.");
|
|
527
|
+
}
|
|
528
|
+
profile = value;
|
|
529
|
+
index += 1;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (token.startsWith("--profile=")) {
|
|
533
|
+
profile = token.slice("--profile=".length);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (token === "--timeout" || token === "-t") {
|
|
537
|
+
const value = argv[index + 1];
|
|
538
|
+
if (!value) {
|
|
539
|
+
throw new Error("Missing value for --timeout.");
|
|
540
|
+
}
|
|
541
|
+
timeoutMs = parseTimeoutFlag(value);
|
|
542
|
+
index += 1;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (token.startsWith("--timeout=")) {
|
|
546
|
+
timeoutMs = parseTimeoutFlag(token.slice("--timeout=".length));
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (token.startsWith("-")) {
|
|
550
|
+
throw new Error(`Unknown option: ${token}`);
|
|
551
|
+
}
|
|
552
|
+
positionals.push(token);
|
|
553
|
+
}
|
|
554
|
+
if (helpRequested) {
|
|
555
|
+
return {
|
|
556
|
+
message: "",
|
|
557
|
+
options: [],
|
|
558
|
+
profile,
|
|
559
|
+
timeoutMs,
|
|
560
|
+
helpRequested,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (positionals.length < 2) {
|
|
564
|
+
throw new Error("Ask requires a message and at least one option. See `nnt ask --help`.");
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
message: positionals[0],
|
|
568
|
+
options: positionals.slice(1),
|
|
569
|
+
profile,
|
|
570
|
+
timeoutMs,
|
|
571
|
+
helpRequested,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function parseTimeoutFlag(value) {
|
|
575
|
+
const seconds = Number(value);
|
|
576
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
577
|
+
throw new Error("--timeout must be a non-negative number of seconds.");
|
|
578
|
+
}
|
|
579
|
+
return Math.round(seconds * 1000);
|
|
580
|
+
}
|
|
581
|
+
function getAskUsage() {
|
|
582
|
+
return [
|
|
583
|
+
"Usage: nnt ask [--profile <name>] [--timeout <seconds>] <message> <option1> [option2 ... option10]",
|
|
584
|
+
"",
|
|
585
|
+
"Prints only the selected option to stdout.",
|
|
586
|
+
].join("\n");
|
|
587
|
+
}
|
|
588
|
+
function renderAskError(error) {
|
|
589
|
+
if (error instanceof AskTimeoutError) {
|
|
590
|
+
process.stderr.write(`${error.message}\n`);
|
|
591
|
+
return 1;
|
|
592
|
+
}
|
|
593
|
+
if (error instanceof AskAbortedError) {
|
|
594
|
+
process.stderr.write("Cancelled.\n");
|
|
595
|
+
return 130;
|
|
596
|
+
}
|
|
597
|
+
process.stderr.write(`${formatAskError(error)}\n`);
|
|
598
|
+
return 1;
|
|
599
|
+
}
|
|
600
|
+
function formatAskError(error) {
|
|
601
|
+
if (error instanceof NotifierError || error instanceof Error) {
|
|
602
|
+
return error.message;
|
|
603
|
+
}
|
|
604
|
+
return String(error);
|
|
605
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { EnvConfigLoader, NoProfilesConfiguredError, Notifier, NotifierError, ProfileNotFoundError, } from "./notifier.js";
|
|
2
|
-
export type { NotifierConfig, NotifierConfigLoader, NotifierProfile, NotifierProfileInput, SendInput, SendResult, } from "./notifier.js";
|
|
1
|
+
export { AskAbortedError, AskTimeoutError, EnvConfigLoader, NoProfilesConfiguredError, Notifier, NotifierError, ProfileNotFoundError, } from "./notifier.js";
|
|
2
|
+
export type { AskInput, AskResult, NotifierConfig, NotifierConfigLoader, NotifierProfile, NotifierProfileInput, SendInput, SendResult, } from "./notifier.js";
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { EnvConfigLoader, NoProfilesConfiguredError, Notifier, NotifierError, ProfileNotFoundError, } from "./notifier.js";
|
|
1
|
+
export { AskAbortedError, AskTimeoutError, EnvConfigLoader, NoProfilesConfiguredError, Notifier, NotifierError, ProfileNotFoundError, } from "./notifier.js";
|
package/dist/notifier.d.ts
CHANGED
|
@@ -26,6 +26,18 @@ export type SendResult = {
|
|
|
26
26
|
profile: string;
|
|
27
27
|
provider: "telegram";
|
|
28
28
|
};
|
|
29
|
+
export type AskInput = {
|
|
30
|
+
message: string;
|
|
31
|
+
options: readonly string[];
|
|
32
|
+
profile?: string;
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
};
|
|
36
|
+
export type AskResult = {
|
|
37
|
+
selected: string;
|
|
38
|
+
profile: string;
|
|
39
|
+
provider: "telegram";
|
|
40
|
+
};
|
|
29
41
|
export declare class NotifierError extends Error {
|
|
30
42
|
constructor(message: string);
|
|
31
43
|
}
|
|
@@ -36,6 +48,12 @@ export declare class ProfileNotFoundError extends NotifierError {
|
|
|
36
48
|
export declare class NoProfilesConfiguredError extends NotifierError {
|
|
37
49
|
constructor();
|
|
38
50
|
}
|
|
51
|
+
export declare class AskTimeoutError extends NotifierError {
|
|
52
|
+
constructor();
|
|
53
|
+
}
|
|
54
|
+
export declare class AskAbortedError extends NotifierError {
|
|
55
|
+
constructor();
|
|
56
|
+
}
|
|
39
57
|
export declare class EnvConfigLoader implements NotifierConfigLoader {
|
|
40
58
|
load(): NotifierConfig;
|
|
41
59
|
}
|
|
@@ -45,5 +63,6 @@ export declare class Notifier {
|
|
|
45
63
|
private readonly profilesByName;
|
|
46
64
|
constructor(source?: NotifierConfig | NotifierConfigLoader);
|
|
47
65
|
send(input: SendInput): Promise<SendResult>;
|
|
66
|
+
ask(input: AskInput): Promise<AskResult>;
|
|
48
67
|
private resolveProfile;
|
|
49
68
|
}
|
package/dist/notifier.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { getConfigPath, getLegacyConfigPath } from "./config.js";
|
|
3
|
-
import { sendTelegramMessage } from "./telegram.js";
|
|
4
|
+
import { answerTelegramCallbackQuery, clearTelegramInlineKeyboard, getLatestUpdateOffset, markTelegramSelectedOption, sendTelegramChoiceMessage, sendTelegramMessage, waitForTelegramCallback, } from "./telegram.js";
|
|
4
5
|
export class NotifierError extends Error {
|
|
5
6
|
constructor(message) {
|
|
6
7
|
super(message);
|
|
@@ -21,6 +22,18 @@ export class NoProfilesConfiguredError extends NotifierError {
|
|
|
21
22
|
this.name = "NoProfilesConfiguredError";
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
export class AskTimeoutError extends NotifierError {
|
|
26
|
+
constructor() {
|
|
27
|
+
super("Timed out waiting for Telegram answer.");
|
|
28
|
+
this.name = "AskTimeoutError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class AskAbortedError extends NotifierError {
|
|
32
|
+
constructor() {
|
|
33
|
+
super("Telegram answer wait was aborted.");
|
|
34
|
+
this.name = "AskAbortedError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
24
37
|
export class EnvConfigLoader {
|
|
25
38
|
load() {
|
|
26
39
|
const configPaths = Array.from(new Set([getConfigPath(), getLegacyConfigPath()]));
|
|
@@ -71,6 +84,81 @@ export class Notifier {
|
|
|
71
84
|
provider: selectedProfile.type,
|
|
72
85
|
};
|
|
73
86
|
}
|
|
87
|
+
async ask(input) {
|
|
88
|
+
const message = input.message.trim();
|
|
89
|
+
if (message === "") {
|
|
90
|
+
throw new NotifierError("Message cannot be empty.");
|
|
91
|
+
}
|
|
92
|
+
if (!Array.isArray(input.options)) {
|
|
93
|
+
throw new NotifierError("Options must be an array.");
|
|
94
|
+
}
|
|
95
|
+
if (input.options.length < 1 || input.options.length > 10) {
|
|
96
|
+
throw new NotifierError("Options must contain between 1 and 10 items.");
|
|
97
|
+
}
|
|
98
|
+
const options = input.options.map((option, index) => {
|
|
99
|
+
if (typeof option !== "string" || option.trim() === "") {
|
|
100
|
+
throw new NotifierError(`Invalid option at index ${index}: expected non-empty string.`);
|
|
101
|
+
}
|
|
102
|
+
return option.trim();
|
|
103
|
+
});
|
|
104
|
+
if (input.timeoutMs !== undefined &&
|
|
105
|
+
(!Number.isFinite(input.timeoutMs) || input.timeoutMs < 0)) {
|
|
106
|
+
throw new NotifierError("timeoutMs must be a non-negative number.");
|
|
107
|
+
}
|
|
108
|
+
const selectedProfile = this.resolveProfile(input.profile);
|
|
109
|
+
const offset = await getLatestUpdateOffset(selectedProfile.botToken);
|
|
110
|
+
const requestId = randomUUID().replaceAll("-", "");
|
|
111
|
+
const callbackOptions = options.map((option, index) => ({
|
|
112
|
+
label: option,
|
|
113
|
+
callbackData: `nnt:${requestId}:${index}`,
|
|
114
|
+
}));
|
|
115
|
+
const sentMessage = await sendTelegramChoiceMessage(selectedProfile.botToken, selectedProfile.chatId, message, callbackOptions);
|
|
116
|
+
try {
|
|
117
|
+
const callback = await waitForTelegramCallback(selectedProfile.botToken, {
|
|
118
|
+
chatId: selectedProfile.chatId,
|
|
119
|
+
messageId: sentMessage.messageId,
|
|
120
|
+
callbackData: callbackOptions.map((option) => option.callbackData),
|
|
121
|
+
offset,
|
|
122
|
+
timeoutMs: input.timeoutMs,
|
|
123
|
+
signal: input.signal,
|
|
124
|
+
});
|
|
125
|
+
const selectedIndex = callbackOptions.findIndex((option) => option.callbackData === callback.data);
|
|
126
|
+
if (selectedIndex === -1) {
|
|
127
|
+
throw new NotifierError("Received an unknown Telegram answer.");
|
|
128
|
+
}
|
|
129
|
+
await answerTelegramCallbackQuery(selectedProfile.botToken, callback.callbackQueryId);
|
|
130
|
+
try {
|
|
131
|
+
await markTelegramSelectedOption(selectedProfile.botToken, selectedProfile.chatId, sentMessage.messageId, callbackOptions, callback.data);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Keep the selected answer even if the visual update fails.
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
selected: options[selectedIndex],
|
|
138
|
+
profile: selectedProfile.name,
|
|
139
|
+
provider: selectedProfile.type,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error instanceof Error && error.message === "Timed out waiting") {
|
|
144
|
+
throw new AskTimeoutError();
|
|
145
|
+
}
|
|
146
|
+
if (isAbortError(error)) {
|
|
147
|
+
throw new AskAbortedError();
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (input.signal?.aborted) {
|
|
153
|
+
try {
|
|
154
|
+
await clearTelegramInlineKeyboard(selectedProfile.botToken, selectedProfile.chatId, sentMessage.messageId);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Do not mask the original ask result or failure if cleanup fails.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
74
162
|
resolveProfile(profileName) {
|
|
75
163
|
if (profileName) {
|
|
76
164
|
const profile = this.profilesByName.get(profileName);
|
|
@@ -169,3 +257,8 @@ function isConfigLoader(value) {
|
|
|
169
257
|
function isNodeError(error) {
|
|
170
258
|
return typeof error === "object" && error !== null && "code" in error;
|
|
171
259
|
}
|
|
260
|
+
function isAbortError(error) {
|
|
261
|
+
return error instanceof DOMException
|
|
262
|
+
? error.name === "AbortError"
|
|
263
|
+
: error instanceof Error && error.name === "AbortError";
|
|
264
|
+
}
|
package/dist/telegram.d.ts
CHANGED
|
@@ -2,6 +2,29 @@ export type TelegramConnection = {
|
|
|
2
2
|
chatId: string;
|
|
3
3
|
username: string | null;
|
|
4
4
|
};
|
|
5
|
+
export type TelegramInlineOption = {
|
|
6
|
+
label: string;
|
|
7
|
+
callbackData: string;
|
|
8
|
+
};
|
|
9
|
+
export type TelegramChoiceMessage = {
|
|
10
|
+
messageId: number;
|
|
11
|
+
};
|
|
12
|
+
export type TelegramCallbackSelection = {
|
|
13
|
+
callbackQueryId: string;
|
|
14
|
+
data: string;
|
|
15
|
+
};
|
|
5
16
|
export declare function getLatestUpdateOffset(botToken: string): Promise<number>;
|
|
6
|
-
export declare function waitForChatId(botToken: string, offset: number, timeoutSeconds?: number): Promise<TelegramConnection>;
|
|
17
|
+
export declare function waitForChatId(botToken: string, offset: number, timeoutSeconds?: number, signal?: AbortSignal): Promise<TelegramConnection>;
|
|
7
18
|
export declare function sendTelegramMessage(botToken: string, chatId: string, text: string): Promise<void>;
|
|
19
|
+
export declare function sendTelegramChoiceMessage(botToken: string, chatId: string, text: string, options: readonly TelegramInlineOption[]): Promise<TelegramChoiceMessage>;
|
|
20
|
+
export declare function waitForTelegramCallback(botToken: string, input: {
|
|
21
|
+
chatId: string;
|
|
22
|
+
messageId: number;
|
|
23
|
+
callbackData: readonly string[];
|
|
24
|
+
offset?: number;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}): Promise<TelegramCallbackSelection>;
|
|
28
|
+
export declare function answerTelegramCallbackQuery(botToken: string, callbackQueryId: string): Promise<void>;
|
|
29
|
+
export declare function clearTelegramInlineKeyboard(botToken: string, chatId: string, messageId: number): Promise<void>;
|
|
30
|
+
export declare function markTelegramSelectedOption(botToken: string, chatId: string, messageId: number, options: readonly TelegramInlineOption[], selectedCallbackData: string): Promise<void>;
|