@ai-digital-factory/lmt-cli 0.1.4 → 0.1.6

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 +47 -4
  2. package/dist/index.js +289 -31
  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.
@@ -51,7 +93,7 @@ Omitting `--date` asks Broker for the exchange's latest completed trading sessio
51
93
 
52
94
  Press Enter to submit each prompt. At the Operator password prompt, Escape returns to the username; Ctrl+U clears either field. Passwords remain masked. Press Ctrl+C to exit the terminal; use the logout command or menu action when you want to revoke the Operator Session.
53
95
 
54
- Interactive mode defaults to `--env local --profile default` when these options are omitted. Keep the same environment and profile across commands to reuse a session:
96
+ Interactive mode defaults to `--env dev --profile default` when these options are omitted. A bare `live-market-terminal` invocation (no subcommand) also starts interactive mode with those defaults. Keep the same environment and profile across commands to reuse a session:
55
97
 
56
98
  ```sh
57
99
  lmt status --env dev --profile my-dev-session
@@ -242,8 +284,9 @@ lmt token read --env dev --profile runner
242
284
  lmt token replace --stdin --env dev --profile runner < /path/to/protected-token-request.json
243
285
  ```
244
286
 
245
- The token request contains accessToken, expected (the metadata returned by token read),
246
- and optionally continueReplacement: true after reviewing an active-trading confirmation.
247
- A same-token submission verifies the submitter without reinstalling the token. Ordinary
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
248
291
  Settings Save never replaces access tokens. Token values and saved credentials do not
249
292
  appear in command results.
package/dist/index.js CHANGED
@@ -14,6 +14,30 @@ 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
42
  function isOperatorMarketProtection(value) {
19
43
  return typeof value === "number" && Number.isInteger(value) && value >= 2 && value <= 90;
@@ -28,6 +52,9 @@ var OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS, OPERATOR_SESSION_EXPIRY_WARNING
28
52
  var init_src = __esm({
29
53
  "../../packages/contracts/src/index.ts"() {
30
54
  "use strict";
55
+ init_price_cross();
56
+ init_price_cross_live();
57
+ init_price_cross_time();
31
58
  OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS = 24 * 60 * 60;
32
59
  OPERATOR_SESSION_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
33
60
  DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS = 7 * 24 * 60 * 60;
@@ -82,7 +109,13 @@ var init_src = __esm({
82
109
  placeMarketWatchOrder: "/api/market-watch/orders/place",
83
110
  retryMarketWatchPlace: "/api/market-watch/orders/place-retry",
84
111
  exitMarketWatchOrder: "/api/market-watch/orders/exit",
85
- 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"
86
119
  };
87
120
  }
88
121
  });
@@ -248,17 +281,17 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
248
281
  const timeout = options.timeoutMs && options.timeoutMs > 0 ? AbortSignal.timeout(options.timeoutMs) : void 0;
249
282
  return combineSignals2(options.signal, actionDeadlineSignal, explicitSignal, timeout);
250
283
  }
251
- async function rejectAbsentSession(response, path4) {
252
- 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;
253
286
  const body = await readJson(response.clone());
254
287
  if (body && typeof body === "object" && "error" in body && body.error === "operator_session_absent") {
255
288
  delete cookies.operatorSessionCookie;
256
289
  throw new OperatorSessionRejectedError(rejectionReason(body));
257
290
  }
258
291
  }
259
- async function postJson(path4, body, explicitSignal, secrets = []) {
292
+ async function postJson(path5, body, explicitSignal, secrets = []) {
260
293
  const signal = actionSignal(explicitSignal);
261
- const url = `${origin}${path4}`;
294
+ const url = `${origin}${path5}`;
262
295
  const response = await requestOrThrow(
263
296
  () => fetchImpl(url, {
264
297
  method: "POST",
@@ -272,12 +305,12 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
272
305
  [...sessionSecrets(), ...otpValues, ...secrets]
273
306
  );
274
307
  rememberCookies(response);
275
- await rejectAbsentSession(response, path4);
308
+ await rejectAbsentSession(response, path5);
276
309
  return response;
277
310
  }
278
- async function getJson(path4, explicitSignal, extraHeaders) {
311
+ async function getJson(path5, explicitSignal, extraHeaders) {
279
312
  const signal = actionSignal(explicitSignal);
280
- const url = `${origin}${path4}`;
313
+ const url = `${origin}${path5}`;
281
314
  const response = await requestOrThrow(
282
315
  () => fetchImpl(url, {
283
316
  method: "GET",
@@ -291,13 +324,13 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
291
324
  [...sessionSecrets(), ...otpValues]
292
325
  );
293
326
  rememberCookies(response);
294
- await rejectAbsentSession(response, path4);
327
+ await rejectAbsentSession(response, path5);
295
328
  return response;
296
329
  }
297
- async function settingsWrite(path4, body) {
330
+ async function settingsWrite(path5, body) {
298
331
  let response;
299
332
  try {
300
- response = await fetchImpl(origin + path4, {
333
+ response = await fetchImpl(origin + path5, {
301
334
  method: "PUT",
302
335
  headers: headers({ "content-type": "application/json", accept: "application/json" }),
303
336
  body: JSON.stringify(body),
@@ -307,7 +340,7 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
307
340
  throw new SettingsActionError("submission_uncertain", true);
308
341
  }
309
342
  rememberCookies(response);
310
- await rejectAbsentSession(response, path4);
343
+ await rejectAbsentSession(response, path5);
311
344
  if (!response.ok) {
312
345
  const result = await readJson(response).catch(() => void 0);
313
346
  const codes = [
@@ -336,10 +369,46 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
336
369
  "settings_recovery_required"
337
370
  ];
338
371
  const code = typeof result?.error === "string" && codes.includes(result.error) ? result.error : "operation_rejected";
339
- throw new SettingsActionError(code, code === "settings_save_failed" || response.status >= 500);
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);
340
375
  }
341
376
  return response;
342
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);
410
+ return response;
411
+ }
343
412
  function ordinarySettings(body) {
344
413
  if (!body || !Number.isSafeInteger(body.settingsVersion) || !Number.isInteger(body.rowCount) || body.rowCount < 1 || body.rowCount > 20 || !isOperatorMarketProtection(body.marketProtection) || !Array.isArray(body.entryCategories)) {
345
414
  throw new SettingsActionError("invalid_response");
@@ -387,6 +456,28 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
387
456
  }
388
457
  return body;
389
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
+ },
390
481
  setActionSignal(signal) {
391
482
  actionDeadlineSignal = signal;
392
483
  if (!signal) otpValues.clear();
@@ -611,21 +702,34 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
611
702
  };
612
703
  return api;
613
704
  }
614
- var SettingsActionError, OperatorSessionRejectedError, ZerodhaLoginFailedError, ZerodhaLoginAttemptUnavailableError, OperatorCredentialsRejectedError;
705
+ var SettingsActionError, PRICE_CROSS_UNCERTAIN_MESSAGE, PriceCrossActionError, OperatorSessionRejectedError, ZerodhaLoginFailedError, ZerodhaLoginAttemptUnavailableError, OperatorCredentialsRejectedError;
615
706
  var init_gate_api_client = __esm({
616
707
  "src/gate-api-client.ts"() {
617
708
  "use strict";
618
709
  init_src();
619
710
  init_profile_name();
620
711
  SettingsActionError = class extends Error {
621
- constructor(code, uncertain = false) {
622
- super(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.");
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."));
623
714
  this.code = code;
624
715
  this.uncertain = uncertain;
625
716
  }
626
717
  code;
627
718
  uncertain;
628
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
+ };
629
733
  OperatorSessionRejectedError = class extends Error {
630
734
  constructor(reason) {
631
735
  super("The saved Operator Session was rejected by Gate.");
@@ -1370,7 +1474,7 @@ var init_app = __esm({
1370
1474
  });
1371
1475
 
1372
1476
  // src/index.ts
1373
- import path3 from "node:path";
1477
+ import path4 from "node:path";
1374
1478
  import { realpathSync } from "node:fs";
1375
1479
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1376
1480
 
@@ -2302,6 +2406,115 @@ async function runZerodhaValidation(options) {
2302
2406
 
2303
2407
  // src/commands.ts
2304
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
2305
2518
  init_gate_api();
2306
2519
 
2307
2520
  // src/validation-scenario.ts
@@ -2604,6 +2817,7 @@ function splitHeadlessArgv(argv) {
2604
2817
  return { positional, options, credentialStdin, otpStdin };
2605
2818
  }
2606
2819
  function parseArguments(argv) {
2820
+ if (!hasArgumentOtp(argv) && isPairInvocation(argv)) return parsePairArguments(argv, parseTimeout, validateCommandProfile);
2607
2821
  const parsedInput = splitHeadlessArgv(argv.filter((value) => value !== "--stdin"));
2608
2822
  const commandVerb = parsedInput.positional[0];
2609
2823
  if (commandVerb === "settings" || commandVerb === "token") {
@@ -2739,7 +2953,7 @@ function durationFor(command) {
2739
2953
  }
2740
2954
  function errorDetails(error, signal) {
2741
2955
  if (signal?.aborted) return { code: "deadline_exceeded", message: "The action exceeded its deadline.", exitCode: 1 };
2742
- if (error instanceof InvocationError || error instanceof ProtectedInputError) {
2956
+ if (error instanceof InvocationError || error instanceof ProtectedInputError || error instanceof PairInvocationError) {
2743
2957
  return { code: error.code, message: error.message, exitCode: 2 };
2744
2958
  }
2745
2959
  if (error instanceof OperatorSessionRejectedError) {
@@ -2956,6 +3170,22 @@ async function runHeadlessCli(argv, dependencies = {}) {
2956
3170
  }, streams);
2957
3171
  return 0;
2958
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
+ }
2959
3189
  if (command.kind === "zerodha") {
2960
3190
  await actions.status();
2961
3191
  if (command.otpStdin) otp = await readProtectedOtp(dependencies.stdin ?? process.stdin, signal);
@@ -3019,17 +3249,18 @@ async function runHeadlessCli(argv, dependencies = {}) {
3019
3249
  if (error instanceof OperatorSessionRejectedError && store && actionApi && target && profile && !signal?.aborted) {
3020
3250
  await store.saveSession(profile, target, actionApi.getCookies()).catch(() => void 0);
3021
3251
  }
3022
- const details = error instanceof SettingsActionError ? { code: error.code, message: error.message, exitCode: 1 } : errorDetails(error, signal);
3252
+ const details = error instanceof SettingsActionError || error instanceof PriceCrossActionError ? { code: error.code, message: error.message, exitCode: 1 } : errorDetails(error, signal);
3023
3253
  const recovery = error instanceof Error ? error : void 0;
3024
3254
  const attemptId = recovery?.attemptId ?? (command?.kind === "zerodha" ? command.attemptId : void 0);
3025
3255
  emit({
3026
3256
  version: 1,
3027
- command: command?.kind === "zerodha" || command?.kind === "settings" || command?.kind === "token" ? `${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 ?? "",
3028
- outcome: error instanceof SettingsActionError && error.uncertain ? "uncertain" : 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",
3029
3259
  reason: details.code,
3030
3260
  ...target ? { target: { environment: target.environment, origin: target.origin } } : command ? { target: { environment: command.environment } } : {},
3031
3261
  ...profile ? { profile } : {},
3032
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 } : {},
3033
3264
  ...recovery?.observedState ? { observedState: recovery.observedState } : {},
3034
3265
  ...recovery?.observedOperator ? { observedOperator: recovery.observedOperator } : {},
3035
3266
  elapsedMs: Math.round(performance.now() - started),
@@ -3066,11 +3297,16 @@ function formatCliHelp() {
3066
3297
  return `Live Market Terminal CLI
3067
3298
 
3068
3299
  Usage (lmt is an alias of live-market-terminal):
3300
+ live-market-terminal [--env local|dev] [--profile NAME]
3069
3301
  live-market-terminal interactive [--env local|dev] [--profile NAME]
3070
3302
  live-market-terminal interactive credential provision [--env local|dev] [--profile NAME]
3071
3303
  live-market-terminal login --env local|dev --profile NAME [--timeout DURATION]
3072
3304
  live-market-terminal logout --env local|dev --profile NAME [--timeout DURATION]
3073
3305
  live-market-terminal candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD] --env local|dev --profile NAME [--timeout DURATION]
3306
+ live-market-terminal pair list --env local|dev --profile NAME [--timeout DURATION]
3307
+ live-market-terminal pair assign --ce ROW --pe ROW --lots N --env local|dev --profile NAME [--timeout DURATION]
3308
+ live-market-terminal pair unpair PAIR_ID --confirm --env local|dev --profile NAME [--timeout DURATION]
3309
+ live-market-terminal pair report PAIR_ID [--publish | --output PATH] --env local|dev --profile NAME [--timeout DURATION]
3074
3310
  live-market-terminal settings read --env local|dev --profile NAME
3075
3311
  live-market-terminal settings save --stdin --env local|dev --profile NAME
3076
3312
  live-market-terminal token read --env local|dev --profile NAME
@@ -3087,16 +3323,26 @@ Usage (lmt is an alias of live-market-terminal):
3087
3323
  Headless options (--env, --profile, --timeout, --attempt, --otp-stdin, --credential-stdin) may appear before or after the command.
3088
3324
 
3089
3325
  From the monorepo root:
3090
- pnpm cli [--profile NAME] # interactive; --env defaults to local
3326
+ pnpm cli [--profile NAME] # interactive; --env defaults to dev
3091
3327
  pnpm cli --env dev --profile my-dev-session
3092
3328
  pnpm -s cli:headless status --env local --profile default # build step runs quietly (-s)
3093
3329
 
3094
3330
  Candle Export publishes a new immutable workbook link and returns its context in candleExport JSON.
3095
3331
  Empty or unavailable sources are explained in the workbook. Export never starts Zerodha login or token recovery.
3096
3332
 
3333
+ Price Cross pair commands operate Paper pairs on the owning Broker through Gate; they never save Settings or place Orders.
3334
+ pair list returns current and past pairs (JSON pairs.active / pairs.past). Broker refusals keep their reason codes
3335
+ (invalid_lots, same_type, already_paired, order_in_flight, flag_disabled, ...) with exit 1.
3336
+ pair unpair requires --confirm and returns exit details (unpair.exit) when a paper position closed; a repeat reports already_unpaired.
3337
+ pair report downloads the lifetime workbook (default: server filename in the current directory, never overwritten);
3338
+ --publish returns a new immutable Hosted URL instead.
3339
+ A pair assign, unpair or publish timeout after submission is uncertain (exit 1); read pair list before resubmitting.
3340
+ The CLI never repeats a submission.
3341
+
3097
3342
  Settings save reads one JSON object from stdin: expectedSettingsVersion plus selected rowCount,
3098
3343
  marketProtection, entryCategories, or complete credentials. Omitted fields are preserved.
3099
- Token replace reads {accessToken, expected, continueReplacement?} from protected stdin.
3344
+ Token replace reads {accessToken?, expected, continueReplacement?, verifyCurrent?} from protected stdin.
3345
+ Use verifyCurrent: true with expected from token read to validate the saved token without supplying accessToken.
3100
3346
  Read token metadata through token read. A timeout after submission is uncertain; never retry blindly.
3101
3347
 
3102
3348
  Targets: local and dev only. Production is not permitted.
@@ -3120,9 +3366,14 @@ Exit codes (headless):
3120
3366
  4 validation blocked or Zerodha awaiting OTP
3121
3367
 
3122
3368
  Options:
3123
- --env local|dev Target environment (interactive default: local; headless: required)
3369
+ --env local|dev Target environment (interactive default: dev; headless: required)
3124
3370
  --profile NAME Client profile (default: default for interactive)
3125
3371
  --timeout DURATION Action deadline, e.g. 45s, 2m, 30000ms (headless)
3372
+ --ce ROW / --pe ROW Market Watch rows for pair assign (CE leg, PE leg)
3373
+ --lots N Pair Lots for pair assign; the Broker enforces 1 through its configured maximum
3374
+ --confirm Required by pair unpair
3375
+ --publish pair report: publish an immutable snapshot and return its URL
3376
+ --output PATH pair report: write the downloaded workbook to PATH (overwrites)
3126
3377
  --date YYYY-MM-DD Single IST date for candle-export; omitted: latest completed exchange session
3127
3378
  --attempt ID Attempt ID returned by zerodha login (required for observe and otp)
3128
3379
  --otp-stdin Read one six-digit OTP line from piped protected stdin (maximum 8 bytes)
@@ -3139,6 +3390,11 @@ var CliInvocationError = class extends Error {
3139
3390
  function normalizeArgv(argv) {
3140
3391
  return argv[0] === "--" ? argv.slice(1) : argv;
3141
3392
  }
3393
+ function resolveCliInvocation(argv) {
3394
+ if (argv.length === 0) return { kind: "interactive", argv: ["interactive"] };
3395
+ if (argv[0] === "interactive") return { kind: "interactive", argv };
3396
+ return { kind: "headless", argv };
3397
+ }
3142
3398
  async function bootstrapCli(dependencies = {}) {
3143
3399
  if (dependencies.runApplication) {
3144
3400
  await dependencies.runApplication();
@@ -3153,14 +3409,15 @@ async function bootstrapCli(dependencies = {}) {
3153
3409
  process.stdout.write(formatCliHelp());
3154
3410
  return;
3155
3411
  }
3156
- if (argv[0] !== "interactive") {
3157
- process.exitCode = await runHeadlessCli(argv);
3412
+ const invocation = resolveCliInvocation(argv);
3413
+ if (invocation.kind === "headless") {
3414
+ process.exitCode = await runHeadlessCli(invocation.argv);
3158
3415
  return;
3159
3416
  }
3160
3417
  let parsed;
3161
3418
  let target;
3162
3419
  try {
3163
- parsed = parseInteractiveArguments(argv);
3420
+ parsed = parseInteractiveArguments(invocation.argv);
3164
3421
  target = resolveCliTarget({ LMT_ENV: parsed.environment });
3165
3422
  } catch (error) {
3166
3423
  throw new CliInvocationError(error instanceof Error ? error.message : "Invalid interactive CLI configuration.");
@@ -3254,7 +3511,7 @@ function parseInteractiveArguments(argv) {
3254
3511
  if (flag === "--profile") profile2 = value;
3255
3512
  }
3256
3513
  assertClientProfileName(profile2);
3257
- return { environment: values2.has("--env") ? environment2 : "local", profile: profile2, credentialProvision: true };
3514
+ return { environment: values2.has("--env") ? environment2 : "dev", profile: profile2, credentialProvision: true };
3258
3515
  }
3259
3516
  let environment = "";
3260
3517
  let profile = "default";
@@ -3271,10 +3528,10 @@ function parseInteractiveArguments(argv) {
3271
3528
  if (flag === "--profile") profile = value;
3272
3529
  }
3273
3530
  assertClientProfileName(profile);
3274
- return { environment: values.has("--env") ? environment : "local", profile, credentialProvision: false };
3531
+ return { environment: values.has("--env") ? environment : "dev", profile, credentialProvision: false };
3275
3532
  }
3276
3533
  var invokedFile = process.argv[1];
3277
- if (invokedFile && path3.resolve(fileURLToPath2(import.meta.url)) === realpathSync(invokedFile)) {
3534
+ if (invokedFile && path4.resolve(fileURLToPath2(import.meta.url)) === realpathSync(invokedFile)) {
3278
3535
  ensureFetchIgnoresEnvProxy();
3279
3536
  bootstrapCli().catch((error) => {
3280
3537
  const message = error instanceof Error ? error.message : "CLI failed to start.";
@@ -3286,5 +3543,6 @@ if (invokedFile && path3.resolve(fileURLToPath2(import.meta.url)) === realpathSy
3286
3543
  export {
3287
3544
  bootstrapCli,
3288
3545
  normalizeArgv,
3289
- parseInteractiveArguments
3546
+ parseInteractiveArguments,
3547
+ resolveCliInvocation
3290
3548
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-digital-factory/lmt-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "live-market-terminal": "dist/index.js",