@pungoyal/kite-cli 0.1.1 → 0.2.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.
@@ -0,0 +1,140 @@
1
+ import { EnvironmentSchema, SANDBOX_CREDENTIALS } from './config.js';
2
+ import { ExitCode, KiteCliError } from './errors.js';
3
+ /**
4
+ * Multi-account resolution.
5
+ *
6
+ * A *profile* is a named Zerodha account. Selection is stateless per invocation
7
+ * — there is deliberately no sticky "active account" that persists across
8
+ * commands, because forgetting which account is active while placing real
9
+ * orders is exactly the mistake this feature must not introduce. Instead the
10
+ * target is resolved fresh every run, and the money-moving preview shows the
11
+ * verified user id (see `safety.ts`).
12
+ *
13
+ * Two profile names are reserved:
14
+ * - `default` — today's single-account setup (top-level apiKey/env in config,
15
+ * secrets stored unprefixed). Chosen so existing installs need
16
+ * no migration.
17
+ * - `sandbox` — Zerodha's public sandbox. One fixed identity, so there is no
18
+ * value in per-account sandbox profiles.
19
+ */
20
+ export const DEFAULT_PROFILE = 'default';
21
+ export const SANDBOX_PROFILE = 'sandbox';
22
+ export const RESERVED_PROFILES = [DEFAULT_PROFILE, SANDBOX_PROFILE];
23
+ /** Filesystem- and keyring-safe. Also keeps names out of `profile:<n>:` key collisions. */
24
+ const PROFILE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/;
25
+ /** Reject names that could escape a filename or collide with a reserved prefix. */
26
+ export function assertValidProfileName(name) {
27
+ if (!PROFILE_NAME_RE.test(name)) {
28
+ throw new KiteCliError(`Invalid profile name "${name}".`, ExitCode.Usage, 'Use 1–32 characters: letters, digits, hyphen or underscore, starting alphanumeric.');
29
+ }
30
+ }
31
+ /**
32
+ * Keyring / encrypted-file namespace for a profile's secrets.
33
+ *
34
+ * The reserved profiles reproduce the historical env-keyed scheme byte-for-byte
35
+ * (`production` unprefixed, `sandbox:` otherwise), so no stored secret has to be
36
+ * migrated. Every other profile gets a `profile:<name>:` namespace that cannot
37
+ * collide with an env name.
38
+ */
39
+ export function storagePrefixFor(profile) {
40
+ if (profile.name === DEFAULT_PROFILE || profile.name === SANDBOX_PROFILE) {
41
+ return profile.env === 'production' ? '' : `${profile.env}:`;
42
+ }
43
+ return `profile:${profile.name}:`;
44
+ }
45
+ /** Every profile the user has, reserved ones first, without duplicates. */
46
+ export function listProfileNames(config) {
47
+ const names = [DEFAULT_PROFILE, SANDBOX_PROFILE];
48
+ for (const name of Object.keys(config.profiles)) {
49
+ if (!names.includes(name))
50
+ names.push(name);
51
+ }
52
+ return names;
53
+ }
54
+ export function isKnownProfile(config, name) {
55
+ return name === DEFAULT_PROFILE || name === SANDBOX_PROFILE || Object.hasOwn(config.profiles, name);
56
+ }
57
+ /**
58
+ * Look up a profile by name without applying command-line overrides.
59
+ *
60
+ * An unknown name yields an empty, production profile rather than throwing, so
61
+ * `kite --profile new login` (and `profiles add`) can create it.
62
+ */
63
+ export function getProfile(config, name) {
64
+ if (name === SANDBOX_PROFILE) {
65
+ return { name, apiKey: SANDBOX_CREDENTIALS.apiKey, env: 'sandbox', trading: undefined, explicit: false };
66
+ }
67
+ if (name === DEFAULT_PROFILE) {
68
+ return { name, apiKey: config.apiKey ?? '', env: config.env, trading: undefined, explicit: false };
69
+ }
70
+ const p = config.profiles[name];
71
+ return {
72
+ name,
73
+ apiKey: p?.apiKey ?? '',
74
+ env: p?.env ?? 'production',
75
+ trading: p?.trading,
76
+ explicit: false,
77
+ };
78
+ }
79
+ function parseEnvFlag(value) {
80
+ if (value === undefined || value.trim() === '')
81
+ return undefined;
82
+ const result = EnvironmentSchema.safeParse(value);
83
+ if (!result.success) {
84
+ throw new KiteCliError(`Unknown environment "${value}". Expected "production" or "sandbox".`, ExitCode.Usage);
85
+ }
86
+ return result.data;
87
+ }
88
+ /**
89
+ * Resolve the effective profile for this invocation.
90
+ *
91
+ * Precedence: `--profile` > KITE_PROFILE > (`--env sandbox` alias) >
92
+ * `config.defaultProfile` > `default`. An explicit `--env`/KITE_ENV then
93
+ * overrides the resolved profile's environment, so `--env production` still
94
+ * forces production the way it always has.
95
+ */
96
+ export function resolveProfile(selectors, config) {
97
+ const named = selectors.profileFlag?.trim() || process.env['KITE_PROFILE']?.trim() || '';
98
+ const explicitEnv = parseEnvFlag(selectors.envFlag ?? process.env['KITE_ENV']);
99
+ let name;
100
+ let explicit;
101
+ if (named) {
102
+ assertValidProfileName(named);
103
+ name = named;
104
+ explicit = true;
105
+ }
106
+ else if (explicitEnv === 'sandbox') {
107
+ // Back-compat: `--env sandbox` selects the sandbox profile. This is an alias,
108
+ // not an explicit account choice, so it does not arm the env-var guard.
109
+ name = SANDBOX_PROFILE;
110
+ explicit = false;
111
+ }
112
+ else {
113
+ name = config.defaultProfile ?? DEFAULT_PROFILE;
114
+ explicit = false;
115
+ }
116
+ const base = getProfile(config, name);
117
+ const env = explicitEnv ?? base.env;
118
+ // Any profile resolved into the sandbox environment uses the public demo key —
119
+ // in particular a `default` profile pinned to sandbox via `config.env`, which
120
+ // would otherwise carry the (production) config apiKey and fail to authenticate.
121
+ const apiKey = env === 'sandbox' ? SANDBOX_CREDENTIALS.apiKey : base.apiKey;
122
+ return { ...base, apiKey, env, explicit };
123
+ }
124
+ /**
125
+ * The trading config actually in force: global settings overlaid with the
126
+ * profile's overrides. Fail-closed by construction — a field the profile leaves
127
+ * unset falls back to the global value, so an omitted cap never becomes "no cap".
128
+ */
129
+ export function resolveTradingConfig(config, profile) {
130
+ const base = config.trading;
131
+ const o = profile.trading;
132
+ if (!o)
133
+ return base;
134
+ return {
135
+ enabled: o.enabled ?? base.enabled,
136
+ confirm: o.confirm ?? base.confirm,
137
+ maxOrderValue: o.maxOrderValue ?? base.maxOrderValue,
138
+ strictConfirmAbove: o.strictConfirmAbove ?? base.strictConfirmAbove,
139
+ };
140
+ }
@@ -141,11 +141,11 @@ export type Margins = z.infer<typeof MarginsSchema>;
141
141
  export type SegmentMargin = z.infer<typeof SegmentMarginSchema>;
142
142
  /** Known terminal states. Not exhaustive — see the note at the top of this file. */
143
143
  export declare const ORDER_STATUS: {
144
- readonly Complete: "COMPLETE";
145
- readonly Cancelled: "CANCELLED";
146
- readonly Rejected: "REJECTED";
147
- readonly Open: "OPEN";
148
- readonly TriggerPending: "TRIGGER PENDING";
144
+ readonly Complete: 'COMPLETE';
145
+ readonly Cancelled: 'CANCELLED';
146
+ readonly Rejected: 'REJECTED';
147
+ readonly Open: 'OPEN';
148
+ readonly TriggerPending: 'TRIGGER PENDING';
149
149
  };
150
150
  export declare const TERMINAL_ORDER_STATUSES: Set<string>;
151
151
  export declare const OrderSchema: z.ZodObject<{
@@ -474,6 +474,79 @@ export type Gtt = z.infer<typeof GttSchema>;
474
474
  export declare const GttCreateResultSchema: z.ZodObject<{
475
475
  trigger_id: z.ZodNumber;
476
476
  }, z.core.$loose>;
477
+ /**
478
+ * An ATO (Alert-Triggers-Order) alert carries a basket — a full order spec that
479
+ * is placed when the alert fires. Every field is optional and the object is
480
+ * loose: we only *read* baskets on existing alerts, and Kite's basket shape is
481
+ * richer than we display, so anything we don't touch must pass through
482
+ * untouched rather than fail parsing (invariant #5).
483
+ */
484
+ export declare const AlertBasketItemSchema: z.ZodObject<{
485
+ type: z.ZodOptional<z.ZodString>;
486
+ tradingsymbol: z.ZodOptional<z.ZodString>;
487
+ exchange: z.ZodOptional<z.ZodString>;
488
+ instrument_token: z.ZodOptional<z.ZodNumber>;
489
+ weight: z.ZodOptional<z.ZodNumber>;
490
+ params: z.ZodOptional<z.ZodObject<{}, z.core.$loose>>;
491
+ }, z.core.$loose>;
492
+ export declare const AlertBasketSchema: z.ZodObject<{
493
+ name: z.ZodOptional<z.ZodString>;
494
+ type: z.ZodOptional<z.ZodString>;
495
+ tags: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
496
+ items: z.ZodDefault<z.ZodArray<z.ZodObject<{
497
+ type: z.ZodOptional<z.ZodString>;
498
+ tradingsymbol: z.ZodOptional<z.ZodString>;
499
+ exchange: z.ZodOptional<z.ZodString>;
500
+ instrument_token: z.ZodOptional<z.ZodNumber>;
501
+ weight: z.ZodOptional<z.ZodNumber>;
502
+ params: z.ZodOptional<z.ZodObject<{}, z.core.$loose>>;
503
+ }, z.core.$loose>>>;
504
+ }, z.core.$loose>;
505
+ export declare const AlertSchema: z.ZodObject<{
506
+ uuid: z.ZodString;
507
+ user_id: z.ZodOptional<z.ZodString>;
508
+ type: z.ZodString;
509
+ name: z.ZodOptional<z.ZodString>;
510
+ status: z.ZodString;
511
+ disabled_reason: z.ZodOptional<z.ZodString>;
512
+ lhs_attribute: z.ZodOptional<z.ZodString>;
513
+ lhs_exchange: z.ZodOptional<z.ZodString>;
514
+ lhs_tradingsymbol: z.ZodOptional<z.ZodString>;
515
+ operator: z.ZodOptional<z.ZodString>;
516
+ rhs_type: z.ZodOptional<z.ZodString>;
517
+ rhs_attribute: z.ZodOptional<z.ZodString>;
518
+ rhs_exchange: z.ZodOptional<z.ZodString>;
519
+ rhs_tradingsymbol: z.ZodOptional<z.ZodString>;
520
+ rhs_constant: z.ZodOptional<z.ZodNumber>;
521
+ alert_count: z.ZodOptional<z.ZodNumber>;
522
+ basket: z.ZodOptional<z.ZodObject<{
523
+ name: z.ZodOptional<z.ZodString>;
524
+ type: z.ZodOptional<z.ZodString>;
525
+ tags: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
526
+ items: z.ZodDefault<z.ZodArray<z.ZodObject<{
527
+ type: z.ZodOptional<z.ZodString>;
528
+ tradingsymbol: z.ZodOptional<z.ZodString>;
529
+ exchange: z.ZodOptional<z.ZodString>;
530
+ instrument_token: z.ZodOptional<z.ZodNumber>;
531
+ weight: z.ZodOptional<z.ZodNumber>;
532
+ params: z.ZodOptional<z.ZodObject<{}, z.core.$loose>>;
533
+ }, z.core.$loose>>>;
534
+ }, z.core.$loose>>;
535
+ created_at: z.ZodOptional<z.ZodString>;
536
+ updated_at: z.ZodOptional<z.ZodString>;
537
+ }, z.core.$loose>;
538
+ export type Alert = z.infer<typeof AlertSchema>;
539
+ /** One entry in an alert's trigger history. `meta`/`order_meta` are large and
540
+ * only shown verbatim in `--json`, so they pass through unvalidated. */
541
+ export declare const AlertHistoryEntrySchema: z.ZodObject<{
542
+ uuid: z.ZodOptional<z.ZodString>;
543
+ type: z.ZodOptional<z.ZodString>;
544
+ condition: z.ZodOptional<z.ZodString>;
545
+ created_at: z.ZodOptional<z.ZodString>;
546
+ meta: z.ZodOptional<z.ZodUnknown>;
547
+ order_meta: z.ZodOptional<z.ZodNullable<z.ZodUnknown>>;
548
+ }, z.core.$loose>;
549
+ export type AlertHistoryEntry = z.infer<typeof AlertHistoryEntrySchema>;
477
550
  export declare const OrderMarginSchema: z.ZodObject<{
478
551
  type: z.ZodOptional<z.ZodString>;
479
552
  tradingsymbol: z.ZodOptional<z.ZodString>;
@@ -330,6 +330,61 @@ export const GttSchema = z.looseObject({
330
330
  });
331
331
  export const GttCreateResultSchema = z.looseObject({ trigger_id: z.number() });
332
332
  // ---------------------------------------------------------------------------
333
+ // Alerts
334
+ // ---------------------------------------------------------------------------
335
+ /**
336
+ * An ATO (Alert-Triggers-Order) alert carries a basket — a full order spec that
337
+ * is placed when the alert fires. Every field is optional and the object is
338
+ * loose: we only *read* baskets on existing alerts, and Kite's basket shape is
339
+ * richer than we display, so anything we don't touch must pass through
340
+ * untouched rather than fail parsing (invariant #5).
341
+ */
342
+ export const AlertBasketItemSchema = z.looseObject({
343
+ type: z.string().optional(),
344
+ tradingsymbol: z.string().optional(),
345
+ exchange: z.string().optional(),
346
+ instrument_token: z.number().optional(),
347
+ weight: z.number().optional(),
348
+ params: z.looseObject({}).optional(),
349
+ });
350
+ export const AlertBasketSchema = z.looseObject({
351
+ name: z.string().optional(),
352
+ type: z.string().optional(),
353
+ tags: z.array(z.unknown()).optional(),
354
+ items: z.array(AlertBasketItemSchema).default([]),
355
+ });
356
+ export const AlertSchema = z.looseObject({
357
+ uuid: z.string(),
358
+ user_id: z.string().optional(),
359
+ type: z.string(),
360
+ name: z.string().optional(),
361
+ status: z.string(),
362
+ disabled_reason: z.string().optional(),
363
+ lhs_attribute: z.string().optional(),
364
+ lhs_exchange: z.string().optional(),
365
+ lhs_tradingsymbol: z.string().optional(),
366
+ operator: z.string().optional(),
367
+ rhs_type: z.string().optional(),
368
+ rhs_attribute: z.string().optional(),
369
+ rhs_exchange: z.string().optional(),
370
+ rhs_tradingsymbol: z.string().optional(),
371
+ rhs_constant: z.number().optional(),
372
+ alert_count: z.number().optional(),
373
+ basket: AlertBasketSchema.optional(),
374
+ created_at: z.string().optional(),
375
+ updated_at: z.string().optional(),
376
+ });
377
+ /** One entry in an alert's trigger history. `meta`/`order_meta` are large and
378
+ * only shown verbatim in `--json`, so they pass through unvalidated. */
379
+ export const AlertHistoryEntrySchema = z.looseObject({
380
+ uuid: z.string().optional(),
381
+ type: z.string().optional(),
382
+ condition: z.string().optional(),
383
+ created_at: z.string().optional(),
384
+ meta: z.unknown().optional(),
385
+ order_meta: z.unknown().nullish(),
386
+ });
387
+ // ---------------------------------------------------------------------------
333
388
  // Margins / charges calculator
334
389
  // ---------------------------------------------------------------------------
335
390
  export const OrderMarginSchema = z.looseObject({
@@ -13,15 +13,16 @@ export declare const SessionMetaSchema: z.ZodObject<{
13
13
  broker: z.ZodOptional<z.ZodString>;
14
14
  env: z.ZodString;
15
15
  apiKey: z.ZodString;
16
+ profile: z.ZodOptional<z.ZodString>;
16
17
  expiresAt: z.ZodString;
17
18
  loginTime: z.ZodOptional<z.ZodString>;
18
19
  exchanges: z.ZodDefault<z.ZodArray<z.ZodString>>;
19
20
  products: z.ZodDefault<z.ZodArray<z.ZodString>>;
20
21
  }, z.core.$strip>;
21
22
  export type SessionMeta = z.infer<typeof SessionMetaSchema>;
22
- export declare function loadSessionMeta(): Promise<SessionMeta | null>;
23
+ export declare function loadSessionMeta(profile?: string): Promise<SessionMeta | null>;
23
24
  export declare function saveSessionMeta(meta: SessionMeta): Promise<void>;
24
- export declare function clearSessionMeta(): Promise<void>;
25
+ export declare function clearSessionMeta(profile?: string): Promise<void>;
25
26
  /**
26
27
  * Kite access tokens expire at 06:00 IST the following day — a regulatory
27
28
  * requirement, not a configurable session length.
@@ -15,16 +15,18 @@ export const SessionMetaSchema = z.object({
15
15
  broker: z.string().optional(),
16
16
  env: z.string(),
17
17
  apiKey: z.string(),
18
+ /** The profile this session belongs to. Optional so pre-profile files parse. */
19
+ profile: z.string().optional(),
18
20
  /** ISO 8601. Kite invalidates all tokens at 06:00 IST daily. */
19
21
  expiresAt: z.string(),
20
22
  loginTime: z.string().optional(),
21
23
  exchanges: z.array(z.string()).default([]),
22
24
  products: z.array(z.string()).default([]),
23
25
  });
24
- export async function loadSessionMeta() {
26
+ export async function loadSessionMeta(profile = 'default') {
25
27
  let raw;
26
28
  try {
27
- raw = await readFile(sessionFile(), 'utf8');
29
+ raw = await readFile(sessionFile(profile), 'utf8');
28
30
  }
29
31
  catch (err) {
30
32
  if (err.code === 'ENOENT')
@@ -41,7 +43,7 @@ export async function loadSessionMeta() {
41
43
  }
42
44
  export async function saveSessionMeta(meta) {
43
45
  await ensurePrivateDir(configDir());
44
- const path = sessionFile();
46
+ const path = sessionFile(meta.profile ?? 'default');
45
47
  await writeFile(path, `${JSON.stringify(meta, null, 2)}\n`, {
46
48
  mode: 0o600,
47
49
  encoding: 'utf8',
@@ -50,9 +52,9 @@ export async function saveSessionMeta(meta) {
50
52
  await chmod(path, 0o600);
51
53
  }
52
54
  }
53
- export async function clearSessionMeta() {
55
+ export async function clearSessionMeta(profile = 'default') {
54
56
  try {
55
- await unlink(sessionFile());
57
+ await unlink(sessionFile(profile));
56
58
  }
57
59
  catch (err) {
58
60
  if (err.code !== 'ENOENT')
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { type ClientOptions, KiteClient, setDispatcher, } from './core/client.js
11
11
  export { type Endpoints, type Environment, endpointsFor, SANDBOX_CREDENTIALS, } from './core/config.js';
12
12
  export { AuthRequiredError, ExitCode, KiteApiError, KiteCliError, type KiteErrorType, NetworkError, UsageError, } from './core/errors.js';
13
13
  export { InstrumentStore, parseInstrumentKey, parseInstrumentsCsv, } from './core/instruments.js';
14
+ export { DEFAULT_PROFILE, getProfile, listProfileNames, type ResolvedProfile, resolveProfile, resolveTradingConfig, SANDBOX_PROFILE, storagePrefixFor, } from './core/profiles.js';
14
15
  export { ORDER_LIMITS, type RateCategory, RateLimiter, } from './core/ratelimit.js';
15
16
  export { maskSecret, redact, redactString, redactUrl, registerSecret, } from './core/redact.js';
16
17
  export * from './core/schemas.js';
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export { KiteClient, setDispatcher, } from './core/client.js';
11
11
  export { endpointsFor, SANDBOX_CREDENTIALS, } from './core/config.js';
12
12
  export { AuthRequiredError, ExitCode, KiteApiError, KiteCliError, NetworkError, UsageError, } from './core/errors.js';
13
13
  export { InstrumentStore, parseInstrumentKey, parseInstrumentsCsv, } from './core/instruments.js';
14
+ export { DEFAULT_PROFILE, getProfile, listProfileNames, resolveProfile, resolveTradingConfig, SANDBOX_PROFILE, storagePrefixFor, } from './core/profiles.js';
14
15
  export { ORDER_LIMITS, RateLimiter, } from './core/ratelimit.js';
15
16
  export { maskSecret, redact, redactString, redactUrl, registerSecret, } from './core/redact.js';
16
17
  export * from './core/schemas.js';
package/dist/run.js CHANGED
@@ -43,6 +43,7 @@ export async function run(opts = {}) {
43
43
  .option('--quiet', 'Suppress informational messages')
44
44
  .option('--debug', 'Print redacted request diagnostics to stderr')
45
45
  .option('--env <env>', 'Environment: production or sandbox')
46
+ .option('--profile <name>', 'Account profile to use (see `kite profiles`)')
46
47
  .option('-y, --yes', 'Skip confirmation prompts (use with care)')
47
48
  .option('--dry-run', 'Show what would happen without sending anything to Kite')
48
49
  .showHelpAfterError('(run `kite --help` for usage)')
@@ -70,10 +71,12 @@ export async function run(opts = {}) {
70
71
  // Registered lazily so a bare `kite quote` never pays to import the
71
72
  // dashboard renderer, the ticker, or the prompt library.
72
73
  const { authCommands } = await import('./commands/auth.js');
74
+ const { profileCommands } = await import('./commands/profiles.js');
73
75
  const { portfolioCommands } = await import('./commands/portfolio.js');
74
76
  const { marketCommands } = await import('./commands/market.js');
75
77
  const { orderCommands } = await import('./commands/orders.js');
76
78
  const { gttCommands } = await import('./commands/gtt.js');
79
+ const { alertCommands } = await import('./commands/alerts.js');
77
80
  const { watchCommands } = await import('./commands/watch.js');
78
81
  const { configCommands } = await import('./commands/config.js');
79
82
  // commandsGroup applies to every command registered after it, so the group
@@ -81,6 +84,7 @@ export async function run(opts = {}) {
81
84
  // difference between a readable --help and a wall of text.
82
85
  program.commandsGroup('Account:');
83
86
  authCommands(program, withContext);
87
+ profileCommands(program, withContext);
84
88
  program.commandsGroup('Portfolio:');
85
89
  portfolioCommands(program, withContext);
86
90
  program.commandsGroup('Market data:');
@@ -88,6 +92,7 @@ export async function run(opts = {}) {
88
92
  program.commandsGroup('Trading:');
89
93
  orderCommands(program, withContext);
90
94
  gttCommands(program, withContext);
95
+ alertCommands(program, withContext);
91
96
  program.commandsGroup('Streaming:');
92
97
  watchCommands(program, withContext);
93
98
  program.commandsGroup('Settings:');
@@ -102,6 +107,7 @@ Examples:
102
107
  $ kite orders place NSE:INFY -s BUY -q 1 --dry-run
103
108
  Preview an order without sending it
104
109
  $ kite positions --json | jq '.net[].pnl' Machine-readable output
110
+ $ kite --profile huf holdings Run against another account (kite profiles)
105
111
 
106
112
  Safety:
107
113
  Order commands preview the resolved order and ask for confirmation.
package/dist/safety.js CHANGED
@@ -106,10 +106,14 @@ function renderPreview(ctx, opts, force = false) {
106
106
  const { io } = ctx;
107
107
  if ((io.json || io.quiet) && !force)
108
108
  return;
109
- const width = Math.max(...opts.details.map((d) => d.label.length));
109
+ const width = Math.max('Account'.length, ...opts.details.map((d) => d.label.length));
110
110
  const emit = force ? (text) => io.forceNote(text) : (text) => io.note(text);
111
111
  emit('');
112
112
  emit(io.bold(opts.action));
113
+ // Which account this will hit, first and by verified identity — not just the
114
+ // profile label a user chose, which can point at the wrong app. This is the
115
+ // primary guard against placing an order on the wrong account.
116
+ emit(` ${io.dim('Account'.padEnd(width))} ${accountLine(ctx)}`);
113
117
  for (const detail of opts.details) {
114
118
  emit(` ${io.dim(detail.label.padEnd(width))} ${detail.value}`);
115
119
  }
@@ -117,6 +121,13 @@ function renderPreview(ctx, opts, force = false) {
117
121
  emit(` ${io.cyan('sandbox — no real money involved')}`);
118
122
  }
119
123
  }
124
+ /** The verified identity behind this invocation, with the profile for context. */
125
+ function accountLine(ctx) {
126
+ const { io, session, profile } = ctx;
127
+ const who = session ? `${session.userName ?? session.userId} (${session.userId})` : io.dim('token from environment');
128
+ const suffix = profile.name === 'default' ? '' : ` · profile ${io.bold(profile.name)}`;
129
+ return `${who}${suffix}`;
130
+ }
120
131
  /**
121
132
  * Generate a unique order tag.
122
133
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pungoyal/kite-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.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",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsc -p tsconfig.build.json",
47
- "dev": "node --experimental-strip-types src/cli.ts",
47
+ "dev": "tsx src/cli.ts",
48
48
  "test": "vitest run",
49
49
  "test:watch": "vitest",
50
50
  "typecheck": "tsc --noEmit",
@@ -79,7 +79,8 @@
79
79
  "msw": "^2.15.0",
80
80
  "publint": "^0.3.0",
81
81
  "strip-ansi": "^7.1.0",
82
- "typescript": "^5.8.0",
82
+ "tsx": "^4.23.1",
83
+ "typescript": "^7.0.2",
83
84
  "vitest": "^4.1.10"
84
85
  }
85
86
  }