omnikey-cli 1.0.27 → 1.0.29

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/src/index.ts CHANGED
@@ -9,6 +9,8 @@ import { statusCmd } from './status';
9
9
  import { showLogs } from './showLogs';
10
10
  import { showConfig } from './showConfig';
11
11
  import { setConfig } from './setConfig';
12
+ import { grantBrowserAccess, reopenBrowserDebugProfile } from './grantBrowserAccess';
13
+ import { scheduleAdd, scheduleList, scheduleRemove, scheduleRunNow } from './scheduleJob';
12
14
 
13
15
  const program = new Command();
14
16
 
@@ -90,4 +92,46 @@ program
90
92
  await startDaemon(port);
91
93
  });
92
94
 
95
+ program
96
+ .command('grant-browser-access')
97
+ .description(
98
+ 'Set up authenticated browser tab access for web fetch. ' +
99
+ 'Detects installed browsers, selects a profile, and configures a remote debugging port (CDP). ' +
100
+ 'On macOS you can also choose AppleScript mode instead.',
101
+ )
102
+ .action(async () => {
103
+ await grantBrowserAccess();
104
+ });
105
+
106
+ program
107
+ .command('browser open')
108
+ .description('Reopen the browser with the Omnikey debug profile')
109
+ .action(async () => {
110
+ await reopenBrowserDebugProfile();
111
+ });
112
+
113
+ const scheduleCmd = program
114
+ .command('schedule')
115
+ .description('Manage scheduled prompt jobs');
116
+
117
+ scheduleCmd
118
+ .command('add')
119
+ .description('Add a new scheduled job')
120
+ .action(async () => { await scheduleAdd(); });
121
+
122
+ scheduleCmd
123
+ .command('list')
124
+ .description('List all scheduled jobs')
125
+ .action(async () => { await scheduleList(); });
126
+
127
+ scheduleCmd
128
+ .command('remove')
129
+ .description('Remove a scheduled job')
130
+ .action(async () => { await scheduleRemove(); });
131
+
132
+ scheduleCmd
133
+ .command('run-now <id>')
134
+ .description('Immediately run a scheduled job by ID')
135
+ .action(async (id: string) => { await scheduleRunNow(id); });
136
+
93
137
  program.parseAsync(process.argv);
package/src/onboard.ts CHANGED
@@ -10,11 +10,12 @@ const AI_PROVIDERS = [
10
10
  ];
11
11
 
12
12
  const SEARCH_PROVIDERS = [
13
- { name: 'Skip (DuckDuckGo will be used by default — no key required)', value: 'skip' },
14
- { name: 'Serper (Google Search API — serper.dev, 2,500 free/mo)', value: 'serper' },
15
- { name: 'Brave Search (brave.com/search/api, 2,000 free/mo)', value: 'brave' },
16
- { name: 'Tavily (tavily.com, 1,000 free/mo, optimized for AI)', value: 'tavily' },
17
- { name: 'SearXNG (self-hosted, no key needed — provide your instance URL)', value: 'searxng' },
13
+ { name: 'Skip', value: 'skip' },
14
+ { name: 'DuckDuckGo', value: 'duckduckgo' },
15
+ { name: 'Serper', value: 'serper' },
16
+ { name: 'Brave Search', value: 'brave' },
17
+ { name: 'Tavily', value: 'tavily' },
18
+ { name: 'SearXNG', value: 'searxng' },
18
19
  ];
19
20
 
20
21
  const AI_PROVIDER_KEY_ENV: Record<string, string> = {
@@ -61,7 +62,8 @@ export async function onboard() {
61
62
  {
62
63
  type: 'list',
63
64
  name: 'provider',
64
- message: 'Select a web search provider for the AI agent (enhances research capabilities):',
65
+ message:
66
+ 'Select a web search provider for the AI agent. Supported providers: DuckDuckGo, Serper, Brave Search, Tavily, SearXNG:',
65
67
  choices: SEARCH_PROVIDERS,
66
68
  default: 'skip',
67
69
  },
@@ -0,0 +1,309 @@
1
+ import axios from 'axios';
2
+ import inquirer from 'inquirer';
3
+ import readline from 'node:readline';
4
+ import { readConfig, getPort } from './utils';
5
+
6
+ interface ScheduledJobDto {
7
+ id: string;
8
+ label: string;
9
+ prompt: string;
10
+ cronExpression?: string | null;
11
+ runAt?: string | null;
12
+ isActive: boolean;
13
+ lastRunAt?: string | null;
14
+ nextRunAt?: string | null;
15
+ sessionId?: string | null;
16
+ }
17
+
18
+ async function getJwt(): Promise<string> {
19
+ const config = readConfig();
20
+ const port = getPort();
21
+ const licenseKey = config.OMNIKEY_LICENSE_KEY || '';
22
+ const baseUrl = `http://localhost:${port}`;
23
+ const res = await axios.post(
24
+ `${baseUrl}/api/subscription/activate`,
25
+ { licenseKey },
26
+ {
27
+ timeout: 10_000,
28
+ },
29
+ );
30
+ return (res.data as { token: string }).token;
31
+ }
32
+
33
+ function getBaseUrl(): string {
34
+ const port = getPort();
35
+ return `http://localhost:${port}`;
36
+ }
37
+
38
+ async function authHeaders(): Promise<Record<string, string>> {
39
+ try {
40
+ const token = await getJwt();
41
+ return { Authorization: `Bearer ${token}` };
42
+ } catch {
43
+ // Self-hosted: try without auth
44
+ return {};
45
+ }
46
+ }
47
+
48
+ async function readMultilinePrompt(): Promise<string> {
49
+ console.log('\nEnter prompt text below.');
50
+ console.log('Type END on a new line when finished.\n');
51
+
52
+ const rl = readline.createInterface({
53
+ input: process.stdin,
54
+ output: process.stdout,
55
+ });
56
+
57
+ const lines: string[] = [];
58
+
59
+ return new Promise((resolve) => {
60
+ const askLine = () => {
61
+ rl.question('', (line) => {
62
+ if (line.trim() === 'END') {
63
+ rl.close();
64
+ resolve(lines.join('\n').trim());
65
+ return;
66
+ }
67
+
68
+ lines.push(line);
69
+ askLine();
70
+ });
71
+ };
72
+
73
+ askLine();
74
+ });
75
+ }
76
+
77
+ export async function scheduleAdd(): Promise<void> {
78
+ const answers = await inquirer.prompt([
79
+ {
80
+ type: 'input',
81
+ name: 'label',
82
+ message: 'Job label (e.g. "Daily standup summary"):',
83
+ validate: (v: string) => v.trim().length > 0 || 'Label is required',
84
+ },
85
+ {
86
+ type: 'list',
87
+ name: 'scheduleType',
88
+ message: 'Schedule type:',
89
+ choices: [
90
+ { name: 'Recurring (cron expression)', value: 'cron' },
91
+ { name: 'One-time (specific date/time)', value: 'once' },
92
+ ],
93
+ },
94
+ ]);
95
+
96
+ const promptText = await readMultilinePrompt();
97
+ if (!promptText) {
98
+ console.error('Prompt is required.');
99
+ return;
100
+ }
101
+
102
+ let cronExpression: string | undefined;
103
+ let runAt: string | undefined;
104
+
105
+ if (answers.scheduleType === 'cron') {
106
+ const presets = [
107
+ { name: 'Every weekday at 9 AM (0 9 * * 1-5)', value: '0 9 * * 1-5' },
108
+ { name: 'Every day at midnight (0 0 * * *)', value: '0 0 * * *' },
109
+ { name: 'Every hour (0 * * * *)', value: '0 * * * *' },
110
+ { name: 'Every Monday at 8 AM (0 8 * * 1)', value: '0 8 * * 1' },
111
+ { name: 'Custom cron expression', value: '__custom__' },
112
+ ];
113
+
114
+ const { preset } = await inquirer.prompt([
115
+ {
116
+ type: 'list',
117
+ name: 'preset',
118
+ message: 'Choose a schedule preset or enter custom:',
119
+ choices: presets,
120
+ },
121
+ ]);
122
+
123
+ if (preset === '__custom__') {
124
+ const { customCron } = await inquirer.prompt([
125
+ {
126
+ type: 'input',
127
+ name: 'customCron',
128
+ message: 'Enter cron expression (5 fields, e.g. "0 9 * * 1-5"):',
129
+ validate: (v: string) =>
130
+ /^(\S+\s){4}\S+$/.test(v.trim()) || 'Invalid cron (must be 5 space-separated fields)',
131
+ },
132
+ ]);
133
+ cronExpression = customCron.trim();
134
+ } else {
135
+ cronExpression = preset;
136
+ }
137
+ } else {
138
+ const { dateStr, timeStr } = await inquirer.prompt([
139
+ {
140
+ type: 'input',
141
+ name: 'dateStr',
142
+ message: 'Date (YYYY-MM-DD):',
143
+ validate: (v: string) => /^\d{4}-\d{2}-\d{2}$/.test(v.trim()) || 'Use YYYY-MM-DD format',
144
+ },
145
+ {
146
+ type: 'input',
147
+ name: 'timeStr',
148
+ message: 'Time (HH:MM, 24-hour local time):',
149
+ validate: (v: string) => /^\d{2}:\d{2}$/.test(v.trim()) || 'Use HH:MM format',
150
+ },
151
+ ]);
152
+ const dt = new Date(`${dateStr.trim()}T${timeStr.trim()}:00`);
153
+ if (isNaN(dt.getTime())) {
154
+ console.error('Invalid date/time combination.');
155
+ return;
156
+ }
157
+ if (dt <= new Date()) {
158
+ console.error('Date/time must be in the future.');
159
+ return;
160
+ }
161
+ runAt = dt.toISOString();
162
+ }
163
+
164
+ try {
165
+ const headers = await authHeaders();
166
+ const res = await axios.post<ScheduledJobDto>(
167
+ `${getBaseUrl()}/api/scheduled-jobs`,
168
+ {
169
+ label: answers.label.trim(),
170
+ prompt: promptText,
171
+ cronExpression,
172
+ runAt,
173
+ },
174
+ { headers, timeout: 15_000 },
175
+ );
176
+ const job = res.data;
177
+ console.log('\nJob created successfully:');
178
+ printJobRow(job);
179
+ if (job.nextRunAt) {
180
+ console.log(` Next run: ${new Date(job.nextRunAt).toLocaleString()}`);
181
+ }
182
+ } catch (err: any) {
183
+ const msg = err.response?.data?.error ?? err.message;
184
+ console.error(`Error creating job: ${msg}`);
185
+ }
186
+ }
187
+
188
+ export async function scheduleList(): Promise<void> {
189
+ try {
190
+ const headers = await authHeaders();
191
+ const res = await axios.get<{ jobs: ScheduledJobDto[] }>(`${getBaseUrl()}/api/scheduled-jobs`, {
192
+ headers,
193
+ timeout: 10_000,
194
+ });
195
+ const { jobs } = res.data;
196
+ if (jobs.length === 0) {
197
+ console.log('No scheduled jobs found.');
198
+ return;
199
+ }
200
+ console.log('\nScheduled Jobs:');
201
+ console.log('─'.repeat(90));
202
+ console.log(
203
+ padRight('ID', 28) +
204
+ padRight('Label', 24) +
205
+ padRight('Schedule', 18) +
206
+ padRight('Next Run', 22) +
207
+ 'Status',
208
+ );
209
+ console.log('─'.repeat(90));
210
+ for (const job of jobs) {
211
+ const schedule =
212
+ job.cronExpression ?? (job.runAt ? `Once: ${new Date(job.runAt).toLocaleString()}` : '—');
213
+ const nextRun = job.nextRunAt ? new Date(job.nextRunAt).toLocaleString() : '—';
214
+ const status = job.isActive ? 'Active' : 'Inactive';
215
+ console.log(
216
+ padRight(job.id.slice(0, 26), 28) +
217
+ padRight(job.label.slice(0, 22), 24) +
218
+ padRight(schedule.slice(0, 16), 18) +
219
+ padRight(nextRun.slice(0, 20), 22) +
220
+ status,
221
+ );
222
+ }
223
+ console.log('─'.repeat(90));
224
+ } catch (err: any) {
225
+ const msg = err.response?.data?.error ?? err.message;
226
+ console.error(`Error fetching jobs: ${msg}`);
227
+ }
228
+ }
229
+
230
+ export async function scheduleRemove(): Promise<void> {
231
+ try {
232
+ const headers = await authHeaders();
233
+ const res = await axios.get<{ jobs: ScheduledJobDto[] }>(`${getBaseUrl()}/api/scheduled-jobs`, {
234
+ headers,
235
+ timeout: 10_000,
236
+ });
237
+ const { jobs } = res.data;
238
+ if (jobs.length === 0) {
239
+ console.log('No scheduled jobs to remove.');
240
+ return;
241
+ }
242
+
243
+ const { jobId } = await inquirer.prompt([
244
+ {
245
+ type: 'list',
246
+ name: 'jobId',
247
+ message: 'Select a job to remove:',
248
+ choices: jobs.map((j) => ({
249
+ name: `${j.label} [${j.cronExpression ?? 'one-time'}] ${j.isActive ? '✓' : '✗'}`,
250
+ value: j.id,
251
+ })),
252
+ },
253
+ ]);
254
+
255
+ const { confirm } = await inquirer.prompt([
256
+ {
257
+ type: 'confirm',
258
+ name: 'confirm',
259
+ message: 'Are you sure you want to delete this job?',
260
+ default: false,
261
+ },
262
+ ]);
263
+
264
+ if (!confirm) {
265
+ console.log('Aborted.');
266
+ return;
267
+ }
268
+
269
+ await axios.delete(`${getBaseUrl()}/api/scheduled-jobs/${jobId}`, {
270
+ headers,
271
+ timeout: 10_000,
272
+ });
273
+ console.log('Job deleted.');
274
+ } catch (err: any) {
275
+ const msg = err.response?.data?.error ?? err.message;
276
+ console.error(`Error removing job: ${msg}`);
277
+ }
278
+ }
279
+
280
+ export async function scheduleRunNow(id: string): Promise<void> {
281
+ try {
282
+ const headers = await authHeaders();
283
+ await axios.post(
284
+ `${getBaseUrl()}/api/scheduled-jobs/${id}/run-now`,
285
+ {},
286
+ {
287
+ headers,
288
+ timeout: 10_000,
289
+ },
290
+ );
291
+ console.log(`Job ${id} triggered.`);
292
+ } catch (err: any) {
293
+ const msg = err.response?.data?.error ?? err.message;
294
+ console.error(`Error triggering job: ${msg}`);
295
+ }
296
+ }
297
+
298
+ function printJobRow(job: ScheduledJobDto): void {
299
+ console.log(` ID: ${job.id}`);
300
+ console.log(` Label: ${job.label}`);
301
+ console.log(
302
+ ` Schedule: ${job.cronExpression ?? (job.runAt ? `Once at ${new Date(job.runAt).toLocaleString()}` : '—')}`,
303
+ );
304
+ console.log(` Active: ${job.isActive}`);
305
+ }
306
+
307
+ function padRight(str: string, width: number): string {
308
+ return str.length >= width ? str.slice(0, width) : str + ' '.repeat(width - str.length);
309
+ }