@pungoyal/kite-cli 0.3.0 → 0.5.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 +101 -9
- 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/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +83 -0
- package/dist/commands/orders.js +75 -1
- package/dist/commands/register.d.ts +25 -0
- package/dist/commands/register.js +78 -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/mcp.d.ts +56 -0
- package/dist/core/mcp.js +166 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/run.js +8 -45
- package/dist/safety.d.ts +8 -0
- package/dist/safety.js +9 -1
- package/package.json +8 -3
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { createServer } from 'node:net';
|
|
3
|
+
import { getSecret, keyringAvailable } from '../core/credentials.js';
|
|
4
|
+
import { ExitCode } from '../core/errors.js';
|
|
5
|
+
import { configFile } from '../core/paths.js';
|
|
6
|
+
import { isExpired, timeUntilExpiry } from '../core/session.js';
|
|
7
|
+
import { renderKeyValue } from '../output/table.js';
|
|
8
|
+
export const doctorCommands = (program, run) => {
|
|
9
|
+
program
|
|
10
|
+
.command('doctor')
|
|
11
|
+
.description('Run offline health checks on your configuration, credentials, and session')
|
|
12
|
+
.action(run(doctor));
|
|
13
|
+
};
|
|
14
|
+
async function doctor(ctx) {
|
|
15
|
+
const checks = [
|
|
16
|
+
checkNode(),
|
|
17
|
+
await checkConfigFile(),
|
|
18
|
+
await checkKeyring(),
|
|
19
|
+
await checkCredentials(ctx),
|
|
20
|
+
checkSession(ctx),
|
|
21
|
+
await checkCallbackPort(ctx),
|
|
22
|
+
];
|
|
23
|
+
const worst = checks.some((c) => c.status === 'fail')
|
|
24
|
+
? 'fail'
|
|
25
|
+
: checks.some((c) => c.status === 'warn')
|
|
26
|
+
? 'warn'
|
|
27
|
+
: 'ok';
|
|
28
|
+
if (ctx.io.json) {
|
|
29
|
+
ctx.io.writeJson({
|
|
30
|
+
ok: worst !== 'fail',
|
|
31
|
+
profile: ctx.profile.name,
|
|
32
|
+
env: ctx.env,
|
|
33
|
+
checks: checks.map((c) => ({
|
|
34
|
+
name: c.name,
|
|
35
|
+
status: c.status,
|
|
36
|
+
detail: c.detail,
|
|
37
|
+
...(c.hint ? { hint: c.hint } : {}),
|
|
38
|
+
})),
|
|
39
|
+
});
|
|
40
|
+
if (worst === 'fail')
|
|
41
|
+
process.exitCode = ExitCode.Failure;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const { io } = ctx;
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const check of checks) {
|
|
47
|
+
rows.push([`${icon(io, check.status)} ${check.name}`, check.detail]);
|
|
48
|
+
if (check.hint)
|
|
49
|
+
rows.push(['', io.dim(check.hint)]);
|
|
50
|
+
}
|
|
51
|
+
io.line(renderKeyValue(io, rows));
|
|
52
|
+
io.note('');
|
|
53
|
+
if (worst === 'fail') {
|
|
54
|
+
io.error('Some checks failed. See the hints above.');
|
|
55
|
+
process.exitCode = ExitCode.Failure;
|
|
56
|
+
}
|
|
57
|
+
else if (worst === 'warn') {
|
|
58
|
+
io.warn('All essential checks passed, with warnings.');
|
|
59
|
+
io.info('Run `kite whoami` to confirm the session is live on Kite (doctor is offline).');
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
io.success('Everything looks healthy.');
|
|
63
|
+
io.info('Run `kite whoami` to confirm the session against Kite (doctor is offline).');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// --- individual checks -----------------------------------------------------
|
|
67
|
+
/** Node must meet the floor declared in package.json `engines`. */
|
|
68
|
+
function checkNode() {
|
|
69
|
+
const current = process.versions.node;
|
|
70
|
+
const floor = '22.12.0';
|
|
71
|
+
return meetsFloor(current, floor)
|
|
72
|
+
? { name: 'Node runtime', status: 'ok', detail: `v${current}` }
|
|
73
|
+
: {
|
|
74
|
+
name: 'Node runtime',
|
|
75
|
+
status: 'fail',
|
|
76
|
+
detail: `v${current} is below the required v${floor}`,
|
|
77
|
+
hint: `Upgrade to Node ${floor} or newer.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function checkConfigFile() {
|
|
81
|
+
const path = configFile();
|
|
82
|
+
let info;
|
|
83
|
+
try {
|
|
84
|
+
info = await stat(path);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err.code === 'ENOENT') {
|
|
88
|
+
// No file is fine — defaults apply until the first `config set`.
|
|
89
|
+
return { name: 'Config file', status: 'ok', detail: 'none yet (using built-in defaults)' };
|
|
90
|
+
}
|
|
91
|
+
return { name: 'Config file', status: 'fail', detail: `cannot read ${path}`, hint: err.message };
|
|
92
|
+
}
|
|
93
|
+
// Windows models permissions through ACLs, not the POSIX mode bits, so the
|
|
94
|
+
// 0600 assertion only makes sense off Windows.
|
|
95
|
+
if (process.platform !== 'win32' && (info.mode & 0o077) !== 0) {
|
|
96
|
+
const mode = (info.mode & 0o777).toString(8).padStart(3, '0');
|
|
97
|
+
return {
|
|
98
|
+
name: 'Config file',
|
|
99
|
+
status: 'warn',
|
|
100
|
+
detail: `${path} is mode ${mode} (world- or group-readable)`,
|
|
101
|
+
hint: `Tighten it: chmod 600 ${path}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { name: 'Config file', status: 'ok', detail: path };
|
|
105
|
+
}
|
|
106
|
+
async function checkKeyring() {
|
|
107
|
+
return (await keyringAvailable())
|
|
108
|
+
? { name: 'Credential store', status: 'ok', detail: 'OS keyring available' }
|
|
109
|
+
: {
|
|
110
|
+
name: 'Credential store',
|
|
111
|
+
status: 'warn',
|
|
112
|
+
detail: 'no OS keyring — falling back to an encrypted file',
|
|
113
|
+
hint: 'Set KITE_CREDENTIALS_PASSPHRASE to enable the encrypted file store on this machine.',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function checkCredentials(ctx) {
|
|
117
|
+
if (ctx.env === 'sandbox') {
|
|
118
|
+
return { name: 'API secret', status: 'ok', detail: 'using the public sandbox credentials' };
|
|
119
|
+
}
|
|
120
|
+
const secret = await getSecret('api_secret', { scope: ctx.credentialScope });
|
|
121
|
+
return secret
|
|
122
|
+
? { name: 'API secret', status: 'ok', detail: `stored (${secret.backend})` }
|
|
123
|
+
: {
|
|
124
|
+
name: 'API secret',
|
|
125
|
+
status: 'warn',
|
|
126
|
+
detail: 'not stored',
|
|
127
|
+
hint: 'Run `kite login` (or set KITE_API_SECRET) so re-login and signing work.',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Cached session metadata only — liveness against Kite is `kite whoami`'s job. */
|
|
131
|
+
function checkSession(ctx) {
|
|
132
|
+
const loginHint = ctx.profile.name === 'default' ? 'Run `kite login`.' : `Run \`kite --profile ${ctx.profile.name} login\`.`;
|
|
133
|
+
if (!ctx.session) {
|
|
134
|
+
return { name: 'Session', status: 'warn', detail: 'not logged in', hint: loginHint };
|
|
135
|
+
}
|
|
136
|
+
if (isExpired(ctx.session)) {
|
|
137
|
+
return {
|
|
138
|
+
name: 'Session',
|
|
139
|
+
status: 'warn',
|
|
140
|
+
detail: 'expired (Kite invalidates tokens at 06:00 IST daily)',
|
|
141
|
+
hint: loginHint,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const who = ctx.session.userName ?? ctx.session.userId;
|
|
145
|
+
return { name: 'Session', status: 'ok', detail: `${who}, expires in ${timeUntilExpiry(ctx.session)}` };
|
|
146
|
+
}
|
|
147
|
+
async function checkCallbackPort(ctx) {
|
|
148
|
+
const port = ctx.config.redirectPort;
|
|
149
|
+
const free = await portIsFree(port);
|
|
150
|
+
return free
|
|
151
|
+
? { name: 'Login callback port', status: 'ok', detail: `127.0.0.1:${port} is free` }
|
|
152
|
+
: {
|
|
153
|
+
name: 'Login callback port',
|
|
154
|
+
status: 'warn',
|
|
155
|
+
detail: `127.0.0.1:${port} is in use`,
|
|
156
|
+
hint: 'Another process holds the callback port; `kite login` may fail until it frees, or change redirectPort.',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// --- helpers ---------------------------------------------------------------
|
|
160
|
+
function icon(io, status) {
|
|
161
|
+
switch (status) {
|
|
162
|
+
case 'ok':
|
|
163
|
+
return io.green('✓');
|
|
164
|
+
case 'warn':
|
|
165
|
+
return io.yellow('!');
|
|
166
|
+
case 'fail':
|
|
167
|
+
return io.red('✗');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** True when `current` is >= `floor`, comparing major.minor.patch numerically. */
|
|
171
|
+
export function meetsFloor(current, floor) {
|
|
172
|
+
const c = current.split('.').map((n) => Number.parseInt(n, 10));
|
|
173
|
+
const f = floor.split('.').map((n) => Number.parseInt(n, 10));
|
|
174
|
+
for (let i = 0; i < 3; i++) {
|
|
175
|
+
const a = c[i] ?? 0;
|
|
176
|
+
const b = f[i] ?? 0;
|
|
177
|
+
if (a !== b)
|
|
178
|
+
return a > b;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
/** Bind-and-release probe: can `kite login` open its loopback callback server? */
|
|
183
|
+
function portIsFree(port, host = '127.0.0.1') {
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const server = createServer();
|
|
186
|
+
server.once('error', () => resolve(false));
|
|
187
|
+
server.once('listening', () => server.close(() => resolve(true)));
|
|
188
|
+
server.listen(port, host);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { McpServer } from '../core/mcp.js';
|
|
4
|
+
/**
|
|
5
|
+
* `kite mcp` — a read-only Model Context Protocol server over stdio.
|
|
6
|
+
*
|
|
7
|
+
* Launched by an MCP client (Claude and others) as a subprocess, it lets an
|
|
8
|
+
* agent inspect a Kite account — holdings, positions, funds, live quotes, the
|
|
9
|
+
* orderbook, instrument search — but never place, modify or cancel an order.
|
|
10
|
+
* The rationale for read-only lives in src/core/mcp.ts.
|
|
11
|
+
*
|
|
12
|
+
* The stdio contract is strict: the JSON-RPC protocol owns stdout, so every
|
|
13
|
+
* human-facing message here goes to stderr (io.info/note already do).
|
|
14
|
+
*/
|
|
15
|
+
// Single source of truth for the version, like run.ts. package.json sits two
|
|
16
|
+
// levels up from both src/commands/ (dev) and dist/commands/ (published).
|
|
17
|
+
const VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
|
|
18
|
+
/** Instruments accepted by quote-style tools, e.g. ["NSE:INFY", "NSE:TCS"]. */
|
|
19
|
+
const instrumentsArg = z.object({
|
|
20
|
+
instruments: z.array(z.string().min(1)).min(1).describe('Instruments as EXCHANGE:TRADINGSYMBOL, e.g. NSE:INFY'),
|
|
21
|
+
});
|
|
22
|
+
const searchArg = z.object({
|
|
23
|
+
query: z.string().min(1).describe('Free-text query over trading symbol and name'),
|
|
24
|
+
exchange: z.string().optional().describe('Restrict to an exchange, e.g. NSE, NFO'),
|
|
25
|
+
type: z.string().optional().describe('Restrict to an instrument type, e.g. EQ, CE, PE, FUT'),
|
|
26
|
+
limit: z.number().int().positive().max(100).optional().describe('Maximum results (default 25)'),
|
|
27
|
+
});
|
|
28
|
+
/** Build one tool, deriving its advertised JSON Schema from the zod schema. */
|
|
29
|
+
function tool(name, description, schema, handler) {
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
description,
|
|
33
|
+
schema,
|
|
34
|
+
inputSchema: z.toJSONSchema(schema),
|
|
35
|
+
handler,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** The read-only Kite surface exposed to an agent. */
|
|
39
|
+
function buildTools(ctx) {
|
|
40
|
+
const { api, instruments, signal } = ctx;
|
|
41
|
+
const noArgs = z.object({});
|
|
42
|
+
return [
|
|
43
|
+
tool('get_profile', 'The authenticated Kite user profile.', noArgs, () => api.getProfile(signal)),
|
|
44
|
+
tool('get_holdings', 'Long-term holdings with average price and P&L.', noArgs, () => api.getHoldings(signal)),
|
|
45
|
+
tool('get_positions', 'Open positions (intraday and overnight).', noArgs, () => api.getPositions(signal)),
|
|
46
|
+
tool('get_funds', 'Available margins and cash balance.', noArgs, () => api.getMargins(signal)),
|
|
47
|
+
tool('get_orders', "Today's orderbook, including status and fills.", noArgs, () => api.getOrders(signal)),
|
|
48
|
+
tool('get_trades', "Today's executed trades.", noArgs, () => api.getTrades(signal)),
|
|
49
|
+
tool('quote', 'Full market quotes for one or more instruments.', instrumentsArg, (a) => api.getQuote(a.instruments, signal)),
|
|
50
|
+
tool('ltp', 'Last traded price for one or more instruments.', instrumentsArg, (a) => api.getLtp(a.instruments, signal)),
|
|
51
|
+
tool('ohlc', 'Open/high/low/close plus last price for one or more instruments.', instrumentsArg, (a) => api.getOhlc(a.instruments, signal)),
|
|
52
|
+
tool('search_instruments', 'Find tradeable instruments by symbol or name.', searchArg, async (a) => {
|
|
53
|
+
await instruments.load({ signal });
|
|
54
|
+
return instruments.search(a.query, {
|
|
55
|
+
exchange: a.exchange,
|
|
56
|
+
type: a.type,
|
|
57
|
+
...(a.limit !== undefined ? { limit: a.limit } : {}),
|
|
58
|
+
});
|
|
59
|
+
}),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
async function mcp(ctx) {
|
|
63
|
+
// A live session is required — every tool hits the API. Fail here (exit 3)
|
|
64
|
+
// rather than starting a server whose every call would 403.
|
|
65
|
+
ctx.requireSession();
|
|
66
|
+
const server = new McpServer({
|
|
67
|
+
name: 'kite-cli',
|
|
68
|
+
version: VERSION,
|
|
69
|
+
tools: buildTools(ctx),
|
|
70
|
+
signal: ctx.signal,
|
|
71
|
+
});
|
|
72
|
+
ctx.io.info('kite MCP server ready on stdio (read-only). Launch this from an MCP client; Ctrl-C to stop.');
|
|
73
|
+
if (ctx.env === 'sandbox')
|
|
74
|
+
ctx.io.info('Running against the sandbox — no real account.');
|
|
75
|
+
// stdio is the transport: the process's own stdin/stdout carry JSON-RPC.
|
|
76
|
+
await server.serve(process.stdin, process.stdout);
|
|
77
|
+
}
|
|
78
|
+
export const mcpCommands = (program, run) => {
|
|
79
|
+
program
|
|
80
|
+
.command('mcp')
|
|
81
|
+
.description('Run a read-only MCP server over stdio for LLM agents (Claude and others)')
|
|
82
|
+
.action(run(mcp));
|
|
83
|
+
};
|
package/dist/commands/orders.js
CHANGED
|
@@ -5,7 +5,7 @@ import { formatInstrumentKey, parseInstrumentKey } from '../core/instruments.js'
|
|
|
5
5
|
import { TERMINAL_ORDER_STATUSES } from '../core/schemas.js';
|
|
6
6
|
import { dateTime, money, quantity, rupees, timeOnly } from '../output/format.js';
|
|
7
7
|
import { heading, printTable, renderKeyValue, renderTable } from '../output/table.js';
|
|
8
|
-
import { assertTradingEnabled, buildOrderTag, confirmAction } from '../safety.js';
|
|
8
|
+
import { assertTradingEnabled, buildOrderTag, CLI_TAG_PREFIX, confirmAction } from '../safety.js';
|
|
9
9
|
export const orderCommands = (program, run) => {
|
|
10
10
|
const orders = program.command('orders').description('View and manage orders');
|
|
11
11
|
orders
|
|
@@ -18,6 +18,11 @@ export const orderCommands = (program, run) => {
|
|
|
18
18
|
.description('Show the full state history of one order')
|
|
19
19
|
.argument('<order-id>')
|
|
20
20
|
.action(run(getOrder));
|
|
21
|
+
orders
|
|
22
|
+
.command('reconcile')
|
|
23
|
+
.description('Check whether a tagged order reached Kite (recovery after an ambiguous failure)')
|
|
24
|
+
.argument('[tag]', 'Order tag to look up; omit to list the orders this CLI placed today')
|
|
25
|
+
.action(run(reconcileOrders));
|
|
21
26
|
orders
|
|
22
27
|
.command('place')
|
|
23
28
|
.description('Place an order')
|
|
@@ -473,6 +478,75 @@ async function reconcileAfterFailure(ctx, tag, cause) {
|
|
|
473
478
|
process.exitCode = ExitCode.Upstream;
|
|
474
479
|
}
|
|
475
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Standalone, after-the-fact reconciliation for the no-idempotency problem.
|
|
483
|
+
*
|
|
484
|
+
* `orders place` reconciles automatically the instant a placement fails (see
|
|
485
|
+
* {@link reconcileAfterFailure}), but that check lives and dies with the
|
|
486
|
+
* process — a killed shell, a crashed script, a slept laptop and it is gone.
|
|
487
|
+
* This re-runs it on demand. Given the unique tag every order carries, it
|
|
488
|
+
* answers the only question that matters after an ambiguous failure — "did it
|
|
489
|
+
* actually reach Kite?" — so you know whether it is safe to place again.
|
|
490
|
+
*
|
|
491
|
+
* With a tag, it looks that tag up. With none, it lists the orders this CLI
|
|
492
|
+
* placed today (those carrying the {@link CLI_TAG_PREFIX} prefix), so you can
|
|
493
|
+
* still find one whose exact tag you did not capture. It is a query, not a
|
|
494
|
+
* mutation: it exits 0 on any clean answer, and the `--json` `placed` flag is
|
|
495
|
+
* the machine-readable verdict.
|
|
496
|
+
*/
|
|
497
|
+
async function reconcileOrders(ctx, _opts, command) {
|
|
498
|
+
ctx.requireSession();
|
|
499
|
+
const { io } = ctx;
|
|
500
|
+
const tag = command.args[0];
|
|
501
|
+
if (tag) {
|
|
502
|
+
const matches = await ctx.api.findOrderByTag(tag, ctx.signal);
|
|
503
|
+
if (io.json) {
|
|
504
|
+
io.writeJson({ tag, placed: matches.length > 0, order_ids: matches.map((o) => o.order_id), orders: matches });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (matches.length === 0) {
|
|
508
|
+
io.warn(`No order found for tag ${io.bold(tag)}.`);
|
|
509
|
+
io.info('If a placement looked like it failed, it most likely did not reach Kite — but check `kite orders list` before retrying, in case it simply has not appeared yet.');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
io.success(`Found ${matches.length === 1 ? 'an order' : `${matches.length} orders`} for tag ${io.bold(tag)}:`);
|
|
513
|
+
io.line(renderTableFor(ctx, matches, reconcileColumns()));
|
|
514
|
+
io.info('If placing this order looked like it failed, it went through — do not place it again.');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const mine = (await ctx.api.getOrders(ctx.signal)).filter(hasCliTag);
|
|
518
|
+
printTable(io, mine, reconcileColumns(), mine, {
|
|
519
|
+
compact: ctx.config.output.compact,
|
|
520
|
+
empty: `No orders tagged by this CLI today (looking for the \`${CLI_TAG_PREFIX}\` prefix).`,
|
|
521
|
+
});
|
|
522
|
+
if (mine.length > 0 && !io.json) {
|
|
523
|
+
io.info('Reconcile a specific one with `kite orders reconcile <tag>`.');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/** True if this order carries a CLI-generated tag (see {@link CLI_TAG_PREFIX}). */
|
|
527
|
+
function hasCliTag(order) {
|
|
528
|
+
if (order.tag?.startsWith(CLI_TAG_PREFIX))
|
|
529
|
+
return true;
|
|
530
|
+
return order.tags?.some((t) => t.startsWith(CLI_TAG_PREFIX)) ?? false;
|
|
531
|
+
}
|
|
532
|
+
function reconcileColumns() {
|
|
533
|
+
return [
|
|
534
|
+
{ header: 'Time', value: (o) => timeOnly(o.order_timestamp) },
|
|
535
|
+
{ header: 'Order ID', value: (o, io) => io.dim(o.order_id) },
|
|
536
|
+
{ header: 'Symbol', value: (o, io) => io.bold(o.tradingsymbol ?? '—') },
|
|
537
|
+
{
|
|
538
|
+
header: 'Side',
|
|
539
|
+
value: (o, io) => o.transaction_type === 'BUY' ? io.green('BUY') : o.transaction_type === 'SELL' ? io.red('SELL') : '—',
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
header: 'Qty',
|
|
543
|
+
value: (o) => `${quantity(o.filled_quantity ?? 0)}/${quantity(o.quantity ?? 0)}`,
|
|
544
|
+
align: 'right',
|
|
545
|
+
},
|
|
546
|
+
{ header: 'Status', value: (o, io) => colourStatus(io, o.status) },
|
|
547
|
+
{ header: 'Tag', value: (o) => o.tag ?? '—' },
|
|
548
|
+
];
|
|
549
|
+
}
|
|
476
550
|
// ---------------------------------------------------------------------------
|
|
477
551
|
// Modify / cancel
|
|
478
552
|
// ---------------------------------------------------------------------------
|
|
@@ -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,78 @@
|
|
|
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
|
+
const { mcpCommands } = await import('./mcp.js');
|
|
53
|
+
// commandsGroup applies to every command registered after it, so the group
|
|
54
|
+
// is set immediately before each block. With ~25 commands this is the
|
|
55
|
+
// difference between a readable --help and a wall of text.
|
|
56
|
+
program.commandsGroup('Account:');
|
|
57
|
+
authCommands(program, run);
|
|
58
|
+
profileCommands(program, run);
|
|
59
|
+
program.commandsGroup('Portfolio:');
|
|
60
|
+
portfolioCommands(program, run);
|
|
61
|
+
program.commandsGroup('Mutual funds:');
|
|
62
|
+
mfCommands(program, run);
|
|
63
|
+
program.commandsGroup('Market data:');
|
|
64
|
+
marketCommands(program, run);
|
|
65
|
+
program.commandsGroup('Trading:');
|
|
66
|
+
orderCommands(program, run);
|
|
67
|
+
gttCommands(program, run);
|
|
68
|
+
alertCommands(program, run);
|
|
69
|
+
marginCommands(program, run);
|
|
70
|
+
program.commandsGroup('Streaming:');
|
|
71
|
+
watchCommands(program, run);
|
|
72
|
+
program.commandsGroup('Integrations:');
|
|
73
|
+
mcpCommands(program, run);
|
|
74
|
+
program.commandsGroup('Settings:');
|
|
75
|
+
configCommands(program, run);
|
|
76
|
+
doctorCommands(program, run);
|
|
77
|
+
completionCommands(program, run);
|
|
78
|
+
}
|
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;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
/** A tool the agent may call. `schema` validates the arguments before dispatch. */
|
|
3
|
+
export interface McpTool<S extends z.ZodType = z.ZodType> {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
schema: S;
|
|
7
|
+
/** JSON Schema for the arguments, advertised in `tools/list`. */
|
|
8
|
+
inputSchema: Record<string, unknown>;
|
|
9
|
+
handler: (args: z.infer<S>, signal?: AbortSignal) => Promise<unknown> | unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface McpServerOptions {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
tools: McpTool[];
|
|
15
|
+
/** Aborting this stops the serve loop after the in-flight message. */
|
|
16
|
+
signal?: AbortSignal | undefined;
|
|
17
|
+
}
|
|
18
|
+
interface JsonRpcRequest {
|
|
19
|
+
jsonrpc?: unknown;
|
|
20
|
+
id?: string | number | null;
|
|
21
|
+
method?: unknown;
|
|
22
|
+
params?: unknown;
|
|
23
|
+
}
|
|
24
|
+
interface JsonRpcResponse {
|
|
25
|
+
jsonrpc: '2.0';
|
|
26
|
+
id: string | number | null;
|
|
27
|
+
result?: unknown;
|
|
28
|
+
error?: {
|
|
29
|
+
code: number;
|
|
30
|
+
message: string;
|
|
31
|
+
data?: unknown;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export declare class McpServer {
|
|
35
|
+
private readonly tools;
|
|
36
|
+
private readonly name;
|
|
37
|
+
private readonly version;
|
|
38
|
+
private readonly signal;
|
|
39
|
+
constructor(opts: McpServerOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Handle a single decoded JSON-RPC message.
|
|
42
|
+
*
|
|
43
|
+
* Returns the response to write back, or `null` for a notification (a request
|
|
44
|
+
* with no `id`), which by JSON-RPC must never be answered — even on error.
|
|
45
|
+
*/
|
|
46
|
+
handle(request: JsonRpcRequest): Promise<JsonRpcResponse | null>;
|
|
47
|
+
private initializeResult;
|
|
48
|
+
private toolList;
|
|
49
|
+
private callTool;
|
|
50
|
+
/**
|
|
51
|
+
* Read newline-delimited JSON-RPC from `input` and write responses to
|
|
52
|
+
* `output`, until the input closes or the abort signal fires.
|
|
53
|
+
*/
|
|
54
|
+
serve(input: NodeJS.ReadableStream, output: NodeJS.WritableStream): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
export {};
|