clawchef 0.1.10 → 0.1.12

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 CHANGED
@@ -14,6 +14,7 @@ Recipe-driven OpenClaw environment orchestrator.
14
14
  - When installed OpenClaw version mismatches recipe version, prompts: ignore / abort / force reinstall (silent mode auto-picks force reinstall).
15
15
  - Supports scoped execution via `--scope full|files|workspace`.
16
16
  - `full` scope runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
17
+ - Factory reset includes removing local `~/.openclaw` directory.
17
18
  - If `openclaw` is missing, auto-installs the recipe version and skips factory reset.
18
19
  - Starts OpenClaw gateway after each recipe execution based on `--gateway-mode`.
19
20
  - Creates workspaces and agents (default workspace path: `~/.openclaw/workspace-<workspace-name>`).
@@ -392,6 +393,7 @@ channels:
392
393
  token: "${telegram_bot_token}"
393
394
  account: "default"
394
395
  agent: "main"
396
+ group_policy: "allowlist"
395
397
 
396
398
  - channel: "telegram"
397
399
  token: "${alerts_bot_token}"
@@ -412,11 +414,13 @@ channels:
412
414
  Supported common fields:
413
415
 
414
416
  - required: `channel`
415
- - optional: `account`, `agent`, `name`, `token`, `token_file`, `use_env`, `bot_token`, `access_token`, `app_token`, `webhook_url`, `webhook_path`, `signal_number`, `password`, `login`, `login_mode`, `login_account`
417
+ - optional: `account`, `agent`, `group_policy`, `name`, `token`, `token_file`, `use_env`, `bot_token`, `access_token`, `app_token`, `webhook_url`, `webhook_path`, `signal_number`, `password`, `login`, `login_mode`, `login_account`
416
418
  - advanced passthrough: `extra_flags` (`snake_case` keys become `--kebab-case` CLI flags)
417
419
 
418
420
  `channels[].agent` currently supports `channel: "telegram"` only.
419
421
  If `agent` is set and `account` is omitted, clawchef defaults `account` to the same value as `agent`.
422
+ `channels[].group_policy` currently supports `channel: "telegram"` only and is applied after `channels add` via `openclaw config set` so it is not overwritten by add-flow writes.
423
+ If `channel: "telegram"` has `token: ""` or `bot_token: ""`, clawchef auto-disables that telegram account (`enabled=false`) and skips channel add/bind.
420
424
 
421
425
  ## Workspace path behavior
422
426
 
@@ -1,4 +1,4 @@
1
- import { tmpdir } from "node:os";
1
+ import { homedir, tmpdir } from "node:os";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
4
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
@@ -129,6 +129,28 @@ function parseVersionOutput(output) {
129
129
  const match = output.match(/\b(\d+\.\d+\.\d+)\b/);
130
130
  return match?.[1] ?? output.trim();
131
131
  }
132
+ function telegramGroupPolicyPath(account) {
133
+ const trimmed = account?.trim();
134
+ if (!trimmed) {
135
+ return "channels.telegram.groupPolicy";
136
+ }
137
+ return `channels.telegram.accounts[${trimmed}].groupPolicy`;
138
+ }
139
+ function telegramEnabledPath(account) {
140
+ const trimmed = account?.trim();
141
+ if (!trimmed) {
142
+ return "channels.telegram.enabled";
143
+ }
144
+ return `channels.telegram.accounts[${trimmed}].enabled`;
145
+ }
146
+ function shouldAutoDisableTelegramChannel(channel) {
147
+ if (channel.channel !== "telegram") {
148
+ return false;
149
+ }
150
+ const emptyToken = channel.token !== undefined && channel.token.trim().length === 0;
151
+ const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
152
+ return emptyToken || emptyBotToken;
153
+ }
132
154
  async function chooseVersionMismatchAction(currentVersion, expectedVersion, silent) {
133
155
  if (silent) {
134
156
  return "force";
@@ -341,10 +363,13 @@ export class CommandOpenClawProvider {
341
363
  async factoryReset(config, dryRun) {
342
364
  const bin = config.bin ?? "openclaw";
343
365
  const resetCmd = commandFor(config, "factory_reset", { bin, version: config.version });
344
- if (!resetCmd.trim()) {
345
- return;
366
+ if (resetCmd.trim()) {
367
+ await runShell(resetCmd, dryRun);
368
+ }
369
+ const openclawHome = path.join(homedir(), ".openclaw");
370
+ if (!dryRun) {
371
+ await rm(openclawHome, { recursive: true, force: true });
346
372
  }
347
- await runShell(resetCmd, dryRun);
348
373
  }
349
374
  async installPlugin(config, pluginSpec, dryRun) {
350
375
  const trimmed = pluginSpec.trim();
@@ -393,6 +418,12 @@ export class CommandOpenClawProvider {
393
418
  }
394
419
  async configureChannel(config, channel, dryRun) {
395
420
  const bin = config.bin ?? "openclaw";
421
+ if (shouldAutoDisableTelegramChannel(channel)) {
422
+ const enabledPath = telegramEnabledPath(channel.account);
423
+ const disableCmd = `${bin} config set ${shellQuote(enabledPath)} false --strict-json`;
424
+ await runShell(disableCmd, dryRun);
425
+ return;
426
+ }
396
427
  const enablePluginTemplate = config.commands?.enable_plugin;
397
428
  if (enablePluginTemplate?.trim()) {
398
429
  const enablePluginCmd = fillTemplate(enablePluginTemplate, {
@@ -443,6 +474,12 @@ export class CommandOpenClawProvider {
443
474
  }
444
475
  const cmd = `${bin} channels add ${flags.join(" ")}`;
445
476
  await runShell(cmd, dryRun);
477
+ if (channel.channel === "telegram" && channel.group_policy) {
478
+ const configPath = telegramGroupPolicyPath(channel.account);
479
+ const policyValue = JSON.stringify(channel.group_policy);
480
+ const setPolicyCmd = `${bin} config set ${shellQuote(configPath)} ${shellQuote(policyValue)} --strict-json`;
481
+ await runShell(setPolicyCmd, dryRun);
482
+ }
446
483
  }
447
484
  async bindChannelAgent(config, channel, agent, dryRun) {
448
485
  const account = channel.account?.trim();
@@ -76,6 +76,14 @@ function isHttpUrl(value) {
76
76
  return false;
77
77
  }
78
78
  }
79
+ function shouldAutoDisableTelegramChannel(channel) {
80
+ if (channel.channel !== "telegram") {
81
+ return false;
82
+ }
83
+ const emptyToken = channel.token !== undefined && channel.token.trim().length === 0;
84
+ const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
85
+ return emptyToken || emptyBotToken;
86
+ }
79
87
  function isNotFoundError(err) {
80
88
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
81
89
  }
@@ -314,7 +322,12 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
314
322
  const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
315
323
  ? { ...channel, account: channel.agent.trim() }
316
324
  : channel;
325
+ const autoDisabledTelegram = shouldAutoDisableTelegramChannel(effectiveChannel);
317
326
  await provider.configureChannel(recipe.openclaw, effectiveChannel, options.dryRun);
327
+ if (autoDisabledTelegram) {
328
+ logger.info(`Telegram channel disabled due to empty bot token: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
329
+ continue;
330
+ }
318
331
  logger.info(`Channel configured: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
319
332
  if (effectiveChannel.agent?.trim()) {
320
333
  await provider.bindChannelAgent(recipe.openclaw, effectiveChannel, effectiveChannel.agent, options.dryRun);
package/dist/recipe.js CHANGED
@@ -44,6 +44,14 @@ const ALLOWED_CHANNELS = new Set([
44
44
  ]);
45
45
  const CHANNEL_SECRET_FIELDS = ["token", "bot_token", "access_token", "app_token", "password"];
46
46
  const TEMPLATE_TOKEN_RE = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/;
47
+ function hasExplicitEmptyTelegramToken(channel) {
48
+ if (channel.channel !== "telegram") {
49
+ return false;
50
+ }
51
+ const emptyToken = channel.token !== undefined && channel.token.trim().length === 0;
52
+ const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
53
+ return emptyToken || emptyBotToken;
54
+ }
47
55
  function assertNoInlineSecrets(recipe) {
48
56
  const bootstrap = recipe.openclaw.bootstrap;
49
57
  if (bootstrap) {
@@ -193,6 +201,9 @@ function semanticValidate(recipe) {
193
201
  (channel.login || channel.login_mode !== undefined || channel.login_account !== undefined)) {
194
202
  throw new ClawChefError("channels[] entry for telegram does not support login/login_mode/login_account. Configure token (or use_env/token_file), then start gateway.");
195
203
  }
204
+ if (channel.group_policy !== undefined && channel.channel !== "telegram") {
205
+ throw new ClawChefError(`channels[] entry for ${channel.channel} does not support group_policy. Use channel: telegram.`);
206
+ }
196
207
  if (channel.agent?.trim()) {
197
208
  if (channel.channel !== "telegram") {
198
209
  throw new ClawChefError(`channels[] entry for ${channel.channel} does not support agent binding. Use channel: telegram with agent.`);
@@ -219,6 +230,9 @@ function semanticValidate(recipe) {
219
230
  }
220
231
  return /(token|password|secret|api[_-]?key|webhook)/i.test(key) && String(value).trim().length > 0;
221
232
  });
233
+ if (hasExplicitEmptyTelegramToken(channel)) {
234
+ continue;
235
+ }
222
236
  if (!hasAuth) {
223
237
  throw new ClawChefError(`channels[] entry for ${channel.channel} requires at least one auth input (for example token/bot_token/access_token/token_file/use_env)`);
224
238
  }
package/dist/schema.d.ts CHANGED
@@ -356,6 +356,7 @@ export declare const recipeSchema: z.ZodObject<{
356
356
  channel: z.ZodString;
357
357
  account: z.ZodOptional<z.ZodString>;
358
358
  agent: z.ZodOptional<z.ZodString>;
359
+ group_policy: z.ZodOptional<z.ZodEnum<["open", "allowlist", "disabled"]>>;
359
360
  login: z.ZodOptional<z.ZodBoolean>;
360
361
  login_mode: z.ZodOptional<z.ZodEnum<["interactive"]>>;
361
362
  login_account: z.ZodOptional<z.ZodString>;
@@ -376,6 +377,7 @@ export declare const recipeSchema: z.ZodObject<{
376
377
  token?: string | undefined;
377
378
  account?: string | undefined;
378
379
  agent?: string | undefined;
380
+ group_policy?: "open" | "allowlist" | "disabled" | undefined;
379
381
  login?: boolean | undefined;
380
382
  login_mode?: "interactive" | undefined;
381
383
  login_account?: string | undefined;
@@ -395,6 +397,7 @@ export declare const recipeSchema: z.ZodObject<{
395
397
  token?: string | undefined;
396
398
  account?: string | undefined;
397
399
  agent?: string | undefined;
400
+ group_policy?: "open" | "allowlist" | "disabled" | undefined;
398
401
  login?: boolean | undefined;
399
402
  login_mode?: "interactive" | undefined;
400
403
  login_account?: string | undefined;
@@ -572,6 +575,7 @@ export declare const recipeSchema: z.ZodObject<{
572
575
  token?: string | undefined;
573
576
  account?: string | undefined;
574
577
  agent?: string | undefined;
578
+ group_policy?: "open" | "allowlist" | "disabled" | undefined;
575
579
  login?: boolean | undefined;
576
580
  login_mode?: "interactive" | undefined;
577
581
  login_account?: string | undefined;
@@ -687,6 +691,7 @@ export declare const recipeSchema: z.ZodObject<{
687
691
  token?: string | undefined;
688
692
  account?: string | undefined;
689
693
  agent?: string | undefined;
694
+ group_policy?: "open" | "allowlist" | "disabled" | undefined;
690
695
  login?: boolean | undefined;
691
696
  login_mode?: "interactive" | undefined;
692
697
  login_account?: string | undefined;
package/dist/schema.js CHANGED
@@ -102,14 +102,15 @@ const channelSchema = z
102
102
  channel: z.string().min(1),
103
103
  account: z.string().min(1).optional(),
104
104
  agent: z.string().min(1).optional(),
105
+ group_policy: z.enum(["open", "allowlist", "disabled"]).optional(),
105
106
  login: z.boolean().optional(),
106
107
  login_mode: z.enum(["interactive"]).optional(),
107
108
  login_account: z.string().min(1).optional(),
108
109
  name: z.string().min(1).optional(),
109
- token: z.string().min(1).optional(),
110
+ token: z.string().optional(),
110
111
  token_file: z.string().min(1).optional(),
111
112
  use_env: z.boolean().optional(),
112
- bot_token: z.string().min(1).optional(),
113
+ bot_token: z.string().optional(),
113
114
  access_token: z.string().min(1).optional(),
114
115
  app_token: z.string().min(1).optional(),
115
116
  webhook_url: z.string().min(1).optional(),
package/dist/types.d.ts CHANGED
@@ -77,6 +77,7 @@ export interface ChannelDef {
77
77
  channel: string;
78
78
  account?: string;
79
79
  agent?: string;
80
+ group_policy?: "open" | "allowlist" | "disabled";
80
81
  login?: boolean;
81
82
  login_mode?: "interactive";
82
83
  login_account?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawchef",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Recipe-driven OpenClaw environment orchestrator",
5
5
  "homepage": "https://renorzr.github.io/clawchef",
6
6
  "repository": {
@@ -1,4 +1,4 @@
1
- import { tmpdir } from "node:os";
1
+ import { homedir, tmpdir } from "node:os";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
4
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
@@ -178,6 +178,31 @@ function parseVersionOutput(output: string): string {
178
178
  return match?.[1] ?? output.trim();
179
179
  }
180
180
 
181
+ function telegramGroupPolicyPath(account: string | undefined): string {
182
+ const trimmed = account?.trim();
183
+ if (!trimmed) {
184
+ return "channels.telegram.groupPolicy";
185
+ }
186
+ return `channels.telegram.accounts[${trimmed}].groupPolicy`;
187
+ }
188
+
189
+ function telegramEnabledPath(account: string | undefined): string {
190
+ const trimmed = account?.trim();
191
+ if (!trimmed) {
192
+ return "channels.telegram.enabled";
193
+ }
194
+ return `channels.telegram.accounts[${trimmed}].enabled`;
195
+ }
196
+
197
+ function shouldAutoDisableTelegramChannel(channel: ChannelDef): boolean {
198
+ if (channel.channel !== "telegram") {
199
+ return false;
200
+ }
201
+ const emptyToken = channel.token !== undefined && channel.token.trim().length === 0;
202
+ const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
203
+ return emptyToken || emptyBotToken;
204
+ }
205
+
181
206
  type VersionMismatchChoice = "ignore" | "abort" | "force";
182
207
 
183
208
  async function chooseVersionMismatchAction(
@@ -440,10 +465,14 @@ export class CommandOpenClawProvider implements OpenClawProvider {
440
465
  async factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void> {
441
466
  const bin = config.bin ?? "openclaw";
442
467
  const resetCmd = commandFor(config, "factory_reset", { bin, version: config.version });
443
- if (!resetCmd.trim()) {
444
- return;
468
+ if (resetCmd.trim()) {
469
+ await runShell(resetCmd, dryRun);
470
+ }
471
+
472
+ const openclawHome = path.join(homedir(), ".openclaw");
473
+ if (!dryRun) {
474
+ await rm(openclawHome, { recursive: true, force: true });
445
475
  }
446
- await runShell(resetCmd, dryRun);
447
476
  }
448
477
 
449
478
  async installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void> {
@@ -498,6 +527,14 @@ export class CommandOpenClawProvider implements OpenClawProvider {
498
527
 
499
528
  async configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
500
529
  const bin = config.bin ?? "openclaw";
530
+
531
+ if (shouldAutoDisableTelegramChannel(channel)) {
532
+ const enabledPath = telegramEnabledPath(channel.account);
533
+ const disableCmd = `${bin} config set ${shellQuote(enabledPath)} false --strict-json`;
534
+ await runShell(disableCmd, dryRun);
535
+ return;
536
+ }
537
+
501
538
  const enablePluginTemplate = config.commands?.enable_plugin;
502
539
  if (enablePluginTemplate?.trim()) {
503
540
  const enablePluginCmd = fillTemplate(enablePluginTemplate, {
@@ -554,6 +591,13 @@ export class CommandOpenClawProvider implements OpenClawProvider {
554
591
 
555
592
  const cmd = `${bin} channels add ${flags.join(" ")}`;
556
593
  await runShell(cmd, dryRun);
594
+
595
+ if (channel.channel === "telegram" && channel.group_policy) {
596
+ const configPath = telegramGroupPolicyPath(channel.account);
597
+ const policyValue = JSON.stringify(channel.group_policy);
598
+ const setPolicyCmd = `${bin} config set ${shellQuote(configPath)} ${shellQuote(policyValue)} --strict-json`;
599
+ await runShell(setPolicyCmd, dryRun);
600
+ }
557
601
  }
558
602
 
559
603
  async bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void> {
@@ -8,7 +8,7 @@ import { validateReply } from "./assertions.js";
8
8
  import { ClawChefError } from "./errors.js";
9
9
  import { Logger } from "./logger.js";
10
10
  import { createProvider } from "./openclaw/factory.js";
11
- import type { Recipe, RunOptions } from "./types.js";
11
+ import type { ChannelDef, Recipe, RunOptions } from "./types.js";
12
12
  import type { RecipeOrigin } from "./recipe.js";
13
13
 
14
14
  async function exists(filePath: string): Promise<boolean> {
@@ -84,6 +84,15 @@ function isHttpUrl(value: string): boolean {
84
84
  }
85
85
  }
86
86
 
87
+ function shouldAutoDisableTelegramChannel(channel: ChannelDef): boolean {
88
+ if (channel.channel !== "telegram") {
89
+ return false;
90
+ }
91
+ const emptyToken = channel.token !== undefined && channel.token.trim().length === 0;
92
+ const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
93
+ return emptyToken || emptyBotToken;
94
+ }
95
+
87
96
  function isNotFoundError(err: unknown): boolean {
88
97
  return typeof err === "object" && err !== null && "code" in err && (err as { code?: string }).code === "ENOENT";
89
98
  }
@@ -362,8 +371,16 @@ export async function runRecipe(
362
371
  const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
363
372
  ? { ...channel, account: channel.agent.trim() }
364
373
  : channel;
374
+ const autoDisabledTelegram = shouldAutoDisableTelegramChannel(effectiveChannel);
365
375
 
366
376
  await provider.configureChannel(recipe.openclaw, effectiveChannel, options.dryRun);
377
+ if (autoDisabledTelegram) {
378
+ logger.info(
379
+ `Telegram channel disabled due to empty bot token: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`,
380
+ );
381
+ continue;
382
+ }
383
+
367
384
  logger.info(`Channel configured: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
368
385
  if (effectiveChannel.agent?.trim()) {
369
386
  await provider.bindChannelAgent(recipe.openclaw, effectiveChannel, effectiveChannel.agent, options.dryRun);
package/src/recipe.ts CHANGED
@@ -7,7 +7,7 @@ import YAML from "js-yaml";
7
7
  import { recipeSchema } from "./schema.js";
8
8
  import { ClawChefError } from "./errors.js";
9
9
  import { deepResolveTemplates } from "./template.js";
10
- import type { Recipe, RunOptions } from "./types.js";
10
+ import type { ChannelDef, Recipe, RunOptions } from "./types.js";
11
11
 
12
12
  export type RecipeOrigin =
13
13
  | {
@@ -73,6 +73,15 @@ const CHANNEL_SECRET_FIELDS = ["token", "bot_token", "access_token", "app_token"
73
73
 
74
74
  const TEMPLATE_TOKEN_RE = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/;
75
75
 
76
+ function hasExplicitEmptyTelegramToken(channel: ChannelDef): boolean {
77
+ if (channel.channel !== "telegram") {
78
+ return false;
79
+ }
80
+ const emptyToken = channel.token !== undefined && channel.token.trim().length === 0;
81
+ const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
82
+ return emptyToken || emptyBotToken;
83
+ }
84
+
76
85
  function assertNoInlineSecrets(recipe: Recipe): void {
77
86
  const bootstrap = recipe.openclaw.bootstrap;
78
87
  if (bootstrap) {
@@ -252,6 +261,12 @@ function semanticValidate(recipe: Recipe): void {
252
261
  );
253
262
  }
254
263
 
264
+ if (channel.group_policy !== undefined && channel.channel !== "telegram") {
265
+ throw new ClawChefError(
266
+ `channels[] entry for ${channel.channel} does not support group_policy. Use channel: telegram.`,
267
+ );
268
+ }
269
+
255
270
  if (channel.agent?.trim()) {
256
271
  if (channel.channel !== "telegram") {
257
272
  throw new ClawChefError(
@@ -285,6 +300,10 @@ function semanticValidate(recipe: Recipe): void {
285
300
  return /(token|password|secret|api[_-]?key|webhook)/i.test(key) && String(value).trim().length > 0;
286
301
  });
287
302
 
303
+ if (hasExplicitEmptyTelegramToken(channel)) {
304
+ continue;
305
+ }
306
+
288
307
  if (!hasAuth) {
289
308
  throw new ClawChefError(
290
309
  `channels[] entry for ${channel.channel} requires at least one auth input (for example token/bot_token/access_token/token_file/use_env)`,
package/src/schema.ts CHANGED
@@ -112,14 +112,15 @@ const channelSchema = z
112
112
  channel: z.string().min(1),
113
113
  account: z.string().min(1).optional(),
114
114
  agent: z.string().min(1).optional(),
115
+ group_policy: z.enum(["open", "allowlist", "disabled"]).optional(),
115
116
  login: z.boolean().optional(),
116
117
  login_mode: z.enum(["interactive"]).optional(),
117
118
  login_account: z.string().min(1).optional(),
118
119
  name: z.string().min(1).optional(),
119
- token: z.string().min(1).optional(),
120
+ token: z.string().optional(),
120
121
  token_file: z.string().min(1).optional(),
121
122
  use_env: z.boolean().optional(),
122
- bot_token: z.string().min(1).optional(),
123
+ bot_token: z.string().optional(),
123
124
  access_token: z.string().min(1).optional(),
124
125
  app_token: z.string().min(1).optional(),
125
126
  webhook_url: z.string().min(1).optional(),
package/src/types.ts CHANGED
@@ -85,6 +85,7 @@ export interface ChannelDef {
85
85
  channel: string;
86
86
  account?: string;
87
87
  agent?: string;
88
+ group_policy?: "open" | "allowlist" | "disabled";
88
89
  login?: boolean;
89
90
  login_mode?: "interactive";
90
91
  login_account?: string;