@pungoyal/kite-cli 0.2.1 → 0.4.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/README.md +50 -1
- package/dist/commands/alerts.d.ts +28 -0
- package/dist/commands/alerts.js +257 -76
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +74 -5
- package/dist/commands/completion.d.ts +35 -0
- package/dist/commands/completion.js +240 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +190 -0
- package/dist/commands/margins.d.ts +34 -0
- package/dist/commands/margins.js +258 -0
- package/dist/commands/mf.d.ts +10 -0
- package/dist/commands/mf.js +85 -0
- package/dist/commands/register.d.ts +25 -0
- package/dist/commands/register.js +75 -0
- package/dist/core/auth.d.ts +9 -0
- package/dist/core/auth.js +39 -0
- package/dist/core/client.js +8 -1
- package/dist/core/schemas.d.ts +3 -0
- package/dist/run.js +7 -39
- package/package.json +2 -2
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { ORDER_TYPES, PRODUCTS, VARIETIES, } from '../core/api.js';
|
|
2
|
+
import { UsageError } from '../core/errors.js';
|
|
3
|
+
import { formatInstrumentKey, parseInstrumentKey } from '../core/instruments.js';
|
|
4
|
+
import { money, rupees } from '../output/format.js';
|
|
5
|
+
import { heading, printTable, renderKeyValue } from '../output/table.js';
|
|
6
|
+
/**
|
|
7
|
+
* Margin and charge calculators — read-only.
|
|
8
|
+
*
|
|
9
|
+
* These POST a hypothetical set of orders to Kite and return what they would
|
|
10
|
+
* cost: `order` gives the per-order required margin (no netting), `basket` the
|
|
11
|
+
* net margin for the set (with spread/hedge benefit), and `charges` the
|
|
12
|
+
* itemised brokerage/tax breakdown (a "virtual contract note"). Nothing is
|
|
13
|
+
* placed, so none of the trading safety layer applies.
|
|
14
|
+
*/
|
|
15
|
+
export const marginCommands = (program, run) => {
|
|
16
|
+
const margins = program.command('margins').description('Calculate order margins and charges (nothing is placed)');
|
|
17
|
+
const orderExample = 'Each order is EXCHANGE:SYMBOL:SIDE:QTY[:TYPE][:PRODUCT][:VARIETY][:PRICE][:trigger=<n>], ' +
|
|
18
|
+
"e.g. 'NFO:NIFTY25AUGFUT:BUY:75:MARKET:NRML'. Product defaults to CNC, variety to regular.";
|
|
19
|
+
margins
|
|
20
|
+
.command('order')
|
|
21
|
+
.description('Required margin for each order on its own')
|
|
22
|
+
.argument('<orders...>', orderExample)
|
|
23
|
+
.action(run(orderMargins));
|
|
24
|
+
margins
|
|
25
|
+
.command('basket')
|
|
26
|
+
.description('Net margin for a basket of orders, with spread/hedge benefit')
|
|
27
|
+
.argument('<orders...>', orderExample)
|
|
28
|
+
.option('--no-consider-positions', 'Ignore existing positions when netting')
|
|
29
|
+
.action(run(basketMargins));
|
|
30
|
+
margins
|
|
31
|
+
.command('charges')
|
|
32
|
+
.description('Itemised charges for a set of executed orders (virtual contract note)')
|
|
33
|
+
.argument('<orders...>', `${orderExample} A non-zero price (the execution price) is required.`)
|
|
34
|
+
.action(run(orderCharges));
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Parse one `EXCHANGE:SYMBOL:SIDE:QTY[:attrs...]` spec.
|
|
38
|
+
*
|
|
39
|
+
* Attributes after QTY are classified by content — an order type, a product, a
|
|
40
|
+
* variety, a bare number (the price), or `trigger=<n>`. Fails closed: an
|
|
41
|
+
* unrecognised token, a duplicated field, or an empty field rejects the whole
|
|
42
|
+
* spec rather than silently defaulting, since a mis-parsed order yields a
|
|
43
|
+
* silently wrong margin or charge.
|
|
44
|
+
*/
|
|
45
|
+
export function parseOrderSpec(spec) {
|
|
46
|
+
const tokens = spec
|
|
47
|
+
.trim()
|
|
48
|
+
.split(':')
|
|
49
|
+
.map((t) => t.trim());
|
|
50
|
+
if (tokens.length < 4) {
|
|
51
|
+
throw new UsageError(`Malformed order "${spec}".`, "Expected at least EXCHANGE:SYMBOL:SIDE:QTY, e.g. 'NFO:NIFTY25AUGFUT:BUY:75:NRML'.");
|
|
52
|
+
}
|
|
53
|
+
const [exchangeTok, symbolTok, sideTok, qtyTok, ...rest] = tokens;
|
|
54
|
+
const { exchange, tradingsymbol } = parseInstrumentKey(`${exchangeTok}:${symbolTok}`);
|
|
55
|
+
const side = sideTok.toUpperCase();
|
|
56
|
+
if (side !== 'BUY' && side !== 'SELL') {
|
|
57
|
+
throw new UsageError(`Order side must be BUY or SELL, got "${sideTok}" in "${spec}".`);
|
|
58
|
+
}
|
|
59
|
+
const quantity = Number(qtyTok);
|
|
60
|
+
if (!Number.isInteger(quantity) || quantity <= 0) {
|
|
61
|
+
throw new UsageError(`Order quantity must be a positive integer, got "${qtyTok}" in "${spec}".`);
|
|
62
|
+
}
|
|
63
|
+
let orderType;
|
|
64
|
+
let product;
|
|
65
|
+
let variety;
|
|
66
|
+
let price;
|
|
67
|
+
let triggerPrice;
|
|
68
|
+
const setOnce = (current, next, label) => {
|
|
69
|
+
if (current !== undefined)
|
|
70
|
+
throw new UsageError(`Order "${spec}" sets ${label} more than once.`);
|
|
71
|
+
return next;
|
|
72
|
+
};
|
|
73
|
+
for (const tok of rest) {
|
|
74
|
+
if (tok === '')
|
|
75
|
+
throw new UsageError(`Order "${spec}" has an empty field. Remove the stray ":".`);
|
|
76
|
+
const eq = tok.indexOf('=');
|
|
77
|
+
if (eq !== -1) {
|
|
78
|
+
const key = tok.slice(0, eq).trim().toLowerCase();
|
|
79
|
+
const value = tok.slice(eq + 1).trim();
|
|
80
|
+
if (key === 'price')
|
|
81
|
+
price = setOnce(price, parsePositive(value, 'price', spec), 'the price');
|
|
82
|
+
else if (key === 'trigger')
|
|
83
|
+
triggerPrice = setOnce(triggerPrice, parsePositive(value, 'trigger', spec), 'the trigger price');
|
|
84
|
+
else if (key === 'type')
|
|
85
|
+
orderType = setOnce(orderType, normalise(value, ORDER_TYPES, 'order type'), 'the order type');
|
|
86
|
+
else if (key === 'product')
|
|
87
|
+
product = setOnce(product, normalise(value, PRODUCTS, 'product'), 'the product');
|
|
88
|
+
else if (key === 'variety')
|
|
89
|
+
variety = setOnce(variety, normalise(value, VARIETIES, 'variety'), 'the variety');
|
|
90
|
+
else
|
|
91
|
+
throw new UsageError(`Order "${spec}" has an unknown field "${key}".`, 'Valid keys: type, product, variety, price, trigger.');
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const upper = tok.toUpperCase();
|
|
95
|
+
const lower = tok.toLowerCase();
|
|
96
|
+
if (ORDER_TYPES.includes(upper)) {
|
|
97
|
+
orderType = setOnce(orderType, upper, 'the order type');
|
|
98
|
+
}
|
|
99
|
+
else if (PRODUCTS.includes(upper)) {
|
|
100
|
+
product = setOnce(product, upper, 'the product');
|
|
101
|
+
}
|
|
102
|
+
else if (VARIETIES.includes(lower)) {
|
|
103
|
+
variety = setOnce(variety, lower, 'the variety');
|
|
104
|
+
}
|
|
105
|
+
else if (isNumeric(tok)) {
|
|
106
|
+
// A bare number is the price. A trigger must be given explicitly.
|
|
107
|
+
price = setOnce(price, parsePositive(tok, 'price', spec), 'the price');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
throw new UsageError(`Order "${spec}" has an unrecognised field "${tok}".`, 'Fields after QTY are an order type, product, variety, a price, or trigger=<n>.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
exchange,
|
|
115
|
+
tradingsymbol,
|
|
116
|
+
transactionType: side,
|
|
117
|
+
quantity,
|
|
118
|
+
orderType: orderType ?? 'MARKET',
|
|
119
|
+
product: product ?? 'CNC',
|
|
120
|
+
variety: variety ?? 'regular',
|
|
121
|
+
price,
|
|
122
|
+
triggerPrice,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function parseSpecs(args) {
|
|
126
|
+
if (args.length === 0)
|
|
127
|
+
throw new UsageError('At least one order is required.');
|
|
128
|
+
return args.map(parseOrderSpec);
|
|
129
|
+
}
|
|
130
|
+
/** Serialise for /margins/orders and /margins/basket (price + trigger_price). */
|
|
131
|
+
function toMarginOrder(o) {
|
|
132
|
+
return {
|
|
133
|
+
exchange: o.exchange,
|
|
134
|
+
tradingsymbol: o.tradingsymbol,
|
|
135
|
+
transaction_type: o.transactionType,
|
|
136
|
+
variety: o.variety,
|
|
137
|
+
product: o.product,
|
|
138
|
+
order_type: o.orderType,
|
|
139
|
+
quantity: o.quantity,
|
|
140
|
+
price: o.price ?? 0,
|
|
141
|
+
trigger_price: o.triggerPrice ?? 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Serialise for /charges/orders (average_price + order_id, no price/trigger).
|
|
146
|
+
* Charges are a percentage of quantity × average_price, so a zero price yields
|
|
147
|
+
* a plausible-looking ≈₹0 that is silently wrong — require a real price.
|
|
148
|
+
*/
|
|
149
|
+
function toChargesOrder(o, index) {
|
|
150
|
+
if (o.price === undefined || o.price <= 0) {
|
|
151
|
+
throw new UsageError(`Order "${formatInstrumentKey(o.exchange, o.tradingsymbol)}" needs a non-zero price for charges.`, 'Charges are computed from quantity × execution price; add a price, e.g. :1500 or price=1500.');
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
// A virtual contract note needs an order_id per leg; the index is fine.
|
|
155
|
+
order_id: String(index + 1),
|
|
156
|
+
exchange: o.exchange,
|
|
157
|
+
tradingsymbol: o.tradingsymbol,
|
|
158
|
+
transaction_type: o.transactionType,
|
|
159
|
+
variety: o.variety,
|
|
160
|
+
product: o.product,
|
|
161
|
+
order_type: o.orderType,
|
|
162
|
+
quantity: o.quantity,
|
|
163
|
+
average_price: o.price,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Commands
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
async function orderMargins(ctx, _opts, command) {
|
|
170
|
+
ctx.requireSession();
|
|
171
|
+
const specs = parseSpecs(command.args);
|
|
172
|
+
const margins = await ctx.api.orderMargins(specs.map(toMarginOrder), ctx.signal);
|
|
173
|
+
const columns = marginColumns();
|
|
174
|
+
printTable(ctx.io, margins, columns, margins, { compact: ctx.config.output.compact, empty: 'No margins returned.' });
|
|
175
|
+
if (ctx.io.json)
|
|
176
|
+
return;
|
|
177
|
+
const total = margins.reduce((sum, m) => sum + (m.total ?? 0), 0);
|
|
178
|
+
ctx.io.line('');
|
|
179
|
+
ctx.io.line(` Total margin required ${rupees(total)}`);
|
|
180
|
+
}
|
|
181
|
+
async function basketMargins(ctx, opts, command) {
|
|
182
|
+
ctx.requireSession();
|
|
183
|
+
const specs = parseSpecs(command.args);
|
|
184
|
+
const basket = await ctx.api.basketMargins(specs.map(toMarginOrder), opts.considerPositions !== false, ctx.signal);
|
|
185
|
+
if (ctx.io.json) {
|
|
186
|
+
ctx.io.writeJson(basket);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const { io } = ctx;
|
|
190
|
+
io.line(heading(io, 'Per order'));
|
|
191
|
+
printTable(io, basket.orders, marginColumns(), basket.orders, {
|
|
192
|
+
compact: ctx.config.output.compact,
|
|
193
|
+
empty: 'No orders.',
|
|
194
|
+
});
|
|
195
|
+
// `final` is the margin actually blocked after spread/hedge benefit; `initial`
|
|
196
|
+
// is the gross figure before it. The difference is the benefit itself.
|
|
197
|
+
const finalTotal = basket.final?.total ?? 0;
|
|
198
|
+
const initialTotal = basket.initial?.total ?? 0;
|
|
199
|
+
io.line('');
|
|
200
|
+
io.line(renderKeyValue(io, [
|
|
201
|
+
['Margin before benefit', rupees(initialTotal)],
|
|
202
|
+
['Spread/hedge benefit', rupees(Math.max(0, initialTotal - finalTotal))],
|
|
203
|
+
['Net margin required', io.bold(rupees(finalTotal))],
|
|
204
|
+
]));
|
|
205
|
+
}
|
|
206
|
+
async function orderCharges(ctx, _opts, command) {
|
|
207
|
+
ctx.requireSession();
|
|
208
|
+
const specs = parseSpecs(command.args);
|
|
209
|
+
const charges = await ctx.api.orderCharges(specs.map((o, i) => toChargesOrder(o, i)), ctx.signal);
|
|
210
|
+
const columns = [
|
|
211
|
+
{ header: 'Symbol', value: (m, io) => io.bold(`${m.exchange ?? '?'}:${m.tradingsymbol ?? '?'}`) },
|
|
212
|
+
{ header: 'Brokerage', value: (m) => money(m.charges?.brokerage), align: 'right' },
|
|
213
|
+
{ header: 'STT/CTT', value: (m) => money(m.charges?.transaction_tax), align: 'right' },
|
|
214
|
+
{ header: 'Txn', value: (m) => money(m.charges?.exchange_turnover_charge), align: 'right' },
|
|
215
|
+
{ header: 'GST', value: (m) => money(m.charges?.gst?.total), align: 'right' },
|
|
216
|
+
{ header: 'Stamp', value: (m) => money(m.charges?.stamp_duty), align: 'right' },
|
|
217
|
+
{ header: 'SEBI', value: (m) => money(m.charges?.sebi_turnover_charge), align: 'right' },
|
|
218
|
+
{ header: 'Total', value: (m, io) => io.bold(money(m.charges?.total)), align: 'right' },
|
|
219
|
+
];
|
|
220
|
+
printTable(ctx.io, charges, columns, charges, {
|
|
221
|
+
compact: ctx.config.output.compact,
|
|
222
|
+
empty: 'No charges returned.',
|
|
223
|
+
});
|
|
224
|
+
if (ctx.io.json)
|
|
225
|
+
return;
|
|
226
|
+
const total = charges.reduce((sum, m) => sum + (m.charges?.total ?? 0), 0);
|
|
227
|
+
ctx.io.line('');
|
|
228
|
+
ctx.io.line(` Total charges ${rupees(total)}`);
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Helpers
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
function marginColumns() {
|
|
234
|
+
return [
|
|
235
|
+
{ header: 'Symbol', value: (m, io) => io.bold(`${m.exchange ?? '?'}:${m.tradingsymbol ?? '?'}`) },
|
|
236
|
+
{ header: 'SPAN', value: (m) => money(m.span), align: 'right' },
|
|
237
|
+
{ header: 'Exposure', value: (m) => money(m.exposure), align: 'right' },
|
|
238
|
+
{ header: 'Premium', value: (m) => money(m.option_premium), align: 'right' },
|
|
239
|
+
{ header: 'Var', value: (m) => money(m.var), align: 'right' },
|
|
240
|
+
{ header: 'Total', value: (m, io) => io.bold(money(m.total)), align: 'right' },
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
function normalise(value, allowed, label) {
|
|
244
|
+
const candidate = label === 'variety' ? value.toLowerCase() : value.toUpperCase();
|
|
245
|
+
if (allowed.includes(candidate))
|
|
246
|
+
return candidate;
|
|
247
|
+
throw new UsageError(`Unknown ${label} "${value}".`, `Valid values: ${allowed.join(', ')}.`);
|
|
248
|
+
}
|
|
249
|
+
function isNumeric(value) {
|
|
250
|
+
return value !== '' && Number.isFinite(Number(value));
|
|
251
|
+
}
|
|
252
|
+
function parsePositive(value, label, spec) {
|
|
253
|
+
const n = Number(value);
|
|
254
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
255
|
+
throw new UsageError(`Order "${spec}" has an invalid ${label} "${value}"; expected a positive number.`);
|
|
256
|
+
}
|
|
257
|
+
return n;
|
|
258
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommandFactory } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mutual funds — read only.
|
|
4
|
+
*
|
|
5
|
+
* Kite Connect does not offer MF order placement or SIP management over the
|
|
6
|
+
* API (an MF purchase needs a bank debit the API can't authorise), so this is
|
|
7
|
+
* holdings, recent orders, and SIPs — the three read endpoints — and nothing
|
|
8
|
+
* that moves money. No kill switch, value cap, or confirmation applies.
|
|
9
|
+
*/
|
|
10
|
+
export declare const mfCommands: CommandFactory;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { dateTime, money, quantity, rupees, signedRupees } from '../output/format.js';
|
|
2
|
+
import { printTable } from '../output/table.js';
|
|
3
|
+
/**
|
|
4
|
+
* Mutual funds — read only.
|
|
5
|
+
*
|
|
6
|
+
* Kite Connect does not offer MF order placement or SIP management over the
|
|
7
|
+
* API (an MF purchase needs a bank debit the API can't authorise), so this is
|
|
8
|
+
* holdings, recent orders, and SIPs — the three read endpoints — and nothing
|
|
9
|
+
* that moves money. No kill switch, value cap, or confirmation applies.
|
|
10
|
+
*/
|
|
11
|
+
export const mfCommands = (program, run) => {
|
|
12
|
+
const mf = program.command('mf').description('View mutual fund holdings, orders and SIPs');
|
|
13
|
+
mf.command('holdings', { isDefault: true }).description('Show your mutual fund holdings').action(run(mfHoldings));
|
|
14
|
+
mf.command('orders').description('Show mutual fund orders from the last 7 days').action(run(mfOrders));
|
|
15
|
+
mf.command('sips').description('Show your mutual fund SIPs').action(run(mfSips));
|
|
16
|
+
};
|
|
17
|
+
async function mfHoldings(ctx) {
|
|
18
|
+
ctx.requireSession();
|
|
19
|
+
const rows = await ctx.api.getMfHoldings(ctx.signal);
|
|
20
|
+
const columns = [
|
|
21
|
+
{ header: 'Fund', value: (h, io) => io.bold(h.fund ?? h.tradingsymbol) },
|
|
22
|
+
{ header: 'Folio', value: (h) => h.folio ?? '—' },
|
|
23
|
+
{ header: 'Units', value: (h) => quantity(h.quantity), align: 'right' },
|
|
24
|
+
{ header: 'Avg', value: (h) => money(h.average_price), align: 'right' },
|
|
25
|
+
{ header: 'NAV', value: (h) => money(h.last_price), align: 'right' },
|
|
26
|
+
{ header: 'Value', value: (h) => money(h.last_price * h.quantity), align: 'right' },
|
|
27
|
+
{ header: 'P&L', value: (h, io) => io.signed(h.pnl, signedRupees(h.pnl)), align: 'right' },
|
|
28
|
+
];
|
|
29
|
+
printTable(ctx.io, rows, columns, rows, {
|
|
30
|
+
compact: ctx.config.output.compact,
|
|
31
|
+
empty: 'No mutual fund holdings.',
|
|
32
|
+
});
|
|
33
|
+
if (ctx.io.json)
|
|
34
|
+
return;
|
|
35
|
+
const totalValue = rows.reduce((sum, h) => sum + h.last_price * h.quantity, 0);
|
|
36
|
+
const totalPnl = rows.reduce((sum, h) => sum + h.pnl, 0);
|
|
37
|
+
if (rows.length > 0) {
|
|
38
|
+
const { io } = ctx;
|
|
39
|
+
io.line('');
|
|
40
|
+
io.line(` Current value ${rupees(totalValue)} P&L ${io.signed(totalPnl, signedRupees(totalPnl))}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function mfOrders(ctx) {
|
|
44
|
+
ctx.requireSession();
|
|
45
|
+
const rows = await ctx.api.getMfOrders(ctx.signal);
|
|
46
|
+
const columns = [
|
|
47
|
+
{ header: 'Order ID', value: (o, io) => io.dim(o.order_id) },
|
|
48
|
+
{ header: 'Fund', value: (o, io) => io.bold(o.fund ?? o.tradingsymbol ?? '—') },
|
|
49
|
+
{
|
|
50
|
+
header: 'Side',
|
|
51
|
+
value: (o, io) => o.transaction_type === 'BUY'
|
|
52
|
+
? io.green('BUY')
|
|
53
|
+
: o.transaction_type === 'SELL'
|
|
54
|
+
? io.red('SELL')
|
|
55
|
+
: (o.transaction_type ?? '—'),
|
|
56
|
+
},
|
|
57
|
+
{ header: 'Status', value: (o) => o.status ?? '—' },
|
|
58
|
+
{ header: 'Units', value: (o) => quantity(o.quantity ?? undefined), align: 'right' },
|
|
59
|
+
{ header: 'Amount', value: (o) => money(o.amount ?? undefined), align: 'right' },
|
|
60
|
+
{ header: 'When', value: (o) => dateTime(o.order_timestamp ?? undefined) },
|
|
61
|
+
];
|
|
62
|
+
printTable(ctx.io, rows, columns, rows, {
|
|
63
|
+
compact: ctx.config.output.compact,
|
|
64
|
+
// An empty list can just mean nothing was placed recently, not that you have
|
|
65
|
+
// no MF history — the endpoint only reaches back 7 days.
|
|
66
|
+
empty: 'No mutual fund orders in the last 7 days.',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function mfSips(ctx) {
|
|
70
|
+
ctx.requireSession();
|
|
71
|
+
const rows = await ctx.api.getMfSips(ctx.signal);
|
|
72
|
+
const columns = [
|
|
73
|
+
{ header: 'SIP ID', value: (s, io) => io.dim(s.sip_id) },
|
|
74
|
+
{ header: 'Fund', value: (s, io) => io.bold(s.fund ?? s.tradingsymbol ?? '—') },
|
|
75
|
+
{ header: 'Status', value: (s) => s.status ?? '—' },
|
|
76
|
+
{ header: 'Instalment', value: (s) => money(s.instalment_amount), align: 'right' },
|
|
77
|
+
{ header: 'Done', value: (s) => quantity(s.instalments), align: 'right' },
|
|
78
|
+
{ header: 'Frequency', value: (s) => s.frequency ?? '—' },
|
|
79
|
+
{ header: 'Next', value: (s) => dateTime(s.next_instalment ?? undefined) },
|
|
80
|
+
];
|
|
81
|
+
printTable(ctx.io, rows, columns, rows, {
|
|
82
|
+
compact: ctx.config.output.compact,
|
|
83
|
+
empty: 'No mutual fund SIPs.',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { Runner } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Command wiring, factored out of run.ts so there is a single source of truth
|
|
5
|
+
* for the command tree.
|
|
6
|
+
*
|
|
7
|
+
* run() consumes this to build the live program; `kite completion` consumes it
|
|
8
|
+
* with a no-op runner to introspect command and flag names for shell
|
|
9
|
+
* completions. Keeping both on the same registration path means a new command
|
|
10
|
+
* or flag is completable the moment it is added, with nothing to keep in sync.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Attach the global options every command inherits. Split from the name /
|
|
14
|
+
* version / output wiring in run.ts, which is invocation-specific, so an
|
|
15
|
+
* introspection-only program can still enumerate the same global flags.
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyGlobalOptions(program: Command): Command;
|
|
18
|
+
/**
|
|
19
|
+
* Register every command group on `program`, grouped for a readable `--help`.
|
|
20
|
+
*
|
|
21
|
+
* The `run` runner wraps each handler with context construction and error
|
|
22
|
+
* reporting in the real CLI; completion passes a no-op that only needs the
|
|
23
|
+
* command definitions, not their behaviour.
|
|
24
|
+
*/
|
|
25
|
+
export declare function registerCommands(program: Command, run: Runner): Promise<void>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command wiring, factored out of run.ts so there is a single source of truth
|
|
3
|
+
* for the command tree.
|
|
4
|
+
*
|
|
5
|
+
* run() consumes this to build the live program; `kite completion` consumes it
|
|
6
|
+
* with a no-op runner to introspect command and flag names for shell
|
|
7
|
+
* completions. Keeping both on the same registration path means a new command
|
|
8
|
+
* or flag is completable the moment it is added, with nothing to keep in sync.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Attach the global options every command inherits. Split from the name /
|
|
12
|
+
* version / output wiring in run.ts, which is invocation-specific, so an
|
|
13
|
+
* introspection-only program can still enumerate the same global flags.
|
|
14
|
+
*/
|
|
15
|
+
export function applyGlobalOptions(program) {
|
|
16
|
+
return (program
|
|
17
|
+
.option('--json', 'Emit JSON instead of formatted tables')
|
|
18
|
+
.option('--color <when>', 'Colour output: auto, always, or never')
|
|
19
|
+
// Deliberately no `-q` short form: it would shadow `-q, --quantity` on the
|
|
20
|
+
// order and GTT subcommands, which is typed far more often than --quiet.
|
|
21
|
+
.option('--quiet', 'Suppress informational messages')
|
|
22
|
+
.option('--debug', 'Print redacted request diagnostics to stderr')
|
|
23
|
+
.option('--env <env>', 'Environment: production or sandbox')
|
|
24
|
+
.option('--profile <name>', 'Account profile to use (see `kite profiles`)')
|
|
25
|
+
.option('-y, --yes', 'Skip confirmation prompts (use with care)')
|
|
26
|
+
.option('--dry-run', 'Show what would happen without sending anything to Kite'));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register every command group on `program`, grouped for a readable `--help`.
|
|
30
|
+
*
|
|
31
|
+
* The `run` runner wraps each handler with context construction and error
|
|
32
|
+
* reporting in the real CLI; completion passes a no-op that only needs the
|
|
33
|
+
* command definitions, not their behaviour.
|
|
34
|
+
*/
|
|
35
|
+
export async function registerCommands(program, run) {
|
|
36
|
+
// Imported here rather than at module top so a single invocation only pays for
|
|
37
|
+
// parsing the command modules once run() (or completion) actually asks for
|
|
38
|
+
// them, not on every `import` of this file.
|
|
39
|
+
const { authCommands } = await import('./auth.js');
|
|
40
|
+
const { profileCommands } = await import('./profiles.js');
|
|
41
|
+
const { portfolioCommands } = await import('./portfolio.js');
|
|
42
|
+
const { marketCommands } = await import('./market.js');
|
|
43
|
+
const { orderCommands } = await import('./orders.js');
|
|
44
|
+
const { gttCommands } = await import('./gtt.js');
|
|
45
|
+
const { alertCommands } = await import('./alerts.js');
|
|
46
|
+
const { marginCommands } = await import('./margins.js');
|
|
47
|
+
const { mfCommands } = await import('./mf.js');
|
|
48
|
+
const { watchCommands } = await import('./watch.js');
|
|
49
|
+
const { configCommands } = await import('./config.js');
|
|
50
|
+
const { doctorCommands } = await import('./doctor.js');
|
|
51
|
+
const { completionCommands } = await import('./completion.js');
|
|
52
|
+
// commandsGroup applies to every command registered after it, so the group
|
|
53
|
+
// is set immediately before each block. With ~25 commands this is the
|
|
54
|
+
// difference between a readable --help and a wall of text.
|
|
55
|
+
program.commandsGroup('Account:');
|
|
56
|
+
authCommands(program, run);
|
|
57
|
+
profileCommands(program, run);
|
|
58
|
+
program.commandsGroup('Portfolio:');
|
|
59
|
+
portfolioCommands(program, run);
|
|
60
|
+
program.commandsGroup('Mutual funds:');
|
|
61
|
+
mfCommands(program, run);
|
|
62
|
+
program.commandsGroup('Market data:');
|
|
63
|
+
marketCommands(program, run);
|
|
64
|
+
program.commandsGroup('Trading:');
|
|
65
|
+
orderCommands(program, run);
|
|
66
|
+
gttCommands(program, run);
|
|
67
|
+
alertCommands(program, run);
|
|
68
|
+
marginCommands(program, run);
|
|
69
|
+
program.commandsGroup('Streaming:');
|
|
70
|
+
watchCommands(program, run);
|
|
71
|
+
program.commandsGroup('Settings:');
|
|
72
|
+
configCommands(program, run);
|
|
73
|
+
doctorCommands(program, run);
|
|
74
|
+
completionCommands(program, run);
|
|
75
|
+
}
|
package/dist/core/auth.d.ts
CHANGED
|
@@ -59,6 +59,15 @@ export declare function waitForCallback(opts: CallbackServerOptions): {
|
|
|
59
59
|
close: () => void;
|
|
60
60
|
};
|
|
61
61
|
export declare function redirectUrlFor(port: number, path: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Copy text to the OS clipboard, returning whether it succeeded.
|
|
64
|
+
*
|
|
65
|
+
* Like {@link openBrowser}, the text is piped to a fixed binary's stdin — never
|
|
66
|
+
* through a shell and never as an argv element — so a URL's `&`/`=` cannot
|
|
67
|
+
* become word-splitting or command injection. On Linux there is no single
|
|
68
|
+
* clipboard tool, so we try the common ones in turn (Wayland first, then X11).
|
|
69
|
+
*/
|
|
70
|
+
export declare function copyToClipboard(text: string): Promise<boolean>;
|
|
62
71
|
/**
|
|
63
72
|
* Open a URL in the user's default browser.
|
|
64
73
|
*
|
package/dist/core/auth.js
CHANGED
|
@@ -175,6 +175,45 @@ function escapeHtml(value) {
|
|
|
175
175
|
export function redirectUrlFor(port, path) {
|
|
176
176
|
return `http://127.0.0.1:${port}${path}`;
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Copy text to the OS clipboard, returning whether it succeeded.
|
|
180
|
+
*
|
|
181
|
+
* Like {@link openBrowser}, the text is piped to a fixed binary's stdin — never
|
|
182
|
+
* through a shell and never as an argv element — so a URL's `&`/`=` cannot
|
|
183
|
+
* become word-splitting or command injection. On Linux there is no single
|
|
184
|
+
* clipboard tool, so we try the common ones in turn (Wayland first, then X11).
|
|
185
|
+
*/
|
|
186
|
+
export async function copyToClipboard(text) {
|
|
187
|
+
const candidates = process.platform === 'darwin'
|
|
188
|
+
? [['pbcopy', []]]
|
|
189
|
+
: process.platform === 'win32'
|
|
190
|
+
? [['clip', []]]
|
|
191
|
+
: [
|
|
192
|
+
['wl-copy', []],
|
|
193
|
+
['xclip', ['-selection', 'clipboard']],
|
|
194
|
+
['xsel', ['--clipboard', '--input']],
|
|
195
|
+
];
|
|
196
|
+
for (const [command, args] of candidates) {
|
|
197
|
+
if (await pipeToCommand(command, args, text))
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
/** Spawn `command`, write `text` to its stdin, resolve true on a clean exit. */
|
|
203
|
+
function pipeToCommand(command, args, text) {
|
|
204
|
+
return import('node:child_process').then(({ spawn }) => new Promise((resolve) => {
|
|
205
|
+
try {
|
|
206
|
+
const child = spawn(command, args, { stdio: ['pipe', 'ignore', 'ignore'], shell: false });
|
|
207
|
+
child.on('error', () => resolve(false));
|
|
208
|
+
child.on('close', (code) => resolve(code === 0));
|
|
209
|
+
child.stdin.on('error', () => resolve(false));
|
|
210
|
+
child.stdin.end(text);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
resolve(false);
|
|
214
|
+
}
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
178
217
|
/**
|
|
179
218
|
* Open a URL in the user's default browser.
|
|
180
219
|
*
|
package/dist/core/client.js
CHANGED
|
@@ -177,7 +177,14 @@ export class KiteClient {
|
|
|
177
177
|
return new KiteCliError('Cancelled.', ExitCode.Aborted);
|
|
178
178
|
}
|
|
179
179
|
const message = err instanceof Error ? err.message : String(err);
|
|
180
|
-
|
|
180
|
+
// undici's fetch wraps the actual failure (ECONNRESET, ENOTFOUND, a TLS
|
|
181
|
+
// error, ...) in `cause` and sets `message` to the unhelpful constant
|
|
182
|
+
// "fetch failed" — surface the cause too, or every transient network
|
|
183
|
+
// problem looks identical and undebuggable.
|
|
184
|
+
const cause = err instanceof Error ? err.cause : undefined;
|
|
185
|
+
const causeMessage = cause instanceof Error ? cause.message : cause !== undefined ? String(cause) : undefined;
|
|
186
|
+
const detail = causeMessage && causeMessage !== message ? `${message}: ${causeMessage}` : message;
|
|
187
|
+
return new NetworkError(`Could not reach Kite: ${redactString(detail)}`, 'Check your network connection.');
|
|
181
188
|
}
|
|
182
189
|
handleResponse(response, text, schema, url) {
|
|
183
190
|
let payload;
|
package/dist/core/schemas.d.ts
CHANGED
|
@@ -683,6 +683,7 @@ export declare const BasketMarginSchema: z.ZodObject<{
|
|
|
683
683
|
}, z.core.$loose>>>;
|
|
684
684
|
charges: z.ZodOptional<z.ZodUnknown>;
|
|
685
685
|
}, z.core.$loose>;
|
|
686
|
+
export type BasketMargin = z.infer<typeof BasketMarginSchema>;
|
|
686
687
|
export declare const MfHoldingSchema: z.ZodObject<{
|
|
687
688
|
folio: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
688
689
|
fund: z.ZodOptional<z.ZodString>;
|
|
@@ -707,6 +708,7 @@ export declare const MfOrderSchema: z.ZodObject<{
|
|
|
707
708
|
amount: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
708
709
|
average_price: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
709
710
|
}, z.core.$loose>;
|
|
711
|
+
export type MfOrder = z.infer<typeof MfOrderSchema>;
|
|
710
712
|
export declare const MfSipSchema: z.ZodObject<{
|
|
711
713
|
sip_id: z.ZodString;
|
|
712
714
|
tradingsymbol: z.ZodOptional<z.ZodString>;
|
|
@@ -717,6 +719,7 @@ export declare const MfSipSchema: z.ZodObject<{
|
|
|
717
719
|
frequency: z.ZodOptional<z.ZodString>;
|
|
718
720
|
next_instalment: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
719
721
|
}, z.core.$loose>;
|
|
722
|
+
export type MfSip = z.infer<typeof MfSipSchema>;
|
|
720
723
|
export declare const InstrumentSchema: z.ZodObject<{
|
|
721
724
|
instrument_token: z.ZodNumber;
|
|
722
725
|
exchange_token: z.ZodOptional<z.ZodNumber>;
|
package/dist/run.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { Command, CommanderError } from 'commander';
|
|
3
|
+
import { applyGlobalOptions, registerCommands } from './commands/register.js';
|
|
3
4
|
import { createContext } from './context.js';
|
|
4
5
|
import { AbortedError, ExitCode, KiteCliError } from './core/errors.js';
|
|
5
6
|
import { redactString } from './core/redact.js';
|
|
@@ -36,16 +37,6 @@ export async function run(opts = {}) {
|
|
|
36
37
|
.name('kite')
|
|
37
38
|
.description('Unofficial command-line interface for the Zerodha Kite Connect API (not affiliated with Zerodha)')
|
|
38
39
|
.version(VERSION, '-v, --version')
|
|
39
|
-
.option('--json', 'Emit JSON instead of formatted tables')
|
|
40
|
-
.option('--color <when>', 'Colour output: auto, always, or never')
|
|
41
|
-
// Deliberately no `-q` short form: it would shadow `-q, --quantity` on the
|
|
42
|
-
// order and GTT subcommands, which is typed far more often than --quiet.
|
|
43
|
-
.option('--quiet', 'Suppress informational messages')
|
|
44
|
-
.option('--debug', 'Print redacted request diagnostics to stderr')
|
|
45
|
-
.option('--env <env>', 'Environment: production or sandbox')
|
|
46
|
-
.option('--profile <name>', 'Account profile to use (see `kite profiles`)')
|
|
47
|
-
.option('-y, --yes', 'Skip confirmation prompts (use with care)')
|
|
48
|
-
.option('--dry-run', 'Show what would happen without sending anything to Kite')
|
|
49
40
|
.showHelpAfterError('(run `kite --help` for usage)')
|
|
50
41
|
.configureOutput({
|
|
51
42
|
writeOut: (str) => (opts.streams?.stdout ?? process.stdout).write(str),
|
|
@@ -54,6 +45,7 @@ export async function run(opts = {}) {
|
|
|
54
45
|
// Throw instead of calling process.exit, so `run()` stays testable and
|
|
55
46
|
// always returns its exit code.
|
|
56
47
|
.exitOverride();
|
|
48
|
+
applyGlobalOptions(program);
|
|
57
49
|
/** Wraps a handler with context construction and error reporting. */
|
|
58
50
|
const withContext = (handler) => async (...args) => {
|
|
59
51
|
// Commander passes (…arguments, options, command).
|
|
@@ -68,35 +60,11 @@ export async function run(opts = {}) {
|
|
|
68
60
|
exitCode = reportError(err, ctx.io);
|
|
69
61
|
}
|
|
70
62
|
};
|
|
71
|
-
// Registered
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const { marketCommands } = await import('./commands/market.js');
|
|
77
|
-
const { orderCommands } = await import('./commands/orders.js');
|
|
78
|
-
const { gttCommands } = await import('./commands/gtt.js');
|
|
79
|
-
const { alertCommands } = await import('./commands/alerts.js');
|
|
80
|
-
const { watchCommands } = await import('./commands/watch.js');
|
|
81
|
-
const { configCommands } = await import('./commands/config.js');
|
|
82
|
-
// commandsGroup applies to every command registered after it, so the group
|
|
83
|
-
// is set immediately before each block. With ~25 commands this is the
|
|
84
|
-
// difference between a readable --help and a wall of text.
|
|
85
|
-
program.commandsGroup('Account:');
|
|
86
|
-
authCommands(program, withContext);
|
|
87
|
-
profileCommands(program, withContext);
|
|
88
|
-
program.commandsGroup('Portfolio:');
|
|
89
|
-
portfolioCommands(program, withContext);
|
|
90
|
-
program.commandsGroup('Market data:');
|
|
91
|
-
marketCommands(program, withContext);
|
|
92
|
-
program.commandsGroup('Trading:');
|
|
93
|
-
orderCommands(program, withContext);
|
|
94
|
-
gttCommands(program, withContext);
|
|
95
|
-
alertCommands(program, withContext);
|
|
96
|
-
program.commandsGroup('Streaming:');
|
|
97
|
-
watchCommands(program, withContext);
|
|
98
|
-
program.commandsGroup('Settings:');
|
|
99
|
-
configCommands(program, withContext);
|
|
63
|
+
// Registered through the shared registration path (see commands/register.ts)
|
|
64
|
+
// so run() and `kite completion` build the exact same command tree. Modules
|
|
65
|
+
// are still imported lazily inside registerCommands, so a bare `kite quote`
|
|
66
|
+
// never pays to parse the dashboard renderer or the ticker up front.
|
|
67
|
+
await registerCommands(program, withContext);
|
|
100
68
|
program.addHelpText('after', `
|
|
101
69
|
Examples:
|
|
102
70
|
$ kite login Authenticate and store a session
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pungoyal/kite-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Unofficial command-line interface for the Zerodha Kite Connect API — not affiliated with or endorsed by Zerodha",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zerodha",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"type": "git",
|
|
19
19
|
"url": "git+https://github.com/pungoyal/kite-cli.git"
|
|
20
20
|
},
|
|
21
|
-
"homepage": "https://github.
|
|
21
|
+
"homepage": "https://pungoyal.github.io/kite-cli",
|
|
22
22
|
"bugs": {
|
|
23
23
|
"url": "https://github.com/pungoyal/kite-cli/issues"
|
|
24
24
|
},
|