@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.
- package/README.md +42 -0
- package/dist/index.js +181 -7
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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" ?
|
|
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.
|