@ethosagent/tools-india-broker-zerodha 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/dist/index.js ADDED
@@ -0,0 +1,850 @@
1
+ // src/auth.ts
2
+ import { createHash } from "crypto";
3
+ var KITE_SESSION_URL = "https://api.kite.trade/session/token";
4
+ function buildLoginUrl(apiKey) {
5
+ return `https://kite.trade/connect/login?v=3&api_key=${apiKey}`;
6
+ }
7
+ function computeChecksum(apiKey, requestToken, apiSecret) {
8
+ return createHash("sha256").update(apiKey + requestToken + apiSecret).digest("hex");
9
+ }
10
+ async function exchangeToken(apiKey, apiSecret, requestToken) {
11
+ const checksum = computeChecksum(apiKey, requestToken, apiSecret);
12
+ const body = new URLSearchParams();
13
+ body.set("api_key", apiKey);
14
+ body.set("request_token", requestToken);
15
+ body.set("checksum", checksum);
16
+ const res = await fetch(KITE_SESSION_URL, {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
19
+ body: body.toString()
20
+ });
21
+ const json = await res.json();
22
+ if (json.status !== "success") {
23
+ throw new Error(`Token exchange failed: ${json.message ?? "unknown error"}`);
24
+ }
25
+ return json.data;
26
+ }
27
+ async function validateToken(apiKey, accessToken) {
28
+ try {
29
+ const res = await fetch("https://api.kite.trade/user/profile", {
30
+ method: "GET",
31
+ headers: {
32
+ Authorization: `token ${apiKey}:${accessToken}`,
33
+ "X-Kite-Version": "3"
34
+ }
35
+ });
36
+ if (res.status === 403) {
37
+ return { valid: false, expiresHint: "Token expired" };
38
+ }
39
+ const json = await res.json();
40
+ if (json.error_type === "TokenException") {
41
+ return { valid: false, expiresHint: "Token expired" };
42
+ }
43
+ if (json.status === "success" && json.data) {
44
+ return {
45
+ valid: true,
46
+ userId: json.data.user_id,
47
+ expiresHint: "tonight at midnight IST"
48
+ };
49
+ }
50
+ return { valid: false, expiresHint: "Unknown validation error" };
51
+ } catch {
52
+ return { valid: false, expiresHint: "Network error during validation" };
53
+ }
54
+ }
55
+
56
+ // src/store.ts
57
+ import Database from "better-sqlite3";
58
+
59
+ // src/schema.ts
60
+ var SQL_CREATE_HOLDINGS_CACHE = `
61
+ CREATE TABLE IF NOT EXISTS holdings_cache (
62
+ symbol TEXT PRIMARY KEY,
63
+ exchange TEXT NOT NULL,
64
+ isin TEXT,
65
+ quantity INTEGER NOT NULL,
66
+ t1_quantity INTEGER NOT NULL DEFAULT 0,
67
+ avg_price REAL NOT NULL,
68
+ ltp REAL,
69
+ pnl REAL,
70
+ pnl_pct REAL,
71
+ day_change REAL,
72
+ product TEXT NOT NULL,
73
+ refreshed_at INTEGER NOT NULL
74
+ ) STRICT;
75
+ `;
76
+ var SQL_CREATE_ORDER_LOG = `
77
+ CREATE TABLE IF NOT EXISTS order_log (
78
+ id TEXT PRIMARY KEY,
79
+ created_at INTEGER NOT NULL,
80
+ symbol TEXT NOT NULL,
81
+ exchange TEXT NOT NULL,
82
+ transaction_type TEXT NOT NULL,
83
+ quantity INTEGER NOT NULL,
84
+ order_type TEXT NOT NULL,
85
+ price REAL,
86
+ product TEXT NOT NULL,
87
+ dry_run INTEGER NOT NULL,
88
+ kite_order_id TEXT,
89
+ status TEXT NOT NULL,
90
+ rejection_reason TEXT,
91
+ agent_session TEXT
92
+ ) STRICT;
93
+ `;
94
+ var SQL_CREATE_SYNC_META = `
95
+ CREATE TABLE IF NOT EXISTS sync_meta (
96
+ key TEXT PRIMARY KEY,
97
+ fetched_at INTEGER NOT NULL,
98
+ status TEXT NOT NULL DEFAULT 'ok'
99
+ ) STRICT;
100
+ `;
101
+ function migrate(db) {
102
+ db.pragma("journal_mode = WAL");
103
+ db.pragma("foreign_keys = ON");
104
+ db.exec(SQL_CREATE_HOLDINGS_CACHE);
105
+ db.exec(SQL_CREATE_ORDER_LOG);
106
+ db.exec(SQL_CREATE_SYNC_META);
107
+ }
108
+
109
+ // src/store.ts
110
+ var TTL = {
111
+ HOLDINGS: 60 * 60 * 1e3
112
+ // 1 hour
113
+ };
114
+ var ZerodhaStore = class {
115
+ db;
116
+ constructor(dbPath) {
117
+ this.db = new Database(dbPath);
118
+ migrate(this.db);
119
+ }
120
+ close() {
121
+ this.db.close();
122
+ }
123
+ // -- Holdings cache --------------------------------------------------------
124
+ replaceHoldings(holdings) {
125
+ const now = Date.now();
126
+ const tx = this.db.transaction(() => {
127
+ this.db.prepare("DELETE FROM holdings_cache").run();
128
+ const stmt = this.db.prepare(`
129
+ INSERT INTO holdings_cache
130
+ (symbol, exchange, isin, quantity, t1_quantity, avg_price, ltp, pnl, pnl_pct, day_change, product, refreshed_at)
131
+ VALUES
132
+ (@symbol, @exchange, @isin, @quantity, @t1Quantity, @avgPrice, @ltp, @pnl, @pnlPct, @dayChange, @product, @refreshedAt)
133
+ `);
134
+ for (const h of holdings) {
135
+ stmt.run({
136
+ symbol: h.symbol,
137
+ exchange: h.exchange,
138
+ isin: h.isin,
139
+ quantity: h.quantity,
140
+ t1Quantity: h.t1Quantity,
141
+ avgPrice: h.avgPrice,
142
+ ltp: h.ltp,
143
+ pnl: h.pnl,
144
+ pnlPct: h.pnlPct,
145
+ dayChange: h.dayChange,
146
+ product: h.product,
147
+ refreshedAt: h.refreshedAt
148
+ });
149
+ }
150
+ this.db.prepare("INSERT OR REPLACE INTO sync_meta (key, fetched_at, status) VALUES (?, ?, ?)").run("holdings", now, "ok");
151
+ });
152
+ tx();
153
+ }
154
+ getHoldings() {
155
+ const rows = this.db.prepare("SELECT * FROM holdings_cache").all();
156
+ return rows.map((r) => ({
157
+ symbol: r.symbol,
158
+ exchange: r.exchange,
159
+ isin: r.isin,
160
+ quantity: r.quantity,
161
+ t1Quantity: r.t1_quantity,
162
+ avgPrice: r.avg_price,
163
+ ltp: r.ltp,
164
+ pnl: r.pnl,
165
+ pnlPct: r.pnl_pct,
166
+ dayChange: r.day_change,
167
+ product: r.product,
168
+ refreshedAt: r.refreshed_at
169
+ }));
170
+ }
171
+ isStale(key, ttlMs) {
172
+ const row = this.db.prepare("SELECT fetched_at FROM sync_meta WHERE key = ?").get(key);
173
+ if (!row) return true;
174
+ return Date.now() - row.fetched_at > ttlMs;
175
+ }
176
+ getLastFetchedAt(key) {
177
+ const row = this.db.prepare("SELECT fetched_at FROM sync_meta WHERE key = ?").get(key);
178
+ return row?.fetched_at ?? 0;
179
+ }
180
+ setSyncMeta(key, status = "ok") {
181
+ this.db.prepare("INSERT OR REPLACE INTO sync_meta (key, fetched_at, status) VALUES (?, ?, ?)").run(key, Date.now(), status);
182
+ }
183
+ // -- Order audit log -------------------------------------------------------
184
+ logOrder(row) {
185
+ this.db.prepare(
186
+ `INSERT INTO order_log
187
+ (id, created_at, symbol, exchange, transaction_type, quantity, order_type, price, product, dry_run, kite_order_id, status, rejection_reason, agent_session)
188
+ VALUES
189
+ (@id, @createdAt, @symbol, @exchange, @transactionType, @quantity, @orderType, @price, @product, @dryRun, @kiteOrderId, @status, @rejectionReason, @agentSession)`
190
+ ).run({
191
+ id: row.id,
192
+ createdAt: row.createdAt,
193
+ symbol: row.symbol,
194
+ exchange: row.exchange,
195
+ transactionType: row.transaction,
196
+ quantity: row.quantity,
197
+ orderType: row.orderType,
198
+ price: row.price,
199
+ product: row.product,
200
+ dryRun: row.dryRun ? 1 : 0,
201
+ kiteOrderId: row.kiteOrderId,
202
+ status: row.status,
203
+ rejectionReason: row.rejectionReason,
204
+ agentSession: row.agentSession
205
+ });
206
+ }
207
+ getOrderLog(limit = 50) {
208
+ const rows = this.db.prepare("SELECT * FROM order_log ORDER BY created_at DESC LIMIT ?").all(limit);
209
+ return rows.map((r) => ({
210
+ id: r.id,
211
+ createdAt: r.created_at,
212
+ symbol: r.symbol,
213
+ exchange: r.exchange,
214
+ transaction: r.transaction_type,
215
+ quantity: r.quantity,
216
+ orderType: r.order_type,
217
+ price: r.price,
218
+ product: r.product,
219
+ dryRun: r.dry_run === 1,
220
+ kiteOrderId: r.kite_order_id,
221
+ status: r.status,
222
+ rejectionReason: r.rejection_reason,
223
+ agentSession: r.agent_session
224
+ }));
225
+ }
226
+ clean() {
227
+ this.db.prepare("DELETE FROM holdings_cache").run();
228
+ this.db.prepare("DELETE FROM order_log").run();
229
+ this.db.prepare("DELETE FROM sync_meta").run();
230
+ return { tablesCleared: ["holdings_cache", "order_log", "sync_meta"] };
231
+ }
232
+ };
233
+
234
+ // src/tools.ts
235
+ import { randomUUID } from "crypto";
236
+ import { mkdirSync, writeFileSync } from "fs";
237
+ import { dirname, join } from "path";
238
+ import { defineTool, err, ok } from "@ethosagent/plugin-sdk/tool-helpers";
239
+
240
+ // src/kite-client.ts
241
+ var KITE_BASE = "https://api.kite.trade";
242
+ function authHeaders(creds) {
243
+ return {
244
+ Authorization: `token ${creds.apiKey}:${creds.accessToken}`,
245
+ "X-Kite-Version": "3",
246
+ "Content-Type": "application/x-www-form-urlencoded"
247
+ };
248
+ }
249
+ var KiteTokenExpiredError = class extends Error {
250
+ code = "TOKEN_EXPIRED";
251
+ constructor(message = "Kite access token expired. Renew via zerodha-broker auth.") {
252
+ super(message);
253
+ this.name = "KiteTokenExpiredError";
254
+ }
255
+ };
256
+ var KiteApiError = class extends Error {
257
+ constructor(message, errorType, statusCode) {
258
+ super(message);
259
+ this.errorType = errorType;
260
+ this.statusCode = statusCode;
261
+ this.name = "KiteApiError";
262
+ }
263
+ errorType;
264
+ statusCode;
265
+ };
266
+ async function kiteGet(creds, path) {
267
+ const res = await fetch(`${KITE_BASE}${path}`, {
268
+ method: "GET",
269
+ headers: authHeaders(creds)
270
+ });
271
+ const body = await res.json();
272
+ if (res.status === 403 || body.error_type === "TokenException") {
273
+ throw new KiteTokenExpiredError(body.message);
274
+ }
275
+ if (body.status === "error") {
276
+ throw new KiteApiError(
277
+ body.message ?? "Unknown error",
278
+ body.error_type ?? "Unknown",
279
+ res.status
280
+ );
281
+ }
282
+ return body.data;
283
+ }
284
+ async function fetchMargins(creds) {
285
+ return kiteGet(creds, "/user/margins");
286
+ }
287
+ async function fetchHoldings(creds) {
288
+ return kiteGet(creds, "/portfolio/holdings");
289
+ }
290
+ async function fetchPositions(creds) {
291
+ return kiteGet(creds, "/portfolio/positions");
292
+ }
293
+ async function fetchOrders(creds) {
294
+ return kiteGet(creds, "/orders");
295
+ }
296
+ async function placeOrder(creds, params) {
297
+ const variety = params.variety ?? "regular";
298
+ const body = new URLSearchParams();
299
+ body.set("exchange", params.exchange);
300
+ body.set("tradingsymbol", params.tradingsymbol);
301
+ body.set("transaction_type", params.transaction_type);
302
+ body.set("quantity", String(params.quantity));
303
+ body.set("order_type", params.order_type);
304
+ body.set("product", params.product);
305
+ body.set("validity", params.validity ?? "DAY");
306
+ if (params.price != null) body.set("price", String(params.price));
307
+ if (params.trigger_price != null) body.set("trigger_price", String(params.trigger_price));
308
+ const res = await fetch(`${KITE_BASE}/orders/${variety}`, {
309
+ method: "POST",
310
+ headers: authHeaders(creds),
311
+ body: body.toString()
312
+ });
313
+ const json = await res.json();
314
+ if (res.status === 403 || json.error_type === "TokenException") {
315
+ throw new KiteTokenExpiredError(json.message);
316
+ }
317
+ if (json.status === "error") {
318
+ throw new KiteApiError(
319
+ json.message ?? "Order placement failed",
320
+ json.error_type ?? "Unknown",
321
+ res.status
322
+ );
323
+ }
324
+ return json.data;
325
+ }
326
+
327
+ // src/tools.ts
328
+ async function getCredentials(ctx) {
329
+ const apiKey = await ctx.secretsResolver?.get("brokers/zerodha/apiKey");
330
+ const accessToken = await ctx.secretsResolver?.get("brokers/zerodha/accessToken");
331
+ if (!apiKey || !accessToken) {
332
+ return err(
333
+ "Zerodha credentials not configured. Store api_key and access_token in ~/.ethos/secrets/brokers/zerodha/",
334
+ "not_available"
335
+ );
336
+ }
337
+ return { apiKey, accessToken };
338
+ }
339
+ function isToolResult(v) {
340
+ return typeof v === "object" && v !== null && "ok" in v;
341
+ }
342
+ function tokenExpiredResult(apiKey) {
343
+ const loginUrl = apiKey ? buildLoginUrl(apiKey) : "https://kite.trade/connect/login?v=3";
344
+ return err(
345
+ `Token expired. Renew daily access token:
346
+ 1. Open ${loginUrl}
347
+ 2. Log in and copy request_token from redirect URL
348
+ 3. Run: zerodha-broker auth --request-token TOKEN`,
349
+ "not_available"
350
+ );
351
+ }
352
+ function kiteHoldingToRow(h, now) {
353
+ return {
354
+ symbol: h.tradingsymbol,
355
+ exchange: h.exchange,
356
+ isin: h.isin,
357
+ quantity: h.quantity,
358
+ t1Quantity: h.t1_quantity,
359
+ avgPrice: h.average_price,
360
+ ltp: h.last_price,
361
+ pnl: h.pnl,
362
+ pnlPct: h.average_price > 0 ? Number((h.pnl / (h.quantity * h.average_price) * 100).toFixed(2)) : null,
363
+ dayChange: h.day_change_percentage,
364
+ product: h.product,
365
+ refreshedAt: now
366
+ };
367
+ }
368
+ function defaultDbPath() {
369
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
370
+ return process.env.ZERODHA_DB ?? `${home}/.ethos/zerodha/zerodha.db`;
371
+ }
372
+ var zerodhaAuthStatus = defineTool({
373
+ name: "zerodha_auth_status",
374
+ description: `Check if the Zerodha access token is valid and the account is ready for API calls.
375
+ Always call this first in a session before reading portfolio data.
376
+ Returns whether token is valid, expiry estimate, and account user_id.
377
+ If token is expired, returns instructions for renewal.`,
378
+ toolset: "broker",
379
+ maxResultChars: 1e3,
380
+ capabilities: {
381
+ network: { allowedHosts: ["api.kite.trade"] },
382
+ secrets: ["brokers/zerodha/apiKey", "brokers/zerodha/accessToken"]
383
+ },
384
+ schema: { type: "object", properties: {} },
385
+ async execute(_args, ctx) {
386
+ try {
387
+ const creds = await getCredentials(ctx);
388
+ if (isToolResult(creds)) return creds;
389
+ const result = await validateToken(creds.apiKey, creds.accessToken);
390
+ if (result.valid) {
391
+ return ok(
392
+ JSON.stringify({
393
+ valid: true,
394
+ user_id: result.userId,
395
+ expires_hint: result.expiresHint,
396
+ warning: null
397
+ })
398
+ );
399
+ }
400
+ return tokenExpiredResult(creds.apiKey);
401
+ } catch (e) {
402
+ if (e instanceof KiteTokenExpiredError) {
403
+ return tokenExpiredResult();
404
+ }
405
+ return err(e instanceof Error ? e.message : String(e));
406
+ }
407
+ }
408
+ });
409
+ var zerodhaAuthComplete = defineTool({
410
+ name: "zerodha_auth_complete",
411
+ description: `Complete daily Zerodha authentication by exchanging a request_token for an access_token.
412
+ Call this after the user has opened the Zerodha login URL and been redirected back with a request_token.
413
+ The request_token appears in the redirect URL as ?request_token=YYY.
414
+ Exchanges it for a fresh access_token and stores it in ~/.ethos/secrets/brokers/zerodha/accessToken.
415
+ The new token is valid until midnight IST.`,
416
+ toolset: "broker",
417
+ maxResultChars: 500,
418
+ capabilities: {
419
+ network: { allowedHosts: ["api.kite.trade"] },
420
+ secrets: ["brokers/zerodha/apiKey", "brokers/zerodha/apiSecret"]
421
+ },
422
+ schema: {
423
+ type: "object",
424
+ properties: {
425
+ request_token: {
426
+ type: "string",
427
+ description: "The request_token from Zerodha redirect URL after login"
428
+ }
429
+ },
430
+ required: ["request_token"]
431
+ },
432
+ async execute(args, ctx) {
433
+ try {
434
+ const apiKey = await ctx.secretsResolver?.get("brokers/zerodha/apiKey");
435
+ const apiSecret = await ctx.secretsResolver?.get("brokers/zerodha/apiSecret");
436
+ if (!apiKey || !apiSecret) {
437
+ return err(
438
+ "Zerodha credentials not configured. Store apiKey and apiSecret in ~/.ethos/secrets/brokers/zerodha/",
439
+ "not_available"
440
+ );
441
+ }
442
+ const result = await exchangeToken(apiKey, apiSecret, args.request_token);
443
+ const ethosDir = process.env.ETHOS_DATA_DIR ?? join(process.env.HOME ?? process.env.USERPROFILE ?? "/tmp", ".ethos");
444
+ const tokenPath = join(ethosDir, "secrets", "brokers", "zerodha", "accessToken");
445
+ mkdirSync(dirname(tokenPath), { recursive: true });
446
+ writeFileSync(tokenPath, result.access_token, { mode: 384 });
447
+ return ok(
448
+ JSON.stringify({
449
+ authenticated: true,
450
+ user_id: result.user_id,
451
+ message: "Token valid until midnight IST."
452
+ })
453
+ );
454
+ } catch (e) {
455
+ return err(e instanceof Error ? e.message : String(e));
456
+ }
457
+ }
458
+ });
459
+ var zerodhaAccount = defineTool({
460
+ name: "zerodha_account",
461
+ description: `Zerodha account summary \u2014 available funds and margin utilisation.
462
+ Returns net available cash, used margin, unrealised M2M, and option premium blocked.
463
+ All values in INR. Live from Kite API.`,
464
+ toolset: "broker",
465
+ maxResultChars: 2e3,
466
+ capabilities: {
467
+ network: { allowedHosts: ["api.kite.trade"] },
468
+ secrets: ["brokers/zerodha/apiKey", "brokers/zerodha/accessToken"]
469
+ },
470
+ schema: { type: "object", properties: {} },
471
+ async execute(_args, ctx) {
472
+ try {
473
+ const creds = await getCredentials(ctx);
474
+ if (isToolResult(creds)) return creds;
475
+ const margins = await fetchMargins(creds);
476
+ const eq = margins.equity;
477
+ return ok(
478
+ JSON.stringify({
479
+ net_available_inr: eq.net,
480
+ opening_balance_inr: eq.available.opening_balance,
481
+ m2m_unrealised_inr: eq.utilised.m2m_unrealised,
482
+ exposure_used_inr: eq.utilised.exposure,
483
+ option_premium_blocked_inr: eq.utilised.option_premium,
484
+ equity_enabled: eq.enabled
485
+ })
486
+ );
487
+ } catch (e) {
488
+ if (e instanceof KiteTokenExpiredError) {
489
+ return tokenExpiredResult();
490
+ }
491
+ return err(e instanceof Error ? e.message : String(e));
492
+ }
493
+ }
494
+ });
495
+ var zerodhaHoldings = defineTool({
496
+ name: "zerodha_holdings",
497
+ description: `Equity holdings (long-term CNC positions) with P&L.
498
+ Returns all stocks held in Zerodha DEMAT \u2014 symbol, quantity, average cost, current price,
499
+ unrealised P&L, and day change. Refreshes cache if >1 hour stale. Values in INR.`,
500
+ toolset: "broker",
501
+ maxResultChars: 8e3,
502
+ capabilities: {
503
+ network: { allowedHosts: ["api.kite.trade"] },
504
+ secrets: ["brokers/zerodha/apiKey", "brokers/zerodha/accessToken"]
505
+ },
506
+ schema: {
507
+ type: "object",
508
+ properties: {
509
+ force_refresh: {
510
+ type: "boolean",
511
+ description: "Force live fetch bypassing 1h cache (default false)"
512
+ }
513
+ }
514
+ },
515
+ async execute(args, ctx) {
516
+ try {
517
+ const creds = await getCredentials(ctx);
518
+ if (isToolResult(creds)) return creds;
519
+ const store = new ZerodhaStore(defaultDbPath());
520
+ try {
521
+ const forceRefresh = args.force_refresh ?? false;
522
+ const stale = store.isStale("holdings", TTL.HOLDINGS);
523
+ if (forceRefresh || stale) {
524
+ const kiteHoldings = await fetchHoldings(creds);
525
+ const now = Date.now();
526
+ const rows = kiteHoldings.map((h) => kiteHoldingToRow(h, now));
527
+ store.replaceHoldings(rows);
528
+ }
529
+ const holdings = store.getHoldings();
530
+ const totalInvested = holdings.reduce((s, h) => s + h.quantity * h.avgPrice, 0);
531
+ const totalCurrent = holdings.reduce((s, h) => s + h.quantity * (h.ltp ?? h.avgPrice), 0);
532
+ const totalPnl = totalCurrent - totalInvested;
533
+ const totalPnlPct = totalInvested > 0 ? totalPnl / totalInvested * 100 : 0;
534
+ return ok(
535
+ JSON.stringify({
536
+ refreshed_at: new Date(store.getLastFetchedAt("holdings") || Date.now()).toISOString(),
537
+ count: holdings.length,
538
+ total_invested_inr: Number(totalInvested.toFixed(2)),
539
+ total_current_inr: Number(totalCurrent.toFixed(2)),
540
+ total_pnl_inr: Number(totalPnl.toFixed(2)),
541
+ total_pnl_pct: Number(totalPnlPct.toFixed(2)),
542
+ holdings: holdings.map((h) => ({
543
+ symbol: h.symbol,
544
+ exchange: h.exchange,
545
+ quantity: h.quantity,
546
+ avg_price: h.avgPrice,
547
+ ltp: h.ltp,
548
+ pnl_inr: h.pnl,
549
+ pnl_pct: h.pnlPct,
550
+ day_change_pct: h.dayChange
551
+ }))
552
+ })
553
+ );
554
+ } finally {
555
+ store.close();
556
+ }
557
+ } catch (e) {
558
+ if (e instanceof KiteTokenExpiredError) {
559
+ return tokenExpiredResult();
560
+ }
561
+ return err(e instanceof Error ? e.message : String(e));
562
+ }
563
+ }
564
+ });
565
+ var zerodhaPositions = defineTool({
566
+ name: "zerodha_positions",
567
+ description: `Open intraday and overnight positions with real-time P&L.
568
+ Returns both day (MIS \u2014 intraday) and net (NRML/CNC \u2014 overnight carry) positions.
569
+ If no open positions, returns an empty list. Values in INR. Always live from Kite API.`,
570
+ toolset: "broker",
571
+ maxResultChars: 6e3,
572
+ capabilities: {
573
+ network: { allowedHosts: ["api.kite.trade"] },
574
+ secrets: ["brokers/zerodha/apiKey", "brokers/zerodha/accessToken"]
575
+ },
576
+ schema: { type: "object", properties: {} },
577
+ async execute(_args, ctx) {
578
+ try {
579
+ const creds = await getCredentials(ctx);
580
+ if (isToolResult(creds)) return creds;
581
+ const positions = await fetchPositions(creds);
582
+ const dayPnl = positions.day.reduce((s, p) => s + p.pnl, 0);
583
+ return ok(
584
+ JSON.stringify({
585
+ day_positions: positions.day.map((p) => ({
586
+ symbol: p.tradingsymbol,
587
+ exchange: p.exchange,
588
+ product: p.product,
589
+ quantity: p.quantity,
590
+ avg_price: p.average_price,
591
+ ltp: p.last_price,
592
+ pnl_inr: p.pnl,
593
+ unrealised_inr: p.unrealised
594
+ })),
595
+ net_positions: positions.net.map((p) => ({
596
+ symbol: p.tradingsymbol,
597
+ exchange: p.exchange,
598
+ product: p.product,
599
+ quantity: p.quantity,
600
+ avg_price: p.average_price,
601
+ ltp: p.last_price,
602
+ pnl_inr: p.pnl,
603
+ unrealised_inr: p.unrealised
604
+ })),
605
+ total_day_pnl_inr: Number(dayPnl.toFixed(2))
606
+ })
607
+ );
608
+ } catch (e) {
609
+ if (e instanceof KiteTokenExpiredError) {
610
+ return tokenExpiredResult();
611
+ }
612
+ return err(e instanceof Error ? e.message : String(e));
613
+ }
614
+ }
615
+ });
616
+ var zerodhaOrders = defineTool({
617
+ name: "zerodha_orders",
618
+ description: `Today's order book \u2014 all orders placed, modified, or cancelled today.
619
+ Returns order status, fill price, rejection reason. Always live from Kite API.
620
+ Does NOT return historical orders from previous days (Zerodha API limitation).`,
621
+ toolset: "broker",
622
+ maxResultChars: 6e3,
623
+ capabilities: {
624
+ network: { allowedHosts: ["api.kite.trade"] },
625
+ secrets: ["brokers/zerodha/apiKey", "brokers/zerodha/accessToken"]
626
+ },
627
+ schema: { type: "object", properties: {} },
628
+ async execute(_args, ctx) {
629
+ try {
630
+ const creds = await getCredentials(ctx);
631
+ if (isToolResult(creds)) return creds;
632
+ const orders = await fetchOrders(creds);
633
+ return ok(
634
+ JSON.stringify({
635
+ count: orders.length,
636
+ orders: orders.map((o) => ({
637
+ order_id: o.order_id,
638
+ symbol: o.tradingsymbol,
639
+ side: o.transaction_type,
640
+ quantity: o.quantity,
641
+ order_type: o.order_type,
642
+ price: o.price,
643
+ status: o.status,
644
+ filled_price: o.average_price,
645
+ time: o.order_timestamp
646
+ }))
647
+ })
648
+ );
649
+ } catch (e) {
650
+ if (e instanceof KiteTokenExpiredError) {
651
+ return tokenExpiredResult();
652
+ }
653
+ return err(e instanceof Error ? e.message : String(e));
654
+ }
655
+ }
656
+ });
657
+ var zerodhaPlaceOrder = defineTool({
658
+ name: "zerodha_place_order",
659
+ description: `Propose or place an equity order via Zerodha Kite.
660
+
661
+ IMPORTANT SAFETY RULES:
662
+ - dry_run defaults to TRUE. With dry_run: true, the order is SIMULATED \u2014 no real trade is placed.
663
+ The agent will describe what would happen and log it to the audit trail.
664
+ - To place a REAL order: the user must explicitly say "confirm: true" in their message.
665
+ Do not set confirm: true without explicit user instruction.
666
+ - Only NSE/BSE equity (CNC product) orders are supported. No MIS intraday, no F&O.
667
+ - Market hours only: 09:15\u201315:30 IST Monday\u2013Friday. AMO (After Market Order) requires variety: 'amo'.
668
+
669
+ Before placing any real order, always call zerodha_holdings to verify the position context.`,
670
+ toolset: "broker",
671
+ maxResultChars: 2e3,
672
+ capabilities: {
673
+ network: { allowedHosts: ["api.kite.trade"] },
674
+ secrets: ["brokers/zerodha/apiKey", "brokers/zerodha/accessToken"]
675
+ },
676
+ schema: {
677
+ type: "object",
678
+ properties: {
679
+ symbol: {
680
+ type: "string",
681
+ description: "NSE/BSE symbol (e.g. RELIANCE, TCS, INFY)"
682
+ },
683
+ exchange: {
684
+ type: "string",
685
+ enum: ["NSE", "BSE"],
686
+ description: "Exchange (default NSE)"
687
+ },
688
+ side: {
689
+ type: "string",
690
+ enum: ["BUY", "SELL"],
691
+ description: "Transaction type"
692
+ },
693
+ quantity: {
694
+ type: "number",
695
+ description: "Number of shares (must be positive integer)"
696
+ },
697
+ order_type: {
698
+ type: "string",
699
+ enum: ["MARKET", "LIMIT"],
700
+ description: "MARKET executes immediately at best price; LIMIT requires price parameter"
701
+ },
702
+ price: {
703
+ type: "number",
704
+ description: "Limit price in INR (required for LIMIT orders)"
705
+ },
706
+ dry_run: {
707
+ type: "boolean",
708
+ description: "Simulate only \u2014 do NOT place a real order (default: true). Set false only when user explicitly confirms."
709
+ },
710
+ variety: {
711
+ type: "string",
712
+ enum: ["regular", "amo"],
713
+ description: "Order variety: 'regular' (market hours) or 'amo' (after market, 3:45\u20138:57 AM next day). Default: regular"
714
+ }
715
+ },
716
+ required: ["symbol", "side", "quantity", "order_type"]
717
+ },
718
+ async execute(args, ctx) {
719
+ try {
720
+ const creds = await getCredentials(ctx);
721
+ if (isToolResult(creds)) return creds;
722
+ const exchange = args.exchange ?? "NSE";
723
+ const dryRun = args.dry_run !== false;
724
+ const variety = args.variety ?? "regular";
725
+ const product = "CNC";
726
+ const orderId = randomUUID();
727
+ const now = Date.now();
728
+ if (dryRun) {
729
+ const priceStr = args.order_type === "LIMIT" && args.price != null ? `LIMIT ${args.price.toFixed(2)}` : "MARKET";
730
+ const estimatedCost = args.price != null ? args.quantity * args.price : null;
731
+ const description = `Would place: ${args.side} ${args.quantity} ${args.symbol} @ ${priceStr} on ${exchange} (${product})`;
732
+ const store2 = new ZerodhaStore(defaultDbPath());
733
+ try {
734
+ store2.logOrder({
735
+ id: orderId,
736
+ createdAt: now,
737
+ symbol: args.symbol,
738
+ exchange,
739
+ transaction: args.side,
740
+ quantity: args.quantity,
741
+ orderType: args.order_type,
742
+ price: args.price ?? null,
743
+ product,
744
+ dryRun: true,
745
+ kiteOrderId: null,
746
+ status: "dry_run",
747
+ rejectionReason: null,
748
+ agentSession: null
749
+ });
750
+ } finally {
751
+ store2.close();
752
+ }
753
+ return ok(
754
+ JSON.stringify({
755
+ dry_run: true,
756
+ description,
757
+ estimated_cost_inr: estimatedCost,
758
+ estimated_brokerage_inr: 0,
759
+ // Zerodha zero brokerage on delivery
760
+ logged: true,
761
+ to_place_for_real: "Add dry_run: false to this request after confirming"
762
+ })
763
+ );
764
+ }
765
+ const result = await placeOrder(creds, {
766
+ exchange,
767
+ tradingsymbol: args.symbol,
768
+ transaction_type: args.side,
769
+ quantity: args.quantity,
770
+ order_type: args.order_type,
771
+ product,
772
+ price: args.price,
773
+ variety
774
+ });
775
+ const store = new ZerodhaStore(defaultDbPath());
776
+ try {
777
+ store.logOrder({
778
+ id: orderId,
779
+ createdAt: now,
780
+ symbol: args.symbol,
781
+ exchange,
782
+ transaction: args.side,
783
+ quantity: args.quantity,
784
+ orderType: args.order_type,
785
+ price: args.price ?? null,
786
+ product,
787
+ dryRun: false,
788
+ kiteOrderId: result.order_id,
789
+ status: "placed",
790
+ rejectionReason: null,
791
+ agentSession: null
792
+ });
793
+ } finally {
794
+ store.close();
795
+ }
796
+ return ok(
797
+ JSON.stringify({
798
+ dry_run: false,
799
+ order_id: result.order_id,
800
+ status: "placed",
801
+ symbol: args.symbol,
802
+ side: args.side,
803
+ quantity: args.quantity,
804
+ order_type: args.order_type,
805
+ price: args.price ?? null
806
+ })
807
+ );
808
+ } catch (e) {
809
+ if (e instanceof KiteTokenExpiredError) {
810
+ return tokenExpiredResult();
811
+ }
812
+ return err(e instanceof Error ? e.message : String(e));
813
+ }
814
+ }
815
+ });
816
+ var plugin = {
817
+ activate(api) {
818
+ for (const tool of createZerodhaTools()) {
819
+ api.registerTool(tool);
820
+ }
821
+ }
822
+ };
823
+ function activate(api) {
824
+ for (const tool of createZerodhaTools()) {
825
+ api.registerTool(tool);
826
+ }
827
+ }
828
+ function createZerodhaTools() {
829
+ return [
830
+ zerodhaAuthStatus,
831
+ zerodhaAuthComplete,
832
+ zerodhaAccount,
833
+ zerodhaHoldings,
834
+ zerodhaPositions,
835
+ zerodhaOrders,
836
+ zerodhaPlaceOrder
837
+ ];
838
+ }
839
+ export {
840
+ TTL,
841
+ ZerodhaStore,
842
+ activate,
843
+ buildLoginUrl,
844
+ createZerodhaTools as createTools,
845
+ createZerodhaTools,
846
+ exchangeToken,
847
+ plugin,
848
+ validateToken
849
+ };
850
+ //# sourceMappingURL=index.js.map