@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.
- package/README.md +85 -0
- package/dist/index.js +443 -19
- 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
|
-
|
|
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,
|
|
230
|
-
if (response.status !== 401 ||
|
|
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(
|
|
292
|
+
async function postJson(path5, body, explicitSignal, secrets = []) {
|
|
238
293
|
const signal = actionSignal(explicitSignal);
|
|
239
|
-
const url = `${origin}${
|
|
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,
|
|
308
|
+
await rejectAbsentSession(response, path5);
|
|
254
309
|
return response;
|
|
255
310
|
}
|
|
256
|
-
async function getJson(
|
|
311
|
+
async function getJson(path5, explicitSignal, extraHeaders) {
|
|
257
312
|
const signal = actionSignal(explicitSignal);
|
|
258
|
-
const url = `${origin}${
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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" ?
|
|
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 &&
|
|
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.";
|