@rezti/dsh-rez-suite 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,4 +8,4 @@ dsh plugin --profile web add @rezti/dsh-rez-suite
8
8
 
9
9
  组成见仓库根 README。子包不要单独 `dsh plugin add`。
10
10
 
11
- 配置在 **设置 → 插件 → 插件配置** 展开 **ReZ-TI**(同页还有独立的 ReZ-TI 标签)。个人微信在卡片的「微信」页扫码,然后直接在微信里说话。
11
+ 配置在 **设置 → 插件 → 插件配置** 展开 **ReZ-TI**(同页还有独立的 ReZ-TI 标签)。Odoo / Nextcloud / WeCom / HA 的地址和密钥都在「配置」页。个人微信在卡片的「微信」页扫码,然后直接在微信里说话。
package/README.zh.md CHANGED
@@ -8,6 +8,6 @@ dsh plugin --profile web add @rezti/dsh-rez-suite
8
8
 
9
9
  密钥用环境变量或 dsh-credentials:`REZ_ODOO_TOKEN`、`REZ_NEXTCLOUD_PASSWORD`、`REZ_WECHAT_WEBHOOK`、`REZ_HA_TOKEN`。未配置时不拉起对应 MCP。
10
10
 
11
- 配置在 **设置 → 插件 → 插件配置** 展开 **ReZ-TI**(同页还有独立的 ReZ-TI 标签)。个人微信在卡片的「微信」页扫码,然后直接在微信里说话。
11
+ 配置在 **设置 → 插件 → 插件配置** 展开 **ReZ-TI**(同页还有独立的 ReZ-TI 标签)。Odoo / Nextcloud / 企微 / HA 的地址和密钥都在「配置」页。个人微信在卡片的「微信」页扫码,然后直接在微信里说话。
12
12
 
13
13
  子包不要再单独 `dsh plugin add`,否则会重复注册 MCP。
package/lib/client.d.ts CHANGED
@@ -7275,16 +7275,23 @@ declare const zh: {
7275
7275
  readonly 'config.env': "环境变量 (JSON)";
7276
7276
  readonly 'config.headers': "Headers (JSON)";
7277
7277
  readonly 'config.jsonInvalid': "JSON 格式错误:{error}";
7278
+ readonly 'config.mcpHint': "四个公司 MCP 在这里配。密钥写入 ~/.dsh/.credentials.yaml(REZ_*)。新填的密钥会立刻拉起连接;改 URL 后如果已经连上,需要重启 dsh web。";
7279
+ readonly 'config.secretConfigured': "已配置(留空则保持)";
7280
+ readonly 'config.secretMissing': "未配置";
7278
7281
  readonly 'config.odoo': "Odoo";
7279
- readonly 'config.odoo.url': "Odoo URL";
7280
- readonly 'config.odoo.apiKey': "API Key";
7282
+ readonly 'config.odoo.url': "Odoo MCP URL";
7283
+ readonly 'config.odoo.apiKey': "API Token";
7281
7284
  readonly 'config.nextcloud': "Nextcloud";
7282
- readonly 'config.nextcloud.url': "Nextcloud URL";
7285
+ readonly 'config.nextcloud.url': "Nextcloud 主机";
7283
7286
  readonly 'config.nextcloud.username': "用户名";
7284
7287
  readonly 'config.nextcloud.appPassword': "App Password";
7285
7288
  readonly 'config.wecom': "企业微信";
7286
7289
  readonly 'config.wecom.botId': "Bot ID";
7287
7290
  readonly 'config.wecom.secret': "Secret";
7291
+ readonly 'config.wecom.webhook': "群机器人 Webhook";
7292
+ readonly 'config.ha': "Home Assistant";
7293
+ readonly 'config.ha.url': "HA MCP URL";
7294
+ readonly 'config.ha.token': "长期访问令牌";
7288
7295
  readonly 'config.fs': "本地文件 (fs-mcp-server)";
7289
7296
  readonly 'config.fs.root': "根目录";
7290
7297
  readonly 'config.sqlite': "SQLite (sqlite-mcp-server)";
package/lib/client.js CHANGED
@@ -36,16 +36,23 @@ const zh = {
36
36
  "config.env": "环境变量 (JSON)",
37
37
  "config.headers": "Headers (JSON)",
38
38
  "config.jsonInvalid": "JSON 格式错误:{error}",
39
+ "config.mcpHint": "四个公司 MCP 在这里配。密钥写入 ~/.dsh/.credentials.yaml(REZ_*)。新填的密钥会立刻拉起连接;改 URL 后如果已经连上,需要重启 dsh web。",
40
+ "config.secretConfigured": "已配置(留空则保持)",
41
+ "config.secretMissing": "未配置",
39
42
  "config.odoo": "Odoo",
40
- "config.odoo.url": "Odoo URL",
41
- "config.odoo.apiKey": "API Key",
43
+ "config.odoo.url": "Odoo MCP URL",
44
+ "config.odoo.apiKey": "API Token",
42
45
  "config.nextcloud": "Nextcloud",
43
- "config.nextcloud.url": "Nextcloud URL",
46
+ "config.nextcloud.url": "Nextcloud 主机",
44
47
  "config.nextcloud.username": "用户名",
45
48
  "config.nextcloud.appPassword": "App Password",
46
49
  "config.wecom": "企业微信",
47
50
  "config.wecom.botId": "Bot ID",
48
51
  "config.wecom.secret": "Secret",
52
+ "config.wecom.webhook": "群机器人 Webhook",
53
+ "config.ha": "Home Assistant",
54
+ "config.ha.url": "HA MCP URL",
55
+ "config.ha.token": "长期访问令牌",
49
56
  "config.fs": "本地文件 (fs-mcp-server)",
50
57
  "config.fs.root": "根目录",
51
58
  "config.sqlite": "SQLite (sqlite-mcp-server)",
@@ -125,16 +132,23 @@ const en = {
125
132
  "config.env": "Env vars (JSON)",
126
133
  "config.headers": "Headers (JSON)",
127
134
  "config.jsonInvalid": "Invalid JSON: {error}",
135
+ "config.mcpHint": "Company MCP lives here. Secrets go to ~/.dsh/.credentials.yaml (REZ_*). A newly saved secret starts the connection immediately; URL changes after a live connection need a dsh web restart.",
136
+ "config.secretConfigured": "Configured (leave blank to keep)",
137
+ "config.secretMissing": "Not configured",
128
138
  "config.odoo": "Odoo",
129
- "config.odoo.url": "Odoo URL",
130
- "config.odoo.apiKey": "API Key",
139
+ "config.odoo.url": "Odoo MCP URL",
140
+ "config.odoo.apiKey": "API token",
131
141
  "config.nextcloud": "Nextcloud",
132
- "config.nextcloud.url": "Nextcloud URL",
142
+ "config.nextcloud.url": "Nextcloud host",
133
143
  "config.nextcloud.username": "Username",
134
- "config.nextcloud.appPassword": "App Password",
144
+ "config.nextcloud.appPassword": "App password",
135
145
  "config.wecom": "WeCom",
136
146
  "config.wecom.botId": "Bot ID",
137
147
  "config.wecom.secret": "Secret",
148
+ "config.wecom.webhook": "Group bot webhook",
149
+ "config.ha": "Home Assistant",
150
+ "config.ha.url": "HA MCP URL",
151
+ "config.ha.token": "Long-lived access token",
138
152
  "config.fs": "Local files (fs-mcp-server)",
139
153
  "config.fs.root": "Root directory",
140
154
  "config.sqlite": "SQLite (sqlite-mcp-server)",
@@ -489,8 +503,8 @@ function AuditTab({ api }) {
489
503
  //#endregion
490
504
  //#region src/client/panel/ConfigTab.tsx
491
505
  /**
492
- * Config tab: role, billing rates, and a generic editor for every MCP server
493
- * in config.servers (stdio and streamable-http), with one-click tests.
506
+ * Config tab: company MCP connections (Odoo / Nextcloud / WeCom / HA),
507
+ * role, and billing. Secrets are write-only and land in credentials.yaml.
494
508
  */
495
509
  const ROLES = [
496
510
  {
@@ -510,6 +524,12 @@ const ROLES = [
510
524
  label: "role.operations"
511
525
  }
512
526
  ];
527
+ const EMPTY_SECRETS = {
528
+ odoo: "",
529
+ nextcloud: "",
530
+ wechat: "",
531
+ homeassistant: ""
532
+ };
513
533
  function Field({ label, children }) {
514
534
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
515
535
  className: panel_module_default.field,
@@ -528,42 +548,81 @@ function Section({ title, children }) {
528
548
  }), children]
529
549
  });
530
550
  }
531
- function parseRecord(text) {
532
- const value = JSON.parse(text);
533
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("expected a JSON object");
534
- const out = {};
535
- for (const [key, item] of Object.entries(value)) out[key] = String(item);
536
- return out;
551
+ function SecretField({ label, configured, value, onChange }) {
552
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
553
+ label,
554
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
555
+ className: panel_module_default.input,
556
+ type: "password",
557
+ autoComplete: "off",
558
+ value,
559
+ placeholder: configured ? tt("config.secretConfigured") : tt("config.secretMissing"),
560
+ onChange: (event) => {
561
+ onChange(event.target.value);
562
+ }
563
+ })
564
+ });
565
+ }
566
+ function McpSection({ title, row, secret, onEnabled, onUrl, onUsername, onSecret, urlLabel, secretLabel, usernameLabel }) {
567
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
568
+ title,
569
+ children: [
570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
571
+ label: tt("config.enabled"),
572
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
573
+ className: panel_module_default.checkbox,
574
+ type: "checkbox",
575
+ checked: row.enabled,
576
+ onChange: (event) => {
577
+ onEnabled(event.target.checked);
578
+ }
579
+ })
580
+ }),
581
+ onUrl !== void 0 && urlLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
582
+ label: urlLabel,
583
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
584
+ className: panel_module_default.input,
585
+ value: row.url ?? "",
586
+ onChange: (event) => {
587
+ onUrl(event.target.value);
588
+ }
589
+ })
590
+ }),
591
+ onUsername !== void 0 && usernameLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
592
+ label: usernameLabel,
593
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
594
+ className: panel_module_default.input,
595
+ value: row.username ?? "",
596
+ onChange: (event) => {
597
+ onUsername(event.target.value);
598
+ }
599
+ })
600
+ }),
601
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretField, {
602
+ label: secretLabel,
603
+ configured: row.secretConfigured,
604
+ value: secret,
605
+ onChange: onSecret
606
+ }),
607
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
608
+ className: panel_module_default.message,
609
+ children: row.secretRef
610
+ })
611
+ ]
612
+ });
537
613
  }
538
614
  function ConfigTab({ api }) {
539
615
  const [config, setConfig] = (0, react.useState)(null);
616
+ const [secrets, setSecrets] = (0, react.useState)(EMPTY_SECRETS);
540
617
  const [loading, setLoading] = (0, react.useState)(true);
541
618
  const [saving, setSaving] = (0, react.useState)(false);
542
619
  const [testing, setTesting] = (0, react.useState)(false);
543
620
  const [message, setMessage] = (0, react.useState)("");
544
621
  const [error, setError] = (0, react.useState)("");
545
622
  const [results, setResults] = (0, react.useState)([]);
546
- const [argsText, setArgsText] = (0, react.useState)({});
547
- const [envText, setEnvText] = (0, react.useState)({});
548
- const [headerText, setHeaderText] = (0, react.useState)({});
549
- const initTexts = (next) => {
550
- const args = {};
551
- const envs = {};
552
- const headers = {};
553
- for (const [id, server] of Object.entries(next.servers)) {
554
- args[id] = (server.args ?? []).join("\n");
555
- envs[id] = JSON.stringify(server.env ?? {}, null, 2);
556
- headers[id] = JSON.stringify(server.headers ?? {}, null, 2);
557
- }
558
- setArgsText(args);
559
- setEnvText(envs);
560
- setHeaderText(headers);
561
- };
562
623
  const load = async () => {
563
624
  try {
564
- const next = await api.getConfig();
565
- setConfig(next);
566
- initTexts(next);
625
+ setConfig(await api.getConfig());
567
626
  } catch (err) {
568
627
  setError(errorMessage(err));
569
628
  } finally {
@@ -587,13 +646,13 @@ function ConfigTab({ api }) {
587
646
  [key]: value
588
647
  });
589
648
  };
590
- const updateServer = (id, patch) => {
649
+ const patchConnection = (id, patch) => {
591
650
  setConfig((prev) => prev === null ? prev : {
592
651
  ...prev,
593
- servers: {
594
- ...prev.servers,
652
+ connections: {
653
+ ...prev.connections,
595
654
  [id]: {
596
- ...prev.servers[id],
655
+ ...prev.connections[id],
597
656
  ...patch
598
657
  }
599
658
  }
@@ -604,9 +663,32 @@ function ConfigTab({ api }) {
604
663
  setMessage("");
605
664
  setError("");
606
665
  try {
607
- const saved = await api.saveConfig(config);
666
+ const payload = {
667
+ enabled: config.enabled,
668
+ role: config.role,
669
+ billing: config.billing,
670
+ connections: {
671
+ odoo: {
672
+ ...config.connections.odoo,
673
+ secret: secrets.odoo || void 0
674
+ },
675
+ nextcloud: {
676
+ ...config.connections.nextcloud,
677
+ secret: secrets.nextcloud || void 0
678
+ },
679
+ wechat: {
680
+ ...config.connections.wechat,
681
+ secret: secrets.wechat || void 0
682
+ },
683
+ homeassistant: {
684
+ ...config.connections.homeassistant,
685
+ secret: secrets.homeassistant || void 0
686
+ }
687
+ }
688
+ };
689
+ const saved = await api.saveConfig(payload);
608
690
  setConfig(saved);
609
- initTexts(saved);
691
+ setSecrets(EMPTY_SECRETS);
610
692
  setMessage(tt("config.saved"));
611
693
  } catch (err) {
612
694
  setError(errorMessage(err));
@@ -628,28 +710,22 @@ function ConfigTab({ api }) {
628
710
  }
629
711
  };
630
712
  const renderResult = (result) => {
631
- if (result.ok) return tt("config.testOk", {
632
- tools: result.toolCount ?? 0,
633
- latency: result.latencyMs ?? 0
634
- });
635
- return tt("config.testFail", { error: result.error ?? "" });
636
- };
637
- const commitArgs = (id) => {
638
- updateServer(id, { args: argsText[id] === "" ? [] : argsText[id].split("\n").map((line) => line.trim()).filter(Boolean) });
639
- };
640
- const commitRecord = (id, kind) => {
641
- const text = kind === "env" ? envText[id] : headerText[id];
642
- try {
643
- const record = parseRecord(text ?? "{}");
644
- updateServer(id, kind === "env" ? { env: record } : { headers: record });
645
- setError("");
646
- } catch (err) {
647
- setError(tt("config.jsonInvalid", { error: errorMessage(err) }));
713
+ if (result.ok) {
714
+ if (result.serverInfo !== void 0) return result.serverInfo;
715
+ return tt("config.testOk", {
716
+ tools: result.toolCount ?? 0,
717
+ latency: result.latencyMs ?? 0
718
+ });
648
719
  }
720
+ return tt("config.testFail", { error: result.error ?? "" });
649
721
  };
650
722
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
651
723
  className: panel_module_default.tabBody,
652
724
  children: [
725
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
726
+ className: panel_module_default.message,
727
+ children: tt("config.mcpHint")
728
+ }),
653
729
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
654
730
  title: tt("config.role"),
655
731
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
@@ -677,110 +753,82 @@ function ConfigTab({ api }) {
677
753
  })
678
754
  })]
679
755
  }),
680
- Object.entries(config.servers).map(([id, server]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
681
- title: id,
682
- children: [
683
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
684
- label: tt("config.enabled"),
685
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
686
- className: panel_module_default.checkbox,
687
- type: "checkbox",
688
- checked: server.enabled,
689
- onChange: (event) => {
690
- updateServer(id, { enabled: event.target.checked });
691
- }
692
- })
693
- }),
694
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
695
- label: tt("config.transport"),
696
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
697
- className: panel_module_default.select,
698
- value: server.transport,
699
- onChange: (event) => {
700
- updateServer(id, { transport: event.target.value });
701
- },
702
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
703
- value: "stdio",
704
- children: "stdio"
705
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
706
- value: "streamable-http",
707
- children: "streamable-http"
708
- })]
709
- })
710
- }),
711
- server.transport === "stdio" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
712
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
713
- label: tt("config.command"),
714
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
715
- className: panel_module_default.input,
716
- value: server.command ?? "",
717
- onChange: (event) => {
718
- updateServer(id, { command: event.target.value });
719
- }
720
- })
721
- }),
722
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
723
- label: tt("config.args"),
724
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
725
- className: panel_module_default.input,
726
- rows: 3,
727
- value: argsText[id] ?? "",
728
- onChange: (event) => {
729
- setArgsText((prev) => ({
730
- ...prev,
731
- [id]: event.target.value
732
- }));
733
- },
734
- onBlur: () => {
735
- commitArgs(id);
736
- }
737
- })
738
- }),
739
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
740
- label: tt("config.env"),
741
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
742
- className: panel_module_default.input,
743
- rows: 4,
744
- value: envText[id] ?? "{}",
745
- onChange: (event) => {
746
- setEnvText((prev) => ({
747
- ...prev,
748
- [id]: event.target.value
749
- }));
750
- },
751
- onBlur: () => {
752
- commitRecord(id, "env");
753
- }
754
- })
755
- })
756
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
757
- label: tt("config.url"),
758
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
759
- className: panel_module_default.input,
760
- value: server.url ?? "",
761
- onChange: (event) => {
762
- updateServer(id, { url: event.target.value });
763
- }
764
- })
765
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
766
- label: tt("config.headers"),
767
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
768
- className: panel_module_default.input,
769
- rows: 4,
770
- value: headerText[id] ?? "{}",
771
- onChange: (event) => {
772
- setHeaderText((prev) => ({
773
- ...prev,
774
- [id]: event.target.value
775
- }));
776
- },
777
- onBlur: () => {
778
- commitRecord(id, "headers");
779
- }
780
- })
781
- })] })
782
- ]
783
- }, id)),
756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(McpSection, {
757
+ title: tt("config.odoo"),
758
+ row: config.connections.odoo,
759
+ secret: secrets.odoo,
760
+ onEnabled: (enabled) => {
761
+ patchConnection("odoo", { enabled });
762
+ },
763
+ onUrl: (url) => {
764
+ patchConnection("odoo", { url });
765
+ },
766
+ onSecret: (value) => {
767
+ setSecrets((prev) => ({
768
+ ...prev,
769
+ odoo: value
770
+ }));
771
+ },
772
+ urlLabel: tt("config.odoo.url"),
773
+ secretLabel: tt("config.odoo.apiKey")
774
+ }),
775
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(McpSection, {
776
+ title: tt("config.nextcloud"),
777
+ row: config.connections.nextcloud,
778
+ secret: secrets.nextcloud,
779
+ onEnabled: (enabled) => {
780
+ patchConnection("nextcloud", { enabled });
781
+ },
782
+ onUrl: (url) => {
783
+ patchConnection("nextcloud", { url });
784
+ },
785
+ onUsername: (username) => {
786
+ patchConnection("nextcloud", { username });
787
+ },
788
+ onSecret: (value) => {
789
+ setSecrets((prev) => ({
790
+ ...prev,
791
+ nextcloud: value
792
+ }));
793
+ },
794
+ urlLabel: tt("config.nextcloud.url"),
795
+ usernameLabel: tt("config.nextcloud.username"),
796
+ secretLabel: tt("config.nextcloud.appPassword")
797
+ }),
798
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(McpSection, {
799
+ title: tt("config.wecom"),
800
+ row: config.connections.wechat,
801
+ secret: secrets.wechat,
802
+ onEnabled: (enabled) => {
803
+ patchConnection("wechat", { enabled });
804
+ },
805
+ onSecret: (value) => {
806
+ setSecrets((prev) => ({
807
+ ...prev,
808
+ wechat: value
809
+ }));
810
+ },
811
+ secretLabel: tt("config.wecom.webhook")
812
+ }),
813
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(McpSection, {
814
+ title: tt("config.ha"),
815
+ row: config.connections.homeassistant,
816
+ secret: secrets.homeassistant,
817
+ onEnabled: (enabled) => {
818
+ patchConnection("homeassistant", { enabled });
819
+ },
820
+ onUrl: (url) => {
821
+ patchConnection("homeassistant", { url });
822
+ },
823
+ onSecret: (value) => {
824
+ setSecrets((prev) => ({
825
+ ...prev,
826
+ homeassistant: value
827
+ }));
828
+ },
829
+ urlLabel: tt("config.ha.url"),
830
+ secretLabel: tt("config.ha.token")
831
+ }),
784
832
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
785
833
  title: tt("config.billing"),
786
834
  children: [
package/lib/index.d.ts CHANGED
@@ -1702,12 +1702,32 @@ interface RezMcpServerSpec {
1702
1702
  /** Per-tool-call timeout in milliseconds. */
1703
1703
  toolCallTimeoutMs?: number;
1704
1704
  }
1705
+ /** Company MCP endpoints persisted under ~/.dsh/dsh-rez-suite.json. Secrets are not stored here. */
1706
+ interface RezConnections {
1707
+ odoo: {
1708
+ enabled: boolean;
1709
+ url: string;
1710
+ };
1711
+ nextcloud: {
1712
+ enabled: boolean;
1713
+ url: string;
1714
+ username: string;
1715
+ };
1716
+ wechat: {
1717
+ enabled: boolean;
1718
+ };
1719
+ homeassistant: {
1720
+ enabled: boolean;
1721
+ url: string;
1722
+ };
1723
+ }
1705
1724
  /** Full plugin configuration persisted under ~/.dsh/dsh-rez-suite.json. */
1706
1725
  interface RezConfig {
1707
1726
  enabled: boolean;
1708
1727
  announceToAgent: boolean;
1709
1728
  role: RezRoleId;
1710
1729
  servers: Record<string, RezMcpServerSpec>;
1730
+ connections: RezConnections;
1711
1731
  billing: {
1712
1732
  inputCostPer1k: number;
1713
1733
  outputCostPer1k: number;