@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Puneet Goyal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # kite-cli
2
+
3
+ [![CI](https://github.com/pungoyal/kite-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/pungoyal/kite-cli/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@pungoyal/kite-cli.svg)](https://www.npmjs.com/package/@pungoyal/kite-cli)
5
+ [![Node.js](https://img.shields.io/badge/node-%E2%89%A5%2022.12-brightgreen.svg)](https://nodejs.org)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ An **unofficial**, secure, scriptable command-line interface for the [Zerodha Kite Connect](https://kite.trade/docs/connect/v3/) API.
9
+
10
+ Check your portfolio, stream live quotes, and place orders from the terminal — with credentials in your OS keyring, confirmations on anything that moves money, and clean JSON for piping into `jq`.
11
+
12
+ > **Unofficial, independent project.** Not affiliated with, endorsed by, or sponsored by Zerodha. "Kite" and "Kite Connect" are trademarks of Zerodha Broking Ltd., referenced here only to describe the third-party API this tool works with.
13
+
14
+ ```console
15
+ $ kite holdings
16
+ ╭──────────┬──────┬─────┬──────────┬──────────┬─────────────┬─────────────┬────────┬────────╮
17
+ │ Symbol │ Exch │ Qty │ Avg │ LTP │ Value │ P&L │ P&L % │ Day │
18
+ ├──────────┼──────┼─────┼──────────┼──────────┼─────────────┼─────────────┼────────┼────────┤
19
+ │ INFY │ NSE │ 50 │ 1,402.30 │ 1,551.85 │ 77,592.50 │ +₹7,477.50 │ +10.66% │ +0.82% │
20
+ │ TCS │ NSE │ 20 │ 3,890.00 │ 3,802.40 │ 76,048.00 │ -₹1,752.00 │ -2.25% │ -0.41% │
21
+ ╰──────────┴──────┴─────┴──────────┴──────────┴─────────────┴─────────────┴────────┴────────╯
22
+
23
+ Invested ₹1,55,915.00
24
+ Current ₹1,53,640.50
25
+ P&L +₹5,725.50 +3.67%
26
+ Day's change +₹287.40
27
+ ```
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install -g @pungoyal/kite-cli
33
+ ```
34
+
35
+ Requires **Node 22.12 or newer**.
36
+
37
+ ## Getting started
38
+
39
+ You need a [Kite Connect](https://developers.kite.trade) app to get an API key and secret. Set your app's redirect URL to:
40
+
41
+ ```
42
+ http://127.0.0.1:51101/callback
43
+ ```
44
+
45
+ Then:
46
+
47
+ ```bash
48
+ kite login
49
+ ```
50
+
51
+ This opens your browser, you log in to Zerodha normally (including your TOTP), and the CLI captures the callback on loopback. Your API secret goes into your OS keyring; the daily access token is stored alongside it.
52
+
53
+ **Want to try it without a subscription or real money?** Zerodha runs a public sandbox:
54
+
55
+ ```bash
56
+ kite login --env sandbox
57
+ kite --env sandbox holdings
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ### Portfolio
63
+
64
+ ```bash
65
+ kite holdings # long-term holdings with P&L
66
+ kite positions # open positions
67
+ kite positions --day # intraday only
68
+ kite funds # available margin
69
+ kite convert NSE:INFY --quantity 10 --from MIS --to CNC
70
+ kite authorise # authorise demat holdings for selling
71
+ ```
72
+
73
+ If a sell order fails with exit code 12 ("needs authorisation at depository"), run `kite authorise` — it requests a CDSL authorisation and opens the browser page that completes it. Pass specific ISINs to authorise only those instruments.
74
+
75
+ ### Market data
76
+
77
+ ```bash
78
+ kite quote NSE:INFY NSE:TCS # full quotes
79
+ kite quote NSE:INFY --depth # with the 5-level order book
80
+ kite ltp NSE:INFY # just the last traded price
81
+ kite ohlc NSE:RELIANCE
82
+
83
+ kite history NSE:INFY --from 90d
84
+ kite history NSE:INFY -i 5minute --from 7d --csv > infy.csv
85
+
86
+ kite instruments search "nifty bank"
87
+ kite instruments search INFY --exchange NFO --type CE
88
+ ```
89
+
90
+ Date arguments accept `YYYY-MM-DD` or relative offsets like `30d`, `6m`, `1y`.
91
+
92
+ Long ranges are chunked automatically to respect Kite's per-interval limits, then merged and de-duplicated.
93
+
94
+ ### Live streaming
95
+
96
+ ```bash
97
+ kite watch NSE:INFY NSE:TCS # self-updating table
98
+ kite watch --holdings # stream your whole portfolio
99
+ kite watch --positions --orders # positions plus live order updates
100
+ kite watch NSE:INFY --json | jq # NDJSON for piping
101
+ ```
102
+
103
+ ### Trading
104
+
105
+ Every order previews the **resolved** order and asks for confirmation:
106
+
107
+ ```console
108
+ $ kite orders place NSE:INFY --side BUY --quantity 10 --type LIMIT --price 1500
109
+
110
+ Place BUY order for 10 INFY
111
+ Instrument NSE:INFY
112
+ Resolved INFOSYS LIMITED (token 408065)
113
+ Side BUY
114
+ Quantity 10
115
+ Order type LIMIT
116
+ Price ₹1,500.00
117
+ Product CNC
118
+ Est. value ₹15,000.00
119
+ Tag kcmrt88o648c1bce
120
+
121
+ ◆ Place BUY order for 10 INFY?
122
+ │ ● Yes / ○ No
123
+ ```
124
+
125
+ ```bash
126
+ kite orders list # today's orderbook
127
+ kite orders list --open # working orders only
128
+ kite orders get 250720000123456 # full state history and fills
129
+ kite orders modify <id> --price 1520
130
+ kite orders cancel <id>
131
+ kite trades # today's fills
132
+
133
+ kite gtt list
134
+ kite gtt place NSE:INFY --side SELL --quantity 10 \
135
+ --trigger 1400 --price 1395 \
136
+ --trigger 1700 --price 1695 # two-leg OCO
137
+ kite gtt delete <id>
138
+ ```
139
+
140
+ Add `--dry-run` to any of these to see exactly what would be sent, without sending it.
141
+
142
+ ### Scripting
143
+
144
+ Every command supports `--json`, writes data to stdout and everything else to stderr, and returns a meaningful exit code.
145
+
146
+ ```bash
147
+ kite positions --json | jq '.net[] | select(.pnl < 0) | .tradingsymbol'
148
+ kite holdings --json | jq '[.[] | .pnl] | add'
149
+
150
+ kite whoami --json || kite login # exit code 3 means "no session"
151
+ ```
152
+
153
+ | Code | Meaning |
154
+ |-----:|---------|
155
+ | 0 | Success |
156
+ | 1 | Unclassified failure |
157
+ | 2 | Bad usage — unknown flag, missing or invalid argument |
158
+ | 3 | Not logged in, or the session expired |
159
+ | 4 | Kite rejected the input |
160
+ | 5 | Order rejected by the exchange |
161
+ | 6 | Insufficient margin |
162
+ | 7 | Insufficient holdings to sell |
163
+ | 8 | Rate limited |
164
+ | 9 | Kite or its upstream OMS is unreachable |
165
+ | 10 | You declined a confirmation |
166
+ | 11 | Confirmation required but the terminal is not interactive |
167
+ | 12 | Holdings need depository authorisation — run `kite authorise` |
168
+ | 13 | Blocked by the local kill switch or order value cap |
169
+
170
+ `NO_COLOR` is honoured, and colour is disabled automatically when stdout is not a TTY.
171
+
172
+ ## Safety
173
+
174
+ This tool spends real money, so the defaults are conservative.
175
+
176
+ **Confirmation.** Order commands render the resolved order — the actual instrument token, lot size, and computed value, not an echo of your flags — and wait for confirmation. That's deliberate: a flag echo can't catch "I typed the wrong symbol and it resolved to a different contract," which is the expensive mistake.
177
+
178
+ **Escalation.** Above ₹1,00,000 (configurable) a keystroke isn't enough; you have to type the trading symbol.
179
+
180
+ **Non-interactive means refuse.** With no TTY and no `--yes`, order commands exit non-zero rather than silently proceeding.
181
+
182
+ **`--yes` is call-site only.** There is deliberately no config setting to disable confirmations globally. Bypassing safety has to be an explicit act every time.
183
+
184
+ **Kill switch and cap.**
185
+
186
+ ```bash
187
+ kite config set trading.enabled false # refuse all order commands
188
+ kite config set trading.maxOrderValue 50000 # refuse orders above ₹50,000
189
+ ```
190
+
191
+ **No blind retries.** Kite has no idempotency key, so a timed-out order placement is genuinely ambiguous — it may have executed. This CLI never retries a write. Instead it tags every order with a unique value, and on failure queries the orderbook for that tag to tell you what actually happened:
192
+
193
+ ```console
194
+ $ kite orders place NSE:INFY -s BUY -q 10 --type LIMIT --price 1500 --yes
195
+ ! The order request failed: Request timed out
196
+ · Checking whether it reached Kite anyway…
197
+ ! The order DID reach Kite: 250720000123456 (COMPLETE).
198
+ · It was not placed twice. Do not re-run this command.
199
+ ```
200
+
201
+ Automatic retries are restricted to `GET`/`HEAD` at the transport layer. `POST`, `PUT` and `DELETE` are never retried — in this API those are place, modify and cancel.
202
+
203
+ ## Security
204
+
205
+ **Credentials** are resolved in this order:
206
+
207
+ 1. `KITE_API_SECRET` / `KITE_ACCESS_TOKEN` environment variables — for CI and containers, never persisted
208
+ 2. OS keyring — macOS Keychain, Windows Credential Manager, Linux Secret Service
209
+ 3. Encrypted file at `~/.config/kite/credentials.enc` — scrypt (N=2¹⁷) + AES-256-GCM, mode `0600`, with the KDF header bound as authenticated data so parameters can't be downgraded
210
+
211
+ Your API secret is never accepted as a command-line argument, because argv is visible to any local process via `ps` and lands in shell history. It's prompted for, or read from the environment.
212
+
213
+ **Redaction.** Access tokens are registered with a scrubber that runs over every log line, error message, and stack trace. The two paths that carry a token — the `Authorization` header and the WebSocket URL, where it's a query parameter — are covered explicitly and [tested](test/redact.test.ts).
214
+
215
+ **TOTP.** This CLI will never ask for or store your 2FA seed. Storing it next to your API secret would collapse both authentication factors into one, which is exactly what the SEBI 2FA mandate exists to prevent. Login happens in your browser; the CLI only sees the resulting request token.
216
+
217
+ **Supply chain.** 10 direct runtime dependencies, most of them zero-dependency. Published from GitHub Actions via [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) with OIDC — no long-lived publish token exists. Provenance attestation is generated automatically; verify it with `npm audit signatures`. All CI actions are pinned to full commit SHAs, and dependency lifecycle scripts are disabled.
218
+
219
+ To report a vulnerability, see [SECURITY.md](SECURITY.md).
220
+
221
+ ## Configuration
222
+
223
+ ```bash
224
+ kite config show
225
+ kite config set <key> <value>
226
+ kite config path
227
+ ```
228
+
229
+ | Key | Description |
230
+ |---|---|
231
+ | `trading.enabled` | Master kill switch for all order commands |
232
+ | `trading.confirm` | Require confirmation before money-moving actions |
233
+ | `trading.maxOrderValue` | Refuse any single order above this rupee value |
234
+ | `trading.strictConfirmAbove` | Above this value, require typing the symbol |
235
+ | `output.color` | `auto`, `always`, or `never` |
236
+ | `output.compact` | Render tables without borders |
237
+ | `redirectPort` / `redirectPath` | Loopback callback URL for login |
238
+
239
+ Config lives at `~/.config/kite/config.json` (`0600`). Override the location with `KITE_CONFIG_DIR`.
240
+
241
+ ## Things worth knowing about Kite
242
+
243
+ - **Sessions expire at 6:00 AM IST daily.** This is a regulatory requirement and there's no way around it — you log in once per trading day. Refresh tokens exist in the API but are only issued to exchange-approved platforms, not individual subscribers.
244
+ - **Logging into Kite web invalidates your API session.** The CLI can't detect this until a request comes back 403.
245
+ - **Order acceptance is not execution.** A returned order ID means the OMS accepted the request. Check `kite orders get <id>` for what actually happened.
246
+ - **Rate limits are tight**: quotes 1/sec, historical 3/sec, orders 10/sec (plus 400/min and 5,000/day). The CLI paces requests for you and batches quotes automatically — one call handles up to 1,000 instruments.
247
+ - **Kite caps order modifications at 25** per order. After that you must cancel and re-place.
248
+ - **Instruments are cached by `exchange:tradingsymbol`, never by token.** Exchanges reuse numeric instrument tokens after expiry, so a token-keyed cache silently resolves to the wrong contract after a rollover.
249
+ - **Mutual funds are read-only** over the API — placing MF orders requires a bank debit that has no API path.
250
+
251
+ ## Library use
252
+
253
+ The client is exported if you want it without the CLI:
254
+
255
+ ```ts
256
+ import { KiteClient, KiteApi, endpointsFor } from '@pungoyal/kite-cli';
257
+
258
+ const client = new KiteClient({
259
+ apiKey: process.env.KITE_API_KEY!,
260
+ accessToken: process.env.KITE_ACCESS_TOKEN!,
261
+ endpoints: endpointsFor('production'),
262
+ });
263
+
264
+ const api = new KiteApi(client);
265
+ console.log(await api.getHoldings());
266
+ ```
267
+
268
+ You get the same rate limiting, response validation, redaction, and error taxonomy the CLI uses.
269
+
270
+ ## Development
271
+
272
+ ```bash
273
+ npm install
274
+ npm run dev -- holdings # run from source
275
+ npm test
276
+ npm run typecheck
277
+ npm run build
278
+ ```
279
+
280
+ ## Contributing
281
+
282
+ Bug reports, ideas, and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and the two non-negotiables (safety defaults and secret redaction), and [CHANGELOG.md](CHANGELOG.md) for release history. Security issues go through [SECURITY.md](SECURITY.md), never a public issue.
283
+
284
+ ## Disclaimer
285
+
286
+ This is an unofficial, independent, community project. It is **not affiliated with, endorsed by, or sponsored by Zerodha**. "Kite", "Kite Connect", and "Zerodha" are trademarks of Zerodha Broking Ltd.; this project references them only to identify the third-party API it interoperates with (nominative use) and claims no rights to those marks.
287
+
288
+ Trading involves risk of financial loss. This software is provided as-is under the MIT licence — you are responsible for every order it places on your behalf. Test with `--env sandbox` and `--dry-run` before trusting it with real money.
289
+
290
+ ## Licence
291
+
292
+ [MIT](LICENSE)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import module from 'node:module';
3
+ // Node's on-disk compile cache. Cheap to enable and a measurable win on
4
+ // startup, which matters for a CLI people run in loops and shell prompts.
5
+ module.enableCompileCache?.();
6
+ const { run } = await import('./run.js');
7
+ process.exitCode = await run();
@@ -0,0 +1,2 @@
1
+ import type { CommandFactory } from './types.js';
2
+ export declare const authCommands: CommandFactory;
@@ -0,0 +1,265 @@
1
+ import { isCancel, note, password, text } from '@clack/prompts';
2
+ import { buildLoginUrl, computeChecksum, generateState, openBrowser, redirectUrlFor, waitForCallback, } from '../core/auth.js';
3
+ import { SANDBOX_CREDENTIALS, saveConfig } from '../core/config.js';
4
+ import { deleteAllSecrets, getSecret, keyringAvailable, setSecret } from '../core/credentials.js';
5
+ import { AbortedError, ExitCode, KiteCliError } from '../core/errors.js';
6
+ import { maskSecret, registerSecret } from '../core/redact.js';
7
+ import { clearSessionMeta, isExpired, loadSessionMeta, nextTokenExpiry, saveSessionMeta, timeUntilExpiry, } from '../core/session.js';
8
+ import { dateTime } from '../output/format.js';
9
+ import { renderKeyValue } from '../output/table.js';
10
+ export const authCommands = (program, run) => {
11
+ program
12
+ .command('login')
13
+ .description('Authenticate with Kite and store a session')
14
+ .option('--manual', 'Paste the request token by hand instead of using a local callback server')
15
+ .option('--api-key <key>', 'Kite Connect API key (prompted for if absent)')
16
+ .option('--force', 'Log in again even if the current session is still valid')
17
+ .action(run(login));
18
+ program
19
+ .command('logout')
20
+ .description('Invalidate the session and remove the stored access token')
21
+ .option('--all', 'Also remove the stored API secret')
22
+ .action(run(logout));
23
+ program.command('whoami').description('Show the current session and account details').action(run(whoami));
24
+ };
25
+ async function login(ctx, opts) {
26
+ const { io } = ctx;
27
+ if (!opts.force && ctx.client.hasSession() && ctx.session && !isExpired(ctx.session)) {
28
+ io.success(`Already logged in as ${io.bold(ctx.session.userName ?? ctx.session.userId)} ` +
29
+ `(expires in ${timeUntilExpiry(ctx.session)}).`);
30
+ io.info('Use --force to log in again.');
31
+ return;
32
+ }
33
+ const isSandbox = ctx.env === 'sandbox';
34
+ // --- credentials -------------------------------------------------------
35
+ let apiKey = opts.apiKey ?? (isSandbox ? SANDBOX_CREDENTIALS.apiKey : (ctx.config.apiKey ?? ''));
36
+ let apiSecret;
37
+ if (isSandbox) {
38
+ apiKey = SANDBOX_CREDENTIALS.apiKey;
39
+ apiSecret = SANDBOX_CREDENTIALS.apiSecret;
40
+ io.info('Using the public sandbox credentials. No real money is involved.');
41
+ }
42
+ else {
43
+ if (!apiKey) {
44
+ apiKey = await promptText('Kite Connect API key', 'Create an app at https://developers.kite.trade to get one.');
45
+ }
46
+ const stored = await getSecret('api_secret', { env: ctx.env });
47
+ if (stored && !opts.force) {
48
+ apiSecret = stored.value;
49
+ io.info(`Using stored API secret (${maskSecret(apiSecret)}) from the ${stored.backend}.`);
50
+ }
51
+ else {
52
+ // Prompted, never accepted as a CLI argument: argv is visible to any
53
+ // local process via `ps`, and lands in shell history.
54
+ apiSecret = await promptSecret('Kite Connect API secret');
55
+ }
56
+ }
57
+ registerSecret(apiSecret);
58
+ ctx.client.setAccessToken(undefined);
59
+ // --- obtain a request token --------------------------------------------
60
+ const state = generateState();
61
+ const loginUrl = buildLoginUrl({ apiKey, endpoints: ctx.endpoints, state });
62
+ let requestToken;
63
+ if (opts.manual) {
64
+ requestToken = await manualFlow(ctx, loginUrl);
65
+ }
66
+ else {
67
+ requestToken = await callbackFlow(ctx, loginUrl);
68
+ }
69
+ // --- exchange for an access token --------------------------------------
70
+ const checksum = computeChecksum(apiKey, requestToken, apiSecret);
71
+ // Rebuild the client with the right API key, in case this is a first login.
72
+ const { KiteClient } = await import('../core/client.js');
73
+ const { KiteApi } = await import('../core/api.js');
74
+ const client = new KiteClient({
75
+ apiKey,
76
+ endpoints: ctx.endpoints,
77
+ limiter: ctx.client.limiter,
78
+ debug: ctx.options.debug ?? false,
79
+ onDebug: (message) => io.note(io.dim(message)),
80
+ });
81
+ const api = new KiteApi(client);
82
+ const session = await api.createSession({ requestToken, checksum });
83
+ registerSecret(session.access_token);
84
+ // --- persist ------------------------------------------------------------
85
+ const backend = await setSecret('access_token', session.access_token, {
86
+ env: ctx.env,
87
+ });
88
+ if (!isSandbox) {
89
+ await setSecret('api_secret', apiSecret, { env: ctx.env });
90
+ }
91
+ const expiresAt = nextTokenExpiry();
92
+ await saveSessionMeta({
93
+ userId: session.user_id,
94
+ userName: session.user_name,
95
+ broker: session.broker,
96
+ env: ctx.env,
97
+ apiKey,
98
+ expiresAt: expiresAt.toISOString(),
99
+ loginTime: session.login_time,
100
+ exchanges: session.exchanges,
101
+ products: session.products,
102
+ });
103
+ if (ctx.config.apiKey !== apiKey && !isSandbox) {
104
+ await saveConfig({ ...ctx.config, apiKey });
105
+ }
106
+ if (io.json) {
107
+ io.writeJson({
108
+ user_id: session.user_id,
109
+ user_name: session.user_name,
110
+ env: ctx.env,
111
+ expires_at: expiresAt.toISOString(),
112
+ storage: backend,
113
+ });
114
+ return;
115
+ }
116
+ io.success(`Logged in as ${io.bold(session.user_name ?? session.user_id)}.`);
117
+ io.info(`Session expires ${dateTime(expiresAt)} IST (Kite invalidates all tokens at 6 AM daily).`);
118
+ io.info(backend === 'keyring'
119
+ ? 'Credentials stored in your OS keyring.'
120
+ : `Credentials stored in an encrypted file (no OS keyring available).`);
121
+ }
122
+ /** Browser + loopback callback. The default, and the least error-prone path. */
123
+ async function callbackFlow(ctx, loginUrl) {
124
+ const { io } = ctx;
125
+ const port = ctx.config.redirectPort;
126
+ const path = ctx.config.redirectPath;
127
+ const redirectUrl = redirectUrlFor(port, path);
128
+ const server = waitForCallback({ port, path, state: extractState(loginUrl) });
129
+ io.note('');
130
+ io.info(`Your Kite app's redirect URL must be exactly: ${io.bold(redirectUrl)}`);
131
+ io.info('Set it at https://developers.kite.trade if login fails.');
132
+ io.note('');
133
+ const opened = await openBrowser(loginUrl);
134
+ if (opened) {
135
+ io.info('Opened your browser to complete login…');
136
+ }
137
+ else {
138
+ io.warn('Could not open a browser automatically. Open this URL manually:');
139
+ io.note(` ${loginUrl}`);
140
+ }
141
+ io.info('Waiting for the callback (Ctrl-C to abort)…');
142
+ try {
143
+ const result = await server.promise;
144
+ return result.requestToken;
145
+ }
146
+ finally {
147
+ server.close();
148
+ }
149
+ }
150
+ /** Fallback for remote shells, where no browser can reach 127.0.0.1. */
151
+ async function manualFlow(ctx, loginUrl) {
152
+ const { io } = ctx;
153
+ io.note('');
154
+ io.info('Open this URL, log in, then copy the `request_token` from the redirect URL:');
155
+ io.note(` ${loginUrl}`);
156
+ io.note('');
157
+ const token = await promptText('request_token');
158
+ if (!token)
159
+ throw new AbortedError();
160
+ registerSecret(token);
161
+ return token;
162
+ }
163
+ function extractState(loginUrl) {
164
+ const params = new URL(loginUrl).searchParams.get('redirect_params') ?? '';
165
+ return new URLSearchParams(params).get('state') ?? '';
166
+ }
167
+ async function logout(ctx, opts) {
168
+ const { io } = ctx;
169
+ // Best-effort server-side invalidation; a failure here must not stop us
170
+ // clearing local state, or the user is stuck with credentials they cannot
171
+ // remove.
172
+ if (ctx.client.hasSession()) {
173
+ const stored = await getSecret('access_token', { env: ctx.env });
174
+ if (stored) {
175
+ try {
176
+ await ctx.api.invalidateSession(stored.value);
177
+ io.info('Session invalidated with Kite.');
178
+ }
179
+ catch {
180
+ io.warn('Could not reach Kite to invalidate the session; clearing local credentials anyway.');
181
+ }
182
+ }
183
+ }
184
+ const { deleteSecret } = await import('../core/credentials.js');
185
+ if (opts.all) {
186
+ await deleteAllSecrets({ env: ctx.env });
187
+ }
188
+ else {
189
+ await deleteSecret('access_token', { env: ctx.env });
190
+ }
191
+ await clearSessionMeta();
192
+ if (io.json) {
193
+ io.writeJson({ logged_out: true, removed_api_secret: Boolean(opts.all) });
194
+ return;
195
+ }
196
+ io.success(opts.all ? 'Logged out and removed the stored API secret.' : 'Logged out.');
197
+ if (!opts.all)
198
+ io.info('Your API secret is still stored. Use `kite logout --all` to remove it.');
199
+ }
200
+ async function whoami(ctx) {
201
+ const { io } = ctx;
202
+ const meta = await loadSessionMeta();
203
+ if (!ctx.client.hasSession()) {
204
+ // Exit non-zero in both modes: a script checking `kite whoami` must be able
205
+ // to branch on the exit code, not parse stdout.
206
+ process.exitCode = ExitCode.Auth;
207
+ if (io.json) {
208
+ io.writeJson({ logged_in: false });
209
+ return;
210
+ }
211
+ io.error('Not logged in.');
212
+ io.info('Run `kite login` to start a session.');
213
+ return;
214
+ }
215
+ // Hit the API so this reflects reality, not just what we cached.
216
+ const profile = await ctx.api.getProfile(ctx.signal);
217
+ if (io.json) {
218
+ io.writeJson({
219
+ logged_in: true,
220
+ env: ctx.env,
221
+ expires_at: meta?.expiresAt,
222
+ profile,
223
+ });
224
+ return;
225
+ }
226
+ io.line(renderKeyValue(io, [
227
+ ['User', `${profile.user_name ?? '—'} (${profile.user_id})`],
228
+ ['Email', profile.email ?? '—'],
229
+ ['Broker', profile.broker ?? '—'],
230
+ ['Environment', ctx.env === 'sandbox' ? io.cyan('sandbox') : 'production'],
231
+ ['Exchanges', profile.exchanges.join(', ') || '—'],
232
+ ['Products', profile.products.join(', ') || '—'],
233
+ [
234
+ 'Session expires',
235
+ meta ? `${dateTime(meta.expiresAt)} IST (${timeUntilExpiry(meta)} left)` : 'unknown (token from environment)',
236
+ ],
237
+ ['Keyring', (await keyringAvailable()) ? 'available' : 'unavailable (using encrypted file)'],
238
+ ]));
239
+ }
240
+ async function promptText(message, hint) {
241
+ if (!process.stdin.isTTY) {
242
+ throw new KiteCliError(`${message} is required but this is not an interactive terminal.`, ExitCode.ConfirmationRequired, 'Supply it via a flag or environment variable.');
243
+ }
244
+ if (hint)
245
+ note(hint);
246
+ const answer = await text({
247
+ message,
248
+ validate: (value) => ((value ?? '').trim() === '' ? 'Required.' : undefined),
249
+ });
250
+ if (isCancel(answer))
251
+ throw new AbortedError();
252
+ return String(answer).trim();
253
+ }
254
+ async function promptSecret(message) {
255
+ if (!process.stdin.isTTY) {
256
+ throw new KiteCliError(`${message} is required but this is not an interactive terminal.`, ExitCode.ConfirmationRequired, 'Set KITE_API_SECRET instead.');
257
+ }
258
+ const answer = await password({
259
+ message,
260
+ validate: (value) => ((value ?? '').trim() === '' ? 'Required.' : undefined),
261
+ });
262
+ if (isCancel(answer))
263
+ throw new AbortedError();
264
+ return String(answer).trim();
265
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandFactory } from './types.js';
2
+ export declare const configCommands: CommandFactory;