@2en/clawly-plugins 1.22.2 → 1.23.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,383 @@
1
+ /**
2
+ * On plugin init, patches openclaw.json to set all business config that was
3
+ * previously written by provision's buildConfig().
4
+ *
5
+ * Domain helpers read the pluginConfig, apply enforce or set-if-missing
6
+ * semantics, and write once if anything changed. This runs before
7
+ * setupModelGateway so agents.defaults.model is available for the model
8
+ * provider setup.
9
+ *
10
+ * Backward compatibility: old sprites have pluginConfig with only 4 fields
11
+ * (skill/model gateway credentials). Helpers check for the new fields and
12
+ * skip gracefully when absent.
13
+ */
14
+
15
+ import path from 'node:path'
16
+
17
+ import type {PluginApi} from './index'
18
+ import {
19
+ PROVIDER_NAME,
20
+ patchModelGateway,
21
+ readOpenclawConfig,
22
+ resolveStateDir,
23
+ writeOpenclawConfig,
24
+ } from './model-gateway-setup'
25
+
26
+ export interface ConfigPluginConfig {
27
+ agentId?: string
28
+ agentName?: string
29
+ workspaceDir?: string
30
+ defaultModel?: string
31
+ defaultImageModel?: string
32
+ sessionMainKey?: string
33
+ elevenlabsApiKey?: string
34
+ elevenlabsVoiceId?: string
35
+ braveSearchApiKey?: string
36
+ }
37
+
38
+ function toPC(api: PluginApi): ConfigPluginConfig {
39
+ return (api.pluginConfig ?? {}) as ConfigPluginConfig
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Domain helpers — each returns true when config was mutated
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export function patchAgent(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
47
+ if (!pc.agentId) return false
48
+
49
+ let dirty = false
50
+ const agents = (config.agents ?? {}) as Record<string, unknown>
51
+ const defaults = (agents.defaults ?? {}) as Record<string, unknown>
52
+
53
+ // agents.list: find-or-create entry by id, then enforce our fields only
54
+ const list = (Array.isArray(agents.list) ? agents.list : []) as Record<string, unknown>[]
55
+ let entry = list.find((e) => e.id === pc.agentId)
56
+ if (!entry) {
57
+ entry = {id: pc.agentId}
58
+ list.push(entry)
59
+ dirty = true
60
+ }
61
+ const agentName = pc.agentName ?? 'Clawly'
62
+ if (entry.name !== agentName) {
63
+ entry.name = agentName
64
+ dirty = true
65
+ }
66
+ if (entry.default !== true) {
67
+ entry.default = true
68
+ dirty = true
69
+ }
70
+ const workspace = pc.workspaceDir ?? ''
71
+ if (entry.workspace !== workspace) {
72
+ entry.workspace = workspace
73
+ dirty = true
74
+ }
75
+ if (agents.list !== list) {
76
+ agents.list = list
77
+ dirty = true
78
+ }
79
+
80
+ // model: set-if-missing
81
+ if (pc.defaultModel && !defaults.model) {
82
+ defaults.model = {primary: `${PROVIDER_NAME}/${pc.defaultModel}`}
83
+ dirty = true
84
+ }
85
+
86
+ // imageModel: set-if-missing
87
+ if (pc.defaultImageModel && !defaults.imageModel) {
88
+ defaults.imageModel = {
89
+ primary: `${PROVIDER_NAME}/${pc.defaultImageModel}`,
90
+ fallbacks: [],
91
+ }
92
+ dirty = true
93
+ }
94
+
95
+ // sandbox.mode: enforce (only set the field we care about)
96
+ const sandbox = (defaults.sandbox ?? {}) as Record<string, unknown>
97
+ if (sandbox.mode !== 'off') {
98
+ sandbox.mode = 'off'
99
+ defaults.sandbox = sandbox
100
+ dirty = true
101
+ }
102
+
103
+ // verboseDefault: set-if-missing
104
+ if (defaults.verboseDefault === undefined) {
105
+ defaults.verboseDefault = 'on'
106
+ dirty = true
107
+ }
108
+
109
+ if (dirty) {
110
+ agents.defaults = defaults
111
+ config.agents = agents
112
+ }
113
+
114
+ return dirty
115
+ }
116
+
117
+ export function patchGateway(config: Record<string, unknown>): boolean {
118
+ let dirty = false
119
+ const gateway = (config.gateway ?? {}) as Record<string, unknown>
120
+
121
+ if (gateway.mode !== 'local') {
122
+ gateway.mode = 'local'
123
+ dirty = true
124
+ }
125
+
126
+ // http.endpoints.chatCompletions.enabled: enforce (only set the field we care about)
127
+ const http = (gateway.http ?? {}) as Record<string, unknown>
128
+ const endpoints = (http.endpoints ?? {}) as Record<string, unknown>
129
+ const chatCompletions = (endpoints.chatCompletions ?? {}) as Record<string, unknown>
130
+ if (chatCompletions.enabled !== true) {
131
+ chatCompletions.enabled = true
132
+ endpoints.chatCompletions = chatCompletions
133
+ http.endpoints = endpoints
134
+ gateway.http = http
135
+ dirty = true
136
+ }
137
+
138
+ const desiredProxies = ['0.0.0.0/0']
139
+ if (JSON.stringify(gateway.trustedProxies) !== JSON.stringify(desiredProxies)) {
140
+ gateway.trustedProxies = desiredProxies
141
+ dirty = true
142
+ }
143
+
144
+ if (dirty) config.gateway = gateway
145
+ return dirty
146
+ }
147
+
148
+ export function patchBrowser(config: Record<string, unknown>): boolean {
149
+ let dirty = false
150
+
151
+ // browser: enforce individual fields only
152
+ const browser = (config.browser ?? {}) as Record<string, unknown>
153
+ if (browser.headless !== true) {
154
+ browser.headless = true
155
+ dirty = true
156
+ }
157
+ if (browser.noSandbox !== true) {
158
+ browser.noSandbox = true
159
+ dirty = true
160
+ }
161
+ if (browser.attachOnly !== false) {
162
+ browser.attachOnly = false
163
+ dirty = true
164
+ }
165
+ if (dirty) config.browser = browser
166
+
167
+ // commands: enforce individual fields only
168
+ const commands = (config.commands ?? {}) as Record<string, unknown>
169
+ if (commands.restart !== true) {
170
+ commands.restart = true
171
+ config.commands = commands
172
+ dirty = true
173
+ }
174
+
175
+ return dirty
176
+ }
177
+
178
+ export function patchSessionMainKey(
179
+ config: Record<string, unknown>,
180
+ pc: ConfigPluginConfig,
181
+ ): boolean {
182
+ if (!pc.sessionMainKey) return false
183
+
184
+ const session = (config.session ?? {}) as Record<string, unknown>
185
+ if (session.mainKey === pc.sessionMainKey) return false
186
+
187
+ session.mainKey = pc.sessionMainKey
188
+ config.session = session
189
+ return true
190
+ }
191
+
192
+ export function patchTts(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
193
+ if (!pc.elevenlabsApiKey) return false
194
+
195
+ let dirty = false
196
+ const messages = (config.messages ?? {}) as Record<string, unknown>
197
+ const tts = (messages.tts ?? {}) as Record<string, unknown>
198
+ const elevenlabs = (tts.elevenlabs ?? {}) as Record<string, unknown>
199
+
200
+ // Credentials: enforce
201
+ if (elevenlabs.apiKey !== pc.elevenlabsApiKey) {
202
+ elevenlabs.apiKey = pc.elevenlabsApiKey
203
+ dirty = true
204
+ }
205
+ if (elevenlabs.baseUrl !== 'https://api.elevenlabs.io') {
206
+ elevenlabs.baseUrl = 'https://api.elevenlabs.io'
207
+ dirty = true
208
+ }
209
+ const voiceId = pc.elevenlabsVoiceId ?? ''
210
+ if (elevenlabs.voiceId !== voiceId) {
211
+ elevenlabs.voiceId = voiceId
212
+ dirty = true
213
+ }
214
+
215
+ // Tuning: set-if-missing
216
+ if (tts.auto === undefined) {
217
+ tts.auto = 'tagged'
218
+ dirty = true
219
+ }
220
+ if (tts.mode === undefined) {
221
+ tts.mode = 'final'
222
+ dirty = true
223
+ }
224
+ if (tts.provider === undefined) {
225
+ tts.provider = 'elevenlabs'
226
+ dirty = true
227
+ }
228
+ if (tts.summaryModel === undefined && pc.defaultModel) {
229
+ tts.summaryModel = `${PROVIDER_NAME}/${pc.defaultModel}`
230
+ dirty = true
231
+ }
232
+ if (tts.modelOverrides === undefined) {
233
+ tts.modelOverrides = {enabled: true}
234
+ dirty = true
235
+ }
236
+ if (tts.maxTextLength === undefined) {
237
+ tts.maxTextLength = 4000
238
+ dirty = true
239
+ }
240
+ if (tts.timeoutMs === undefined) {
241
+ tts.timeoutMs = 30000
242
+ dirty = true
243
+ }
244
+ if (tts.prefsPath === undefined) {
245
+ tts.prefsPath = '~/.openclaw/settings/tts.json'
246
+ dirty = true
247
+ }
248
+
249
+ // elevenlabs tuning: set-if-missing
250
+ if (elevenlabs.modelId === undefined) {
251
+ elevenlabs.modelId = 'eleven_multilingual_v2'
252
+ dirty = true
253
+ }
254
+ if (elevenlabs.seed === undefined) {
255
+ elevenlabs.seed = 42
256
+ dirty = true
257
+ }
258
+ if (elevenlabs.applyTextNormalization === undefined) {
259
+ elevenlabs.applyTextNormalization = 'auto'
260
+ dirty = true
261
+ }
262
+ if (elevenlabs.languageCode === undefined) {
263
+ elevenlabs.languageCode = 'en'
264
+ dirty = true
265
+ }
266
+ if (elevenlabs.voiceSettings === undefined) {
267
+ elevenlabs.voiceSettings = {
268
+ stability: 0.5,
269
+ similarityBoost: 0.75,
270
+ style: 0.0,
271
+ useSpeakerBoost: true,
272
+ speed: 1.0,
273
+ }
274
+ dirty = true
275
+ }
276
+
277
+ if (dirty) {
278
+ tts.elevenlabs = elevenlabs
279
+ messages.tts = tts
280
+ config.messages = messages
281
+ }
282
+
283
+ return dirty
284
+ }
285
+
286
+ export function patchWebSearch(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
287
+ if (!pc.braveSearchApiKey) return false
288
+
289
+ let dirty = false
290
+ const tools = (config.tools ?? {}) as Record<string, unknown>
291
+ const web = (tools.web ?? {}) as Record<string, unknown>
292
+ const search = (web.search ?? {}) as Record<string, unknown>
293
+
294
+ // Credentials: enforce
295
+ if (search.provider !== 'brave') {
296
+ search.provider = 'brave'
297
+ dirty = true
298
+ }
299
+ if (search.apiKey !== pc.braveSearchApiKey) {
300
+ search.apiKey = pc.braveSearchApiKey
301
+ dirty = true
302
+ }
303
+
304
+ // Tuning: set-if-missing
305
+ if (search.maxResults === undefined) {
306
+ search.maxResults = 5
307
+ dirty = true
308
+ }
309
+ if (search.timeoutSeconds === undefined) {
310
+ search.timeoutSeconds = 30
311
+ dirty = true
312
+ }
313
+
314
+ if (dirty) {
315
+ web.search = search
316
+ tools.web = web
317
+ config.tools = tools
318
+ }
319
+
320
+ return dirty
321
+ }
322
+
323
+ export function patchSession(config: Record<string, unknown>): boolean {
324
+ let dirty = false
325
+
326
+ // session.dmScope: enforce
327
+ const session = (config.session ?? {}) as Record<string, unknown>
328
+ if (session.dmScope !== 'per-channel-peer') {
329
+ session.dmScope = 'per-channel-peer'
330
+ config.session = session
331
+ dirty = true
332
+ }
333
+
334
+ // tools.sessions.visibility: enforce
335
+ const tools = (config.tools ?? {}) as Record<string, unknown>
336
+ const sessions = (tools.sessions ?? {}) as Record<string, unknown>
337
+ if (sessions.visibility !== 'agent') {
338
+ sessions.visibility = 'agent'
339
+ tools.sessions = sessions
340
+ config.tools = tools
341
+ dirty = true
342
+ }
343
+
344
+ return dirty
345
+ }
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // Entry point — single read, patch all, single write
349
+ // ---------------------------------------------------------------------------
350
+
351
+ export function setupConfig(api: PluginApi): void {
352
+ const stateDir = resolveStateDir(api)
353
+ if (!stateDir) {
354
+ api.logger.warn('Cannot resolve state dir — config setup skipped.')
355
+ return
356
+ }
357
+
358
+ const configPath = path.join(stateDir, 'openclaw.json')
359
+ const config = readOpenclawConfig(configPath)
360
+ const pc = toPC(api)
361
+
362
+ let dirty = false
363
+ dirty = patchAgent(config, pc) || dirty
364
+ dirty = patchGateway(config) || dirty
365
+ dirty = patchBrowser(config) || dirty
366
+ dirty = patchSessionMainKey(config, pc) || dirty
367
+ dirty = patchSession(config) || dirty
368
+ dirty = patchTts(config, pc) || dirty
369
+ dirty = patchWebSearch(config, pc) || dirty
370
+ dirty = patchModelGateway(config, api) || dirty
371
+
372
+ if (!dirty) {
373
+ api.logger.info('Config setup: no changes needed.')
374
+ return
375
+ }
376
+
377
+ try {
378
+ writeOpenclawConfig(configPath, config)
379
+ api.logger.info('Config setup: patched openclaw.json.')
380
+ } catch (err) {
381
+ api.logger.error(`Config setup failed: ${(err as Error).message}`)
382
+ }
383
+ }
@@ -279,9 +279,20 @@ describe('shouldSkipPushForMessage', () => {
279
279
  expect(shouldSkipPushForMessage('All good. HEARTBEAT_OK')).toBe('heartbeat ack')
280
280
  })
281
281
 
282
- test('does not skip long message containing HEARTBEAT_OK', () => {
283
- const longMsg = 'HEARTBEAT_OK ' + 'x'.repeat(400)
284
- expect(shouldSkipPushForMessage(longMsg)).toBeNull()
282
+ test('skips verbose heartbeat ack ending with HEARTBEAT_OK', () => {
283
+ const verbose =
284
+ 'The user said hello recently. Looking at HEARTBEAT.md checklist: nothing needs attention. HEARTBEAT_OK'
285
+ expect(shouldSkipPushForMessage(verbose)).toBe('heartbeat ack')
286
+ })
287
+
288
+ test('does not skip message mentioning HEARTBEAT_OK mid-text', () => {
289
+ expect(
290
+ shouldSkipPushForMessage('HEARTBEAT_OK is a status code I output after each check.'),
291
+ ).toBeNull()
292
+ })
293
+
294
+ test('skips heartbeat ack with non-ASCII trailing punctuation', () => {
295
+ expect(shouldSkipPushForMessage('Nothing to report. HEARTBEAT_OK。')).toBe('heartbeat ack')
285
296
  })
286
297
 
287
298
  test('skips system prompt leak', () => {
@@ -79,8 +79,8 @@ export function shouldSkipPushForMessage(text: string): string | null {
79
79
  // Agent sentinel "nothing to say" — mobile hides as "silentReply"
80
80
  if (trimmed === 'NO_REPLY') return 'silent reply'
81
81
 
82
- // Short heartbeat acknowledgment — mobile hides as "heartbeatAck"
83
- if (text.includes('HEARTBEAT_OK') && text.length < 400) return 'heartbeat ack'
82
+ // Heartbeat acknowledgment (HEARTBEAT_OK as ending sentinel) — mobile hides as "heartbeatAck"
83
+ if (/HEARTBEAT_OK[\p{P}\s]*$/u.test(text)) return 'heartbeat ack'
84
84
 
85
85
  // Agent echoed system prompt metadata — mobile hides as "systemPromptLeak"
86
86
  if (text.includes('Conversation info (untrusted metadata)')) return 'system prompt leak'
package/index.ts CHANGED
@@ -32,13 +32,12 @@
32
32
  import {registerAutoPair} from './auto-pair'
33
33
  import {registerCalendar} from './calendar'
34
34
  import {registerCommands} from './command'
35
+ import {setupConfig} from './config-setup'
35
36
  import {registerCronHook} from './cron-hook'
36
37
  import {registerEmail} from './email'
37
38
  import {registerGateway} from './gateway'
38
39
  import {getGatewayConfig} from './gateway-fetch'
39
- import {setupModelGateway} from './model-gateway-setup'
40
40
  import {registerOutboundHook, registerOutboundHttpRoute, registerOutboundMethods} from './outbound'
41
- import {setupSession} from './session-setup'
42
41
  import {registerSkillCommandRestore} from './skill-command-restore'
43
42
  import {registerTools} from './tools'
44
43
 
@@ -184,10 +183,9 @@ export default {
184
183
  registerCommands(api)
185
184
  registerTools(api)
186
185
  registerCronHook(api)
186
+ setupConfig(api)
187
187
  registerGateway(api)
188
188
  registerAutoPair(api)
189
- setupModelGateway(api)
190
- setupSession(api)
191
189
 
192
190
  // Email & calendar (optional — requires skillGatewayBaseUrl + skillGatewayToken in config)
193
191
  const gw = getGatewayConfig(api)
@@ -83,16 +83,7 @@ export function writeOpenclawConfig(configPath: string, config: Record<string, u
83
83
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
84
84
  }
85
85
 
86
- export function setupModelGateway(api: PluginApi): void {
87
- const stateDir = resolveStateDir(api)
88
- if (!stateDir) {
89
- api.logger.warn('Cannot resolve state dir — model gateway setup skipped.')
90
- return
91
- }
92
-
93
- const configPath = path.join(stateDir, 'openclaw.json')
94
- const config = readOpenclawConfig(configPath)
95
-
86
+ export function patchModelGateway(config: Record<string, unknown>, api: PluginApi): boolean {
96
87
  // If provider already exists, check if extra models or aliases need updating.
97
88
  // This runs before the credentials check because provisioned sprites have
98
89
  // credentials in openclaw.json directly, not in pluginConfig.
@@ -150,16 +141,11 @@ export function setupModelGateway(api: PluginApi): void {
150
141
  defaults.models = existingAliases
151
142
  agents.defaults = defaults
152
143
  config.agents = agents
153
- try {
154
- writeOpenclawConfig(configPath, config)
155
- api.logger.info(`Model gateway updated: appended missing extra models/aliases.`)
156
- } catch (err) {
157
- api.logger.error(`Failed to update model gateway: ${(err as Error).message}`)
158
- }
144
+ api.logger.info(`Model gateway updated: appended missing extra models/aliases.`)
159
145
  } else {
160
146
  api.logger.info('Model gateway provider already configured.')
161
147
  }
162
- return
148
+ return dirty
163
149
  }
164
150
 
165
151
  // No existing provider — need pluginConfig credentials to create one
@@ -170,7 +156,7 @@ export function setupModelGateway(api: PluginApi): void {
170
156
 
171
157
  if (!baseUrl || !token) {
172
158
  api.logger.info('Model gateway not configured (missing baseUrl or token), skipping.')
173
- return
159
+ return false
174
160
  }
175
161
 
176
162
  // Derive model IDs from agents.defaults
@@ -187,7 +173,7 @@ export function setupModelGateway(api: PluginApi): void {
187
173
 
188
174
  if (!defaultModel) {
189
175
  api.logger.warn('No default model found in agents.defaults — model gateway setup skipped.')
190
- return
176
+ return false
191
177
  }
192
178
 
193
179
  const defaultModels =
@@ -233,9 +219,25 @@ export function setupModelGateway(api: PluginApi): void {
233
219
  agents.defaults = defaults
234
220
  config.agents = agents
235
221
 
222
+ api.logger.info(`Model gateway provider configured: ${baseUrl} with ${models.length} model(s).`)
223
+ return true
224
+ }
225
+
226
+ /** Standalone wrapper — reads config, patches, writes. Used by tests. */
227
+ export function setupModelGateway(api: PluginApi): void {
228
+ const stateDir = resolveStateDir(api)
229
+ if (!stateDir) {
230
+ api.logger.warn('Cannot resolve state dir — model gateway setup skipped.')
231
+ return
232
+ }
233
+
234
+ const configPath = path.join(stateDir, 'openclaw.json')
235
+ const config = readOpenclawConfig(configPath)
236
+
237
+ if (!patchModelGateway(config, api)) return
238
+
236
239
  try {
237
240
  writeOpenclawConfig(configPath, config)
238
- api.logger.info(`Model gateway provider configured: ${baseUrl} with ${models.length} model(s).`)
239
241
  } catch (err) {
240
242
  api.logger.error(`Failed to setup model gateway: ${(err as Error).message}`)
241
243
  }
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "configSchema": {
40
40
  "type": "object",
41
- "additionalProperties": false,
41
+ "additionalProperties": true,
42
42
  "properties": {
43
43
  "memoryDir": { "type": "string", "minLength": 1 },
44
44
  "bin": { "type": "string", "minLength": 1 },
@@ -49,7 +49,16 @@
49
49
  "skillGatewayBaseUrl": { "type": "string" },
50
50
  "skillGatewayToken": { "type": "string" },
51
51
  "modelGatewayBaseUrl": { "type": "string" },
52
- "modelGatewayToken": { "type": "string" }
52
+ "modelGatewayToken": { "type": "string" },
53
+ "agentId": { "type": "string" },
54
+ "agentName": { "type": "string" },
55
+ "workspaceDir": { "type": "string" },
56
+ "defaultModel": { "type": "string" },
57
+ "defaultImageModel": { "type": "string" },
58
+ "sessionMainKey": { "type": "string" },
59
+ "elevenlabsApiKey": { "type": "string" },
60
+ "elevenlabsVoiceId": { "type": "string" },
61
+ "braveSearchApiKey": { "type": "string" }
53
62
  },
54
63
  "required": []
55
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2en/clawly-plugins",
3
- "version": "1.22.2",
3
+ "version": "1.23.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -19,12 +19,12 @@
19
19
  "index.ts",
20
20
  "auto-pair.ts",
21
21
  "calendar.ts",
22
+ "config-setup.ts",
22
23
  "cron-hook.ts",
23
24
  "email.ts",
24
25
  "gateway-fetch.ts",
25
26
  "outbound.ts",
26
27
  "model-gateway-setup.ts",
27
- "session-setup.ts",
28
28
  "skill-command-restore.ts",
29
29
  "openclaw.plugin.json"
30
30
  ],
package/session-setup.ts DELETED
@@ -1,61 +0,0 @@
1
- /**
2
- * On plugin init, patches openclaw.json to:
3
- * 1. Set `session.dmScope` to "per-channel-peer" — isolates Telegram (and
4
- * other channel) DMs from the Clawly mobile session.
5
- * 2. Set `tools.sessions.visibility` to "agent" — lets the agent read
6
- * conversation history from other sessions under the same agent (e.g.
7
- * reference recent Telegram chats from the mobile session).
8
- *
9
- * Runs synchronously during plugin registration, same pattern as
10
- * model-gateway-setup.ts.
11
- */
12
-
13
- import path from 'node:path'
14
-
15
- import type {PluginApi} from './index'
16
- import {readOpenclawConfig, resolveStateDir, writeOpenclawConfig} from './model-gateway-setup'
17
-
18
- const DESIRED_DM_SCOPE = 'per-channel-peer'
19
- const DESIRED_SESSION_VISIBILITY = 'agent'
20
-
21
- export function setupSession(api: PluginApi): void {
22
- const stateDir = resolveStateDir(api)
23
- if (!stateDir) {
24
- api.logger.warn('Cannot resolve state dir — session setup skipped.')
25
- return
26
- }
27
-
28
- const configPath = path.join(stateDir, 'openclaw.json')
29
- const config = readOpenclawConfig(configPath)
30
-
31
- let dirty = false
32
-
33
- // 1. dmScope
34
- const session = ((config.session as Record<string, unknown>) ?? {}) as Record<string, unknown>
35
- if (session.dmScope !== DESIRED_DM_SCOPE) {
36
- session.dmScope = DESIRED_DM_SCOPE
37
- config.session = session
38
- dirty = true
39
- }
40
-
41
- // 2. tools.sessions.visibility
42
- const tools = ((config.tools as Record<string, unknown>) ?? {}) as Record<string, unknown>
43
- const sessions = ((tools.sessions as Record<string, unknown>) ?? {}) as Record<string, unknown>
44
- if (sessions.visibility !== DESIRED_SESSION_VISIBILITY) {
45
- sessions.visibility = DESIRED_SESSION_VISIBILITY
46
- tools.sessions = sessions
47
- config.tools = tools
48
- dirty = true
49
- }
50
-
51
- if (!dirty) return
52
-
53
- try {
54
- writeOpenclawConfig(configPath, config)
55
- api.logger.info(
56
- `Session: dmScope="${DESIRED_DM_SCOPE}", tools.sessions.visibility="${DESIRED_SESSION_VISIBILITY}".`,
57
- )
58
- } catch (err) {
59
- api.logger.error(`Failed to update session config: ${(err as Error).message}`)
60
- }
61
- }