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

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 +42 -0
  2. package/dist/index.js +181 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -205,3 +205,45 @@ node .github/scripts/test-lmt-cli-package.mjs "$tarball"
205
205
  `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
206
 
207
207
  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).
208
+
209
+ ## Settings and token commands for Runner
210
+
211
+ Read ordinary Settings and their durable Settings Version with:
212
+
213
+ ```sh
214
+ lmt settings read --env dev --profile runner
215
+ ```
216
+
217
+ Save one intent by piping a single JSON object to protected stdin. Supply the version
218
+ you reviewed and only the settings you intend to change. For example:
219
+
220
+ ```sh
221
+ printf '%s\n' '{"expectedSettingsVersion":7,"entryCategories":[]}' |
222
+ lmt settings save --stdin --env dev --profile runner
223
+ ```
224
+
225
+ An empty list disables all Entry Categories; omitted fields keep their saved values.
226
+ Replacing credentials requires all four fields (userId, password, apiKey, apiSecret)
227
+ inside a credentials object supplied through protected stdin. Keep secrets in an
228
+ owner-readable input file or a protected pipe, never in command arguments or shell history.
229
+ One changed save advances one Settings Version; a current unchanged save returns
230
+ unchanged without a write. Every ordinary API write, including standalone credential
231
+ saves, requires expectedSettingsVersion. Stale saves return settings_conflict; read
232
+ Settings again and review the intended changes before submitting. A transport timeout
233
+ after submission or an unconfirmed Broker save returns uncertain with exit code 1.
234
+ Commands never retry a save. An ambiguous SMS write blocks authoritative Settings
235
+ reads and further Operator-record writes until Broker restarts and reloads its durable
236
+ record. The existing Market Watch resize recovery limitation still applies across stores.
237
+
238
+ Access-token operations use a separate version and endpoint:
239
+
240
+ ```sh
241
+ lmt token read --env dev --profile runner
242
+ lmt token replace --stdin --env dev --profile runner < /path/to/protected-token-request.json
243
+ ```
244
+
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
248
+ Settings Save never replaces access tokens. Token values and saved credentials do not
249
+ appear in command results.
package/dist/index.js CHANGED
@@ -15,7 +15,16 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // ../../packages/contracts/src/index.ts
18
- var OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS, OPERATOR_SESSION_EXPIRY_WARNING_MS, DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS, ZERODHA_SESSION_ADMISSION_HEADER, ZERODHA_SESSION_ADMISSION_VERIFY, API_PATHS;
18
+ function isOperatorMarketProtection(value) {
19
+ return typeof value === "number" && Number.isInteger(value) && value >= 2 && value <= 90;
20
+ }
21
+ function validateEntryCategories(value) {
22
+ if (!Array.isArray(value) || value.some((category) => typeof category !== "string" || !ENTRY_CATEGORIES.includes(category)) || new Set(value).size !== value.length) {
23
+ throw new Error("entryCategories must be a list of supported Entry Categories with no duplicates.");
24
+ }
25
+ return [...value];
26
+ }
27
+ 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
28
  var init_src = __esm({
20
29
  "../../packages/contracts/src/index.ts"() {
21
30
  "use strict";
@@ -24,6 +33,16 @@ var init_src = __esm({
24
33
  DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS = 7 * 24 * 60 * 60;
25
34
  ZERODHA_SESSION_ADMISSION_HEADER = "x-lmt-session-admission";
26
35
  ZERODHA_SESSION_ADMISSION_VERIFY = "verify";
36
+ ENTRY_CATEGORIES = [
37
+ "NSE EQ",
38
+ "BSE EQ",
39
+ "NFO FUT",
40
+ "NFO CE",
41
+ "NFO PE",
42
+ "BFO FUT",
43
+ "BFO CE",
44
+ "BFO PE"
45
+ ];
27
46
  API_PATHS = {
28
47
  health: "/api/health",
29
48
  unlockOperatorSession: "/api/operator-session/unlock",
@@ -37,6 +56,9 @@ var init_src = __esm({
37
56
  operatorLoginEvents: "/api/operators/login/events",
38
57
  operatorLoginOtp: "/api/operators/login/otp",
39
58
  cancelOperatorLogin: "/api/operators/login/cancel",
59
+ zerodhaAccessToken: "/api/zerodha/access-token",
60
+ zerodhaAccessTokenRecovery: "/api/zerodha/access-token/recovery",
61
+ zerodhaAccessTokenPublication: "/api/zerodha/access-token/publication",
40
62
  zerodhaCredentials: "/api/zerodha/credentials",
41
63
  entryCategorySettings: "/api/settings/entry-categories",
42
64
  operatorSettings: "/api/settings",
@@ -272,7 +294,91 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
272
294
  await rejectAbsentSession(response, path4);
273
295
  return response;
274
296
  }
297
+ async function settingsWrite(path4, body) {
298
+ let response;
299
+ try {
300
+ response = await fetchImpl(origin + path4, {
301
+ method: "PUT",
302
+ headers: headers({ "content-type": "application/json", accept: "application/json" }),
303
+ body: JSON.stringify(body),
304
+ signal: actionSignal()
305
+ });
306
+ } catch {
307
+ throw new SettingsActionError("submission_uncertain", true);
308
+ }
309
+ rememberCookies(response);
310
+ await rejectAbsentSession(response, path4);
311
+ if (!response.ok) {
312
+ const result = await readJson(response).catch(() => void 0);
313
+ const codes = [
314
+ "settings_conflict",
315
+ "settings_version_required",
316
+ "invalid_settings",
317
+ "trade_cycle_active",
318
+ "settings_save_failed",
319
+ "token_conflict",
320
+ "token_rejected",
321
+ "replacement_confirmation_required",
322
+ "token_write_pending",
323
+ "invalid_request",
324
+ "zerodha_session_absent",
325
+ "validation_failed",
326
+ "credentials_changed",
327
+ "replacement_blocked",
328
+ "token_changed",
329
+ "invalid_token_request",
330
+ "token_write_unresolved",
331
+ "active_trading_confirmation",
332
+ "validation_unavailable",
333
+ "unresolved_order",
334
+ "sharing_token_unavailable",
335
+ "token_recovery_unavailable",
336
+ "settings_recovery_required"
337
+ ];
338
+ 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);
340
+ }
341
+ return response;
342
+ }
343
+ function ordinarySettings(body) {
344
+ if (!body || !Number.isSafeInteger(body.settingsVersion) || !Number.isInteger(body.rowCount) || body.rowCount < 1 || body.rowCount > 20 || !isOperatorMarketProtection(body.marketProtection) || !Array.isArray(body.entryCategories)) {
345
+ throw new SettingsActionError("invalid_response");
346
+ }
347
+ return { settingsVersion: body.settingsVersion, rowCount: body.rowCount, marketProtection: body.marketProtection, entryCategories: validateEntryCategories(body.entryCategories) };
348
+ }
275
349
  const api = {
350
+ async getTokenMetadata() {
351
+ const response = await getJson(API_PATHS.zerodhaCredentials);
352
+ const body = await readJson(response);
353
+ if (!response.ok || !body?.accessTokenMetadata) throw new SettingsActionError("token_metadata_unavailable");
354
+ const { fingerprint, version, updatedAt, writePending } = body.accessTokenMetadata;
355
+ return { fingerprint, version, updatedAt, ...writePending === void 0 ? {} : { writePending } };
356
+ },
357
+ async getSettings() {
358
+ const response = await getJson(API_PATHS.operatorSettings);
359
+ if (!response.ok) throw new SettingsActionError("settings_read_failed");
360
+ return ordinarySettings(await readJson(response));
361
+ },
362
+ async saveSettings(request) {
363
+ const response = await settingsWrite(API_PATHS.operatorSettings, request);
364
+ try {
365
+ const body = await readJson(response);
366
+ if (body.outcome !== "saved" && body.outcome !== "unchanged") throw new Error("Invalid outcome");
367
+ return { outcome: body.outcome, settings: ordinarySettings(body.settings) };
368
+ } catch {
369
+ throw new SettingsActionError("submission_uncertain", true);
370
+ }
371
+ },
372
+ async replaceToken(request) {
373
+ const response = await settingsWrite(API_PATHS.zerodhaAccessToken, request);
374
+ try {
375
+ const body = await readJson(response);
376
+ if (body.outcome !== "verified" && body.outcome !== "replaced") throw new Error("Invalid outcome");
377
+ return { outcome: body.outcome, sharing: body.sharing, metadata: body.metadata, message: "Token operation completed." };
378
+ } catch {
379
+ throw new SettingsActionError("submission_uncertain", true);
380
+ }
381
+ },
276
382
  async exportCandles(request) {
277
383
  const response = await postJson(API_PATHS.candleExportOnDemand, request);
278
384
  const body = await readJson(response);
@@ -505,12 +611,21 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
505
611
  };
506
612
  return api;
507
613
  }
508
- var OperatorSessionRejectedError, ZerodhaLoginFailedError, ZerodhaLoginAttemptUnavailableError, OperatorCredentialsRejectedError;
614
+ var SettingsActionError, OperatorSessionRejectedError, ZerodhaLoginFailedError, ZerodhaLoginAttemptUnavailableError, OperatorCredentialsRejectedError;
509
615
  var init_gate_api_client = __esm({
510
616
  "src/gate-api-client.ts"() {
511
617
  "use strict";
512
618
  init_src();
513
619
  init_profile_name();
620
+ 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.");
623
+ this.code = code;
624
+ this.uncertain = uncertain;
625
+ }
626
+ code;
627
+ uncertain;
628
+ };
514
629
  OperatorSessionRejectedError = class extends Error {
515
630
  constructor(reason) {
516
631
  super("The saved Operator Session was rejected by Gate.");
@@ -2489,7 +2604,23 @@ function splitHeadlessArgv(argv) {
2489
2604
  return { positional, options, credentialStdin, otpStdin };
2490
2605
  }
2491
2606
  function parseArguments(argv) {
2492
- const { positional, options, credentialStdin, otpStdin } = splitHeadlessArgv(argv);
2607
+ const parsedInput = splitHeadlessArgv(argv.filter((value) => value !== "--stdin"));
2608
+ const commandVerb = parsedInput.positional[0];
2609
+ if (commandVerb === "settings" || commandVerb === "token") {
2610
+ const stdin = argv.includes("--stdin");
2611
+ if (argv.filter((value) => value === "--stdin").length > 1) throw new InvocationError("--stdin may be supplied only once.");
2612
+ const parsed = parsedInput;
2613
+ const [kind2, action] = parsed.positional;
2614
+ 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))) {
2615
+ throw new InvocationError("Use settings read, settings save --stdin, or token replace --stdin.");
2616
+ }
2617
+ const environment2 = parsed.options.get("--env"), profile2 = parsed.options.get("--profile");
2618
+ if (!environment2 || !profile2) throw new InvocationError("Headless commands require --env local|dev and --profile NAME.");
2619
+ validateCommandProfile(profile2);
2620
+ return { kind: kind2, action, environment: environment2, profile: profile2, ...parsed.options.has("--timeout") ? { timeoutMs: parseTimeout(parsed.options.get("--timeout")) } : {} };
2621
+ }
2622
+ if (argv.includes("--stdin")) throw new InvocationError("--stdin is only supported for Settings save and token replacement.");
2623
+ const { positional, options, credentialStdin, otpStdin } = parsedInput;
2493
2624
  if (positional[0] === "candle-export") {
2494
2625
  const instrument = positional[1]?.match(/^([A-Za-z][A-Za-z0-9]{0,31}):([^:\s]{1,100})$/);
2495
2626
  const date = options.get("--date");
@@ -2668,8 +2799,11 @@ async function readProtectedInput(stdin, signal, label = "SMS credential") {
2668
2799
  }
2669
2800
  async function runHeadlessCli(argv, dependencies = {}) {
2670
2801
  let otp;
2802
+ let securePayload;
2671
2803
  const protect = (stream) => ({ write(chunk) {
2672
- return stream.write(otp ? chunk.replaceAll(otp, "[redacted]") : chunk);
2804
+ let output = otp ? chunk.replaceAll(otp, "[redacted]") : chunk;
2805
+ if (securePayload) output = output.replaceAll(securePayload, "[redacted]");
2806
+ return stream.write(output);
2673
2807
  } });
2674
2808
  const streams = { stdout: protect(dependencies.stdout ?? defaultOut), stderr: protect(dependencies.stderr ?? defaultErr) };
2675
2809
  const started = performance.now();
@@ -2772,6 +2906,36 @@ async function runHeadlessCli(argv, dependencies = {}) {
2772
2906
  ...dependencies.readLoginCredentials ? { readLoginCredentials: dependencies.readLoginCredentials } : {}
2773
2907
  });
2774
2908
  actionApi = await actions.getApi();
2909
+ if (command.kind === "settings" || command.kind === "token") {
2910
+ await actions.status();
2911
+ let payload;
2912
+ if (command.action !== "read") {
2913
+ securePayload = await readProtectedInput(dependencies.stdin ?? process.stdin, signal, "Settings JSON");
2914
+ try {
2915
+ payload = JSON.parse(securePayload);
2916
+ } catch {
2917
+ throw new InvocationError("Protected stdin must contain one JSON object.");
2918
+ }
2919
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new InvocationError("Protected stdin must contain one JSON object.");
2920
+ }
2921
+ const settings = command.kind === "settings" ? command.action === "read" ? { outcome: "read", settings: await actionApi.getSettings() } : await actionApi.saveSettings(payload) : void 0;
2922
+ const tokenMetadata = command.kind === "token" && command.action === "read" ? await actionApi.getTokenMetadata() : void 0;
2923
+ const token = command.kind === "token" && command.action === "replace" ? await actionApi.replaceToken(payload) : void 0;
2924
+ emit({
2925
+ version: 1,
2926
+ command: command.kind + " " + command.action,
2927
+ outcome: "success",
2928
+ reason: settings?.outcome ?? token?.outcome,
2929
+ ...settings ? { settings: settings.settings } : {},
2930
+ ...token ? { token } : {},
2931
+ ...tokenMetadata ? { tokenMetadata } : {},
2932
+ target: { environment: target.environment, origin: target.origin },
2933
+ profile,
2934
+ elapsedMs: Math.round(performance.now() - started),
2935
+ metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
2936
+ }, streams);
2937
+ return 0;
2938
+ }
2775
2939
  if (command.kind === "candle-export") {
2776
2940
  await actions.status();
2777
2941
  const candleExport = await actionApi.exportCandles({
@@ -2855,13 +3019,13 @@ async function runHeadlessCli(argv, dependencies = {}) {
2855
3019
  if (error instanceof OperatorSessionRejectedError && store && actionApi && target && profile && !signal?.aborted) {
2856
3020
  await store.saveSession(profile, target, actionApi.getCookies()).catch(() => void 0);
2857
3021
  }
2858
- const details = errorDetails(error, signal);
3022
+ const details = error instanceof SettingsActionError ? { code: error.code, message: error.message, exitCode: 1 } : errorDetails(error, signal);
2859
3023
  const recovery = error instanceof Error ? error : void 0;
2860
3024
  const attemptId = recovery?.attemptId ?? (command?.kind === "zerodha" ? command.attemptId : void 0);
2861
3025
  emit({
2862
3026
  version: 1,
2863
- command: command?.kind === "zerodha" ? `zerodha ${command.action}` : command?.kind === "credential-provision" ? "credential provision" : command?.kind === "validate-zerodha" ? "validate zerodha-session" : command?.kind === "validate" ? "validate operator-session" : command?.kind ?? "",
2864
- outcome: details.exitCode === 4 ? "blocked" : "fail",
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",
2865
3029
  reason: details.code,
2866
3030
  ...target ? { target: { environment: target.environment, origin: target.origin } } : command ? { target: { environment: command.environment } } : {},
2867
3031
  ...profile ? { profile } : {},
@@ -2878,6 +3042,7 @@ async function runHeadlessCli(argv, dependencies = {}) {
2878
3042
  return details.exitCode;
2879
3043
  } finally {
2880
3044
  otp = void 0;
3045
+ securePayload = void 0;
2881
3046
  if (deadlineTimer) clearTimeout(deadlineTimer);
2882
3047
  }
2883
3048
  }
@@ -2906,6 +3071,10 @@ Usage (lmt is an alias of live-market-terminal):
2906
3071
  live-market-terminal login --env local|dev --profile NAME [--timeout DURATION]
2907
3072
  live-market-terminal logout --env local|dev --profile NAME [--timeout DURATION]
2908
3073
  live-market-terminal candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD] --env local|dev --profile NAME [--timeout DURATION]
3074
+ live-market-terminal settings read --env local|dev --profile NAME
3075
+ live-market-terminal settings save --stdin --env local|dev --profile NAME
3076
+ live-market-terminal token read --env local|dev --profile NAME
3077
+ live-market-terminal token replace --stdin --env local|dev --profile NAME
2909
3078
  live-market-terminal status --env local|dev --profile NAME [--timeout DURATION]
2910
3079
  live-market-terminal zerodha login --env local|dev --profile NAME [--timeout DURATION]
2911
3080
  live-market-terminal zerodha observe --attempt ID --env local|dev --profile NAME [--timeout DURATION]
@@ -2925,6 +3094,11 @@ From the monorepo root:
2925
3094
  Candle Export publishes a new immutable workbook link and returns its context in candleExport JSON.
2926
3095
  Empty or unavailable sources are explained in the workbook. Export never starts Zerodha login or token recovery.
2927
3096
 
3097
+ Settings save reads one JSON object from stdin: expectedSettingsVersion plus selected rowCount,
3098
+ marketProtection, entryCategories, or complete credentials. Omitted fields are preserved.
3099
+ Token replace reads {accessToken, expected, continueReplacement?} from protected stdin.
3100
+ Read token metadata through token read. A timeout after submission is uncertain; never retry blindly.
3101
+
2928
3102
  Targets: local and dev only. Production is not permitted.
2929
3103
 
2930
3104
  Zerodha login is two steps: zerodha login, then zerodha otp --attempt ID --otp-stdin.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-digital-factory/lmt-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "live-market-terminal": "dist/index.js",