@rezti/dsh-rez-suite 0.1.0

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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Status tab: live MCP server states and registered-tool counts.
3
+ */
4
+
5
+ import { useEffect, useState } from 'react'
6
+ import type { RezApi } from '../api.ts'
7
+ import type { RezStatusResponse } from '../../protocol.ts'
8
+ import { errorMessage, tt } from './helpers.ts'
9
+ import css from './panel.module.css'
10
+
11
+ export function StatusTab({ api }: { api: RezApi }) {
12
+ const [status, setStatus] = useState<RezStatusResponse | null>(null)
13
+ const [error, setError] = useState('')
14
+ const [loading, setLoading] = useState(true)
15
+
16
+ const load = async (): Promise<void> => {
17
+ setLoading(true)
18
+ setError('')
19
+ try {
20
+ setStatus(await api.status())
21
+ } catch (err) {
22
+ setError(errorMessage(err))
23
+ } finally {
24
+ setLoading(false)
25
+ }
26
+ }
27
+
28
+ useEffect(() => { void load() }, [])
29
+
30
+ if (loading) return <div className={css.message}>{tt('common.loading')}</div>
31
+ if (status === null) return <div className={css.error}>{error}</div>
32
+
33
+ return (
34
+ <div className={css.tabBody}>
35
+ <div className={css.toolbar}>
36
+ <span className={css.message}>{tt('status.total', { role: status.role, count: status.totalRegisteredTools })}</span>
37
+ <button type="button" className={css.button} onClick={() => { void load() }}>{tt('common.refresh')}</button>
38
+ </div>
39
+ {error !== '' && <div className={css.error}>{error}</div>}
40
+ <div className={css.tableWrap}>
41
+ <table className={css.table}>
42
+ <thead>
43
+ <tr>
44
+ <th>{tt('status.col.server')}</th>
45
+ <th>{tt('status.col.state')}</th>
46
+ <th>{tt('status.col.tools')}</th>
47
+ <th>{tt('status.col.registered')}</th>
48
+ <th>{tt('status.col.error')}</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ {status.servers.map(server => (
53
+ <tr key={server.server}>
54
+ <td>{server.server}</td>
55
+ <td><span className={css.badge + ' ' + (server.state === 'connected' ? css.badgeOk : css.badgeFail)}>{server.state}</span></td>
56
+ <td>{server.toolCount}</td>
57
+ <td>{server.registeredTools}</td>
58
+ <td>{server.lastError ?? ''}</td>
59
+ </tr>
60
+ ))}
61
+ </tbody>
62
+ </table>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Rez panel controller: single owner of open/closed state (browser session).
3
+ */
4
+
5
+ export interface PanelControllerSnapshot {
6
+ panelOpen: boolean
7
+ }
8
+
9
+ export class PanelController {
10
+ private panelOpen = false
11
+ private listeners = new Set<() => void>()
12
+
13
+ getSnapshot(): PanelControllerSnapshot {
14
+ return { panelOpen: this.panelOpen }
15
+ }
16
+
17
+ subscribe(fn: () => void): () => void {
18
+ this.listeners.add(fn)
19
+ return () => { this.listeners.delete(fn) }
20
+ }
21
+
22
+ open(): void {
23
+ if (this.panelOpen) return
24
+ this.panelOpen = true
25
+ this.notify()
26
+ }
27
+
28
+ close(): void {
29
+ if (!this.panelOpen) return
30
+ this.panelOpen = false
31
+ this.notify()
32
+ }
33
+
34
+ toggle(): void {
35
+ if (this.panelOpen) this.close()
36
+ else this.open()
37
+ }
38
+
39
+ private notify(): void {
40
+ for (const fn of [...this.listeners]) fn()
41
+ }
42
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared panel helpers: active-language dictionary pick and error extraction.
3
+ */
4
+
5
+ import { en, t, zh, type RezKey } from '../locales.ts'
6
+
7
+ export type TranslateValues = Record<string, string | number>
8
+
9
+ export function dictionary(): Record<string, string> {
10
+ const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh'
11
+ return lang.toLowerCase().startsWith('en') ? { ...en } : { ...zh }
12
+ }
13
+
14
+ export function tt(key: RezKey, values?: TranslateValues): string {
15
+ return t(dictionary(), key, values)
16
+ }
17
+
18
+ export function errorMessage(error: unknown): string {
19
+ if (error instanceof Error) return error.message
20
+ return String(error)
21
+ }
@@ -0,0 +1,316 @@
1
+ /**
2
+ * dsh-rez-suite panel styles. Scoped by plugin data attributes, colors ride
3
+ * dsh --dsw-* tokens. The center-column takeover rules are attribute-scoped
4
+ * and must stay in this stylesheet (imported by mount.tsx).
5
+ */
6
+
7
+ [data-pane='conversation'] {
8
+ position: relative;
9
+ }
10
+
11
+ [data-dsh-rez-view] {
12
+ position: absolute;
13
+ inset: 0;
14
+ display: none;
15
+ z-index: 60;
16
+ background: var(--dsw-alias-bg-base);
17
+ }
18
+
19
+ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-dsh-rez-view] {
20
+ display: block;
21
+ }
22
+
23
+ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-pane='conversation'] > :not([data-dsh-rez-view]),
24
+ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [class*='centerCol'] > :not([data-dsh-rez-view]) {
25
+ display: none !important;
26
+ }
27
+
28
+ .entry {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 8px;
32
+ width: 100%;
33
+ height: 32px;
34
+ padding: 0 12px;
35
+ background: transparent;
36
+ border: none;
37
+ border-radius: 8px;
38
+ color: var(--dsw-alias-label-secondary);
39
+ cursor: pointer;
40
+ font-size: 13px;
41
+ white-space: nowrap;
42
+ }
43
+
44
+ .entry:hover {
45
+ background: var(--dsw-specific-sidebar-nav-item-hover);
46
+ color: var(--dsw-alias-label-primary);
47
+ }
48
+
49
+ .entryIcon {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ flex: none;
54
+ }
55
+
56
+ .entryLabel {
57
+ overflow: hidden;
58
+ text-overflow: ellipsis;
59
+ }
60
+
61
+ [data-dsh-frame][data-sidebar-collapsed] .entry {
62
+ justify-content: center;
63
+ padding: 0;
64
+ width: 100%;
65
+ }
66
+
67
+ [data-dsh-frame][data-sidebar-collapsed] .entryLabel {
68
+ display: none;
69
+ }
70
+
71
+ .view {
72
+ overflow: hidden;
73
+ }
74
+
75
+ .panel {
76
+ display: flex;
77
+ flex-direction: column;
78
+ height: 100%;
79
+ min-width: 0;
80
+ min-height: 0;
81
+ padding: 14px 16px 16px;
82
+ gap: 10px;
83
+ background: var(--dsw-alias-bg-base);
84
+ color: var(--dsw-alias-label-primary);
85
+ font-family: var(--dsw-font-family);
86
+ }
87
+
88
+ .panelHeader {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 10px;
92
+ flex: none;
93
+ }
94
+
95
+ .panelTitle {
96
+ margin: 0;
97
+ flex: 1;
98
+ font-size: 16px;
99
+ font-weight: 700;
100
+ }
101
+
102
+ .iconButton {
103
+ border: none;
104
+ background: transparent;
105
+ color: var(--dsw-alias-label-secondary);
106
+ cursor: pointer;
107
+ font-size: 14px;
108
+ padding: 4px 8px;
109
+ border-radius: 6px;
110
+ }
111
+
112
+ .iconButton:hover {
113
+ background: var(--dsw-alias-interactive-bg-hover);
114
+ color: var(--dsw-alias-label-primary);
115
+ }
116
+
117
+ .tabBar {
118
+ display: flex;
119
+ gap: 2px;
120
+ flex: none;
121
+ border-bottom: 1px solid var(--dsw-alias-border-l1);
122
+ }
123
+
124
+ .tab {
125
+ padding: 7px 14px;
126
+ font-size: 13px;
127
+ color: var(--dsw-alias-label-secondary);
128
+ background: transparent;
129
+ border: none;
130
+ border-bottom: 2px solid transparent;
131
+ border-radius: 6px 6px 0 0;
132
+ cursor: pointer;
133
+ }
134
+
135
+ .tab:hover {
136
+ color: var(--dsw-alias-label-primary);
137
+ background: var(--dsw-alias-interactive-bg-hover);
138
+ }
139
+
140
+ .tab[data-active] {
141
+ color: var(--dsw-alias-label-primary);
142
+ font-weight: 600;
143
+ border-bottom-color: var(--dsw-alias-state-business-primary);
144
+ }
145
+
146
+ .panelContent {
147
+ flex: 1;
148
+ min-height: 0;
149
+ display: flex;
150
+ flex-direction: column;
151
+ overflow: hidden;
152
+ }
153
+
154
+ .tabBody {
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: 12px;
158
+ flex: 1;
159
+ min-height: 0;
160
+ overflow-y: auto;
161
+ }
162
+
163
+ .section {
164
+ display: flex;
165
+ flex-direction: column;
166
+ gap: 8px;
167
+ padding: 12px;
168
+ border: 1px solid var(--dsw-alias-border-l1);
169
+ border-radius: 10px;
170
+ background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-base));
171
+ }
172
+
173
+ .sectionTitle {
174
+ margin: 0;
175
+ font-size: 13px;
176
+ font-weight: 600;
177
+ }
178
+
179
+ .field {
180
+ display: grid;
181
+ grid-template-columns: 140px 1fr;
182
+ gap: 8px;
183
+ align-items: center;
184
+ }
185
+
186
+ .label {
187
+ font-size: 12.5px;
188
+ color: var(--dsw-alias-label-secondary);
189
+ }
190
+
191
+ .input, .select {
192
+ padding: 6px 8px;
193
+ font-size: 13px;
194
+ color: var(--dsw-alias-label-primary);
195
+ background: var(--dsw-specific-input-major);
196
+ border: 1px solid var(--dsw-alias-border-l2);
197
+ border-radius: 8px;
198
+ outline: none;
199
+ }
200
+
201
+ .checkbox {
202
+ justify-self: start;
203
+ width: 15px;
204
+ height: 15px;
205
+ }
206
+
207
+ .toolbar {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 8px;
211
+ flex: none;
212
+ flex-wrap: wrap;
213
+ }
214
+
215
+ .button {
216
+ padding: 7px 12px;
217
+ font-size: 13px;
218
+ border-radius: 8px;
219
+ border: 1px solid var(--dsw-alias-border-l2);
220
+ background: var(--dsw-specific-input-major);
221
+ color: var(--dsw-alias-label-primary);
222
+ cursor: pointer;
223
+ }
224
+
225
+ .button:hover {
226
+ background: var(--dsw-alias-interactive-bg-hover);
227
+ }
228
+
229
+ .primaryButton {
230
+ background: var(--dsw-alias-state-business-primary);
231
+ border-color: var(--dsw-alias-state-business-primary);
232
+ color: #fff;
233
+ }
234
+
235
+ .dangerButton {
236
+ background: var(--dsw-alias-state-danger, #c62828);
237
+ border-color: var(--dsw-alias-state-danger, #c62828);
238
+ color: #fff;
239
+ }
240
+
241
+ .message {
242
+ font-size: 12.5px;
243
+ color: var(--dsw-alias-label-secondary);
244
+ }
245
+
246
+ .error {
247
+ color: var(--dsw-alias-state-danger, #c62828);
248
+ }
249
+
250
+ .tableWrap {
251
+ flex: 1;
252
+ min-height: 0;
253
+ overflow: auto;
254
+ border: 1px solid var(--dsw-alias-border-l1);
255
+ border-radius: 10px;
256
+ }
257
+
258
+ .table {
259
+ width: 100%;
260
+ border-collapse: collapse;
261
+ font-size: 12.5px;
262
+ }
263
+
264
+ .table th {
265
+ position: sticky;
266
+ top: 0;
267
+ z-index: 1;
268
+ padding: 8px 10px;
269
+ text-align: left;
270
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));
271
+ }
272
+
273
+ .table td {
274
+ padding: 8px 10px;
275
+ border-top: 1px solid var(--dsw-alias-border-l1);
276
+ vertical-align: top;
277
+ }
278
+
279
+ .badge {
280
+ display: inline-block;
281
+ padding: 2px 8px;
282
+ border-radius: 999px;
283
+ font-size: 11px;
284
+ }
285
+
286
+ .badgeOk {
287
+ background: rgba(34, 154, 85, 0.14);
288
+ color: #229a55;
289
+ }
290
+
291
+ .badgeFail {
292
+ background: rgba(198, 40, 40, 0.14);
293
+ color: #c62828;
294
+ }
295
+
296
+ .cards {
297
+ display: grid;
298
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
299
+ gap: 8px;
300
+ }
301
+
302
+ .card {
303
+ padding: 10px;
304
+ border: 1px solid var(--dsw-alias-border-l1);
305
+ border-radius: 10px;
306
+ }
307
+
308
+ .cardLabel {
309
+ font-size: 11px;
310
+ color: var(--dsw-alias-label-tertiary);
311
+ }
312
+
313
+ .cardValue {
314
+ font-size: 15px;
315
+ font-weight: 600;
316
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Sidebar entry injection (DOM-level extension, task-board/ssh precedent).
3
+ */
4
+
5
+ import type { PanelController } from './panel/controller.ts'
6
+ import { tt } from './panel/helpers.ts'
7
+ import css from './panel/panel.module.css'
8
+
9
+ export const ENTRY_SELECTOR = '[data-dsh-rez-entry]'
10
+
11
+ const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="2.5" width="11" height="11" rx="2"/><path d="M5 8l2 2 4-4"/></svg>'
12
+
13
+ function sidebarRoot(): HTMLElement | undefined {
14
+ const column = document.querySelector<HTMLElement>('[data-pane="sidebar"], [class*="sidebarCol"]')
15
+ if (column === null) return undefined
16
+ const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
17
+ return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
18
+ }
19
+
20
+ function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
21
+ const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
22
+ if (nested !== null) return nested
23
+ for (const child of root.children) {
24
+ if (child.tagName === 'BUTTON') return child as HTMLButtonElement
25
+ }
26
+ return undefined
27
+ }
28
+
29
+ function createEntry(controller: PanelController): HTMLButtonElement {
30
+ const entry = document.createElement('button')
31
+ entry.type = 'button'
32
+ entry.dataset.dshRezEntry = ''
33
+ entry.className = css.entry
34
+ entry.setAttribute('aria-label', tt('entry.label'))
35
+ entry.setAttribute('title', tt('entry.tooltip'))
36
+ entry.innerHTML = '<span class="' + css.entryIcon + '">' + ICON + '</span><span class="' + css.entryLabel + '">' + tt('entry.label') + '</span>'
37
+ entry.addEventListener('click', () => { controller.toggle() })
38
+ return entry
39
+ }
40
+
41
+ function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
42
+ const button = newSessionButton(root)
43
+ if (button === undefined) return false
44
+ if (entry.parentElement !== root) {
45
+ const row = button.closest('[class*="logoRow"]')
46
+ const base = (row !== null && row.parentElement === root) ? row : button
47
+ const family = Array.from(root.children).filter(
48
+ (el): el is HTMLElement => el instanceof HTMLElement && el.matches('[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-rez-entry]'),
49
+ )
50
+ const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling
51
+ root.insertBefore(entry, anchor)
52
+ }
53
+ return true
54
+ }
55
+
56
+ export function mountSidebarEntry(controller: PanelController): () => void {
57
+ const entry = createEntry(controller)
58
+ let root: HTMLElement | undefined
59
+ let placed = false
60
+
61
+ const place = (): void => {
62
+ const next = sidebarRoot()
63
+ if (next === undefined) return
64
+ if (root !== undefined && root !== next) {
65
+ if (entry.parentElement === root) entry.remove()
66
+ placed = false
67
+ }
68
+ root = next
69
+ if (!placed) placed = placeEntry(root, entry)
70
+ }
71
+
72
+ const observer = new MutationObserver(place)
73
+ observer.observe(document.body, { childList: true, subtree: true })
74
+ place()
75
+
76
+ return () => {
77
+ observer.disconnect()
78
+ entry.remove()
79
+ }
80
+ }
package/src/index.ts ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * dsh-rez-suite — employee-facing host half.
3
+ *
4
+ * MCP connections are owned by official @deepseek-ai/dsh-mcp-client rows in
5
+ * this package's cordis.patch.yml. This plugin only mounts the Rez panel,
6
+ * loopback settings API, and a short system-prompt announcement. Do not
7
+ * start a second MCP host here.
8
+ */
9
+
10
+ import type { Context } from '@deepseek-ai/cordis'
11
+ import { homedir } from 'node:os'
12
+ import { join } from 'node:path'
13
+ import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
14
+ import z from '@deepseek-ai/schemastery'
15
+ import type {} from '@deepseek-ai/dsh-host-webserver'
16
+ import type {} from '@deepseek-ai/dsh-system-prompt'
17
+ import type {} from '@deepseek-ai/dsh-tools'
18
+ import type { McpHost } from './mcp-host.ts'
19
+ import { makeRoutes } from './routes.ts'
20
+ import { defaultConfig, mergeConfig, saveConfig } from './store.ts'
21
+ import { TokenManager } from './token-manager.ts'
22
+ import { rezConfigTool, rezStatusTool } from './tools.ts'
23
+ import type { RezConfig, RezServerStatus, RezTestResult } from './protocol.ts'
24
+
25
+ export const name = 'rez-suite'
26
+
27
+ export const inject = ['webServer', 'tools', 'systemPrompt']
28
+
29
+ export const REZ_SETTINGS_NAMESPACE = settingsNamespace('dsh-rez-suite')
30
+
31
+ const serverSchema = z.object({
32
+ enabled: z.boolean().default(false),
33
+ transport: z.union(['stdio', 'streamable-http'] as const).default('stdio'),
34
+ command: z.string().required(false),
35
+ args: z.array(z.string()).required(false),
36
+ env: z.dict(z.string()).required(false),
37
+ cwd: z.string().required(false),
38
+ url: z.string().required(false),
39
+ headers: z.dict(z.string()).required(false),
40
+ toolCallTimeoutMs: z.number().required(false),
41
+ })
42
+
43
+ export const Config: z<RezConfig> = z.object({
44
+ enabled: z.boolean().default(true),
45
+ announceToAgent: z.boolean().default(true),
46
+ role: z.union(['engineer', 'sales', 'operations', 'all'] as const).default('all'),
47
+ servers: z.dict(serverSchema).default({}),
48
+ billing: z.object({
49
+ inputCostPer1k: z.number().default(0.001),
50
+ outputCostPer1k: z.number().default(0.002),
51
+ monthlyBudget: z.number().default(0),
52
+ }).default({ inputCostPer1k: 0.001, outputCostPer1k: 0.002, monthlyBudget: 0 }),
53
+ })
54
+
55
+ const SECTION_ORDER = 150
56
+
57
+ const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant'] as const
58
+
59
+ export const REZ_GUIDANCE = [
60
+ '本机已安装 ReZ-TI 套件(一次安装):工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。',
61
+ '工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。',
62
+ '密钥走官方 dsh-credentials / 环境变量:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。',
63
+ '意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
64
+ '涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
65
+ ].join('')
66
+
67
+ function normalize(input: Partial<RezConfig> | undefined): RezConfig {
68
+ return mergeConfig(defaultConfig(), input ?? {})
69
+ }
70
+
71
+ function delegatedHost(): McpHost {
72
+ const status = (): RezServerStatus[] => COMPOSED_SERVERS.map(server => ({
73
+ server,
74
+ enabled: true,
75
+ state: 'disconnected',
76
+ toolCount: 0,
77
+ registeredTools: 0,
78
+ lastError: 'owned by @deepseek-ai/dsh-mcp-client',
79
+ }))
80
+ const test = async (): Promise<RezTestResult[]> => COMPOSED_SERVERS.map(server => ({
81
+ server,
82
+ ok: false,
83
+ error: 'MCP is composed by dsh-mcp-client; set REZ_* credentials and check dsh logs',
84
+ }))
85
+ return {
86
+ status,
87
+ test,
88
+ sync: async () => undefined,
89
+ dispose: async () => undefined,
90
+ } as unknown as McpHost
91
+ }
92
+
93
+ export function apply(ctx: Context, config?: RezConfig): void {
94
+ let current: () => RezConfig = () => normalize(config)
95
+
96
+ const tokens = new TokenManager(joinTokenDb())
97
+ const host = delegatedHost()
98
+ ctx.effect(() => () => { tokens.close() }, 'dsh-rez-suite: ledger view')
99
+
100
+ const tools = [
101
+ rezStatusTool(host, () => current()),
102
+ rezConfigTool(() => current()),
103
+ ]
104
+
105
+ const routes = makeRoutes({
106
+ getConfig: () => current(),
107
+ updateConfig: async (next) => {
108
+ const value = normalize(next as Partial<RezConfig>)
109
+ saveConfig(value)
110
+ current = () => value
111
+ try {
112
+ await ctx.settings?.update(REZ_SETTINGS_NAMESPACE, value)
113
+ } catch (error) {
114
+ console.warn('[dsh-rez-suite] settings update failed, using local store:', error)
115
+ }
116
+ return value
117
+ },
118
+ host,
119
+ tokens,
120
+ })
121
+
122
+ let disposeRoutes: (() => void) | undefined
123
+ let disposeTools: (() => void) | undefined
124
+ let disposeSection: (() => void) | undefined
125
+
126
+ const sync = (): void => {
127
+ if (disposeSection !== undefined) { disposeSection(); disposeSection = undefined }
128
+ if (disposeRoutes !== undefined) { disposeRoutes(); disposeRoutes = undefined }
129
+ if (disposeTools !== undefined) { disposeTools(); disposeTools = undefined }
130
+
131
+ const value = current()
132
+ if (!value.enabled) return
133
+
134
+ if (value.announceToAgent) {
135
+ disposeSection = ctx.systemPrompt.section({
136
+ name: 'plugin:dsh-rez-suite',
137
+ order: SECTION_ORDER,
138
+ text: REZ_GUIDANCE,
139
+ })
140
+ }
141
+
142
+ disposeRoutes = ctx.effect(() => {
143
+ const disposers = routes.map(route => ctx.webServer.register(route))
144
+ return () => { for (const dispose of disposers) dispose() }
145
+ }, 'dsh-rez-suite: routes')
146
+
147
+ disposeTools = ctx.effect(() => {
148
+ const disposers = tools.map(tool => ctx.tools.register(tool))
149
+ return () => { for (const dispose of disposers) dispose() }
150
+ }, 'dsh-rez-suite: tools')
151
+ }
152
+
153
+ installSettingsSection(ctx, REZ_SETTINGS_NAMESPACE, Config, normalize(config), {
154
+ setSource: (source) => {
155
+ current = source
156
+ sync()
157
+ },
158
+ onChange: sync,
159
+ })
160
+
161
+ sync()
162
+ }
163
+
164
+ function joinTokenDb(): string {
165
+ return join(homedir(), '.dsh', 'dsh-rez-token-manager.sqlite')
166
+ }