@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.
- package/LICENSE +21 -0
- package/README.md +292 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +7 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +265 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +182 -0
- package/dist/commands/gtt.d.ts +12 -0
- package/dist/commands/gtt.js +276 -0
- package/dist/commands/market.d.ts +2 -0
- package/dist/commands/market.js +389 -0
- package/dist/commands/orders.d.ts +2 -0
- package/dist/commands/orders.js +679 -0
- package/dist/commands/portfolio.d.ts +2 -0
- package/dist/commands/portfolio.js +286 -0
- package/dist/commands/types.d.ts +16 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/watch.d.ts +18 -0
- package/dist/commands/watch.js +285 -0
- package/dist/context.d.ts +40 -0
- package/dist/context.js +126 -0
- package/dist/core/api.d.ts +843 -0
- package/dist/core/api.js +472 -0
- package/dist/core/auth.d.ts +68 -0
- package/dist/core/auth.js +220 -0
- package/dist/core/client.d.ts +76 -0
- package/dist/core/client.js +285 -0
- package/dist/core/config.d.ts +114 -0
- package/dist/core/config.js +163 -0
- package/dist/core/credentials.d.ts +27 -0
- package/dist/core/credentials.js +182 -0
- package/dist/core/errors.d.ts +83 -0
- package/dist/core/errors.js +152 -0
- package/dist/core/instruments.d.ts +87 -0
- package/dist/core/instruments.js +288 -0
- package/dist/core/paths.d.ts +11 -0
- package/dist/core/paths.js +56 -0
- package/dist/core/ratelimit.d.ts +57 -0
- package/dist/core/ratelimit.js +196 -0
- package/dist/core/redact.d.ts +48 -0
- package/dist/core/redact.js +181 -0
- package/dist/core/schemas.d.ts +662 -0
- package/dist/core/schemas.js +433 -0
- package/dist/core/secretstore.d.ts +11 -0
- package/dist/core/secretstore.js +138 -0
- package/dist/core/session.d.ts +37 -0
- package/dist/core/session.js +102 -0
- package/dist/core/ticker.d.ts +131 -0
- package/dist/core/ticker.js +367 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/output/format.d.ts +31 -0
- package/dist/output/format.js +162 -0
- package/dist/output/io.d.ts +70 -0
- package/dist/output/io.js +143 -0
- package/dist/output/table.d.ts +28 -0
- package/dist/output/table.js +73 -0
- package/dist/run.d.ts +15 -0
- package/dist/run.js +161 -0
- package/dist/safety.d.ts +93 -0
- package/dist/safety.js +154 -0
- package/package.json +85 -0
package/dist/safety.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Context } from './context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Guard rails for money-moving commands.
|
|
4
|
+
*
|
|
5
|
+
* Layered, cheapest first:
|
|
6
|
+
*
|
|
7
|
+
* 1. Kill switch — config `trading.enabled: false` refuses before any network call
|
|
8
|
+
* 2. Value cap — config `trading.maxOrderValue` refuses oversized orders
|
|
9
|
+
* 3. --dry-run — renders the resolved action and exits 0 without calling the API
|
|
10
|
+
* 4. Confirmation — y/N, escalating to type-the-symbol above a threshold
|
|
11
|
+
*
|
|
12
|
+
* Two rules that are easy to get wrong and important to get right:
|
|
13
|
+
*
|
|
14
|
+
* - No TTY and no `--yes` means REFUSE, exit non-zero. Never silently proceed
|
|
15
|
+
* just because stdin is not interactive.
|
|
16
|
+
* - The prompt renders the RESOLVED action — the actual instrument token,
|
|
17
|
+
* the computed value — not an echo of the flags. A flag echo cannot catch
|
|
18
|
+
* "I typed the wrong symbol and it resolved to a different contract",
|
|
19
|
+
* which is precisely the expensive mistake.
|
|
20
|
+
*/
|
|
21
|
+
export interface ConfirmationDetail {
|
|
22
|
+
label: string;
|
|
23
|
+
value: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ConfirmOptions {
|
|
26
|
+
/** Short imperative summary, e.g. "Place BUY order". */
|
|
27
|
+
action: string;
|
|
28
|
+
/** The resolved facts shown to the user before they commit. */
|
|
29
|
+
details: ConfirmationDetail[];
|
|
30
|
+
/** Notional value in rupees, used for the cap and escalation checks. */
|
|
31
|
+
notionalValue?: number | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Does this action create or increase market exposure?
|
|
34
|
+
*
|
|
35
|
+
* Only these are subject to `trading.maxOrderValue`, and only these fail
|
|
36
|
+
* closed on an unknown value. Cancelling an order or converting a position
|
|
37
|
+
* REDUCES or merely reshapes risk — blocking those because a cap could not
|
|
38
|
+
* be evaluated would be exactly backwards, and would leave a user unable to
|
|
39
|
+
* cancel their way out of a position.
|
|
40
|
+
*/
|
|
41
|
+
increasesExposure?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* When escalation triggers, the user must type this string exactly.
|
|
44
|
+
* Usually the trading symbol.
|
|
45
|
+
*/
|
|
46
|
+
challengeToken?: string | undefined;
|
|
47
|
+
/** Set for actions that place, modify or cancel orders. */
|
|
48
|
+
mutatesOrders?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Refuse early if the local kill switch is off. */
|
|
51
|
+
export declare function assertTradingEnabled(ctx: Context): void;
|
|
52
|
+
/**
|
|
53
|
+
* Refuse orders above the configured notional cap.
|
|
54
|
+
*
|
|
55
|
+
* Fails CLOSED when the value cannot be determined. An unknown notional is the
|
|
56
|
+
* normal outcome of a failed quote lookup (the quote bucket is 1 req/sec, so a
|
|
57
|
+
* 429 is easy to hit), and treating "unknown" as "within the cap" would mean
|
|
58
|
+
* the one guard the user explicitly configured silently stops applying exactly
|
|
59
|
+
* when the CLI is least sure what it is about to do.
|
|
60
|
+
*/
|
|
61
|
+
export declare function assertWithinValueCap(ctx: Context, notionalValue: number | undefined): void;
|
|
62
|
+
/**
|
|
63
|
+
* Render the resolved action and obtain consent.
|
|
64
|
+
*
|
|
65
|
+
* Returns true to proceed. Throws rather than returning false, so a caller can
|
|
66
|
+
* never accidentally treat a declined confirmation as approval.
|
|
67
|
+
*/
|
|
68
|
+
export declare function confirmAction(ctx: Context, opts: ConfirmOptions): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Generate a unique order tag.
|
|
71
|
+
*
|
|
72
|
+
* This is the cornerstone of safe order placement. Kite has NO idempotency
|
|
73
|
+
* key — the `guid` in the response is server-assigned — so a timed-out POST
|
|
74
|
+
* /orders may or may not have executed. The only safe recovery is to search the
|
|
75
|
+
* orderbook for a tag we chose ourselves.
|
|
76
|
+
*
|
|
77
|
+
* Kite caps `tag` at 20 alphanumeric characters.
|
|
78
|
+
*/
|
|
79
|
+
export declare function generateOrderTag(prefix?: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* Build the tag actually sent to Kite, guaranteeing uniqueness even when the
|
|
82
|
+
* user supplied their own.
|
|
83
|
+
*
|
|
84
|
+
* A user tag is a label; the reconciliation key must be unique or the recovery
|
|
85
|
+
* path actively lies. With a reused tag, a failed placement would find the
|
|
86
|
+
* EARLIER order carrying that tag and report "it was placed, do not retry" —
|
|
87
|
+
* when in fact nothing was placed. So the user's tag is kept as a prefix for
|
|
88
|
+
* their own filtering, and a random suffix makes it unique.
|
|
89
|
+
*
|
|
90
|
+
* Kite caps `tag` at 20 alphanumeric characters, so the user prefix is
|
|
91
|
+
* truncated to leave room for the suffix.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildOrderTag(userTag?: string): string;
|
package/dist/safety.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { confirm, isCancel, text } from '@clack/prompts';
|
|
3
|
+
import { AbortedError, ExitCode, KiteCliError } from './core/errors.js';
|
|
4
|
+
import { rupees } from './output/format.js';
|
|
5
|
+
/** Refuse early if the local kill switch is off. */
|
|
6
|
+
export function assertTradingEnabled(ctx) {
|
|
7
|
+
if (!ctx.config.trading.enabled) {
|
|
8
|
+
throw new KiteCliError('Trading is disabled by the local kill switch.', ExitCode.TradingDisabled, 'Re-enable it with `kite config set trading.enabled true`.');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Refuse orders above the configured notional cap.
|
|
13
|
+
*
|
|
14
|
+
* Fails CLOSED when the value cannot be determined. An unknown notional is the
|
|
15
|
+
* normal outcome of a failed quote lookup (the quote bucket is 1 req/sec, so a
|
|
16
|
+
* 429 is easy to hit), and treating "unknown" as "within the cap" would mean
|
|
17
|
+
* the one guard the user explicitly configured silently stops applying exactly
|
|
18
|
+
* when the CLI is least sure what it is about to do.
|
|
19
|
+
*/
|
|
20
|
+
export function assertWithinValueCap(ctx, notionalValue) {
|
|
21
|
+
const cap = ctx.config.trading.maxOrderValue;
|
|
22
|
+
if (cap === undefined)
|
|
23
|
+
return;
|
|
24
|
+
if (notionalValue === undefined) {
|
|
25
|
+
throw new KiteCliError(`Cannot verify this order against your configured cap of ${rupees(cap)} — its value is unknown.`, ExitCode.TradingDisabled, 'Specify an explicit --price so the value can be computed, or unset the cap with `kite config unset trading.maxOrderValue`.');
|
|
26
|
+
}
|
|
27
|
+
if (notionalValue > cap) {
|
|
28
|
+
throw new KiteCliError(`Order value ${rupees(notionalValue)} exceeds your configured cap of ${rupees(cap)}.`, ExitCode.TradingDisabled, 'Raise it with `kite config set trading.maxOrderValue <amount>`, or unset it to remove the cap.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Render the resolved action and obtain consent.
|
|
33
|
+
*
|
|
34
|
+
* Returns true to proceed. Throws rather than returning false, so a caller can
|
|
35
|
+
* never accidentally treat a declined confirmation as approval.
|
|
36
|
+
*/
|
|
37
|
+
export async function confirmAction(ctx, opts) {
|
|
38
|
+
if (opts.mutatesOrders) {
|
|
39
|
+
assertTradingEnabled(ctx);
|
|
40
|
+
}
|
|
41
|
+
// The cap is about how much exposure you can take on, not about whether you
|
|
42
|
+
// may unwind it.
|
|
43
|
+
if (opts.increasesExposure) {
|
|
44
|
+
assertWithinValueCap(ctx, opts.notionalValue);
|
|
45
|
+
}
|
|
46
|
+
const { io } = ctx;
|
|
47
|
+
// Whether we will actually stop and ask. Computed up front because it
|
|
48
|
+
// decides whether the preview may be suppressed.
|
|
49
|
+
const willPrompt = !ctx.options.dryRun && !ctx.options.yes && (ctx.config.trading.confirm || opts.mutatesOrders === true);
|
|
50
|
+
// Always show what is about to happen, even with --yes, so the record exists
|
|
51
|
+
// in the terminal scrollback.
|
|
52
|
+
//
|
|
53
|
+
// `force` when we are about to prompt: --quiet and --json normally suppress
|
|
54
|
+
// the preview, but asking someone to approve an order while showing them
|
|
55
|
+
// none of the resolved facts is worse than printing in a mode they asked to
|
|
56
|
+
// be quiet. A prompt without its preview is not informed consent.
|
|
57
|
+
renderPreview(ctx, opts, willPrompt);
|
|
58
|
+
if (ctx.options.dryRun) {
|
|
59
|
+
io.note('');
|
|
60
|
+
io.note(io.cyan('Dry run — nothing was sent to Kite.'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// --yes is a call-site-only bypass. It is deliberately not readable from the
|
|
64
|
+
// config file: disabling safety must be an explicit act every time.
|
|
65
|
+
if (ctx.options.yes)
|
|
66
|
+
return;
|
|
67
|
+
if (!ctx.config.trading.confirm && opts.mutatesOrders !== true)
|
|
68
|
+
return;
|
|
69
|
+
if (!process.stdin.isTTY || !io.stderrIsTty) {
|
|
70
|
+
throw new KiteCliError(`${opts.action} requires confirmation, but this is not an interactive terminal.`, ExitCode.ConfirmationRequired, 'Pass --yes to confirm non-interactively, or --dry-run to preview without sending.');
|
|
71
|
+
}
|
|
72
|
+
const threshold = ctx.config.trading.strictConfirmAbove;
|
|
73
|
+
// An unknown value escalates rather than de-escalates. If we could not price
|
|
74
|
+
// the order, we cannot claim it is small — and a mispriced market order is
|
|
75
|
+
// precisely the case where typing the symbol is worth the friction.
|
|
76
|
+
// Unknown value only escalates for exposure-increasing actions; demanding a
|
|
77
|
+
// typed confirmation to unwind a position would be friction in the wrong
|
|
78
|
+
// direction.
|
|
79
|
+
const valueUnknown = opts.notionalValue === undefined && opts.increasesExposure === true;
|
|
80
|
+
const needsChallenge = opts.challengeToken !== undefined &&
|
|
81
|
+
(valueUnknown || (opts.notionalValue !== undefined && opts.notionalValue >= threshold));
|
|
82
|
+
if (needsChallenge) {
|
|
83
|
+
// Above the threshold a keystroke is too easy to fire by accident, so we
|
|
84
|
+
// require the symbol to be typed out.
|
|
85
|
+
const reason = valueUnknown
|
|
86
|
+
? 'This order value could not be determined'
|
|
87
|
+
: `This is a large order (${rupees(opts.notionalValue)})`;
|
|
88
|
+
const answer = await text({
|
|
89
|
+
message: `${reason}. Type ${opts.challengeToken} to confirm:`,
|
|
90
|
+
validate: (value) => value === opts.challengeToken ? undefined : `Type "${opts.challengeToken}" exactly, or press Ctrl-C to abort.`,
|
|
91
|
+
});
|
|
92
|
+
if (isCancel(answer) || answer !== opts.challengeToken) {
|
|
93
|
+
throw new AbortedError('Aborted — confirmation did not match.');
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const answer = await confirm({
|
|
98
|
+
message: `${opts.action}?`,
|
|
99
|
+
initialValue: false,
|
|
100
|
+
});
|
|
101
|
+
if (isCancel(answer) || answer !== true) {
|
|
102
|
+
throw new AbortedError();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function renderPreview(ctx, opts, force = false) {
|
|
106
|
+
const { io } = ctx;
|
|
107
|
+
if ((io.json || io.quiet) && !force)
|
|
108
|
+
return;
|
|
109
|
+
const width = Math.max(...opts.details.map((d) => d.label.length));
|
|
110
|
+
const emit = force ? (text) => io.forceNote(text) : (text) => io.note(text);
|
|
111
|
+
emit('');
|
|
112
|
+
emit(io.bold(opts.action));
|
|
113
|
+
for (const detail of opts.details) {
|
|
114
|
+
emit(` ${io.dim(detail.label.padEnd(width))} ${detail.value}`);
|
|
115
|
+
}
|
|
116
|
+
if (ctx.env === 'sandbox') {
|
|
117
|
+
emit(` ${io.cyan('sandbox — no real money involved')}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Generate a unique order tag.
|
|
122
|
+
*
|
|
123
|
+
* This is the cornerstone of safe order placement. Kite has NO idempotency
|
|
124
|
+
* key — the `guid` in the response is server-assigned — so a timed-out POST
|
|
125
|
+
* /orders may or may not have executed. The only safe recovery is to search the
|
|
126
|
+
* orderbook for a tag we chose ourselves.
|
|
127
|
+
*
|
|
128
|
+
* Kite caps `tag` at 20 alphanumeric characters.
|
|
129
|
+
*/
|
|
130
|
+
export function generateOrderTag(prefix = 'kc') {
|
|
131
|
+
const random = randomBytes(6).toString('hex');
|
|
132
|
+
const stamp = Date.now().toString(36);
|
|
133
|
+
return `${prefix}${stamp}${random}`.slice(0, 20);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build the tag actually sent to Kite, guaranteeing uniqueness even when the
|
|
137
|
+
* user supplied their own.
|
|
138
|
+
*
|
|
139
|
+
* A user tag is a label; the reconciliation key must be unique or the recovery
|
|
140
|
+
* path actively lies. With a reused tag, a failed placement would find the
|
|
141
|
+
* EARLIER order carrying that tag and report "it was placed, do not retry" —
|
|
142
|
+
* when in fact nothing was placed. So the user's tag is kept as a prefix for
|
|
143
|
+
* their own filtering, and a random suffix makes it unique.
|
|
144
|
+
*
|
|
145
|
+
* Kite caps `tag` at 20 alphanumeric characters, so the user prefix is
|
|
146
|
+
* truncated to leave room for the suffix.
|
|
147
|
+
*/
|
|
148
|
+
export function buildOrderTag(userTag) {
|
|
149
|
+
if (!userTag)
|
|
150
|
+
return generateOrderTag();
|
|
151
|
+
const suffix = randomBytes(4).toString('hex'); // 8 chars
|
|
152
|
+
const prefix = userTag.slice(0, 20 - suffix.length);
|
|
153
|
+
return `${prefix}${suffix}`;
|
|
154
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pungoyal/kite-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unofficial command-line interface for the Zerodha Kite Connect API — not affiliated with or endorsed by Zerodha",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"zerodha",
|
|
7
|
+
"kite",
|
|
8
|
+
"kiteconnect",
|
|
9
|
+
"trading",
|
|
10
|
+
"stocks",
|
|
11
|
+
"cli",
|
|
12
|
+
"nse",
|
|
13
|
+
"bse"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Puneet Goyal",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/pungoyal/kite-cli.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/pungoyal/kite-cli#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/pungoyal/kite-cli/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22.12.0"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"kite": "./dist/cli.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"default": "./dist/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"dev": "node --experimental-strip-types src/cli.ts",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"lint": "biome check",
|
|
52
|
+
"lint:fix": "biome check --write",
|
|
53
|
+
"format": "biome format --write",
|
|
54
|
+
"hooks:install": "git config core.hooksPath .githooks",
|
|
55
|
+
"check:deps": "node scripts/check-deps.mjs",
|
|
56
|
+
"lint:publish": "publint && attw --pack . --profile esm-only",
|
|
57
|
+
"check:dist": "node -e \"const{existsSync,readFileSync}=require('node:fs');if(!existsSync('dist/cli.js'))throw new Error('dist/ is missing — run npm run build before publishing');if(!readFileSync('dist/cli.js','utf8').startsWith('#!/usr/bin/env node'))throw new Error('dist/cli.js lost its shebang')\"",
|
|
58
|
+
"prepublishOnly": "npm run build && npm run check:dist"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@clack/prompts": "^1.7.0",
|
|
62
|
+
"@napi-rs/keyring": "^1.3.0",
|
|
63
|
+
"cli-table3": "^0.6.5",
|
|
64
|
+
"commander": "^15.0.0",
|
|
65
|
+
"log-update": "^8.0.0",
|
|
66
|
+
"picocolors": "^1.1.1",
|
|
67
|
+
"undici": "^8.7.0",
|
|
68
|
+
"ws": "^8.21.1",
|
|
69
|
+
"yocto-spinner": "^1.2.2",
|
|
70
|
+
"zod": "^4.4.3"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@arethetypeswrong/cli": "^0.18.0",
|
|
74
|
+
"@biomejs/biome": "^2.5.4",
|
|
75
|
+
"@types/node": "^22.15.0",
|
|
76
|
+
"@types/ws": "^8.18.0",
|
|
77
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
78
|
+
"execa": "^10.0.0",
|
|
79
|
+
"msw": "^2.15.0",
|
|
80
|
+
"publint": "^0.3.0",
|
|
81
|
+
"strip-ansi": "^7.1.0",
|
|
82
|
+
"typescript": "^5.8.0",
|
|
83
|
+
"vitest": "^4.1.10"
|
|
84
|
+
}
|
|
85
|
+
}
|