nonotify 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -0
- package/dist/cli.js +420 -0
- package/dist/config.js +49 -0
- package/dist/display.js +24 -0
- package/dist/prompt.js +47 -0
- package/dist/telegram.js +57 -0
- package/package.json +31 -0
- package/src/cli.ts +520 -0
- package/src/config.ts +74 -0
- package/src/display.ts +38 -0
- package/src/prompt.ts +67 -0
- package/src/telegram.ts +109 -0
- package/tsconfig.json +17 -0
package/dist/telegram.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nonotify",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "nnt CLI for Telegram notifications",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nnt": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p tsconfig.json",
|
|
11
|
+
"dev": "tsx src/cli.ts",
|
|
12
|
+
"start": "node dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cli",
|
|
16
|
+
"telegram",
|
|
17
|
+
"notifications",
|
|
18
|
+
"incur"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@clack/prompts": "^1.0.1",
|
|
23
|
+
"cli-table3": "^0.6.5",
|
|
24
|
+
"incur": "^0.1.8"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.6.2",
|
|
28
|
+
"tsx": "^4.20.6",
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
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
|
+
|
|
8
|
+
const profileCli = Cli.create('profile', {
|
|
9
|
+
description: 'Manage notification profiles',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
profileCli.command('add', {
|
|
13
|
+
description: 'Add a notification profile',
|
|
14
|
+
outputPolicy: 'agent-only',
|
|
15
|
+
args: z.object({
|
|
16
|
+
provider: z.enum(['telegram']).default('telegram').describe('Profile provider'),
|
|
17
|
+
}),
|
|
18
|
+
async run(c) {
|
|
19
|
+
if (c.args.provider !== 'telegram') {
|
|
20
|
+
throw new Error(`Unsupported provider: ${c.args.provider}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const config = await loadConfig()
|
|
24
|
+
const profileName = await askRequired('Profile name: ')
|
|
25
|
+
|
|
26
|
+
if (config.profiles[profileName]) {
|
|
27
|
+
throw new Error(`Profile "${profileName}" already exists.`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const botToken = await askRequired('Telegram bot token: ')
|
|
31
|
+
|
|
32
|
+
if (shouldRenderPretty(c.agent)) {
|
|
33
|
+
process.stdout.write('\nSend any message to your bot in Telegram.\n')
|
|
34
|
+
process.stdout.write('Waiting for message to detect chat_id (up to 120s)...\n')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const offset = await getLatestUpdateOffset(botToken)
|
|
38
|
+
const connection = await waitForChatId(botToken, offset, 120)
|
|
39
|
+
const chatId = connection.chatId
|
|
40
|
+
|
|
41
|
+
if (shouldRenderPretty(c.agent)) {
|
|
42
|
+
if (connection.username) {
|
|
43
|
+
process.stdout.write(`Connected Telegram username: @${connection.username}\n`)
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
process.stdout.write('Connected Telegram user has no username set.\n')
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
config.profiles[profileName] = {
|
|
51
|
+
type: 'telegram',
|
|
52
|
+
name: profileName,
|
|
53
|
+
botToken,
|
|
54
|
+
chatId,
|
|
55
|
+
createdAt: new Date().toISOString(),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!config.defaultProfile) {
|
|
59
|
+
config.defaultProfile = profileName
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await saveConfig(config)
|
|
63
|
+
|
|
64
|
+
let confirmationSent = false
|
|
65
|
+
let confirmationWarning: string | null = null
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await sendTelegramMessage(
|
|
69
|
+
botToken,
|
|
70
|
+
chatId,
|
|
71
|
+
`nnt: profile "${profileName}" connected successfully. You can now send notifications from CLI.`,
|
|
72
|
+
)
|
|
73
|
+
confirmationSent = true
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
confirmationWarning = error instanceof Error ? error.message : 'Unknown error while sending confirmation'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (shouldRenderPretty(c.agent)) {
|
|
80
|
+
printKeyValueTable('Profile added', [
|
|
81
|
+
{ key: 'profile', value: profileName },
|
|
82
|
+
{ key: 'provider', value: 'telegram' },
|
|
83
|
+
{ key: 'chat_id', value: chatId },
|
|
84
|
+
{ key: 'username', value: connection.username ? `@${connection.username}` : '(none)' },
|
|
85
|
+
{ key: 'default', value: config.defaultProfile ?? '(none)' },
|
|
86
|
+
])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
added: true,
|
|
91
|
+
profile: profileName,
|
|
92
|
+
provider: 'telegram',
|
|
93
|
+
chatId,
|
|
94
|
+
username: connection.username,
|
|
95
|
+
defaultProfile: config.defaultProfile,
|
|
96
|
+
configPath: getConfigPath(),
|
|
97
|
+
confirmationSent,
|
|
98
|
+
confirmationWarning,
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
profileCli.command('list', {
|
|
104
|
+
description: 'List configured profiles',
|
|
105
|
+
outputPolicy: 'agent-only',
|
|
106
|
+
async run(c) {
|
|
107
|
+
const config = await loadConfig()
|
|
108
|
+
const names = Object.keys(config.profiles).sort((a, b) => a.localeCompare(b))
|
|
109
|
+
const profiles = names.map(name => ({
|
|
110
|
+
name,
|
|
111
|
+
provider: config.profiles[name].type,
|
|
112
|
+
isDefault: name === config.defaultProfile,
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
if (shouldRenderPretty(c.agent)) {
|
|
116
|
+
printProfilesTable(profiles)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
defaultProfile: config.defaultProfile,
|
|
121
|
+
totalProfiles: names.length,
|
|
122
|
+
profiles,
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
profileCli.command('default', {
|
|
128
|
+
description: 'Get or set default profile',
|
|
129
|
+
outputPolicy: 'agent-only',
|
|
130
|
+
args: z.object({
|
|
131
|
+
profile: z.string().optional().describe('Profile name to set as default'),
|
|
132
|
+
}),
|
|
133
|
+
async run(c) {
|
|
134
|
+
const config = await loadConfig()
|
|
135
|
+
|
|
136
|
+
if (!c.args.profile) {
|
|
137
|
+
if (shouldRenderPretty(c.agent)) {
|
|
138
|
+
printKeyValueTable('Default profile', [
|
|
139
|
+
{ key: 'default', value: config.defaultProfile ?? '(not set)' },
|
|
140
|
+
])
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
defaultProfile: config.defaultProfile,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!config.profiles[c.args.profile]) {
|
|
149
|
+
throw new Error(`Profile "${c.args.profile}" not found.`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
config.defaultProfile = c.args.profile
|
|
153
|
+
await saveConfig(config)
|
|
154
|
+
|
|
155
|
+
if (shouldRenderPretty(c.agent)) {
|
|
156
|
+
printKeyValueTable('Default profile updated', [
|
|
157
|
+
{ key: 'default', value: config.defaultProfile ?? '(not set)' },
|
|
158
|
+
])
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
updated: true,
|
|
163
|
+
defaultProfile: config.defaultProfile,
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
profileCli.command('delete', {
|
|
169
|
+
description: 'Delete a profile',
|
|
170
|
+
outputPolicy: 'agent-only',
|
|
171
|
+
args: z.object({
|
|
172
|
+
profile: z.string().describe('Profile name to delete'),
|
|
173
|
+
}),
|
|
174
|
+
async run(c) {
|
|
175
|
+
const config = await loadConfig()
|
|
176
|
+
const targetName = c.args.profile
|
|
177
|
+
const profile = config.profiles[targetName]
|
|
178
|
+
|
|
179
|
+
if (!profile) {
|
|
180
|
+
throw new Error(`Profile "${targetName}" not found.`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
delete config.profiles[targetName]
|
|
184
|
+
|
|
185
|
+
if (config.defaultProfile === targetName) {
|
|
186
|
+
const remaining = Object.keys(config.profiles).sort((a, b) => a.localeCompare(b))
|
|
187
|
+
config.defaultProfile = remaining[0] ?? null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await saveConfig(config)
|
|
191
|
+
|
|
192
|
+
if (shouldRenderPretty(c.agent)) {
|
|
193
|
+
printKeyValueTable('Profile deleted', [
|
|
194
|
+
{ key: 'profile', value: targetName },
|
|
195
|
+
{ key: 'provider', value: profile.type },
|
|
196
|
+
{ key: 'default', value: config.defaultProfile ?? '(not set)' },
|
|
197
|
+
])
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
deleted: true,
|
|
202
|
+
profile: targetName,
|
|
203
|
+
provider: profile.type,
|
|
204
|
+
defaultProfile: config.defaultProfile,
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
profileCli.command('edit', {
|
|
210
|
+
description: 'Edit profile data',
|
|
211
|
+
outputPolicy: 'agent-only',
|
|
212
|
+
args: z.object({
|
|
213
|
+
profile: z.string().optional().describe('Existing profile name'),
|
|
214
|
+
}),
|
|
215
|
+
options: z.object({
|
|
216
|
+
newName: z.string().optional().describe('Rename profile to a new name'),
|
|
217
|
+
botToken: z.string().optional().describe('Replace Telegram bot token'),
|
|
218
|
+
chatId: z.string().optional().describe('Replace Telegram chat id'),
|
|
219
|
+
reconnect: z.boolean().optional().describe('Re-detect chat id from next Telegram message'),
|
|
220
|
+
}),
|
|
221
|
+
alias: {
|
|
222
|
+
newName: 'n',
|
|
223
|
+
botToken: 't',
|
|
224
|
+
chatId: 'c',
|
|
225
|
+
reconnect: 'r',
|
|
226
|
+
},
|
|
227
|
+
async run(c) {
|
|
228
|
+
const config = await loadConfig()
|
|
229
|
+
const profileNames = Object.keys(config.profiles).sort((a, b) => a.localeCompare(b))
|
|
230
|
+
|
|
231
|
+
if (profileNames.length === 0) {
|
|
232
|
+
throw new Error('No profiles found. Run `nnt profile add` first.')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const hasDirectEditOptions = Boolean(c.options.newName || c.options.botToken || c.options.chatId || c.options.reconnect)
|
|
236
|
+
|
|
237
|
+
const sourceName = await resolveProfileForEdit(c.args.profile, profileNames, hasDirectEditOptions)
|
|
238
|
+
const sourceProfile = config.profiles[sourceName]
|
|
239
|
+
|
|
240
|
+
if (!sourceProfile) {
|
|
241
|
+
throw new Error(`Profile "${sourceName}" not found.`)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const targetName = c.options.newName ?? sourceName
|
|
245
|
+
|
|
246
|
+
if (targetName !== sourceName && config.profiles[targetName]) {
|
|
247
|
+
throw new Error(`Profile "${targetName}" already exists.`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const botToken = c.options.botToken ?? sourceProfile.botToken
|
|
251
|
+
let nextName = targetName
|
|
252
|
+
let nextBotToken = botToken
|
|
253
|
+
let chatId = c.options.chatId ?? sourceProfile.chatId
|
|
254
|
+
let connectedUsername: string | null = null
|
|
255
|
+
|
|
256
|
+
if (!hasDirectEditOptions && canPromptInteractively()) {
|
|
257
|
+
nextName = await askRequiredWithInitial('Profile name', sourceName)
|
|
258
|
+
|
|
259
|
+
if (nextName !== sourceName && config.profiles[nextName]) {
|
|
260
|
+
throw new Error(`Profile "${nextName}" already exists.`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
nextBotToken = await askRequiredWithInitial('Telegram bot token', sourceProfile.botToken)
|
|
264
|
+
|
|
265
|
+
const shouldReconnect = await askConfirm('Reconnect and detect chat_id from a new message?', false)
|
|
266
|
+
|
|
267
|
+
if (shouldReconnect) {
|
|
268
|
+
if (shouldRenderPretty(c.agent)) {
|
|
269
|
+
process.stdout.write('\nSend any message to your bot in Telegram.\n')
|
|
270
|
+
process.stdout.write('Waiting for message to detect chat_id (up to 120s)...\n')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const offset = await getLatestUpdateOffset(nextBotToken)
|
|
274
|
+
const connection = await waitForChatId(nextBotToken, offset, 120)
|
|
275
|
+
chatId = connection.chatId
|
|
276
|
+
connectedUsername = connection.username
|
|
277
|
+
|
|
278
|
+
if (shouldRenderPretty(c.agent)) {
|
|
279
|
+
if (connection.username) {
|
|
280
|
+
process.stdout.write(`Connected Telegram username: @${connection.username}\n`)
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
process.stdout.write('Connected Telegram user has no username set.\n')
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
chatId = await askRequiredWithInitial('Telegram chat_id', sourceProfile.chatId)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (c.options.reconnect) {
|
|
293
|
+
if (shouldRenderPretty(c.agent)) {
|
|
294
|
+
process.stdout.write('\nSend any message to your bot in Telegram.\n')
|
|
295
|
+
process.stdout.write('Waiting for message to detect chat_id (up to 120s)...\n')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const offset = await getLatestUpdateOffset(nextBotToken)
|
|
299
|
+
const connection = await waitForChatId(nextBotToken, offset, 120)
|
|
300
|
+
chatId = connection.chatId
|
|
301
|
+
connectedUsername = connection.username
|
|
302
|
+
|
|
303
|
+
if (shouldRenderPretty(c.agent)) {
|
|
304
|
+
if (connection.username) {
|
|
305
|
+
process.stdout.write(`Connected Telegram username: @${connection.username}\n`)
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
process.stdout.write('Connected Telegram user has no username set.\n')
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const updatedProfile = {
|
|
314
|
+
...sourceProfile,
|
|
315
|
+
name: nextName,
|
|
316
|
+
botToken: nextBotToken,
|
|
317
|
+
chatId,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (nextName !== sourceName) {
|
|
321
|
+
delete config.profiles[sourceName]
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
config.profiles[nextName] = updatedProfile
|
|
325
|
+
|
|
326
|
+
if (config.defaultProfile === sourceName) {
|
|
327
|
+
config.defaultProfile = nextName
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const hasChanges =
|
|
331
|
+
nextName !== sourceName
|
|
332
|
+
|| nextBotToken !== sourceProfile.botToken
|
|
333
|
+
|| chatId !== sourceProfile.chatId
|
|
334
|
+
|
|
335
|
+
if (!hasChanges) {
|
|
336
|
+
return {
|
|
337
|
+
updated: false,
|
|
338
|
+
profile: sourceName,
|
|
339
|
+
provider: sourceProfile.type,
|
|
340
|
+
defaultProfile: config.defaultProfile,
|
|
341
|
+
connectedUsername,
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await saveConfig(config)
|
|
346
|
+
|
|
347
|
+
if (shouldRenderPretty(c.agent)) {
|
|
348
|
+
printKeyValueTable('Profile updated', [
|
|
349
|
+
{ key: 'profile', value: nextName },
|
|
350
|
+
{ key: 'provider', value: updatedProfile.type },
|
|
351
|
+
{ key: 'chat_id', value: chatId },
|
|
352
|
+
{ key: 'username', value: connectedUsername ? `@${connectedUsername}` : '(unchanged/none)' },
|
|
353
|
+
{ key: 'default', value: config.defaultProfile ?? '(not set)' },
|
|
354
|
+
])
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
updated: true,
|
|
359
|
+
previousProfile: sourceName,
|
|
360
|
+
profile: nextName,
|
|
361
|
+
provider: updatedProfile.type,
|
|
362
|
+
defaultProfile: config.defaultProfile,
|
|
363
|
+
connectedUsername,
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
const cli = Cli.create('nnt', {
|
|
369
|
+
description: 'Send Telegram notifications from terminal and agents',
|
|
370
|
+
})
|
|
371
|
+
.command('send', {
|
|
372
|
+
description: 'Send a message via a saved profile',
|
|
373
|
+
args: z.object({
|
|
374
|
+
message: z.string().describe('Message text to send'),
|
|
375
|
+
}),
|
|
376
|
+
options: z.object({
|
|
377
|
+
profile: z.string().optional().describe('Profile name from config'),
|
|
378
|
+
}),
|
|
379
|
+
alias: {
|
|
380
|
+
profile: 'p',
|
|
381
|
+
},
|
|
382
|
+
async run(c) {
|
|
383
|
+
const config = await loadConfig()
|
|
384
|
+
const profileName = c.options.profile ?? config.defaultProfile
|
|
385
|
+
|
|
386
|
+
if (!profileName) {
|
|
387
|
+
throw new Error('No default profile found. Run `nnt profile add` first.')
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const profile = config.profiles[profileName]
|
|
391
|
+
|
|
392
|
+
if (!profile) {
|
|
393
|
+
throw new Error(`Profile "${profileName}" not found.`)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await sendTelegramMessage(profile.botToken, profile.chatId, c.args.message)
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
sent: true,
|
|
400
|
+
profile: profileName,
|
|
401
|
+
provider: profile.type,
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
.command(profileCli)
|
|
406
|
+
|
|
407
|
+
function routeDefaultCommand(argv: string[]): string[] {
|
|
408
|
+
if (argv.length === 0) {
|
|
409
|
+
return argv
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const topLevelCommands = new Set(['profile', 'send', 'skills', 'mcp'])
|
|
413
|
+
const bareGlobalFlags = new Set(['--help', '-h', '--version', '--llms', '--mcp', '--json', '--verbose'])
|
|
414
|
+
|
|
415
|
+
let index = 0
|
|
416
|
+
while (index < argv.length) {
|
|
417
|
+
const token = argv[index]
|
|
418
|
+
|
|
419
|
+
if (token === '--format') {
|
|
420
|
+
index += 2
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (bareGlobalFlags.has(token)) {
|
|
425
|
+
index += 1
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
break
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (index >= argv.length) {
|
|
433
|
+
return argv
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (topLevelCommands.has(argv[index])) {
|
|
437
|
+
return argv
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return [...argv.slice(0, index), 'send', ...argv.slice(index)]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function normalizeFormatFlag(argv: string[]): string[] {
|
|
444
|
+
const normalized: string[] = []
|
|
445
|
+
|
|
446
|
+
for (const token of argv) {
|
|
447
|
+
if (token.startsWith('--format=')) {
|
|
448
|
+
normalized.push('--format', token.slice('--format='.length))
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
normalized.push(token)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return normalized
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function isStrictOutputRequested(argv: string[]): boolean {
|
|
459
|
+
for (const token of argv) {
|
|
460
|
+
if (token === '--json' || token === '--verbose' || token === '--format') {
|
|
461
|
+
return true
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return false
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function isAgentEnvironment(): boolean {
|
|
469
|
+
return [
|
|
470
|
+
process.env.OPENCODE,
|
|
471
|
+
process.env.CLAUDECODE,
|
|
472
|
+
process.env.CURSOR_AGENT,
|
|
473
|
+
process.env.AIDER_SESSION,
|
|
474
|
+
process.env.NNT_AGENT_MODE,
|
|
475
|
+
].some(Boolean)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function withAgentDefaultFormat(argv: string[], strictOutputRequested: boolean): string[] {
|
|
479
|
+
if (strictOutputRequested || !isAgentEnvironment()) {
|
|
480
|
+
return argv
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return ['--format', 'toon', ...argv]
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function canPromptInteractively(): boolean {
|
|
487
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function resolveProfileForEdit(
|
|
491
|
+
profileFromArgs: string | undefined,
|
|
492
|
+
profileNames: string[],
|
|
493
|
+
hasDirectEditOptions: boolean,
|
|
494
|
+
): Promise<string> {
|
|
495
|
+
if (profileFromArgs) {
|
|
496
|
+
return profileFromArgs
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (hasDirectEditOptions || !canPromptInteractively()) {
|
|
500
|
+
throw new Error('Profile name is required in non-interactive mode.')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return askSelect('Select profile to edit', profileNames.map(name => ({
|
|
504
|
+
value: name,
|
|
505
|
+
label: name,
|
|
506
|
+
})))
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const normalizedArgv = normalizeFormatFlag(process.argv.slice(2))
|
|
510
|
+
const strictOutputRequested = isStrictOutputRequested(normalizedArgv)
|
|
511
|
+
const argvWithAgentDefaults = withAgentDefaultFormat(normalizedArgv, strictOutputRequested)
|
|
512
|
+
|
|
513
|
+
function shouldRenderPretty(agent: boolean): boolean {
|
|
514
|
+
return !agent && !strictOutputRequested && !isAgentEnvironment()
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const routedArgv = routeDefaultCommand(argvWithAgentDefaults)
|
|
518
|
+
await cli.serve(routedArgv)
|
|
519
|
+
|
|
520
|
+
export default cli
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
import { mkdir, readFile, writeFile, chmod } from 'node:fs/promises'
|
|
4
|
+
|
|
5
|
+
export type TelegramProfile = {
|
|
6
|
+
type: 'telegram'
|
|
7
|
+
name: string
|
|
8
|
+
botToken: string
|
|
9
|
+
chatId: string
|
|
10
|
+
createdAt: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type NntConfig = {
|
|
14
|
+
defaultProfile: string | null
|
|
15
|
+
profiles: Record<string, TelegramProfile>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_CONFIG: NntConfig = {
|
|
19
|
+
defaultProfile: null,
|
|
20
|
+
profiles: {},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getConfigDir(): string {
|
|
24
|
+
const dir = process.env.NNT_CONFIG_DIR
|
|
25
|
+
if (dir && dir.trim() !== '') {
|
|
26
|
+
return resolve(dir)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return join(homedir(), '.nnt')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getConfigPath(): string {
|
|
33
|
+
return join(getConfigDir(), 'config')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function loadConfig(): Promise<NntConfig> {
|
|
37
|
+
const path = getConfigPath()
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const raw = await readFile(path, 'utf8')
|
|
41
|
+
const parsed = JSON.parse(raw) as Partial<NntConfig>
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
defaultProfile: typeof parsed.defaultProfile === 'string' ? parsed.defaultProfile : null,
|
|
45
|
+
profiles: parsed.profiles ?? {},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
50
|
+
return { ...DEFAULT_CONFIG }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function saveConfig(config: NntConfig): Promise<void> {
|
|
58
|
+
const dir = getConfigDir()
|
|
59
|
+
const path = getConfigPath()
|
|
60
|
+
|
|
61
|
+
await mkdir(dir, { recursive: true })
|
|
62
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 })
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await chmod(path, 0o600)
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Best effort only.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
73
|
+
return typeof error === 'object' && error !== null && 'code' in error
|
|
74
|
+
}
|