@ai-digital-factory/lmt-cli 0.1.3 → 0.1.5

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.
Files changed (3) hide show
  1. package/README.md +85 -0
  2. package/dist/index.js +443 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,6 +30,48 @@ lmt candle-export NSE:IDEA --date 2026-10-23 --env dev --profile runner --timeou
30
30
 
31
31
  Omitting `--date` asks Broker for the exchange's latest completed trading session, accounting for holidays and Finalization Grace. The stdout JSON `candleExport` contains the immutable Hosted URL, filename, resolved identity/date, source availability, reference-fetch metadata, and comparison counts. Missing data or Zerodha authentication is explained in the workbook. Export does not start Zerodha login or token recovery. Local/dev only; see `lmt --help` for exit conventions.
32
32
 
33
+ ## Price Cross pairs for Runner
34
+
35
+ Price Cross runs in Paper mode only. These commands reach the owning Broker through Gate with the saved Operator Session. They never save Settings, so the Settings Version does not change. They never place Orders, and the CLI has no strategy store or Kite client of its own.
36
+
37
+ ```sh
38
+ lmt pair list --env dev --profile runner
39
+ lmt pair assign --ce 1 --pe 3 --lots 2 --env dev --profile runner
40
+ lmt pair report PAIR_ID --output ./pair.xlsx --env dev --profile runner
41
+ lmt pair report PAIR_ID --publish --env dev --profile runner
42
+ lmt pair unpair PAIR_ID --confirm --env dev --profile runner
43
+ ```
44
+
45
+ | Command | Success JSON (one line on stdout) |
46
+ |---|---|
47
+ | `pair list` | `pairs.active` and `pairs.past`. Each pair has its globally unique `pairId`, `ceRow`/`peRow`, frozen `ceInstrument`/`peInstrument`, `pairLots`, `frozenLotSize`, `quantity`, `assignedAt`, `unpairedAt` (past pairs only) and `pnl` (exact per-leg, pair and daily Decision P&L). Ended pairs stay listed after their rows are reused. |
48
+ | `pair assign` | `reason: "pair_assigned"` and `pair`, the Broker's summary with its identity, roles, frozen lot size and quantity. |
49
+ | `pair unpair --confirm` | `reason: "unpaired"` with `unpair.requestedAt`. When a paper position was closed, `unpair.exit` holds the exit decision, including `requestedAt` and `fills[].priceMinute`. When the pair was flat there is no `exit`. A repeat gives `reason: "already_unpaired"`. |
50
+ | `pair report` | Downloads the lifetime workbook unchanged. `report.path`, `report.bytes`, `report.sha256` and `report.decisionCount` describe it. Without `--output`, the file is saved under the server filename in the current directory and an existing file is never overwritten. |
51
+ | `pair report --publish` | `report.url` is a new immutable Hosted URL. It matches the `exportUrl` field on the Broker's `price_cross_report_snapshot_published` log. Publishing again gives a new URL and leaves the old one unchanged. |
52
+
53
+ Exit codes: 0 success; 1 refusal or uncertain; 2 invalid invocation (including `--env prod`); 3 Operator Login required.
54
+
55
+ **Refusals.** A refusal is `outcome: "fail"` with exit 1, and `reason` and `error.code` both hold the Broker's code, unchanged. The codes are `invalid_lots` (Pair Lots outside 1 through the configured maximum, default 10), `same_type`, `already_paired`, `order_in_flight`, `real_position_open`, `row_unavailable`, `row_empty`, the `*_mismatch` codes, `flag_disabled`, `store_unavailable` and, for reports, `pair_not_found`. Each assign refusal matches the `fields_reason` on its `pair_refused` log. `pair unpair` without `--confirm` is refused locally with exit 2 and sends nothing.
56
+
57
+ **Uncertain outcome.** If `pair assign`, `pair unpair` or `pair report --publish` is submitted but no answer arrives before `--timeout` (or Gate or Broker fails with a 5xx other than `store_unavailable`), the result is `outcome: "uncertain"`, `reason: "submission_uncertain"`, exit 1. The `recovery` field says to read `pair list`. The CLI never repeats the submission. Run `lmt pair list` to see whether the change happened before submitting again.
58
+
59
+ ### Dev-safe QA sequence
60
+
61
+ Run this outside 09:15–15:30 IST, using a same-expiry CE row and PE row already applied on dev (rows 1 and 3 here).
62
+
63
+ 1. `lmt settings read …` and note `settingsVersion`.
64
+ 2. `lmt pair assign --ce 1 --pe 3 --lots 2 …`: exit 0. Record `pair.pairId`, `frozenLotSize` and `quantity`.
65
+ 3. Check the refusals: `--lots 0` and `--lots 11` give `invalid_lots`, re-running step 2 gives `already_paired`, and `--ce 1 --pe 1` gives `same_type`. Each exits 1.
66
+ 4. `lmt pair list …`: the pair is in `pairs.active`.
67
+ 5. `lmt pair report PAIR_ID --output ./pair.xlsx …`, then `lmt pair report PAIR_ID --publish …` twice. The two URLs differ, and each matches a `price_cross_report_snapshot_published` row.
68
+ 6. `lmt pair unpair PAIR_ID …` without `--confirm`: exit 2, and no `pair_unpaired` row is logged.
69
+ 7. `lmt pair unpair PAIR_ID --confirm …`: `unpaired`, with exit details or a flat removal. Repeat it: `already_unpaired`, and there is still only one `pair_unpaired` row.
70
+ 8. Uncertain: `lmt pair unpair PAIR_ID --confirm --timeout 1ms …` on a fresh pair either fails before submission or gives `uncertain` with exit 1. In both cases run `lmt pair list` to reconcile. The CI test `a submitted assign or unpair that outlives the timeout is uncertain, nonzero and never repeated` proves that exactly one submission is made.
71
+ 9. `lmt pair list …`: the pair is in `pairs.past`. `lmt settings read …` shows the same `settingsVersion` as step 1.
72
+
73
+ Automated coverage: `pnpm --filter @ai-digital-factory/lmt-cli test` runs `src/pair-commands.test.ts` (stubbed Gate) and `integration/price-cross-pairs.test.ts`. The integration test uses real Gate and Broker, a temporary Price Cross Store and a recording order client that must see zero calls. It also covers flag-off: existing pairs are listed and can be Unpaired, and a new assign is refused with `flag_disabled`.
74
+
33
75
  ## Quick start: interactive dev login
34
76
 
35
77
  1. Obtain your Operator Login username and password. For headless login and Operator Session validation, also obtain an SMS read credential authorized to read `live-market-terminal-operator-directory` in the selected Gate scope from your administrator.
@@ -205,3 +247,46 @@ node .github/scripts/test-lmt-cli-package.mjs "$tarball"
205
247
  `lmt validate zerodha-session --env local --profile NAME` starts a new Broker-owned attempt and returns an attributed blocked report when a human OTP is needed. Resume with the same profile and explicit `--attempt ID`; add `--otp-stdin` to submit the protected human code. Without that flag, the supplied attempt is only observed. A pass requires authoritative attempt state followed by a hard session check; status alone does not pass the scenario. The resulting session stays available on the caller profile.
206
248
 
207
249
  Interactive **Validate Zerodha Session** runs the same scenario with masked OTP input and a visible report. **Resume Zerodha Validation** asks for an explicit attempt ID. Fresh validation can replace the Operator's active Broker attempt and successful login can refresh its shared token. CLI profile isolation does not isolate that shared state. See [local/dev validation smoke steps and recorded prerequisites](ZERODHA-SMOKE.md).
250
+
251
+ ## Settings and token commands for Runner
252
+
253
+ Read ordinary Settings and their durable Settings Version with:
254
+
255
+ ```sh
256
+ lmt settings read --env dev --profile runner
257
+ ```
258
+
259
+ Save one intent by piping a single JSON object to protected stdin. Supply the version
260
+ you reviewed and only the settings you intend to change. For example:
261
+
262
+ ```sh
263
+ printf '%s\n' '{"expectedSettingsVersion":7,"entryCategories":[]}' |
264
+ lmt settings save --stdin --env dev --profile runner
265
+ ```
266
+
267
+ An empty list disables all Entry Categories; omitted fields keep their saved values.
268
+ Replacing credentials requires all four fields (userId, password, apiKey, apiSecret)
269
+ inside a credentials object supplied through protected stdin. Keep secrets in an
270
+ owner-readable input file or a protected pipe, never in command arguments or shell history.
271
+ One changed save advances one Settings Version; a current unchanged save returns
272
+ unchanged without a write. Every ordinary API write, including standalone credential
273
+ saves, requires expectedSettingsVersion. Stale saves return settings_conflict; read
274
+ Settings again and review the intended changes before submitting. A transport timeout
275
+ after submission or an unconfirmed Broker save returns uncertain with exit code 1.
276
+ Commands never retry a save. An ambiguous SMS write blocks authoritative Settings
277
+ reads and further Operator-record writes until Broker restarts and reloads its durable
278
+ record. The existing Market Watch resize recovery limitation still applies across stores.
279
+
280
+ Access-token operations use a separate version and endpoint:
281
+
282
+ ```sh
283
+ lmt token read --env dev --profile runner
284
+ lmt token replace --stdin --env dev --profile runner < /path/to/protected-token-request.json
285
+ ```
286
+
287
+ The token request contains expected (the metadata returned by token read), optionally
288
+ accessToken for a new value, verifyCurrent: true to validate the saved token without
289
+ reading its value, and continueReplacement: true after reviewing an active-trading
290
+ confirmation. A same-token submission verifies the submitter without reinstalling the token. Ordinary
291
+ Settings Save never replaces access tokens. Token values and saved credentials do not
292
+ appear in command results.
package/dist/index.js CHANGED
@@ -14,16 +14,62 @@ var __export = (target, all) => {
14
14
  __defProp(target, name, { get: all[name], enumerable: true });
15
15
  };
16
16
 
17
+ // ../../packages/contracts/src/price-cross.ts
18
+ var init_price_cross = __esm({
19
+ "../../packages/contracts/src/price-cross.ts"() {
20
+ "use strict";
21
+ }
22
+ });
23
+
24
+ // ../../packages/contracts/src/price-cross-time.ts
25
+ var IST_OFFSET_MS;
26
+ var init_price_cross_time = __esm({
27
+ "../../packages/contracts/src/price-cross-time.ts"() {
28
+ "use strict";
29
+ IST_OFFSET_MS = 330 * 6e4;
30
+ }
31
+ });
32
+
33
+ // ../../packages/contracts/src/price-cross-live.ts
34
+ var init_price_cross_live = __esm({
35
+ "../../packages/contracts/src/price-cross-live.ts"() {
36
+ "use strict";
37
+ init_price_cross_time();
38
+ }
39
+ });
40
+
17
41
  // ../../packages/contracts/src/index.ts
18
- var OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS, OPERATOR_SESSION_EXPIRY_WARNING_MS, DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS, ZERODHA_SESSION_ADMISSION_HEADER, ZERODHA_SESSION_ADMISSION_VERIFY, API_PATHS;
42
+ function isOperatorMarketProtection(value) {
43
+ return typeof value === "number" && Number.isInteger(value) && value >= 2 && value <= 90;
44
+ }
45
+ function validateEntryCategories(value) {
46
+ if (!Array.isArray(value) || value.some((category) => typeof category !== "string" || !ENTRY_CATEGORIES.includes(category)) || new Set(value).size !== value.length) {
47
+ throw new Error("entryCategories must be a list of supported Entry Categories with no duplicates.");
48
+ }
49
+ return [...value];
50
+ }
51
+ var OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS, OPERATOR_SESSION_EXPIRY_WARNING_MS, DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS, ZERODHA_SESSION_ADMISSION_HEADER, ZERODHA_SESSION_ADMISSION_VERIFY, ENTRY_CATEGORIES, API_PATHS;
19
52
  var init_src = __esm({
20
53
  "../../packages/contracts/src/index.ts"() {
21
54
  "use strict";
55
+ init_price_cross();
56
+ init_price_cross_live();
57
+ init_price_cross_time();
22
58
  OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS = 24 * 60 * 60;
23
59
  OPERATOR_SESSION_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
24
60
  DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS = 7 * 24 * 60 * 60;
25
61
  ZERODHA_SESSION_ADMISSION_HEADER = "x-lmt-session-admission";
26
62
  ZERODHA_SESSION_ADMISSION_VERIFY = "verify";
63
+ ENTRY_CATEGORIES = [
64
+ "NSE EQ",
65
+ "BSE EQ",
66
+ "NFO FUT",
67
+ "NFO CE",
68
+ "NFO PE",
69
+ "BFO FUT",
70
+ "BFO CE",
71
+ "BFO PE"
72
+ ];
27
73
  API_PATHS = {
28
74
  health: "/api/health",
29
75
  unlockOperatorSession: "/api/operator-session/unlock",
@@ -37,6 +83,9 @@ var init_src = __esm({
37
83
  operatorLoginEvents: "/api/operators/login/events",
38
84
  operatorLoginOtp: "/api/operators/login/otp",
39
85
  cancelOperatorLogin: "/api/operators/login/cancel",
86
+ zerodhaAccessToken: "/api/zerodha/access-token",
87
+ zerodhaAccessTokenRecovery: "/api/zerodha/access-token/recovery",
88
+ zerodhaAccessTokenPublication: "/api/zerodha/access-token/publication",
40
89
  zerodhaCredentials: "/api/zerodha/credentials",
41
90
  entryCategorySettings: "/api/settings/entry-categories",
42
91
  operatorSettings: "/api/settings",
@@ -60,7 +109,13 @@ var init_src = __esm({
60
109
  placeMarketWatchOrder: "/api/market-watch/orders/place",
61
110
  retryMarketWatchPlace: "/api/market-watch/orders/place-retry",
62
111
  exitMarketWatchOrder: "/api/market-watch/orders/exit",
63
- cancelMarketWatchOrder: "/api/market-watch/orders/cancel"
112
+ cancelMarketWatchOrder: "/api/market-watch/orders/cancel",
113
+ assignPriceCrossPair: "/api/price-cross/pairs/assign",
114
+ unpairPriceCrossPair: "/api/price-cross/pairs/unpair",
115
+ priceCrossReport: "/api/price-cross/report",
116
+ priceCrossReportShare: "/api/price-cross/report/share",
117
+ priceCrossPairs: "/api/price-cross/pairs",
118
+ priceCrossHistory: "/api/price-cross/history"
64
119
  };
65
120
  }
66
121
  });
@@ -226,17 +281,17 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
226
281
  const timeout = options.timeoutMs && options.timeoutMs > 0 ? AbortSignal.timeout(options.timeoutMs) : void 0;
227
282
  return combineSignals2(options.signal, actionDeadlineSignal, explicitSignal, timeout);
228
283
  }
229
- async function rejectAbsentSession(response, path4) {
230
- if (response.status !== 401 || path4 === API_PATHS.unlockOperatorSession || path4 === API_PATHS.logoutUnauthenticatedOperator) return;
284
+ async function rejectAbsentSession(response, path5) {
285
+ if (response.status !== 401 || path5 === API_PATHS.unlockOperatorSession || path5 === API_PATHS.logoutUnauthenticatedOperator) return;
231
286
  const body = await readJson(response.clone());
232
287
  if (body && typeof body === "object" && "error" in body && body.error === "operator_session_absent") {
233
288
  delete cookies.operatorSessionCookie;
234
289
  throw new OperatorSessionRejectedError(rejectionReason(body));
235
290
  }
236
291
  }
237
- async function postJson(path4, body, explicitSignal, secrets = []) {
292
+ async function postJson(path5, body, explicitSignal, secrets = []) {
238
293
  const signal = actionSignal(explicitSignal);
239
- const url = `${origin}${path4}`;
294
+ const url = `${origin}${path5}`;
240
295
  const response = await requestOrThrow(
241
296
  () => fetchImpl(url, {
242
297
  method: "POST",
@@ -250,12 +305,12 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
250
305
  [...sessionSecrets(), ...otpValues, ...secrets]
251
306
  );
252
307
  rememberCookies(response);
253
- await rejectAbsentSession(response, path4);
308
+ await rejectAbsentSession(response, path5);
254
309
  return response;
255
310
  }
256
- async function getJson(path4, explicitSignal, extraHeaders) {
311
+ async function getJson(path5, explicitSignal, extraHeaders) {
257
312
  const signal = actionSignal(explicitSignal);
258
- const url = `${origin}${path4}`;
313
+ const url = `${origin}${path5}`;
259
314
  const response = await requestOrThrow(
260
315
  () => fetchImpl(url, {
261
316
  method: "GET",
@@ -269,10 +324,130 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
269
324
  [...sessionSecrets(), ...otpValues]
270
325
  );
271
326
  rememberCookies(response);
272
- await rejectAbsentSession(response, path4);
327
+ await rejectAbsentSession(response, path5);
328
+ return response;
329
+ }
330
+ async function settingsWrite(path5, body) {
331
+ let response;
332
+ try {
333
+ response = await fetchImpl(origin + path5, {
334
+ method: "PUT",
335
+ headers: headers({ "content-type": "application/json", accept: "application/json" }),
336
+ body: JSON.stringify(body),
337
+ signal: actionSignal()
338
+ });
339
+ } catch {
340
+ throw new SettingsActionError("submission_uncertain", true);
341
+ }
342
+ rememberCookies(response);
343
+ await rejectAbsentSession(response, path5);
344
+ if (!response.ok) {
345
+ const result = await readJson(response).catch(() => void 0);
346
+ const codes = [
347
+ "settings_conflict",
348
+ "settings_version_required",
349
+ "invalid_settings",
350
+ "trade_cycle_active",
351
+ "settings_save_failed",
352
+ "token_conflict",
353
+ "token_rejected",
354
+ "replacement_confirmation_required",
355
+ "token_write_pending",
356
+ "invalid_request",
357
+ "zerodha_session_absent",
358
+ "validation_failed",
359
+ "credentials_changed",
360
+ "replacement_blocked",
361
+ "token_changed",
362
+ "invalid_token_request",
363
+ "token_write_unresolved",
364
+ "active_trading_confirmation",
365
+ "validation_unavailable",
366
+ "unresolved_order",
367
+ "sharing_token_unavailable",
368
+ "token_recovery_unavailable",
369
+ "settings_recovery_required"
370
+ ];
371
+ const code = typeof result?.error === "string" && codes.includes(result.error) ? result.error : "operation_rejected";
372
+ const serverMessage = typeof result?.message === "string" && result.message.trim() ? result.message.trim() : void 0;
373
+ const relayMessage = serverMessage && (code === "token_rejected" || code === "validation_unavailable" || code === "invalid_token_request") ? serverMessage : void 0;
374
+ throw new SettingsActionError(code, code === "settings_save_failed" || response.status >= 500, relayMessage);
375
+ }
376
+ return response;
377
+ }
378
+ async function priceCrossMutation(path5, body, accept) {
379
+ let response;
380
+ try {
381
+ response = await fetchImpl(origin + path5, {
382
+ method: "POST",
383
+ headers: headers({ "content-type": "application/json", accept: "application/json" }),
384
+ body: JSON.stringify(body),
385
+ signal: actionSignal()
386
+ });
387
+ } catch {
388
+ throw new PriceCrossActionError("submission_uncertain", true);
389
+ }
390
+ rememberCookies(response);
391
+ await rejectAbsentSession(response, path5);
392
+ const parsed = await readJson(response);
393
+ if (response.ok) {
394
+ if (accept(parsed)) return parsed;
395
+ throw new PriceCrossActionError("submission_uncertain", true);
396
+ }
397
+ const refusal = priceCrossRefusal(parsed, response.status);
398
+ if (response.status >= 500 && refusal.code !== "store_unavailable") throw new PriceCrossActionError("submission_uncertain", true);
399
+ throw refusal;
400
+ }
401
+ function priceCrossRefusal(body, status) {
402
+ const result = body;
403
+ const code = typeof result?.error === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(result.error) ? result.error : `http_${status}`;
404
+ const message = typeof result?.message === "string" && result.message.trim() ? redactSecrets(result.message.trim(), sessionSecrets()) : void 0;
405
+ return new PriceCrossActionError(code, false, message);
406
+ }
407
+ async function priceCrossRead(path5, body) {
408
+ const response = body === void 0 ? await getJson(path5) : await postJson(path5, body);
409
+ if (!response.ok) throw priceCrossRefusal(await readJson(response), response.status);
273
410
  return response;
274
411
  }
412
+ function ordinarySettings(body) {
413
+ if (!body || !Number.isSafeInteger(body.settingsVersion) || !Number.isInteger(body.rowCount) || body.rowCount < 1 || body.rowCount > 20 || !isOperatorMarketProtection(body.marketProtection) || !Array.isArray(body.entryCategories)) {
414
+ throw new SettingsActionError("invalid_response");
415
+ }
416
+ return { settingsVersion: body.settingsVersion, rowCount: body.rowCount, marketProtection: body.marketProtection, entryCategories: validateEntryCategories(body.entryCategories) };
417
+ }
275
418
  const api = {
419
+ async getTokenMetadata() {
420
+ const response = await getJson(API_PATHS.zerodhaCredentials);
421
+ const body = await readJson(response);
422
+ if (!response.ok || !body?.accessTokenMetadata) throw new SettingsActionError("token_metadata_unavailable");
423
+ const { fingerprint, version, updatedAt, writePending } = body.accessTokenMetadata;
424
+ return { fingerprint, version, updatedAt, ...writePending === void 0 ? {} : { writePending } };
425
+ },
426
+ async getSettings() {
427
+ const response = await getJson(API_PATHS.operatorSettings);
428
+ if (!response.ok) throw new SettingsActionError("settings_read_failed");
429
+ return ordinarySettings(await readJson(response));
430
+ },
431
+ async saveSettings(request) {
432
+ const response = await settingsWrite(API_PATHS.operatorSettings, request);
433
+ try {
434
+ const body = await readJson(response);
435
+ if (body.outcome !== "saved" && body.outcome !== "unchanged") throw new Error("Invalid outcome");
436
+ return { outcome: body.outcome, settings: ordinarySettings(body.settings) };
437
+ } catch {
438
+ throw new SettingsActionError("submission_uncertain", true);
439
+ }
440
+ },
441
+ async replaceToken(request) {
442
+ const response = await settingsWrite(API_PATHS.zerodhaAccessToken, request);
443
+ try {
444
+ const body = await readJson(response);
445
+ if (body.outcome !== "verified" && body.outcome !== "replaced") throw new Error("Invalid outcome");
446
+ return { outcome: body.outcome, sharing: body.sharing, metadata: body.metadata, message: "Token operation completed." };
447
+ } catch {
448
+ throw new SettingsActionError("submission_uncertain", true);
449
+ }
450
+ },
276
451
  async exportCandles(request) {
277
452
  const response = await postJson(API_PATHS.candleExportOnDemand, request);
278
453
  const body = await readJson(response);
@@ -281,6 +456,28 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
281
456
  }
282
457
  return body;
283
458
  },
459
+ async listPriceCrossPairs() {
460
+ const body = await readJson(await priceCrossRead(API_PATHS.priceCrossPairs));
461
+ if (!body || !Array.isArray(body.active) || !Array.isArray(body.past)) throw new PriceCrossActionError("invalid_response");
462
+ return { active: body.active, past: body.past };
463
+ },
464
+ assignPriceCrossPair(request) {
465
+ return priceCrossMutation(API_PATHS.assignPriceCrossPair, request, (body) => !!body && typeof body === "object" && "pair" in body && !!body.pair && typeof body.pair.pairId === "string");
466
+ },
467
+ unpairPriceCrossPair(request) {
468
+ return priceCrossMutation(API_PATHS.unpairPriceCrossPair, request, (body) => !!body && typeof body === "object" && "outcome" in body && (body.outcome === "unpaired" || body.outcome === "already_unpaired"));
469
+ },
470
+ async downloadPriceCrossReport(pairId) {
471
+ const response = await priceCrossRead(API_PATHS.priceCrossReport, { pairId });
472
+ const filename = /filename="([^"]+)"/.exec(response.headers.get("content-disposition") ?? "")?.[1];
473
+ const count = Number(response.headers.get("x-price-cross-decision-count"));
474
+ const bytes = new Uint8Array(await response.arrayBuffer());
475
+ if (!filename || bytes.length === 0) throw new PriceCrossActionError("invalid_response");
476
+ return { bytes, filename, decisionCount: response.headers.has("x-price-cross-decision-count") && Number.isSafeInteger(count) ? count : null };
477
+ },
478
+ publishPriceCrossReport(pairId) {
479
+ return priceCrossMutation(API_PATHS.priceCrossReportShare, { pairId }, (body) => !!body && typeof body === "object" && "url" in body && typeof body.url === "string" && "filename" in body && typeof body.filename === "string");
480
+ },
284
481
  setActionSignal(signal) {
285
482
  actionDeadlineSignal = signal;
286
483
  if (!signal) otpValues.clear();
@@ -505,12 +702,34 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
505
702
  };
506
703
  return api;
507
704
  }
508
- var OperatorSessionRejectedError, ZerodhaLoginFailedError, ZerodhaLoginAttemptUnavailableError, OperatorCredentialsRejectedError;
705
+ var SettingsActionError, PRICE_CROSS_UNCERTAIN_MESSAGE, PriceCrossActionError, OperatorSessionRejectedError, ZerodhaLoginFailedError, ZerodhaLoginAttemptUnavailableError, OperatorCredentialsRejectedError;
509
706
  var init_gate_api_client = __esm({
510
707
  "src/gate-api-client.ts"() {
511
708
  "use strict";
512
709
  init_src();
513
710
  init_profile_name();
711
+ SettingsActionError = class extends Error {
712
+ constructor(code, uncertain = false, messageOverride) {
713
+ super(messageOverride ?? (uncertain ? "Submission could not be confirmed. Read current Settings or token metadata before another submission." : code === "settings_conflict" ? "Settings changed elsewhere. Read saved Settings and submit again with its version." : code === "active_trading_confirmation" ? "Another device has active trading. Review it, then explicitly submit continueReplacement: true to continue." : code === "token_changed" ? "The token changed. Read token metadata and review a fresh submission." : code === "token_write_unresolved" ? "A token write is unresolved. Resolve interrupted save in Web before replacing." : code === "unresolved_order" ? "An Order remains unresolved. Reconcile it before replacing the token." : "The Settings or token operation was rejected."));
714
+ this.code = code;
715
+ this.uncertain = uncertain;
716
+ }
717
+ code;
718
+ uncertain;
719
+ };
720
+ PRICE_CROSS_UNCERTAIN_MESSAGE = "Submission could not be confirmed and was not retried. Run pair list before another submission.";
721
+ PriceCrossActionError = class extends Error {
722
+ constructor(code, uncertain = false, message) {
723
+ super(message ?? (uncertain ? PRICE_CROSS_UNCERTAIN_MESSAGE : "The Price Cross request was rejected."));
724
+ this.code = code;
725
+ this.uncertain = uncertain;
726
+ this.name = "PriceCrossActionError";
727
+ if (uncertain) this.recovery = "Read current and past pairs with pair list; the CLI never repeats a submission.";
728
+ }
729
+ code;
730
+ uncertain;
731
+ recovery;
732
+ };
514
733
  OperatorSessionRejectedError = class extends Error {
515
734
  constructor(reason) {
516
735
  super("The saved Operator Session was rejected by Gate.");
@@ -1255,7 +1474,7 @@ var init_app = __esm({
1255
1474
  });
1256
1475
 
1257
1476
  // src/index.ts
1258
- import path3 from "node:path";
1477
+ import path4 from "node:path";
1259
1478
  import { realpathSync } from "node:fs";
1260
1479
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1261
1480
 
@@ -2187,6 +2406,115 @@ async function runZerodhaValidation(options) {
2187
2406
 
2188
2407
  // src/commands.ts
2189
2408
  init_zerodha_actions();
2409
+
2410
+ // src/pair-commands.ts
2411
+ import { createHash } from "node:crypto";
2412
+ import { writeFile } from "node:fs/promises";
2413
+ import path3 from "node:path";
2414
+ var PairInvocationError = class extends Error {
2415
+ code = "invalid_invocation";
2416
+ };
2417
+ var PAIR_USAGE = "Use pair list, pair assign --ce ROW --pe ROW --lots N, pair unpair PAIR_ID --confirm, or pair report PAIR_ID [--publish | --output PATH].";
2418
+ var VALUE_OPTIONS = /* @__PURE__ */ new Set(["--env", "--profile", "--timeout", "--ce", "--pe", "--lots", "--output"]);
2419
+ var FLAG_OPTIONS = /* @__PURE__ */ new Set(["--confirm", "--publish"]);
2420
+ var PAIR_ID = /^[A-Za-z0-9_-]{1,200}$/;
2421
+ function isPairInvocation(argv) {
2422
+ for (let index = 0; index < argv.length; index += 1) {
2423
+ const token = argv[index];
2424
+ if (token === "--env" || token === "--profile" || token === "--timeout") {
2425
+ index += 1;
2426
+ continue;
2427
+ }
2428
+ if (token.startsWith("--")) continue;
2429
+ return token === "pair";
2430
+ }
2431
+ return false;
2432
+ }
2433
+ function parsePairArguments(argv, parseTimeout2, validateProfile) {
2434
+ const options = /* @__PURE__ */ new Map();
2435
+ const flags = /* @__PURE__ */ new Set();
2436
+ const positional = [];
2437
+ for (let index = 0; index < argv.length; index += 1) {
2438
+ const token = argv[index];
2439
+ if (VALUE_OPTIONS.has(token)) {
2440
+ const value = argv[index + 1];
2441
+ if (value === void 0 || value.startsWith("--")) throw new PairInvocationError(`Invalid or incomplete command argument: ${token}`);
2442
+ if (options.has(token)) throw new PairInvocationError(`Argument ${token} may be specified only once.`);
2443
+ options.set(token, value);
2444
+ index += 1;
2445
+ } else if (FLAG_OPTIONS.has(token)) {
2446
+ if (flags.has(token)) throw new PairInvocationError(`Argument ${token} may be specified only once.`);
2447
+ flags.add(token);
2448
+ } else if (token.startsWith("--")) {
2449
+ throw new PairInvocationError(`Unsupported command option: ${token.split("=")[0]}`);
2450
+ } else positional.push(token);
2451
+ }
2452
+ const [, action, pairId, ...extra] = positional;
2453
+ const allowed = {
2454
+ list: [],
2455
+ assign: ["--ce", "--pe", "--lots"],
2456
+ unpair: ["--confirm"],
2457
+ report: ["--publish", "--output"]
2458
+ };
2459
+ if (!action || !(action in allowed) || extra.length > 0 || [...options.keys(), ...flags].some((key) => !["--env", "--profile", "--timeout", ...allowed[action]].includes(key))) {
2460
+ throw new PairInvocationError(PAIR_USAGE);
2461
+ }
2462
+ const environment = options.get("--env"), profile = options.get("--profile");
2463
+ if (!environment || !profile) throw new PairInvocationError("Headless commands require both --env local|dev and --profile NAME.");
2464
+ validateProfile(profile);
2465
+ const target = { environment, profile, ...options.has("--timeout") ? { timeoutMs: parseTimeout2(options.get("--timeout")) } : {} };
2466
+ if (action === "list") {
2467
+ if (pairId !== void 0) throw new PairInvocationError(PAIR_USAGE);
2468
+ return { kind: "pair", action, ...target };
2469
+ }
2470
+ if (action === "assign") {
2471
+ if (pairId !== void 0) throw new PairInvocationError(PAIR_USAGE);
2472
+ const ce = options.get("--ce"), pe = options.get("--pe"), lots = options.get("--lots");
2473
+ if (!ce || !pe || !lots || !/^\d{1,3}$/.test(ce) || !/^\d{1,3}$/.test(pe) || !/^-?\d{1,6}$/.test(lots)) {
2474
+ throw new PairInvocationError("pair assign requires --ce ROW --pe ROW --lots N as whole numbers.");
2475
+ }
2476
+ return { kind: "pair", action, ceRow: Number(ce), peRow: Number(pe), pairLots: Number(lots), ...target };
2477
+ }
2478
+ if (!pairId || !PAIR_ID.test(pairId)) throw new PairInvocationError(`pair ${action} requires a Price Cross pair ID from pair list.`);
2479
+ if (action === "unpair") {
2480
+ if (!flags.has("--confirm")) {
2481
+ throw new PairInvocationError("pair unpair requires --confirm. Unpair closes any paper position at its latest complete close.");
2482
+ }
2483
+ return { kind: "pair", action, pairId, ...target };
2484
+ }
2485
+ const publish = flags.has("--publish");
2486
+ if (publish && options.has("--output")) throw new PairInvocationError("--output downloads the workbook; --publish returns a URL. Choose one.");
2487
+ return { kind: "pair", action: "report", pairId, publish, ...options.has("--output") ? { output: options.get("--output") } : {}, ...target };
2488
+ }
2489
+ async function runPairCommand(command, api) {
2490
+ if (command.action === "list") return { pairs: await api.listPriceCrossPairs() };
2491
+ if (command.action === "assign") {
2492
+ const { pair } = await api.assignPriceCrossPair({ ceRow: command.ceRow, peRow: command.peRow, pairLots: command.pairLots });
2493
+ return { reason: "pair_assigned", pair };
2494
+ }
2495
+ if (command.action === "unpair") {
2496
+ const { pairs: _pairs, ...unpair } = await api.unpairPriceCrossPair({ pairId: command.pairId, confirmExit: true });
2497
+ return { reason: unpair.outcome, unpair: { pairId: command.pairId, ...unpair } };
2498
+ }
2499
+ if (command.publish) {
2500
+ const published = await api.publishPriceCrossReport(command.pairId);
2501
+ return { reason: "report_published", report: { pairId: command.pairId, published: true, url: published.url, filename: published.filename } };
2502
+ }
2503
+ const download = await api.downloadPriceCrossReport(command.pairId);
2504
+ const output = path3.resolve(command.output ?? path3.basename(download.filename));
2505
+ await writeFile(output, download.bytes, { flag: command.output ? "w" : "wx" });
2506
+ return { reason: "report_downloaded", report: {
2507
+ pairId: command.pairId,
2508
+ published: false,
2509
+ filename: download.filename,
2510
+ path: output,
2511
+ bytes: download.bytes.length,
2512
+ sha256: createHash("sha256").update(download.bytes).digest("hex"),
2513
+ decisionCount: download.decisionCount
2514
+ } };
2515
+ }
2516
+
2517
+ // src/commands.ts
2190
2518
  init_gate_api();
2191
2519
 
2192
2520
  // src/validation-scenario.ts
@@ -2489,7 +2817,24 @@ function splitHeadlessArgv(argv) {
2489
2817
  return { positional, options, credentialStdin, otpStdin };
2490
2818
  }
2491
2819
  function parseArguments(argv) {
2492
- const { positional, options, credentialStdin, otpStdin } = splitHeadlessArgv(argv);
2820
+ if (!hasArgumentOtp(argv) && isPairInvocation(argv)) return parsePairArguments(argv, parseTimeout, validateCommandProfile);
2821
+ const parsedInput = splitHeadlessArgv(argv.filter((value) => value !== "--stdin"));
2822
+ const commandVerb = parsedInput.positional[0];
2823
+ if (commandVerb === "settings" || commandVerb === "token") {
2824
+ const stdin = argv.includes("--stdin");
2825
+ if (argv.filter((value) => value === "--stdin").length > 1) throw new InvocationError("--stdin may be supplied only once.");
2826
+ const parsed = parsedInput;
2827
+ const [kind2, action] = parsed.positional;
2828
+ if (parsed.positional.length !== 2 || parsed.credentialStdin || parsed.otpStdin || [...parsed.options.keys()].some((key) => !["--env", "--profile", "--timeout"].includes(key)) || !(kind2 === "settings" && (action === "read" && !stdin || action === "save" && stdin) || kind2 === "token" && (action === "replace" && stdin || action === "read" && !stdin))) {
2829
+ throw new InvocationError("Use settings read, settings save --stdin, or token replace --stdin.");
2830
+ }
2831
+ const environment2 = parsed.options.get("--env"), profile2 = parsed.options.get("--profile");
2832
+ if (!environment2 || !profile2) throw new InvocationError("Headless commands require --env local|dev and --profile NAME.");
2833
+ validateCommandProfile(profile2);
2834
+ return { kind: kind2, action, environment: environment2, profile: profile2, ...parsed.options.has("--timeout") ? { timeoutMs: parseTimeout(parsed.options.get("--timeout")) } : {} };
2835
+ }
2836
+ if (argv.includes("--stdin")) throw new InvocationError("--stdin is only supported for Settings save and token replacement.");
2837
+ const { positional, options, credentialStdin, otpStdin } = parsedInput;
2493
2838
  if (positional[0] === "candle-export") {
2494
2839
  const instrument = positional[1]?.match(/^([A-Za-z][A-Za-z0-9]{0,31}):([^:\s]{1,100})$/);
2495
2840
  const date = options.get("--date");
@@ -2608,7 +2953,7 @@ function durationFor(command) {
2608
2953
  }
2609
2954
  function errorDetails(error, signal) {
2610
2955
  if (signal?.aborted) return { code: "deadline_exceeded", message: "The action exceeded its deadline.", exitCode: 1 };
2611
- if (error instanceof InvocationError || error instanceof ProtectedInputError) {
2956
+ if (error instanceof InvocationError || error instanceof ProtectedInputError || error instanceof PairInvocationError) {
2612
2957
  return { code: error.code, message: error.message, exitCode: 2 };
2613
2958
  }
2614
2959
  if (error instanceof OperatorSessionRejectedError) {
@@ -2668,8 +3013,11 @@ async function readProtectedInput(stdin, signal, label = "SMS credential") {
2668
3013
  }
2669
3014
  async function runHeadlessCli(argv, dependencies = {}) {
2670
3015
  let otp;
3016
+ let securePayload;
2671
3017
  const protect = (stream) => ({ write(chunk) {
2672
- return stream.write(otp ? chunk.replaceAll(otp, "[redacted]") : chunk);
3018
+ let output = otp ? chunk.replaceAll(otp, "[redacted]") : chunk;
3019
+ if (securePayload) output = output.replaceAll(securePayload, "[redacted]");
3020
+ return stream.write(output);
2673
3021
  } });
2674
3022
  const streams = { stdout: protect(dependencies.stdout ?? defaultOut), stderr: protect(dependencies.stderr ?? defaultErr) };
2675
3023
  const started = performance.now();
@@ -2772,6 +3120,36 @@ async function runHeadlessCli(argv, dependencies = {}) {
2772
3120
  ...dependencies.readLoginCredentials ? { readLoginCredentials: dependencies.readLoginCredentials } : {}
2773
3121
  });
2774
3122
  actionApi = await actions.getApi();
3123
+ if (command.kind === "settings" || command.kind === "token") {
3124
+ await actions.status();
3125
+ let payload;
3126
+ if (command.action !== "read") {
3127
+ securePayload = await readProtectedInput(dependencies.stdin ?? process.stdin, signal, "Settings JSON");
3128
+ try {
3129
+ payload = JSON.parse(securePayload);
3130
+ } catch {
3131
+ throw new InvocationError("Protected stdin must contain one JSON object.");
3132
+ }
3133
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new InvocationError("Protected stdin must contain one JSON object.");
3134
+ }
3135
+ const settings = command.kind === "settings" ? command.action === "read" ? { outcome: "read", settings: await actionApi.getSettings() } : await actionApi.saveSettings(payload) : void 0;
3136
+ const tokenMetadata = command.kind === "token" && command.action === "read" ? await actionApi.getTokenMetadata() : void 0;
3137
+ const token = command.kind === "token" && command.action === "replace" ? await actionApi.replaceToken(payload) : void 0;
3138
+ emit({
3139
+ version: 1,
3140
+ command: command.kind + " " + command.action,
3141
+ outcome: "success",
3142
+ reason: settings?.outcome ?? token?.outcome,
3143
+ ...settings ? { settings: settings.settings } : {},
3144
+ ...token ? { token } : {},
3145
+ ...tokenMetadata ? { tokenMetadata } : {},
3146
+ target: { environment: target.environment, origin: target.origin },
3147
+ profile,
3148
+ elapsedMs: Math.round(performance.now() - started),
3149
+ metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
3150
+ }, streams);
3151
+ return 0;
3152
+ }
2775
3153
  if (command.kind === "candle-export") {
2776
3154
  await actions.status();
2777
3155
  const candleExport = await actionApi.exportCandles({
@@ -2792,6 +3170,22 @@ async function runHeadlessCli(argv, dependencies = {}) {
2792
3170
  }, streams);
2793
3171
  return 0;
2794
3172
  }
3173
+ if (command.kind === "pair") {
3174
+ await actions.status();
3175
+ const result2 = await runPairCommand(command, actionApi);
3176
+ emit({
3177
+ version: 1,
3178
+ command: `pair ${command.action}`,
3179
+ outcome: "success",
3180
+ ...result2,
3181
+ target: { environment: target.environment, origin: target.origin },
3182
+ profile,
3183
+ observedOperator: target.operatorId,
3184
+ elapsedMs: Math.round(performance.now() - started),
3185
+ metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
3186
+ }, streams);
3187
+ return 0;
3188
+ }
2795
3189
  if (command.kind === "zerodha") {
2796
3190
  await actions.status();
2797
3191
  if (command.otpStdin) otp = await readProtectedOtp(dependencies.stdin ?? process.stdin, signal);
@@ -2855,17 +3249,18 @@ async function runHeadlessCli(argv, dependencies = {}) {
2855
3249
  if (error instanceof OperatorSessionRejectedError && store && actionApi && target && profile && !signal?.aborted) {
2856
3250
  await store.saveSession(profile, target, actionApi.getCookies()).catch(() => void 0);
2857
3251
  }
2858
- const details = errorDetails(error, signal);
3252
+ const details = error instanceof SettingsActionError || error instanceof PriceCrossActionError ? { code: error.code, message: error.message, exitCode: 1 } : errorDetails(error, signal);
2859
3253
  const recovery = error instanceof Error ? error : void 0;
2860
3254
  const attemptId = recovery?.attemptId ?? (command?.kind === "zerodha" ? command.attemptId : void 0);
2861
3255
  emit({
2862
3256
  version: 1,
2863
- command: command?.kind === "zerodha" ? `zerodha ${command.action}` : command?.kind === "credential-provision" ? "credential provision" : command?.kind === "validate-zerodha" ? "validate zerodha-session" : command?.kind === "validate" ? "validate operator-session" : command?.kind ?? "",
2864
- outcome: details.exitCode === 4 ? "blocked" : "fail",
3257
+ command: command?.kind === "zerodha" || command?.kind === "settings" || command?.kind === "token" || command?.kind === "pair" ? `${command.kind} ${command.action}` : command?.kind === "credential-provision" ? "credential provision" : command?.kind === "validate-zerodha" ? "validate zerodha-session" : command?.kind === "validate" ? "validate operator-session" : command?.kind ?? "",
3258
+ outcome: (error instanceof SettingsActionError || error instanceof PriceCrossActionError) && error.uncertain ? "uncertain" : details.exitCode === 4 ? "blocked" : "fail",
2865
3259
  reason: details.code,
2866
3260
  ...target ? { target: { environment: target.environment, origin: target.origin } } : command ? { target: { environment: command.environment } } : {},
2867
3261
  ...profile ? { profile } : {},
2868
3262
  ...attemptId ? { attemptId, recovery: recovery?.recovery ?? "Observe this exact attempt with the same target and profile; start explicitly only if Gate reports it inactive." } : {},
3263
+ ...!attemptId && error instanceof PriceCrossActionError && error.recovery ? { recovery: error.recovery } : {},
2869
3264
  ...recovery?.observedState ? { observedState: recovery.observedState } : {},
2870
3265
  ...recovery?.observedOperator ? { observedOperator: recovery.observedOperator } : {},
2871
3266
  elapsedMs: Math.round(performance.now() - started),
@@ -2878,6 +3273,7 @@ async function runHeadlessCli(argv, dependencies = {}) {
2878
3273
  return details.exitCode;
2879
3274
  } finally {
2880
3275
  otp = void 0;
3276
+ securePayload = void 0;
2881
3277
  if (deadlineTimer) clearTimeout(deadlineTimer);
2882
3278
  }
2883
3279
  }
@@ -2906,6 +3302,14 @@ Usage (lmt is an alias of live-market-terminal):
2906
3302
  live-market-terminal login --env local|dev --profile NAME [--timeout DURATION]
2907
3303
  live-market-terminal logout --env local|dev --profile NAME [--timeout DURATION]
2908
3304
  live-market-terminal candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD] --env local|dev --profile NAME [--timeout DURATION]
3305
+ live-market-terminal pair list --env local|dev --profile NAME [--timeout DURATION]
3306
+ live-market-terminal pair assign --ce ROW --pe ROW --lots N --env local|dev --profile NAME [--timeout DURATION]
3307
+ live-market-terminal pair unpair PAIR_ID --confirm --env local|dev --profile NAME [--timeout DURATION]
3308
+ live-market-terminal pair report PAIR_ID [--publish | --output PATH] --env local|dev --profile NAME [--timeout DURATION]
3309
+ live-market-terminal settings read --env local|dev --profile NAME
3310
+ live-market-terminal settings save --stdin --env local|dev --profile NAME
3311
+ live-market-terminal token read --env local|dev --profile NAME
3312
+ live-market-terminal token replace --stdin --env local|dev --profile NAME
2909
3313
  live-market-terminal status --env local|dev --profile NAME [--timeout DURATION]
2910
3314
  live-market-terminal zerodha login --env local|dev --profile NAME [--timeout DURATION]
2911
3315
  live-market-terminal zerodha observe --attempt ID --env local|dev --profile NAME [--timeout DURATION]
@@ -2925,6 +3329,21 @@ From the monorepo root:
2925
3329
  Candle Export publishes a new immutable workbook link and returns its context in candleExport JSON.
2926
3330
  Empty or unavailable sources are explained in the workbook. Export never starts Zerodha login or token recovery.
2927
3331
 
3332
+ Price Cross pair commands operate Paper pairs on the owning Broker through Gate; they never save Settings or place Orders.
3333
+ pair list returns current and past pairs (JSON pairs.active / pairs.past). Broker refusals keep their reason codes
3334
+ (invalid_lots, same_type, already_paired, order_in_flight, flag_disabled, ...) with exit 1.
3335
+ pair unpair requires --confirm and returns exit details (unpair.exit) when a paper position closed; a repeat reports already_unpaired.
3336
+ pair report downloads the lifetime workbook (default: server filename in the current directory, never overwritten);
3337
+ --publish returns a new immutable Hosted URL instead.
3338
+ A pair assign, unpair or publish timeout after submission is uncertain (exit 1); read pair list before resubmitting.
3339
+ The CLI never repeats a submission.
3340
+
3341
+ Settings save reads one JSON object from stdin: expectedSettingsVersion plus selected rowCount,
3342
+ marketProtection, entryCategories, or complete credentials. Omitted fields are preserved.
3343
+ Token replace reads {accessToken?, expected, continueReplacement?, verifyCurrent?} from protected stdin.
3344
+ Use verifyCurrent: true with expected from token read to validate the saved token without supplying accessToken.
3345
+ Read token metadata through token read. A timeout after submission is uncertain; never retry blindly.
3346
+
2928
3347
  Targets: local and dev only. Production is not permitted.
2929
3348
 
2930
3349
  Zerodha login is two steps: zerodha login, then zerodha otp --attempt ID --otp-stdin.
@@ -2949,6 +3368,11 @@ Options:
2949
3368
  --env local|dev Target environment (interactive default: local; headless: required)
2950
3369
  --profile NAME Client profile (default: default for interactive)
2951
3370
  --timeout DURATION Action deadline, e.g. 45s, 2m, 30000ms (headless)
3371
+ --ce ROW / --pe ROW Market Watch rows for pair assign (CE leg, PE leg)
3372
+ --lots N Pair Lots for pair assign; the Broker enforces 1 through its configured maximum
3373
+ --confirm Required by pair unpair
3374
+ --publish pair report: publish an immutable snapshot and return its URL
3375
+ --output PATH pair report: write the downloaded workbook to PATH (overwrites)
2952
3376
  --date YYYY-MM-DD Single IST date for candle-export; omitted: latest completed exchange session
2953
3377
  --attempt ID Attempt ID returned by zerodha login (required for observe and otp)
2954
3378
  --otp-stdin Read one six-digit OTP line from piped protected stdin (maximum 8 bytes)
@@ -3100,7 +3524,7 @@ function parseInteractiveArguments(argv) {
3100
3524
  return { environment: values.has("--env") ? environment : "local", profile, credentialProvision: false };
3101
3525
  }
3102
3526
  var invokedFile = process.argv[1];
3103
- if (invokedFile && path3.resolve(fileURLToPath2(import.meta.url)) === realpathSync(invokedFile)) {
3527
+ if (invokedFile && path4.resolve(fileURLToPath2(import.meta.url)) === realpathSync(invokedFile)) {
3104
3528
  ensureFetchIgnoresEnvProxy();
3105
3529
  bootstrapCli().catch((error) => {
3106
3530
  const message = error instanceof Error ? error.message : "CLI failed to start.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-digital-factory/lmt-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "live-market-terminal": "dist/index.js",