nonotify 0.1.0 → 0.1.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.
@@ -0,0 +1,113 @@
1
+ name: Release & Publish
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: write
9
+
10
+ jobs:
11
+ release:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout
16
+ uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+
20
+ - name: Setup Node
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 20
24
+ registry-url: https://registry.npmjs.org/
25
+
26
+ - name: Decide whether to release
27
+ id: guard
28
+ shell: bash
29
+ run: |
30
+ set -euo pipefail
31
+
32
+ VERSION="$(node -p "require('./package.json').version")"
33
+ NAME="$(node -p "require('./package.json').name")"
34
+
35
+ git fetch --tags --force
36
+
37
+ # Latest tag (empty if no tags yet)
38
+ LATEST_TAG="$(git tag --sort=-v:refname | head -n 1 || true)"
39
+
40
+ # Does tag for this version already exist?
41
+ if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
42
+ TAG_EXISTS="true"
43
+ else
44
+ TAG_EXISTS="false"
45
+ fi
46
+
47
+ # Is that version already on npm?
48
+ if npm view "${NAME}@${VERSION}" version >/dev/null 2>&1; then
49
+ NPM_EXISTS="true"
50
+ else
51
+ NPM_EXISTS="false"
52
+ fi
53
+
54
+ # Decide
55
+ SHOULD_RELEASE="true"
56
+ REASON="ok"
57
+
58
+ if [[ "$TAG_EXISTS" == "true" ]]; then
59
+ SHOULD_RELEASE="false"
60
+ REASON="git tag already exists"
61
+ elif [[ -n "$LATEST_TAG" && "$VERSION" == "$LATEST_TAG" ]]; then
62
+ SHOULD_RELEASE="false"
63
+ REASON="package.json version equals latest tag"
64
+ elif [[ "$NPM_EXISTS" == "true" ]]; then
65
+ SHOULD_RELEASE="false"
66
+ REASON="version already published on npm"
67
+ fi
68
+
69
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
70
+ echo "latest_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
71
+ echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
72
+ echo "npm_exists=$NPM_EXISTS" >> "$GITHUB_OUTPUT"
73
+ echo "should_release=$SHOULD_RELEASE" >> "$GITHUB_OUTPUT"
74
+ echo "reason=$REASON" >> "$GITHUB_OUTPUT"
75
+
76
+ echo "Decision: should_release=$SHOULD_RELEASE ($REASON)"
77
+
78
+ - name: Stop summary
79
+ if: steps.guard.outputs.should_release != 'true'
80
+ run: |
81
+ echo "Skipping release: ${{ steps.guard.outputs.reason }}"
82
+ echo "Version: ${{ steps.guard.outputs.version }}"
83
+ echo "Latest tag: ${{ steps.guard.outputs.latest_tag }}"
84
+
85
+ - name: Install dependencies
86
+ if: steps.guard.outputs.should_release == 'true'
87
+ run: npm ci
88
+
89
+ # - name: Run tests
90
+ # if: steps.guard.outputs.should_release == 'true'
91
+ # run: npm test
92
+
93
+ - name: Create git tag
94
+ if: steps.guard.outputs.should_release == 'true'
95
+ run: |
96
+ git config user.name "github-actions"
97
+ git config user.email "github-actions@github.com"
98
+ git tag "${{ steps.guard.outputs.version }}"
99
+ git push origin "${{ steps.guard.outputs.version }}"
100
+
101
+ - name: Create GitHub Release
102
+ if: steps.guard.outputs.should_release == 'true'
103
+ uses: softprops/action-gh-release@v2
104
+ with:
105
+ tag_name: ${{ steps.guard.outputs.version }}
106
+ name: Release ${{ steps.guard.outputs.version }}
107
+ generate_release_notes: true
108
+
109
+ - name: Publish to npm
110
+ if: steps.guard.outputs.should_release == 'true'
111
+ run: npm publish
112
+ env:
113
+ NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
package/README.md CHANGED
@@ -7,12 +7,10 @@ Use it to send yourself a message when tasks are done, including from coding age
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install
11
- npm run build
12
- npm link
10
+ npm install -g nonotify
13
11
  ```
14
12
 
15
- After linking, `nnt` is available globally.
13
+ After install, `nnt` is available globally.
16
14
 
17
15
  ## Config location
18
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nonotify",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "nnt CLI for Telegram notifications",
5
5
  "type": "module",
6
6
  "bin": {
package/dist/cli.js DELETED
@@ -1,420 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Cli, z } from 'incur';
3
- import { askConfirm, askRequired, askRequiredWithInitial, askSelect } from './prompt.js';
4
- import { getConfigPath, loadConfig, saveConfig } from './config.js';
5
- import { printKeyValueTable, printProfilesTable } from './display.js';
6
- import { getLatestUpdateOffset, sendTelegramMessage, waitForChatId } from './telegram.js';
7
- const profileCli = Cli.create('profile', {
8
- description: 'Manage notification profiles',
9
- });
10
- profileCli.command('add', {
11
- description: 'Add a notification profile',
12
- outputPolicy: 'agent-only',
13
- args: z.object({
14
- provider: z.enum(['telegram']).default('telegram').describe('Profile provider'),
15
- }),
16
- async run(c) {
17
- if (c.args.provider !== 'telegram') {
18
- throw new Error(`Unsupported provider: ${c.args.provider}`);
19
- }
20
- const config = await loadConfig();
21
- const profileName = await askRequired('Profile name: ');
22
- if (config.profiles[profileName]) {
23
- throw new Error(`Profile "${profileName}" already exists.`);
24
- }
25
- const botToken = await askRequired('Telegram bot token: ');
26
- if (shouldRenderPretty(c.agent)) {
27
- process.stdout.write('\nSend any message to your bot in Telegram.\n');
28
- process.stdout.write('Waiting for message to detect chat_id (up to 120s)...\n');
29
- }
30
- const offset = await getLatestUpdateOffset(botToken);
31
- const connection = await waitForChatId(botToken, offset, 120);
32
- const chatId = connection.chatId;
33
- if (shouldRenderPretty(c.agent)) {
34
- if (connection.username) {
35
- process.stdout.write(`Connected Telegram username: @${connection.username}\n`);
36
- }
37
- else {
38
- process.stdout.write('Connected Telegram user has no username set.\n');
39
- }
40
- }
41
- config.profiles[profileName] = {
42
- type: 'telegram',
43
- name: profileName,
44
- botToken,
45
- chatId,
46
- createdAt: new Date().toISOString(),
47
- };
48
- if (!config.defaultProfile) {
49
- config.defaultProfile = profileName;
50
- }
51
- await saveConfig(config);
52
- let confirmationSent = false;
53
- let confirmationWarning = null;
54
- try {
55
- await sendTelegramMessage(botToken, chatId, `nnt: profile "${profileName}" connected successfully. You can now send notifications from CLI.`);
56
- confirmationSent = true;
57
- }
58
- catch (error) {
59
- confirmationWarning = error instanceof Error ? error.message : 'Unknown error while sending confirmation';
60
- }
61
- if (shouldRenderPretty(c.agent)) {
62
- printKeyValueTable('Profile added', [
63
- { key: 'profile', value: profileName },
64
- { key: 'provider', value: 'telegram' },
65
- { key: 'chat_id', value: chatId },
66
- { key: 'username', value: connection.username ? `@${connection.username}` : '(none)' },
67
- { key: 'default', value: config.defaultProfile ?? '(none)' },
68
- ]);
69
- }
70
- return {
71
- added: true,
72
- profile: profileName,
73
- provider: 'telegram',
74
- chatId,
75
- username: connection.username,
76
- defaultProfile: config.defaultProfile,
77
- configPath: getConfigPath(),
78
- confirmationSent,
79
- confirmationWarning,
80
- };
81
- },
82
- });
83
- profileCli.command('list', {
84
- description: 'List configured profiles',
85
- outputPolicy: 'agent-only',
86
- async run(c) {
87
- const config = await loadConfig();
88
- const names = Object.keys(config.profiles).sort((a, b) => a.localeCompare(b));
89
- const profiles = names.map(name => ({
90
- name,
91
- provider: config.profiles[name].type,
92
- isDefault: name === config.defaultProfile,
93
- }));
94
- if (shouldRenderPretty(c.agent)) {
95
- printProfilesTable(profiles);
96
- }
97
- return {
98
- defaultProfile: config.defaultProfile,
99
- totalProfiles: names.length,
100
- profiles,
101
- };
102
- },
103
- });
104
- profileCli.command('default', {
105
- description: 'Get or set default profile',
106
- outputPolicy: 'agent-only',
107
- args: z.object({
108
- profile: z.string().optional().describe('Profile name to set as default'),
109
- }),
110
- async run(c) {
111
- const config = await loadConfig();
112
- if (!c.args.profile) {
113
- if (shouldRenderPretty(c.agent)) {
114
- printKeyValueTable('Default profile', [
115
- { key: 'default', value: config.defaultProfile ?? '(not set)' },
116
- ]);
117
- }
118
- return {
119
- defaultProfile: config.defaultProfile,
120
- };
121
- }
122
- if (!config.profiles[c.args.profile]) {
123
- throw new Error(`Profile "${c.args.profile}" not found.`);
124
- }
125
- config.defaultProfile = c.args.profile;
126
- await saveConfig(config);
127
- if (shouldRenderPretty(c.agent)) {
128
- printKeyValueTable('Default profile updated', [
129
- { key: 'default', value: config.defaultProfile ?? '(not set)' },
130
- ]);
131
- }
132
- return {
133
- updated: true,
134
- defaultProfile: config.defaultProfile,
135
- };
136
- },
137
- });
138
- profileCli.command('delete', {
139
- description: 'Delete a profile',
140
- outputPolicy: 'agent-only',
141
- args: z.object({
142
- profile: z.string().describe('Profile name to delete'),
143
- }),
144
- async run(c) {
145
- const config = await loadConfig();
146
- const targetName = c.args.profile;
147
- const profile = config.profiles[targetName];
148
- if (!profile) {
149
- throw new Error(`Profile "${targetName}" not found.`);
150
- }
151
- delete config.profiles[targetName];
152
- if (config.defaultProfile === targetName) {
153
- const remaining = Object.keys(config.profiles).sort((a, b) => a.localeCompare(b));
154
- config.defaultProfile = remaining[0] ?? null;
155
- }
156
- await saveConfig(config);
157
- if (shouldRenderPretty(c.agent)) {
158
- printKeyValueTable('Profile deleted', [
159
- { key: 'profile', value: targetName },
160
- { key: 'provider', value: profile.type },
161
- { key: 'default', value: config.defaultProfile ?? '(not set)' },
162
- ]);
163
- }
164
- return {
165
- deleted: true,
166
- profile: targetName,
167
- provider: profile.type,
168
- defaultProfile: config.defaultProfile,
169
- };
170
- },
171
- });
172
- profileCli.command('edit', {
173
- description: 'Edit profile data',
174
- outputPolicy: 'agent-only',
175
- args: z.object({
176
- profile: z.string().optional().describe('Existing profile name'),
177
- }),
178
- options: z.object({
179
- newName: z.string().optional().describe('Rename profile to a new name'),
180
- botToken: z.string().optional().describe('Replace Telegram bot token'),
181
- chatId: z.string().optional().describe('Replace Telegram chat id'),
182
- reconnect: z.boolean().optional().describe('Re-detect chat id from next Telegram message'),
183
- }),
184
- alias: {
185
- newName: 'n',
186
- botToken: 't',
187
- chatId: 'c',
188
- reconnect: 'r',
189
- },
190
- async run(c) {
191
- const config = await loadConfig();
192
- const profileNames = Object.keys(config.profiles).sort((a, b) => a.localeCompare(b));
193
- if (profileNames.length === 0) {
194
- throw new Error('No profiles found. Run `nnt profile add` first.');
195
- }
196
- const hasDirectEditOptions = Boolean(c.options.newName || c.options.botToken || c.options.chatId || c.options.reconnect);
197
- const sourceName = await resolveProfileForEdit(c.args.profile, profileNames, hasDirectEditOptions);
198
- const sourceProfile = config.profiles[sourceName];
199
- if (!sourceProfile) {
200
- throw new Error(`Profile "${sourceName}" not found.`);
201
- }
202
- const targetName = c.options.newName ?? sourceName;
203
- if (targetName !== sourceName && config.profiles[targetName]) {
204
- throw new Error(`Profile "${targetName}" already exists.`);
205
- }
206
- const botToken = c.options.botToken ?? sourceProfile.botToken;
207
- let nextName = targetName;
208
- let nextBotToken = botToken;
209
- let chatId = c.options.chatId ?? sourceProfile.chatId;
210
- let connectedUsername = null;
211
- if (!hasDirectEditOptions && canPromptInteractively()) {
212
- nextName = await askRequiredWithInitial('Profile name', sourceName);
213
- if (nextName !== sourceName && config.profiles[nextName]) {
214
- throw new Error(`Profile "${nextName}" already exists.`);
215
- }
216
- nextBotToken = await askRequiredWithInitial('Telegram bot token', sourceProfile.botToken);
217
- const shouldReconnect = await askConfirm('Reconnect and detect chat_id from a new message?', false);
218
- if (shouldReconnect) {
219
- if (shouldRenderPretty(c.agent)) {
220
- process.stdout.write('\nSend any message to your bot in Telegram.\n');
221
- process.stdout.write('Waiting for message to detect chat_id (up to 120s)...\n');
222
- }
223
- const offset = await getLatestUpdateOffset(nextBotToken);
224
- const connection = await waitForChatId(nextBotToken, offset, 120);
225
- chatId = connection.chatId;
226
- connectedUsername = connection.username;
227
- if (shouldRenderPretty(c.agent)) {
228
- if (connection.username) {
229
- process.stdout.write(`Connected Telegram username: @${connection.username}\n`);
230
- }
231
- else {
232
- process.stdout.write('Connected Telegram user has no username set.\n');
233
- }
234
- }
235
- }
236
- else {
237
- chatId = await askRequiredWithInitial('Telegram chat_id', sourceProfile.chatId);
238
- }
239
- }
240
- if (c.options.reconnect) {
241
- if (shouldRenderPretty(c.agent)) {
242
- process.stdout.write('\nSend any message to your bot in Telegram.\n');
243
- process.stdout.write('Waiting for message to detect chat_id (up to 120s)...\n');
244
- }
245
- const offset = await getLatestUpdateOffset(nextBotToken);
246
- const connection = await waitForChatId(nextBotToken, offset, 120);
247
- chatId = connection.chatId;
248
- connectedUsername = connection.username;
249
- if (shouldRenderPretty(c.agent)) {
250
- if (connection.username) {
251
- process.stdout.write(`Connected Telegram username: @${connection.username}\n`);
252
- }
253
- else {
254
- process.stdout.write('Connected Telegram user has no username set.\n');
255
- }
256
- }
257
- }
258
- const updatedProfile = {
259
- ...sourceProfile,
260
- name: nextName,
261
- botToken: nextBotToken,
262
- chatId,
263
- };
264
- if (nextName !== sourceName) {
265
- delete config.profiles[sourceName];
266
- }
267
- config.profiles[nextName] = updatedProfile;
268
- if (config.defaultProfile === sourceName) {
269
- config.defaultProfile = nextName;
270
- }
271
- const hasChanges = nextName !== sourceName
272
- || nextBotToken !== sourceProfile.botToken
273
- || chatId !== sourceProfile.chatId;
274
- if (!hasChanges) {
275
- return {
276
- updated: false,
277
- profile: sourceName,
278
- provider: sourceProfile.type,
279
- defaultProfile: config.defaultProfile,
280
- connectedUsername,
281
- };
282
- }
283
- await saveConfig(config);
284
- if (shouldRenderPretty(c.agent)) {
285
- printKeyValueTable('Profile updated', [
286
- { key: 'profile', value: nextName },
287
- { key: 'provider', value: updatedProfile.type },
288
- { key: 'chat_id', value: chatId },
289
- { key: 'username', value: connectedUsername ? `@${connectedUsername}` : '(unchanged/none)' },
290
- { key: 'default', value: config.defaultProfile ?? '(not set)' },
291
- ]);
292
- }
293
- return {
294
- updated: true,
295
- previousProfile: sourceName,
296
- profile: nextName,
297
- provider: updatedProfile.type,
298
- defaultProfile: config.defaultProfile,
299
- connectedUsername,
300
- };
301
- },
302
- });
303
- const cli = Cli.create('nnt', {
304
- description: 'Send Telegram notifications from terminal and agents',
305
- })
306
- .command('send', {
307
- description: 'Send a message via a saved profile',
308
- args: z.object({
309
- message: z.string().describe('Message text to send'),
310
- }),
311
- options: z.object({
312
- profile: z.string().optional().describe('Profile name from config'),
313
- }),
314
- alias: {
315
- profile: 'p',
316
- },
317
- async run(c) {
318
- const config = await loadConfig();
319
- const profileName = c.options.profile ?? config.defaultProfile;
320
- if (!profileName) {
321
- throw new Error('No default profile found. Run `nnt profile add` first.');
322
- }
323
- const profile = config.profiles[profileName];
324
- if (!profile) {
325
- throw new Error(`Profile "${profileName}" not found.`);
326
- }
327
- await sendTelegramMessage(profile.botToken, profile.chatId, c.args.message);
328
- return {
329
- sent: true,
330
- profile: profileName,
331
- provider: profile.type,
332
- };
333
- },
334
- })
335
- .command(profileCli);
336
- function routeDefaultCommand(argv) {
337
- if (argv.length === 0) {
338
- return argv;
339
- }
340
- const topLevelCommands = new Set(['profile', 'send', 'skills', 'mcp']);
341
- const bareGlobalFlags = new Set(['--help', '-h', '--version', '--llms', '--mcp', '--json', '--verbose']);
342
- let index = 0;
343
- while (index < argv.length) {
344
- const token = argv[index];
345
- if (token === '--format') {
346
- index += 2;
347
- continue;
348
- }
349
- if (bareGlobalFlags.has(token)) {
350
- index += 1;
351
- continue;
352
- }
353
- break;
354
- }
355
- if (index >= argv.length) {
356
- return argv;
357
- }
358
- if (topLevelCommands.has(argv[index])) {
359
- return argv;
360
- }
361
- return [...argv.slice(0, index), 'send', ...argv.slice(index)];
362
- }
363
- function normalizeFormatFlag(argv) {
364
- const normalized = [];
365
- for (const token of argv) {
366
- if (token.startsWith('--format=')) {
367
- normalized.push('--format', token.slice('--format='.length));
368
- continue;
369
- }
370
- normalized.push(token);
371
- }
372
- return normalized;
373
- }
374
- function isStrictOutputRequested(argv) {
375
- for (const token of argv) {
376
- if (token === '--json' || token === '--verbose' || token === '--format') {
377
- return true;
378
- }
379
- }
380
- return false;
381
- }
382
- function isAgentEnvironment() {
383
- return [
384
- process.env.OPENCODE,
385
- process.env.CLAUDECODE,
386
- process.env.CURSOR_AGENT,
387
- process.env.AIDER_SESSION,
388
- process.env.NNT_AGENT_MODE,
389
- ].some(Boolean);
390
- }
391
- function withAgentDefaultFormat(argv, strictOutputRequested) {
392
- if (strictOutputRequested || !isAgentEnvironment()) {
393
- return argv;
394
- }
395
- return ['--format', 'toon', ...argv];
396
- }
397
- function canPromptInteractively() {
398
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
399
- }
400
- async function resolveProfileForEdit(profileFromArgs, profileNames, hasDirectEditOptions) {
401
- if (profileFromArgs) {
402
- return profileFromArgs;
403
- }
404
- if (hasDirectEditOptions || !canPromptInteractively()) {
405
- throw new Error('Profile name is required in non-interactive mode.');
406
- }
407
- return askSelect('Select profile to edit', profileNames.map(name => ({
408
- value: name,
409
- label: name,
410
- })));
411
- }
412
- const normalizedArgv = normalizeFormatFlag(process.argv.slice(2));
413
- const strictOutputRequested = isStrictOutputRequested(normalizedArgv);
414
- const argvWithAgentDefaults = withAgentDefaultFormat(normalizedArgv, strictOutputRequested);
415
- function shouldRenderPretty(agent) {
416
- return !agent && !strictOutputRequested && !isAgentEnvironment();
417
- }
418
- const routedArgv = routeDefaultCommand(argvWithAgentDefaults);
419
- await cli.serve(routedArgv);
420
- export default cli;
package/dist/config.js DELETED
@@ -1,49 +0,0 @@
1
- import { homedir } from 'node:os';
2
- import { join, resolve } from 'node:path';
3
- import { mkdir, readFile, writeFile, chmod } from 'node:fs/promises';
4
- const DEFAULT_CONFIG = {
5
- defaultProfile: null,
6
- profiles: {},
7
- };
8
- export function getConfigDir() {
9
- const dir = process.env.NNT_CONFIG_DIR;
10
- if (dir && dir.trim() !== '') {
11
- return resolve(dir);
12
- }
13
- return join(homedir(), '.nnt');
14
- }
15
- export function getConfigPath() {
16
- return join(getConfigDir(), 'config');
17
- }
18
- export async function loadConfig() {
19
- const path = getConfigPath();
20
- try {
21
- const raw = await readFile(path, 'utf8');
22
- const parsed = JSON.parse(raw);
23
- return {
24
- defaultProfile: typeof parsed.defaultProfile === 'string' ? parsed.defaultProfile : null,
25
- profiles: parsed.profiles ?? {},
26
- };
27
- }
28
- catch (error) {
29
- if (isNodeError(error) && error.code === 'ENOENT') {
30
- return { ...DEFAULT_CONFIG };
31
- }
32
- throw error;
33
- }
34
- }
35
- export async function saveConfig(config) {
36
- const dir = getConfigDir();
37
- const path = getConfigPath();
38
- await mkdir(dir, { recursive: true });
39
- await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
40
- try {
41
- await chmod(path, 0o600);
42
- }
43
- catch {
44
- // Best effort only.
45
- }
46
- }
47
- function isNodeError(error) {
48
- return typeof error === 'object' && error !== null && 'code' in error;
49
- }
package/dist/display.js DELETED
@@ -1,24 +0,0 @@
1
- import Table from 'cli-table3';
2
- export function printProfilesTable(rows) {
3
- if (rows.length === 0) {
4
- process.stdout.write('No profiles configured. Run `nnt profile add`.\n');
5
- return;
6
- }
7
- const table = new Table({
8
- head: ['Profile', 'Provider', 'Default'],
9
- });
10
- for (const row of rows) {
11
- table.push([row.name, row.provider, row.isDefault ? 'yes' : '']);
12
- }
13
- process.stdout.write(`${table.toString()}\n`);
14
- }
15
- export function printKeyValueTable(title, rows) {
16
- process.stdout.write(`${title}\n`);
17
- const table = new Table({
18
- head: ['Field', 'Value'],
19
- });
20
- for (const row of rows) {
21
- table.push([row.key, row.value]);
22
- }
23
- process.stdout.write(`${table.toString()}\n`);
24
- }
package/dist/prompt.js DELETED
@@ -1,47 +0,0 @@
1
- import { cancel, confirm, isCancel, select, text } from '@clack/prompts';
2
- export async function askRequired(question) {
3
- return askRequiredWithInitial(question);
4
- }
5
- export async function askRequiredWithInitial(question, initialValue) {
6
- const message = normalizeQuestion(question);
7
- const value = await text({
8
- message,
9
- initialValue,
10
- validate(input) {
11
- if (!input || input.trim() === '') {
12
- return 'Value cannot be empty';
13
- }
14
- return undefined;
15
- },
16
- });
17
- if (isCancel(value)) {
18
- cancel('Operation cancelled.');
19
- process.exit(1);
20
- }
21
- return value.trim();
22
- }
23
- export async function askConfirm(question, initialValue = false) {
24
- const value = await confirm({
25
- message: normalizeQuestion(question),
26
- initialValue,
27
- });
28
- if (isCancel(value)) {
29
- cancel('Operation cancelled.');
30
- process.exit(1);
31
- }
32
- return value;
33
- }
34
- export async function askSelect(question, options) {
35
- const value = await select({
36
- message: normalizeQuestion(question),
37
- options,
38
- });
39
- if (isCancel(value)) {
40
- cancel('Operation cancelled.');
41
- process.exit(1);
42
- }
43
- return value;
44
- }
45
- function normalizeQuestion(question) {
46
- return question.trim().replace(/:\s*$/, '');
47
- }
package/dist/telegram.js DELETED
@@ -1,57 +0,0 @@
1
- async function telegramRequest(botToken, method, payload) {
2
- const response = await fetch(`https://api.telegram.org/bot${botToken}/${method}`, {
3
- method: 'POST',
4
- headers: {
5
- 'content-type': 'application/json',
6
- },
7
- body: JSON.stringify(payload),
8
- });
9
- if (!response.ok) {
10
- throw new Error(`Telegram API HTTP ${response.status}`);
11
- }
12
- const json = await response.json();
13
- if (!json.ok) {
14
- throw new Error(json.description);
15
- }
16
- return json.result;
17
- }
18
- export async function getLatestUpdateOffset(botToken) {
19
- const updates = await telegramRequest(botToken, 'getUpdates', {
20
- timeout: 0,
21
- allowed_updates: ['message'],
22
- });
23
- if (updates.length === 0) {
24
- return 0;
25
- }
26
- const maxUpdateId = updates.reduce((acc, item) => Math.max(acc, item.update_id), 0);
27
- return maxUpdateId + 1;
28
- }
29
- export async function waitForChatId(botToken, offset, timeoutSeconds = 120) {
30
- const startedAt = Date.now();
31
- let currentOffset = offset;
32
- while ((Date.now() - startedAt) / 1000 < timeoutSeconds) {
33
- const remainingSeconds = timeoutSeconds - Math.floor((Date.now() - startedAt) / 1000);
34
- const pollTimeout = Math.max(1, Math.min(25, remainingSeconds));
35
- const updates = await telegramRequest(botToken, 'getUpdates', {
36
- offset: currentOffset,
37
- timeout: pollTimeout,
38
- allowed_updates: ['message'],
39
- });
40
- for (const update of updates) {
41
- currentOffset = Math.max(currentOffset, update.update_id + 1);
42
- if (update.message?.chat?.id !== undefined) {
43
- return {
44
- chatId: String(update.message.chat.id),
45
- username: update.message.from?.username ?? update.message.chat.username ?? null,
46
- };
47
- }
48
- }
49
- }
50
- throw new Error('Timed out waiting for Telegram message. Send a message to your bot and try again.');
51
- }
52
- export async function sendTelegramMessage(botToken, chatId, text) {
53
- await telegramRequest(botToken, 'sendMessage', {
54
- chat_id: chatId,
55
- text,
56
- });
57
- }