@pungoyal/kite-cli 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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +292 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +7 -0
  5. package/dist/commands/auth.d.ts +2 -0
  6. package/dist/commands/auth.js +265 -0
  7. package/dist/commands/config.d.ts +2 -0
  8. package/dist/commands/config.js +182 -0
  9. package/dist/commands/gtt.d.ts +12 -0
  10. package/dist/commands/gtt.js +276 -0
  11. package/dist/commands/market.d.ts +2 -0
  12. package/dist/commands/market.js +389 -0
  13. package/dist/commands/orders.d.ts +2 -0
  14. package/dist/commands/orders.js +679 -0
  15. package/dist/commands/portfolio.d.ts +2 -0
  16. package/dist/commands/portfolio.js +286 -0
  17. package/dist/commands/types.d.ts +16 -0
  18. package/dist/commands/types.js +1 -0
  19. package/dist/commands/watch.d.ts +18 -0
  20. package/dist/commands/watch.js +285 -0
  21. package/dist/context.d.ts +40 -0
  22. package/dist/context.js +126 -0
  23. package/dist/core/api.d.ts +843 -0
  24. package/dist/core/api.js +472 -0
  25. package/dist/core/auth.d.ts +68 -0
  26. package/dist/core/auth.js +220 -0
  27. package/dist/core/client.d.ts +76 -0
  28. package/dist/core/client.js +285 -0
  29. package/dist/core/config.d.ts +114 -0
  30. package/dist/core/config.js +163 -0
  31. package/dist/core/credentials.d.ts +27 -0
  32. package/dist/core/credentials.js +182 -0
  33. package/dist/core/errors.d.ts +83 -0
  34. package/dist/core/errors.js +152 -0
  35. package/dist/core/instruments.d.ts +87 -0
  36. package/dist/core/instruments.js +288 -0
  37. package/dist/core/paths.d.ts +11 -0
  38. package/dist/core/paths.js +56 -0
  39. package/dist/core/ratelimit.d.ts +57 -0
  40. package/dist/core/ratelimit.js +196 -0
  41. package/dist/core/redact.d.ts +48 -0
  42. package/dist/core/redact.js +181 -0
  43. package/dist/core/schemas.d.ts +662 -0
  44. package/dist/core/schemas.js +433 -0
  45. package/dist/core/secretstore.d.ts +11 -0
  46. package/dist/core/secretstore.js +138 -0
  47. package/dist/core/session.d.ts +37 -0
  48. package/dist/core/session.js +102 -0
  49. package/dist/core/ticker.d.ts +131 -0
  50. package/dist/core/ticker.js +367 -0
  51. package/dist/index.d.ts +17 -0
  52. package/dist/index.js +17 -0
  53. package/dist/output/format.d.ts +31 -0
  54. package/dist/output/format.js +162 -0
  55. package/dist/output/io.d.ts +70 -0
  56. package/dist/output/io.js +143 -0
  57. package/dist/output/table.d.ts +28 -0
  58. package/dist/output/table.js +73 -0
  59. package/dist/run.d.ts +15 -0
  60. package/dist/run.js +161 -0
  61. package/dist/safety.d.ts +93 -0
  62. package/dist/safety.js +154 -0
  63. package/package.json +85 -0
@@ -0,0 +1,472 @@
1
+ import { z } from 'zod';
2
+ import { UsageError } from './errors.js';
3
+ import { AuctionSchema, BasketMarginSchema, CandlesSchema, GttCreateResultSchema, GttSchema, HoldingSchema, LtpMapSchema, MarginsSchema, MfHoldingSchema, MfOrderSchema, MfSipSchema, OhlcMapSchema, OrderMarginSchema, OrderSchema, PlaceOrderResultSchema, PositionsSchema, ProfileSchema, QuoteMapSchema, SessionSchema, TradeSchema, } from './schemas.js';
4
+ export const VARIETIES = ['regular', 'amo', 'co', 'iceberg', 'auction'];
5
+ export const ORDER_TYPES = ['MARKET', 'LIMIT', 'SL', 'SL-M'];
6
+ export const PRODUCTS = ['CNC', 'NRML', 'MIS', 'MTF'];
7
+ export const VALIDITIES = ['DAY', 'IOC', 'TTL'];
8
+ export const EXCHANGES = ['NSE', 'BSE', 'NFO', 'CDS', 'BCD', 'MCX', 'BFO'];
9
+ export class KiteApi {
10
+ client;
11
+ constructor(client) {
12
+ this.client = client;
13
+ }
14
+ // -------------------------------------------------------------------------
15
+ // Session
16
+ // -------------------------------------------------------------------------
17
+ /** Exchange a request_token for an access_token. */
18
+ async createSession(params) {
19
+ return this.client.request({
20
+ method: 'POST',
21
+ path: '/session/token',
22
+ schema: SessionSchema,
23
+ form: {
24
+ api_key: this.client.apiKey,
25
+ request_token: params.requestToken,
26
+ checksum: params.checksum,
27
+ },
28
+ });
29
+ }
30
+ async invalidateSession(accessToken) {
31
+ return this.client.request({
32
+ method: 'DELETE',
33
+ path: '/session/token',
34
+ schema: z.unknown(),
35
+ query: { api_key: this.client.apiKey, access_token: accessToken },
36
+ });
37
+ }
38
+ async getProfile(signal) {
39
+ return this.client.request({
40
+ method: 'GET',
41
+ path: '/user/profile',
42
+ schema: ProfileSchema,
43
+ signal,
44
+ });
45
+ }
46
+ async getMargins(signal) {
47
+ return this.client.request({
48
+ method: 'GET',
49
+ path: '/user/margins',
50
+ schema: MarginsSchema,
51
+ signal,
52
+ });
53
+ }
54
+ // -------------------------------------------------------------------------
55
+ // Orders
56
+ // -------------------------------------------------------------------------
57
+ async getOrders(signal) {
58
+ return this.client.request({
59
+ method: 'GET',
60
+ path: '/orders',
61
+ schema: z.array(OrderSchema),
62
+ signal,
63
+ });
64
+ }
65
+ /** Returns the full state history of one order, oldest first. */
66
+ async getOrderHistory(orderId, signal) {
67
+ return this.client.request({
68
+ method: 'GET',
69
+ path: `/orders/${encodeURIComponent(orderId)}`,
70
+ schema: z.array(OrderSchema),
71
+ signal,
72
+ });
73
+ }
74
+ async getTrades(signal) {
75
+ return this.client.request({
76
+ method: 'GET',
77
+ path: '/trades',
78
+ schema: z.array(TradeSchema),
79
+ signal,
80
+ });
81
+ }
82
+ async getOrderTrades(orderId, signal) {
83
+ return this.client.request({
84
+ method: 'GET',
85
+ path: `/orders/${encodeURIComponent(orderId)}/trades`,
86
+ schema: z.array(TradeSchema),
87
+ signal,
88
+ });
89
+ }
90
+ /**
91
+ * Place an order.
92
+ *
93
+ * This is never retried automatically anywhere in the stack. If it fails at
94
+ * the network level the caller must reconcile via `findOrderByTag`, because
95
+ * a timed-out request may still have been executed.
96
+ */
97
+ async placeOrder(params, signal) {
98
+ const { variety, ...rest } = params;
99
+ return this.client.request({
100
+ method: 'POST',
101
+ path: `/orders/${variety}`,
102
+ category: 'order',
103
+ schema: PlaceOrderResultSchema,
104
+ form: rest,
105
+ signal,
106
+ });
107
+ }
108
+ async modifyOrder(params, signal) {
109
+ const { variety, order_id, ...rest } = params;
110
+ return this.client.request({
111
+ method: 'PUT',
112
+ path: `/orders/${variety}/${encodeURIComponent(order_id)}`,
113
+ category: 'order',
114
+ schema: z.looseObject({ order_id: z.string() }),
115
+ form: rest,
116
+ signal,
117
+ });
118
+ }
119
+ async cancelOrder(params, signal) {
120
+ return this.client.request({
121
+ method: 'DELETE',
122
+ path: `/orders/${params.variety}/${encodeURIComponent(params.order_id)}`,
123
+ category: 'order',
124
+ schema: z.looseObject({ order_id: z.string() }),
125
+ query: params.parent_order_id ? { parent_order_id: params.parent_order_id } : undefined,
126
+ signal,
127
+ });
128
+ }
129
+ /**
130
+ * Reconciliation helper for the no-idempotency problem.
131
+ *
132
+ * Kite has no client-suppliable idempotency key (the `guid` field in the
133
+ * response is server-assigned, despite its confusing description). Zerodha's
134
+ * own guidance for a failed/timed-out placement is: do NOT retry — fetch the
135
+ * orderbook and look for your tag. Only place again if it is absent.
136
+ */
137
+ async findOrderByTag(tag, signal) {
138
+ const orders = await this.getOrders(signal);
139
+ return orders.filter((order) => order.tag === tag || order.tags?.includes(tag));
140
+ }
141
+ // -------------------------------------------------------------------------
142
+ // GTT
143
+ // -------------------------------------------------------------------------
144
+ async getGtts(signal) {
145
+ return this.client.request({
146
+ method: 'GET',
147
+ path: '/gtt/triggers',
148
+ schema: z.array(GttSchema),
149
+ signal,
150
+ });
151
+ }
152
+ async getGtt(id, signal) {
153
+ return this.client.request({
154
+ method: 'GET',
155
+ path: `/gtt/triggers/${id}`,
156
+ schema: GttSchema,
157
+ signal,
158
+ });
159
+ }
160
+ async placeGtt(params, signal) {
161
+ return this.client.request({
162
+ method: 'POST',
163
+ path: '/gtt/triggers',
164
+ schema: GttCreateResultSchema,
165
+ form: serialiseGtt(params),
166
+ signal,
167
+ });
168
+ }
169
+ async modifyGtt(id, params, signal) {
170
+ return this.client.request({
171
+ method: 'PUT',
172
+ path: `/gtt/triggers/${id}`,
173
+ schema: GttCreateResultSchema,
174
+ form: serialiseGtt(params),
175
+ signal,
176
+ });
177
+ }
178
+ async deleteGtt(id, signal) {
179
+ return this.client.request({
180
+ method: 'DELETE',
181
+ path: `/gtt/triggers/${id}`,
182
+ schema: GttCreateResultSchema,
183
+ signal,
184
+ });
185
+ }
186
+ // -------------------------------------------------------------------------
187
+ // Portfolio
188
+ // -------------------------------------------------------------------------
189
+ async getHoldings(signal) {
190
+ return this.client.request({
191
+ method: 'GET',
192
+ path: '/portfolio/holdings',
193
+ schema: z.array(HoldingSchema),
194
+ signal,
195
+ });
196
+ }
197
+ async getPositions(signal) {
198
+ return this.client.request({
199
+ method: 'GET',
200
+ path: '/portfolio/positions',
201
+ schema: PositionsSchema,
202
+ signal,
203
+ });
204
+ }
205
+ async getAuctions(signal) {
206
+ return this.client.request({
207
+ method: 'GET',
208
+ path: '/portfolio/holdings/auctions',
209
+ schema: z.array(AuctionSchema),
210
+ signal,
211
+ });
212
+ }
213
+ /** Convert a position between products. Note this is PUT, not POST. */
214
+ async convertPosition(params, signal) {
215
+ return this.client.request({
216
+ method: 'PUT',
217
+ path: '/portfolio/positions',
218
+ schema: z.boolean().or(z.unknown()),
219
+ form: params,
220
+ signal,
221
+ });
222
+ }
223
+ /**
224
+ * Initiate CDSL authorisation for selling holdings — the HTTP 428 recovery
225
+ * path.
226
+ *
227
+ * With no ISINs this authorises the whole demat account; with ISINs it
228
+ * authorises only those instruments. The client encodes an array as repeated
229
+ * `isin` form fields, which is the shape Kite expects.
230
+ */
231
+ async authoriseHoldings(isins, signal) {
232
+ return this.client.request({
233
+ method: 'POST',
234
+ path: '/portfolio/holdings/authorise',
235
+ schema: z.looseObject({ request_id: z.string() }),
236
+ form: isins?.length ? { isin: isins } : {},
237
+ signal,
238
+ });
239
+ }
240
+ /** Browser URL the user must visit to complete a holdings authorisation. */
241
+ authorisationUrl(requestId) {
242
+ return `https://kite.zerodha.com/connect/portfolio/authorise/holdings/${encodeURIComponent(this.client.apiKey)}/${encodeURIComponent(requestId)}`;
243
+ }
244
+ // -------------------------------------------------------------------------
245
+ // Market data
246
+ // -------------------------------------------------------------------------
247
+ /**
248
+ * Full quotes. Kite caps this at 500 instruments per call and 1 request per
249
+ * second, so large lists are chunked and paced by the limiter.
250
+ */
251
+ async getQuote(instruments, signal) {
252
+ return this.batchedQuote(instruments, 500, '/quote', QuoteMapSchema, signal);
253
+ }
254
+ /** OHLC + last price. Caps at 1000 instruments per call. */
255
+ async getOhlc(instruments, signal) {
256
+ return this.batchedQuote(instruments, 1000, '/quote/ohlc', OhlcMapSchema, signal);
257
+ }
258
+ /** Last traded price only. Caps at 1000 instruments per call. */
259
+ async getLtp(instruments, signal) {
260
+ return this.batchedQuote(instruments, 1000, '/quote/ltp', LtpMapSchema, signal);
261
+ }
262
+ async batchedQuote(instruments, chunkSize, path, schema, signal) {
263
+ if (instruments.length === 0)
264
+ return {};
265
+ const merged = {};
266
+ for (const chunk of chunks(instruments, chunkSize)) {
267
+ const result = await this.client.request({
268
+ method: 'GET',
269
+ path,
270
+ category: 'quote',
271
+ schema,
272
+ query: { i: chunk },
273
+ signal,
274
+ });
275
+ Object.assign(merged, result);
276
+ }
277
+ return merged;
278
+ }
279
+ /**
280
+ * Historical candles, transparently chunked to respect per-interval range
281
+ * limits, then merged and de-duplicated.
282
+ */
283
+ async getHistorical(params, signal) {
284
+ const maxDays = MAX_DAYS_PER_REQUEST[params.interval];
285
+ const ranges = splitDateRange(params.from, params.to, maxDays);
286
+ const all = [];
287
+ for (const range of ranges) {
288
+ const result = await this.client.request({
289
+ method: 'GET',
290
+ path: `/instruments/historical/${params.instrument_token}/${params.interval}`,
291
+ category: 'historical',
292
+ schema: CandlesSchema,
293
+ query: {
294
+ from: formatIstDateTime(range.from),
295
+ to: formatIstDateTime(range.to),
296
+ continuous: params.continuous ? 1 : undefined,
297
+ oi: params.oi ? 1 : undefined,
298
+ },
299
+ signal,
300
+ });
301
+ all.push(...result.candles);
302
+ }
303
+ // Chunk boundaries are inclusive on both ends, so adjacent ranges can
304
+ // repeat a candle. De-duplicate on timestamp, preserving order.
305
+ const seen = new Set();
306
+ return all.filter((candle) => {
307
+ const key = candle[0];
308
+ if (seen.has(key))
309
+ return false;
310
+ seen.add(key);
311
+ return true;
312
+ });
313
+ }
314
+ /** The daily instrument dump as raw CSV. Not an API envelope. */
315
+ async getInstrumentsCsv(exchange, signal) {
316
+ return this.client.requestText({
317
+ path: exchange ? `/instruments/${exchange}` : '/instruments',
318
+ // /instruments is the one route the sandbox does NOT serve under /oms.
319
+ noPrefix: true,
320
+ signal,
321
+ });
322
+ }
323
+ // -------------------------------------------------------------------------
324
+ // Margins & charges (JSON bodies, unlike the rest of the API)
325
+ // -------------------------------------------------------------------------
326
+ async orderMargins(orders, signal) {
327
+ return this.client.request({
328
+ method: 'POST',
329
+ path: '/margins/orders',
330
+ schema: z.array(OrderMarginSchema),
331
+ json: orders,
332
+ signal,
333
+ });
334
+ }
335
+ async basketMargins(orders, considerPositions = true, signal) {
336
+ return this.client.request({
337
+ method: 'POST',
338
+ path: '/margins/basket',
339
+ schema: BasketMarginSchema,
340
+ query: { consider_positions: considerPositions },
341
+ json: orders,
342
+ signal,
343
+ });
344
+ }
345
+ async orderCharges(orders, signal) {
346
+ return this.client.request({
347
+ method: 'POST',
348
+ path: '/charges/orders',
349
+ schema: z.array(OrderMarginSchema),
350
+ json: orders,
351
+ signal,
352
+ });
353
+ }
354
+ // -------------------------------------------------------------------------
355
+ // Mutual funds (read-only — the current docs state that placing MF orders is
356
+ // not available over the API because it requires a bank debit)
357
+ // -------------------------------------------------------------------------
358
+ async getMfHoldings(signal) {
359
+ return this.client.request({
360
+ method: 'GET',
361
+ path: '/mf/holdings',
362
+ schema: z.array(MfHoldingSchema),
363
+ signal,
364
+ });
365
+ }
366
+ /** Note: only returns the last 7 days of orders. */
367
+ async getMfOrders(signal) {
368
+ return this.client.request({
369
+ method: 'GET',
370
+ path: '/mf/orders',
371
+ schema: z.array(MfOrderSchema),
372
+ signal,
373
+ });
374
+ }
375
+ async getMfSips(signal) {
376
+ return this.client.request({
377
+ method: 'GET',
378
+ path: '/mf/sips',
379
+ schema: z.array(MfSipSchema),
380
+ signal,
381
+ });
382
+ }
383
+ }
384
+ /**
385
+ * GTT takes JSON-encoded strings inside form fields — not a JSON body, and not
386
+ * plain form fields. An easy thing to get wrong.
387
+ */
388
+ function serialiseGtt(params) {
389
+ return {
390
+ type: params.type,
391
+ condition: JSON.stringify(params.condition),
392
+ orders: JSON.stringify(params.orders),
393
+ };
394
+ }
395
+ // ---------------------------------------------------------------------------
396
+ // Historical helpers
397
+ // ---------------------------------------------------------------------------
398
+ export const HISTORICAL_INTERVALS = [
399
+ 'minute',
400
+ '3minute',
401
+ '5minute',
402
+ '10minute',
403
+ '15minute',
404
+ '30minute',
405
+ '60minute',
406
+ 'day',
407
+ ];
408
+ /**
409
+ * Maximum date range per request, by interval.
410
+ *
411
+ * These limits are NOT in the official docs — they come from Zerodha staff on
412
+ * the developer forum. The `day` value is 1900 rather than the 2000 quoted
413
+ * there, because Zerodha's own sandbox chunking helper uses 1900 and the docs
414
+ * note that larger `day` ranges error out.
415
+ */
416
+ export const MAX_DAYS_PER_REQUEST = {
417
+ minute: 60,
418
+ '3minute': 100,
419
+ '5minute': 100,
420
+ '10minute': 100,
421
+ '15minute': 200,
422
+ '30minute': 200,
423
+ '60minute': 400,
424
+ day: 1900,
425
+ };
426
+ export function parseInterval(value) {
427
+ if (HISTORICAL_INTERVALS.includes(value)) {
428
+ return value;
429
+ }
430
+ throw new UsageError(`Unknown interval "${value}".`, `Supported intervals: ${HISTORICAL_INTERVALS.join(', ')}.`);
431
+ }
432
+ const DAY_MS = 86_400_000;
433
+ export function splitDateRange(from, to, maxDays) {
434
+ if (from.getTime() > to.getTime()) {
435
+ throw new UsageError('`--from` must be earlier than `--to`.');
436
+ }
437
+ const ranges = [];
438
+ let cursor = from.getTime();
439
+ const end = to.getTime();
440
+ const span = maxDays * DAY_MS;
441
+ while (cursor <= end) {
442
+ const chunkEnd = Math.min(cursor + span, end);
443
+ ranges.push({ from: new Date(cursor), to: new Date(chunkEnd) });
444
+ if (chunkEnd >= end)
445
+ break;
446
+ // Step one second past the chunk end so ranges do not overlap by a full day.
447
+ cursor = chunkEnd + 1000;
448
+ }
449
+ return ranges;
450
+ }
451
+ /** Kite expects "yyyy-mm-dd hh:mm:ss" in IST. */
452
+ export function formatIstDateTime(date) {
453
+ const parts = new Intl.DateTimeFormat('en-CA', {
454
+ timeZone: 'Asia/Kolkata',
455
+ year: 'numeric',
456
+ month: '2-digit',
457
+ day: '2-digit',
458
+ hour: '2-digit',
459
+ minute: '2-digit',
460
+ second: '2-digit',
461
+ hour12: false,
462
+ }).formatToParts(date);
463
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? '00';
464
+ // Intl can render midnight as hour "24" in some locales/engines.
465
+ const hour = get('hour') === '24' ? '00' : get('hour');
466
+ return `${get('year')}-${get('month')}-${get('day')} ${hour}:${get('minute')}:${get('second')}`;
467
+ }
468
+ export function* chunks(items, size) {
469
+ for (let i = 0; i < items.length; i += size) {
470
+ yield items.slice(i, i + size);
471
+ }
472
+ }
@@ -0,0 +1,68 @@
1
+ import type { Endpoints } from './config.js';
2
+ /**
3
+ * The Kite Connect login handshake.
4
+ *
5
+ * 1. Open https://kite.zerodha.com/connect/login?v=3&api_key=...
6
+ * 2. User authenticates in their browser (including mandatory TOTP 2FA).
7
+ * 3. Kite redirects to the registered URL with ?request_token=...
8
+ * 4. POST /session/token with checksum = SHA256(api_key + request_token + api_secret)
9
+ *
10
+ * The CLI never sees the user's password or TOTP. We deliberately do not offer
11
+ * to store a TOTP seed: holding the 2FA secret alongside the API secret would
12
+ * collapse both factors into one, which is precisely what the SEBI 2FA mandate
13
+ * exists to prevent.
14
+ */
15
+ /** checksum = SHA256(api_key + request_token + api_secret), hex. */
16
+ export declare function computeChecksum(apiKey: string, requestToken: string, apiSecret: string): string;
17
+ /**
18
+ * Postback checksum — note this is a DIFFERENT concatenation from the login
19
+ * checksum: SHA256(order_id + order_timestamp + api_secret).
20
+ */
21
+ export declare function computePostbackChecksum(orderId: string, orderTimestamp: string, apiSecret: string): string;
22
+ export declare function verifyPostbackChecksum(received: string, orderId: string, orderTimestamp: string, apiSecret: string): boolean;
23
+ /**
24
+ * Constant-time string comparison that cannot throw.
25
+ *
26
+ * `timingSafeEqual` requires equal BYTE lengths and throws RangeError
27
+ * otherwise, so the length guard must be on the encoded buffers — not on
28
+ * `String.length`, which counts UTF-16 code units and can match while the byte
29
+ * lengths differ. Both call sites compare attacker-influenced input.
30
+ */
31
+ export declare function safeCompare(a: string, b: string): boolean;
32
+ export interface LoginUrlOptions {
33
+ apiKey: string;
34
+ endpoints: Endpoints;
35
+ /** Opaque CSRF value echoed back by Kite via redirect_params. */
36
+ state: string;
37
+ }
38
+ export declare function buildLoginUrl(opts: LoginUrlOptions): string;
39
+ export declare function generateState(): string;
40
+ export interface CallbackResult {
41
+ requestToken: string;
42
+ }
43
+ export interface CallbackServerOptions {
44
+ port: number;
45
+ path: string;
46
+ state: string;
47
+ timeoutMs?: number;
48
+ }
49
+ /**
50
+ * Run a one-shot loopback HTTP server to capture the request_token.
51
+ *
52
+ * Kite's developer console requires HTTPS for redirect URLs but explicitly
53
+ * permits plain http:// for localhost and 127.0.0.1, which is the standard
54
+ * pattern for native apps. We bind to 127.0.0.1 specifically (never 0.0.0.0),
55
+ * so the callback is not reachable from the network.
56
+ */
57
+ export declare function waitForCallback(opts: CallbackServerOptions): {
58
+ promise: Promise<CallbackResult>;
59
+ close: () => void;
60
+ };
61
+ export declare function redirectUrlFor(port: number, path: string): string;
62
+ /**
63
+ * Open a URL in the user's default browser.
64
+ *
65
+ * The URL is passed as an argv element to a fixed binary, never through a
66
+ * shell, so a crafted URL cannot become command injection.
67
+ */
68
+ export declare function openBrowser(url: string): Promise<boolean>;