omnikey-cli 1.5.3 → 1.5.5

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 CHANGED
@@ -21,6 +21,7 @@ OmnikeyAI is a productivity tool that helps you quickly rewrite selected text us
21
21
  - `omnikey grant-browser-access`: One-time setup to give Omnikey access to authenticated browser tabs for web fetch.
22
22
  - Scheduled Jobs commands to create, list, delete, and trigger jobs from the CLI.
23
23
  - `omnikey mcp`: manage MCP (Model Context Protocol) servers available to the agent (stdio, HTTP, SSE transports).
24
+ - `omnikey telegram`: run the Telegram client as a persistent background daemon — receive Omnikey notifications in your Telegram chat and interact with the agent from your phone.
24
25
 
25
26
  ## Usage
26
27
 
@@ -93,6 +94,24 @@ omnikey mcp toggle <id>
93
94
 
94
95
  # Edit an MCP server by ID (interactive, current values as defaults)
95
96
  omnikey mcp update <id>
97
+
98
+ # Install and start the Telegram bot daemon (prompts for credentials on first run)
99
+ omnikey telegram start
100
+
101
+ # Stop the Telegram bot daemon
102
+ omnikey telegram stop
103
+
104
+ # Restart the Telegram bot daemon
105
+ omnikey telegram restart
106
+
107
+ # Show daemon status (launchd/NSSM info + port check)
108
+ omnikey telegram status
109
+
110
+ # Tail the Telegram bot daemon logs
111
+ omnikey telegram logs
112
+
113
+ # Stop and fully remove the Telegram bot daemon
114
+ omnikey telegram uninstall
96
115
  ```
97
116
 
98
117
  ### Command reference
@@ -119,6 +138,12 @@ omnikey mcp update <id>
119
138
  | `omnikey mcp remove` | Remove an MCP server via interactive selection and confirmation |
120
139
  | `omnikey mcp toggle <id>` | Enable or disable an MCP server by ID |
121
140
  | `omnikey mcp update <id>` | Edit an MCP server by ID (interactive, current values as defaults) |
141
+ | `omnikey telegram start` | Install and start the Telegram bot daemon (prompts for credentials) |
142
+ | `omnikey telegram stop` | Stop the Telegram bot daemon |
143
+ | `omnikey telegram restart` | Restart the Telegram bot daemon |
144
+ | `omnikey telegram status` | Show daemon status (launchd/NSSM info + port check) |
145
+ | `omnikey telegram logs` | Tail the Telegram bot daemon logs |
146
+ | `omnikey telegram uninstall` | Stop and fully remove the Telegram bot daemon |
122
147
 
123
148
  ## Scheduled Jobs
124
149
 
@@ -231,6 +256,45 @@ The CLI automatically enables **"Allow JavaScript from Apple Events"** for every
231
256
 
232
257
  Both changes are permanent and survive reboots. Restart each browser once after setup for the change to take effect.
233
258
 
259
+ ## Telegram Bot Daemon
260
+
261
+ The `omnikey telegram` command group runs a Telegram bot as a persistent background service. Once started, the bot sends Omnikey notifications to your Telegram chat and lets you interact with the agent directly from your phone.
262
+
263
+ For setup instructions (creating a bot, finding your chat ID, and configuring credentials) see the [Telegram README](../telegram/README.md).
264
+
265
+ ### `omnikey telegram start`
266
+
267
+ Installs and starts the daemon. If `TELEGRAM_BOT_TOKEN` or `TELEGRAM_CHAT_ID` are not yet saved, the CLI prompts for them, validates the token against the Telegram API, and saves them to `~/.omnikey/config.json` before installing the service. On macOS the bot runs as a **launchd agent**; on Windows as an **NSSM service**. Both auto-restart on crash and start automatically on login.
268
+
269
+ ### `omnikey telegram stop`
270
+
271
+ Unloads the launchd agent (macOS) or stops the NSSM service (Windows). The service definition is kept so `omnikey telegram start` can bring it back without re-entering credentials.
272
+
273
+ ### `omnikey telegram restart`
274
+
275
+ Equivalent to `stop` followed by `start`. Useful after updating credentials or rotating the bot token.
276
+
277
+ ### `omnikey telegram status`
278
+
279
+ Prints:
280
+
281
+ - Path to the service definition (plist on macOS, service name on Windows)
282
+ - Current launchd / NSSM status including the running PID
283
+ - Whether anything is listening on the bot's HTTP port (default `6666`)
284
+
285
+ ### `omnikey telegram logs`
286
+
287
+ Tails the last 100 lines of both stdout and stderr logs and follows new output (Ctrl-C to stop).
288
+
289
+ | Platform | Log files |
290
+ | -------- | --------- |
291
+ | macOS | `~/Library/Logs/telegram/out.log`, `~/Library/Logs/telegram/err.log` |
292
+ | Windows | `~/.omnikey/telegram/daemon.log`, `~/.omnikey/telegram/daemon-error.log` |
293
+
294
+ ### `omnikey telegram uninstall`
295
+
296
+ Stops the daemon and removes the service definition entirely. Run `omnikey telegram start` to reinstall from scratch.
297
+
234
298
  ## Platform notes
235
299
 
236
300
  ### macOS
package/dist/index.js CHANGED
@@ -13,11 +13,12 @@ const setConfig_1 = require("./setConfig");
13
13
  const grantBrowserAccess_1 = require("./grantBrowserAccess");
14
14
  const scheduleJob_1 = require("./scheduleJob");
15
15
  const mcpServer_1 = require("./mcpServer");
16
+ const telegramDaemon_1 = require("./telegramDaemon");
16
17
  const program = new commander_1.Command();
17
18
  program
18
19
  .name('omnikey')
19
20
  .description('Omnikey CLI for onboarding and configuration')
20
- .version('1.0.0');
21
+ .version('1.5.4');
21
22
  program
22
23
  .command('onboard')
23
24
  .description('Onboard and configure your AI provider')
@@ -28,9 +29,13 @@ program
28
29
  .command('daemon')
29
30
  .description('Start the Omnikey API backend as a daemon on a specified port')
30
31
  .option('--port <port>', 'Port to run the backend on', '7071')
32
+ .option('--telegram', 'Also install and start the Telegram bot daemon')
31
33
  .action(async (options) => {
32
34
  const port = Number(options.port) || 7071;
33
35
  await (0, daemon_1.startDaemon)(port);
36
+ if (options.telegram) {
37
+ await (0, telegramDaemon_1.startTelegramDaemon)();
38
+ }
34
39
  });
35
40
  program
36
41
  .command('kill-daemon')
@@ -77,10 +82,14 @@ program
77
82
  .command('restart-daemon')
78
83
  .description('Restart the Omnikey API backend daemon')
79
84
  .option('--port <port>', 'Port to run the backend on', '7071')
85
+ .option('--telegram', 'Also restart the Telegram bot daemon')
80
86
  .action(async (options) => {
81
87
  (0, killDaemon_1.killDaemon)();
82
88
  const port = Number(options.port) || 7071;
83
89
  await (0, daemon_1.startDaemon)(port);
90
+ if (options.telegram) {
91
+ await (0, telegramDaemon_1.restartTelegramDaemon)();
92
+ }
84
93
  });
85
94
  program
86
95
  .command('grant-browser-access')
@@ -154,4 +163,44 @@ mcpCmd
154
163
  .action(async (id) => {
155
164
  await (0, mcpServer_1.mcpUpdate)(id);
156
165
  });
166
+ const telegramDaemonCmd = program
167
+ .command('telegram')
168
+ .description('Manage the Telegram bot daemon (launchd on macOS, NSSM on Windows). ' +
169
+ 'Run `omnikey telegram start` to install and start.');
170
+ telegramDaemonCmd
171
+ .command('start')
172
+ .description('Install and start the Telegram bot daemon (survives reboots, auto-restarts)')
173
+ .action(async () => {
174
+ await (0, telegramDaemon_1.startTelegramDaemon)();
175
+ });
176
+ telegramDaemonCmd
177
+ .command('stop')
178
+ .description('Stop the Telegram bot daemon')
179
+ .action(() => {
180
+ (0, telegramDaemon_1.stopTelegramDaemon)();
181
+ });
182
+ telegramDaemonCmd
183
+ .command('restart')
184
+ .description('Restart the Telegram bot daemon')
185
+ .action(async () => {
186
+ await (0, telegramDaemon_1.restartTelegramDaemon)();
187
+ });
188
+ telegramDaemonCmd
189
+ .command('status')
190
+ .description('Show the current status of the Telegram bot daemon')
191
+ .action(() => {
192
+ (0, telegramDaemon_1.statusTelegramDaemon)();
193
+ });
194
+ telegramDaemonCmd
195
+ .command('logs')
196
+ .description('Tail the Telegram bot daemon logs')
197
+ .action(() => {
198
+ (0, telegramDaemon_1.logsTelegramDaemon)();
199
+ });
200
+ telegramDaemonCmd
201
+ .command('uninstall')
202
+ .description('Stop and remove the Telegram bot daemon')
203
+ .action(() => {
204
+ (0, telegramDaemon_1.uninstallTelegramDaemon)();
205
+ });
157
206
  program.parseAsync(process.argv);
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ensureTelegramConfig = ensureTelegramConfig;
7
+ exports.spawnTelegramClient = spawnTelegramClient;
8
+ exports.startTelegramClientCommand = startTelegramClientCommand;
9
+ const path_1 = __importDefault(require("path"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const child_process_1 = require("child_process");
12
+ const https_1 = require("https");
13
+ const inquirer_1 = __importDefault(require("inquirer"));
14
+ const utils_1 = require("./utils");
15
+ const REQUIRED_ENV_KEYS = ['TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID'];
16
+ /**
17
+ * Where the bundled bot lives at runtime. The CLI's `build:telegram-client`
18
+ * script populates this directory with the bot's compiled output plus its
19
+ * production `node_modules`.
20
+ */
21
+ function resolveBundleRoot() {
22
+ // dist/telegramClient.js → ../telegram-client-dist
23
+ return path_1.default.resolve(__dirname, '..', 'telegram-client-dist');
24
+ }
25
+ function resolveBundledEntry() {
26
+ return path_1.default.join(resolveBundleRoot(), 'dist', 'index.js');
27
+ }
28
+ function persistConfig(values) {
29
+ const configDir = (0, utils_1.getConfigDir)();
30
+ const configPath = (0, utils_1.getConfigPath)();
31
+ const existing = (0, utils_1.readConfig)();
32
+ const merged = { ...existing, ...values };
33
+ fs_1.default.mkdirSync(configDir, { recursive: true });
34
+ fs_1.default.writeFileSync(configPath, JSON.stringify(merged, null, 2), 'utf-8');
35
+ }
36
+ /**
37
+ * Verify a Telegram bot token via the Bot API's getMe endpoint.
38
+ * Resolves to the bot's @username on success, throws on failure.
39
+ */
40
+ function verifyToken(token) {
41
+ return new Promise((resolve, reject) => {
42
+ const req = (0, https_1.request)({
43
+ method: 'GET',
44
+ hostname: 'api.telegram.org',
45
+ path: `/bot${token}/getMe`,
46
+ }, (res) => {
47
+ let body = '';
48
+ res.on('data', (chunk) => (body += chunk));
49
+ res.on('end', () => {
50
+ try {
51
+ const parsed = JSON.parse(body);
52
+ if (!parsed.ok) {
53
+ reject(new Error(parsed.description || 'Telegram rejected the token'));
54
+ return;
55
+ }
56
+ resolve(parsed.result?.username || 'bot');
57
+ }
58
+ catch (err) {
59
+ reject(new Error(`Invalid JSON from Telegram: ${body}`));
60
+ }
61
+ });
62
+ });
63
+ req.on('error', reject);
64
+ req.end();
65
+ });
66
+ }
67
+ /**
68
+ * Ensure TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID are present in
69
+ * ~/.omnikey/config.json. Prompts for any missing values (unless
70
+ * `nonInteractive` is set) and validates the token against the Bot API.
71
+ *
72
+ * Returns the resolved config values (existing or newly captured).
73
+ */
74
+ async function ensureTelegramConfig(options = {}) {
75
+ const existing = (0, utils_1.readConfig)();
76
+ const resolved = {};
77
+ const missing = [];
78
+ for (const key of REQUIRED_ENV_KEYS) {
79
+ const value = existing[key];
80
+ if (typeof value === 'string' && value.trim() !== '') {
81
+ resolved[key] = value.trim();
82
+ }
83
+ else {
84
+ missing.push(key);
85
+ }
86
+ }
87
+ if (missing.length === 0) {
88
+ return resolved;
89
+ }
90
+ if (options.nonInteractive) {
91
+ throw new Error(`Missing required Telegram config in ${(0, utils_1.getConfigPath)()}: ${missing.join(', ')}`);
92
+ }
93
+ console.log('\nTelegram client configuration required.');
94
+ console.log('See telegram/README.md for how to create a bot with @BotFather and find your chat id.\n');
95
+ const toPersist = {};
96
+ if (missing.includes('TELEGRAM_BOT_TOKEN')) {
97
+ const { token } = await inquirer_1.default.prompt([
98
+ {
99
+ type: 'password',
100
+ name: 'token',
101
+ mask: '*',
102
+ message: 'Enter your Telegram bot token (from @BotFather):',
103
+ validate: (input) => /^\d+:[A-Za-z0-9_-]{20,}$/.test(input.trim()) ||
104
+ 'Token should look like 123456789:ABC... (digits, colon, 20+ chars)',
105
+ },
106
+ ]);
107
+ try {
108
+ const username = await verifyToken(token.trim());
109
+ console.log(`Token validated for @${username}.`);
110
+ }
111
+ catch (err) {
112
+ throw new Error(`Telegram rejected the token: ${err instanceof Error ? err.message : String(err)}`);
113
+ }
114
+ resolved.TELEGRAM_BOT_TOKEN = token.trim();
115
+ toPersist.TELEGRAM_BOT_TOKEN = token.trim();
116
+ }
117
+ if (missing.includes('TELEGRAM_CHAT_ID')) {
118
+ const { chatId } = await inquirer_1.default.prompt([
119
+ {
120
+ type: 'input',
121
+ name: 'chatId',
122
+ message: 'Enter the chat id to receive notifications (curl https://api.telegram.org/bot<token>/getUpdates):',
123
+ validate: (input) => /^-?\d+$/.test(input.trim()) || 'Chat id must be a numeric value (groups are negative)',
124
+ },
125
+ ]);
126
+ resolved.TELEGRAM_CHAT_ID = chatId.trim();
127
+ toPersist.TELEGRAM_CHAT_ID = chatId.trim();
128
+ }
129
+ if (Object.keys(toPersist).length > 0) {
130
+ persistConfig(toPersist);
131
+ console.log(`Saved Telegram config to ${(0, utils_1.getConfigPath)()}.`);
132
+ }
133
+ return resolved;
134
+ }
135
+ /**
136
+ * Spawn the bundled telegram-bot server as a long-lived child process.
137
+ * The bot reads PORT from process.env (defaults to 7072 in the app), so we
138
+ * inject the CLI's chosen port that way to keep the upstream code untouched.
139
+ */
140
+ function spawnTelegramClient(port, env) {
141
+ const bundleRoot = resolveBundleRoot();
142
+ const entry = resolveBundledEntry();
143
+ if (!fs_1.default.existsSync(entry)) {
144
+ throw new Error(`Bundled telegram-client not found at ${entry}. ` +
145
+ 'Reinstall omnikey-cli or run `npm run build` from the cli/ directory.');
146
+ }
147
+ const configDir = (0, utils_1.getConfigDir)();
148
+ fs_1.default.mkdirSync(configDir, { recursive: true });
149
+ const logPath = path_1.default.join(configDir, 'telegram-client.log');
150
+ const errorLogPath = path_1.default.join(configDir, 'telegram-client-error.log');
151
+ const { out, err } = (0, utils_1.initLogFiles)(logPath, errorLogPath);
152
+ const child = (0, child_process_1.spawn)(process.execPath, [entry], {
153
+ detached: false,
154
+ stdio: ['ignore', out, err],
155
+ // cwd = bundle root so the bot's dotenv.config() and relative paths line up
156
+ cwd: bundleRoot,
157
+ env: {
158
+ ...process.env,
159
+ ...env,
160
+ PORT: String(port),
161
+ },
162
+ });
163
+ child.on('exit', (code, signal) => {
164
+ fs_1.default.closeSync(out);
165
+ fs_1.default.closeSync(err);
166
+ if (code !== 0) {
167
+ console.error(`telegram-client exited with code=${code} signal=${signal ?? 'none'}`);
168
+ }
169
+ });
170
+ return child;
171
+ }
172
+ /** Top-level command: `omnikey telegram-client [--port <port>]`. */
173
+ async function startTelegramClientCommand(port) {
174
+ const cfg = await ensureTelegramConfig();
175
+ const child = spawnTelegramClient(port, cfg);
176
+ console.log(`telegram-client started (pid=${child.pid}) on port ${port}. ` +
177
+ `Logs: ${path_1.default.join((0, utils_1.getConfigDir)(), 'telegram-client.log')}`);
178
+ // Keep the CLI process alive until the child exits so users can Ctrl+C.
179
+ await new Promise((resolve) => {
180
+ child.on('exit', () => resolve());
181
+ process.on('SIGINT', () => {
182
+ child.kill('SIGINT');
183
+ });
184
+ process.on('SIGTERM', () => {
185
+ child.kill('SIGTERM');
186
+ });
187
+ });
188
+ }