@yeaft/webchat-agent 1.0.412 → 1.0.414

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.
Files changed (53) hide show
  1. package/browser-runtime/browser-install.js +497 -0
  2. package/browser-runtime/cli.js +88 -0
  3. package/browser-runtime/config.js +116 -0
  4. package/browser-runtime/errors.js +8 -0
  5. package/browser-runtime/extension/manifest.json +18 -0
  6. package/browser-runtime/extension/offscreen.html +5 -0
  7. package/browser-runtime/extension/offscreen.js +101 -0
  8. package/browser-runtime/extension/popup.html +5 -0
  9. package/browser-runtime/extension/popup.js +1 -0
  10. package/browser-runtime/extension/service-worker.js +48 -0
  11. package/browser-runtime/extension.js +45 -0
  12. package/browser-runtime/index.js +5 -0
  13. package/browser-runtime/probe.js +427 -0
  14. package/browser-runtime/protocol.js +71 -0
  15. package/browser-runtime/service.js +132 -0
  16. package/browser-runtime/windows-version-job.ps1 +233 -0
  17. package/browser-runtime/windows-version-worker.js +75 -0
  18. package/browser-runtime/windows-version.js +85 -0
  19. package/cli.js +24 -7
  20. package/context.js +1 -0
  21. package/index.js +18 -1
  22. package/llm-config-cli.js +24 -21
  23. package/local-runtime/version.json +1 -1
  24. package/local-runtime/web/app.bundle.js +22 -5
  25. package/local-runtime/web/app.bundle.js.gz +0 -0
  26. package/local-runtime/web/index.html +2 -2
  27. package/local-runtime/web/style.bundle.css +1 -1
  28. package/local-runtime/web/style.bundle.css.gz +0 -0
  29. package/package.json +5 -1
  30. package/service/config.js +23 -2
  31. package/service/index.js +1 -0
  32. package/service/linux.js +3 -2
  33. package/yeaft/config-api.js +138 -192
  34. package/yeaft/config-store.js +192 -0
  35. package/yeaft/config.js +3 -0
  36. package/yeaft/init.js +20 -7
  37. package/yeaft/sessions/feature-flag.js +15 -33
  38. package/yeaft/storage/atomic.js +43 -17
  39. package/yeaft/tools/create-work-item.js +1 -1
  40. package/yeaft/tools/process-runner.js +86 -13
  41. package/yeaft/work-center/bridge.js +3 -2
  42. package/yeaft/work-center/completion-contract.js +6 -0
  43. package/yeaft/work-center/controller.js +2 -1
  44. package/yeaft/work-center/coordinator.js +45 -14
  45. package/yeaft/work-center/durable-model.js +45 -1
  46. package/yeaft/work-center/dynamic-coordination.js +34 -0
  47. package/yeaft/work-center/evidence.js +235 -0
  48. package/yeaft/work-center/mainline-projection.js +4 -1
  49. package/yeaft/work-center/projection.js +45 -4
  50. package/yeaft/work-center/runner.js +82 -7
  51. package/yeaft/work-center/service.js +6 -0
  52. package/yeaft/work-center/store.js +162 -19
  53. package/yeaft/work-center/workflow.js +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.412",
3
+ "version": "1.0.414",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -41,6 +41,8 @@
41
41
  "docker-entrypoint.sh",
42
42
  "yeaft/templates/**/*.md",
43
43
  "yeaft/dream/prompts/**/*.md",
44
+ "browser-runtime/extension/**/*",
45
+ "browser-runtime/*.ps1",
44
46
  "local-runtime/**/*"
45
47
  ],
46
48
  "license": "MIT",
@@ -62,6 +64,7 @@
62
64
  },
63
65
  "dependencies": {
64
66
  "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
67
+ "@puppeteer/browsers": "2.13.0",
65
68
  "bcrypt": "^6.0.0",
66
69
  "compression": "^1.8.1",
67
70
  "dotenv": "^16.3.1",
@@ -69,6 +72,7 @@
69
72
  "jsonwebtoken": "^9.0.2",
70
73
  "multer": "^2.2.0",
71
74
  "nodemailer": "^9.0.3",
75
+ "puppeteer-core": "24.41.0",
72
76
  "qrcode": "^1.5.4",
73
77
  "speakeasy": "^2.0.0",
74
78
  "tweetnacl": "^1.0.3",
package/service/config.js CHANGED
@@ -113,7 +113,7 @@ export function resolveServiceInstanceId(args = [], env = process.env, options =
113
113
  }
114
114
 
115
115
  /** Resolve the CLI/env/default Yeaft data root for one Agent instance. */
116
- export function resolveYeaftDir(args = [], env = process.env, instanceId = DEFAULT_INSTANCE_ID) {
116
+ function explicitYeaftDirFromArgs(args = []) {
117
117
  let explicitYeaftDir = null;
118
118
  for (let index = 0; index < args.length; index += 1) {
119
119
  if (args[index] !== '--yeaft-dir') continue;
@@ -122,7 +122,28 @@ export function resolveYeaftDir(args = [], env = process.env, instanceId = DEFAU
122
122
  explicitYeaftDir = value;
123
123
  index += 1;
124
124
  }
125
- return explicitYeaftDir || env.YEAFT_DIR || getDefaultYeaftDir(instanceId);
125
+ return explicitYeaftDir;
126
+ }
127
+
128
+ export function resolveYeaftDir(args = [], env = process.env, instanceId = DEFAULT_INSTANCE_ID) {
129
+ return explicitYeaftDirFromArgs(args) || env.YEAFT_DIR || getDefaultYeaftDir(instanceId);
130
+ }
131
+
132
+ /** Resolve the persisted data root for an explicitly selected management target. */
133
+ export function resolveManagedYeaftDir(
134
+ args = [],
135
+ env = process.env,
136
+ instanceId = DEFAULT_INSTANCE_ID,
137
+ dependencies = {},
138
+ ) {
139
+ const explicitYeaftDir = explicitYeaftDirFromArgs(args);
140
+ if (explicitYeaftDir) return explicitYeaftDir;
141
+ const hasExplicitIdentity = args.includes('--name') || args.includes('--instance');
142
+ if (!hasExplicitIdentity && env.YEAFT_DIR) return env.YEAFT_DIR;
143
+ const readServiceConfig = dependencies.loadServiceConfig || loadServiceConfig;
144
+ const defaultYeaftDir = dependencies.getDefaultYeaftDir || getDefaultYeaftDir;
145
+ const persisted = readServiceConfig(instanceId)?.yeaftDir;
146
+ return persisted || defaultYeaftDir(instanceId);
126
147
  }
127
148
 
128
149
  /** Legacy alias for resolveServiceInstanceId(). */
package/service/index.js CHANGED
@@ -29,6 +29,7 @@ export {
29
29
  resolveDisplayName,
30
30
  resolveRuntimeIdentity,
31
31
  resolveServiceInstanceId,
32
+ resolveManagedYeaftDir,
32
33
  shouldLoadLegacyLocalConfig,
33
34
  normalizeInstanceId,
34
35
  isDefaultInstance,
package/service/linux.js CHANGED
@@ -12,7 +12,7 @@ export function getSystemdServicePath(instanceId = DEFAULT_INSTANCE_ID) {
12
12
  return join(homedir(), '.config', 'systemd', 'user', `${getServiceName(instanceId)}.service`);
13
13
  }
14
14
 
15
- function generateSystemdUnit(config) {
15
+ export function generateSystemdUnit(config) {
16
16
  const nodePath = getNodePath();
17
17
  const cliPath = getCliPath();
18
18
  const logDir = getLogDir(config.instanceId);
@@ -38,7 +38,8 @@ ExecStart=${nodePath} ${cliPath}
38
38
  WorkingDirectory=${config.workDir || homedir()}
39
39
  Restart=on-failure
40
40
  RestartSec=10
41
- KillMode=process
41
+ KillMode=mixed
42
+ TimeoutStopSec=15
42
43
  ${envLines.join('\n')}
43
44
  Environment=PATH=${nodeBinDir}:${homedir()}/.local/bin:${homedir()}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
44
45
 
@@ -8,12 +8,14 @@
8
8
  * like maxContinueTurns or debug that don't belong in the UI.
9
9
  */
10
10
 
11
- import { existsSync, readFileSync, writeFileSync } from 'fs';
11
+ import { existsSync, readFileSync } from 'fs';
12
12
  import { join } from 'path';
13
13
  import { DEFAULT_YEAFT_DIR } from './init.js';
14
14
  import { normalizeProviderModels, parseModelRef, serializeModelForPersistence } from './models.js';
15
15
  import { normaliseTelemetrySection, normaliseYeaftSection } from './config.js';
16
+ import { normaliseBrowserRuntimeSection, validateBrowserRuntimeUpdate } from '../browser-runtime/config.js';
16
17
  import { normalizePluginConfig } from './plugins.js';
18
+ import { mutateAgentConfig, readAgentConfigForWrite } from './config-store.js';
17
19
  import { isGitHubCopilotProvider, serializeKnownProviderForPersistence } from './llm/known-providers.js';
18
20
 
19
21
  /**
@@ -28,16 +30,7 @@ import { isGitHubCopilotProvider, serializeKnownProviderForPersistence } from '.
28
30
  * @throws {Error} when an existing config cannot be safely preserved
29
31
  */
30
32
  function readConfigForWrite(configPath) {
31
- if (!existsSync(configPath)) return {};
32
- const json = JSON.parse(readFileSync(configPath, 'utf8'));
33
- if (!json || typeof json !== 'object' || Array.isArray(json)
34
- || Object.getPrototypeOf(json) !== Object.prototype) {
35
- throw new Error('config.json must contain an object');
36
- }
37
- if (Object.prototype.hasOwnProperty.call(json, 'plugins')) {
38
- normalizePluginConfig(json.plugins);
39
- }
40
- return json;
33
+ return readAgentConfigForWrite(configPath);
41
34
  }
42
35
 
43
36
  /**
@@ -142,85 +135,57 @@ function normalizeManagedModelDefaults(config) {
142
135
  */
143
136
  export function updateLlmConfig(update, dir) {
144
137
  const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
145
- const configPath = join(root, 'config.json');
146
-
147
- // Preserve all existing fields only when the on-disk document and its
148
- // Plugins policy are valid. Never turn a failed read into a fresh config.
149
- let existing;
150
- try {
151
- existing = readConfigForWrite(configPath);
152
- } catch (err) {
153
- return { error: `Failed to read config.json: ${err?.message || err}` };
154
- }
155
138
 
156
- // Validate providers structure
157
139
  if (update.providers !== undefined) {
158
- if (!Array.isArray(update.providers)) {
159
- return { error: 'providers must be an array' };
160
- }
161
- for (const p of update.providers) {
162
- if (!p.name || typeof p.name !== 'string') {
140
+ if (!Array.isArray(update.providers)) return { error: 'providers must be an array' };
141
+ for (const provider of update.providers) {
142
+ if (!provider.name || typeof provider.name !== 'string') {
163
143
  return { error: 'Each provider must have a name' };
164
144
  }
165
- if (isGitHubCopilotProvider(p)) continue;
166
- if (!p.baseUrl || typeof p.baseUrl !== 'string') {
167
- return { error: `Provider "${p.name}" must have a baseUrl` };
145
+ if (isGitHubCopilotProvider(provider)) continue;
146
+ if (!provider.baseUrl || typeof provider.baseUrl !== 'string') {
147
+ return { error: `Provider "${provider.name}" must have a baseUrl` };
168
148
  }
169
- if (!Array.isArray(p.models) || p.models.length === 0) {
170
- return { error: `Provider "${p.name}" must have at least one model` };
149
+ if (!Array.isArray(provider.models) || provider.models.length === 0) {
150
+ return { error: `Provider "${provider.name}" must have at least one model` };
171
151
  }
172
152
  }
173
- // Normalize + re-serialize each provider's models so that:
174
- // - id-only entries are persisted as plain strings (back-compat)
175
- // - entries with ctx / maxOutput are persisted as objects
176
- // - empty / 0 / NaN values get stripped
177
- existing.providers = update.providers.map(p => {
178
- const managed = serializeKnownProviderForPersistence(p);
179
- if (managed) return managed;
180
- const normalized = normalizeProviderModels(p);
181
- return {
182
- ...p,
183
- models: normalized.map(serializeModelForPersistence),
184
- };
185
- });
186
153
  }
187
154
 
188
- // Update model selections. A managed provider catalog is authoritative:
189
- // when it drops the old Agent default, keep no hidden reference to that
190
- // model in primaryModel or fastModel.
191
- if (update.primaryModel !== undefined) {
192
- existing.primaryModel = update.primaryModel || null;
193
- }
194
- if (update.fastModel !== undefined) {
195
- existing.fastModel = update.fastModel || null;
196
- }
197
- if (update.providers !== undefined) normalizeManagedModelDefaults(existing);
198
- if (update.language !== undefined) {
199
- existing.language = update.language;
200
- }
201
- if (update.debug !== undefined) {
202
- existing.debug = update.debug === true;
203
- }
204
-
205
- // Write back
206
155
  try {
207
- writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
208
- } catch (e) {
209
- return { error: `Failed to write config.json: ${e.message}` };
156
+ return mutateAgentConfig(root, existing => {
157
+ if (update.providers !== undefined) {
158
+ existing.providers = update.providers.map(provider => {
159
+ const managed = serializeKnownProviderForPersistence(provider);
160
+ if (managed) return managed;
161
+ return {
162
+ ...provider,
163
+ models: normalizeProviderModels(provider).map(serializeModelForPersistence),
164
+ };
165
+ });
166
+ }
167
+ if (update.primaryModel !== undefined) existing.primaryModel = update.primaryModel || null;
168
+ if (update.fastModel !== undefined) existing.fastModel = update.fastModel || null;
169
+ if (update.providers !== undefined) normalizeManagedModelDefaults(existing);
170
+ if (update.language !== undefined) existing.language = update.language;
171
+ if (update.debug !== undefined) existing.debug = update.debug === true;
172
+
173
+ const agentConfig = {
174
+ providers: Array.isArray(existing.providers) ? existing.providers : [],
175
+ primaryModel: existing.primaryModel || null,
176
+ fastModel: existing.fastModel || null,
177
+ language: existing.language || 'en',
178
+ debug: existing.debug === true,
179
+ };
180
+ return {
181
+ ...agentConfig,
182
+ agentConfig,
183
+ effectiveConfig: agentConfig,
184
+ };
185
+ });
186
+ } catch (error) {
187
+ return { error: `Failed to read config.json or persist update: ${error?.message || error}` };
210
188
  }
211
-
212
- const agentConfig = {
213
- providers: Array.isArray(existing.providers) ? existing.providers : [],
214
- primaryModel: existing.primaryModel || null,
215
- fastModel: existing.fastModel || null,
216
- language: existing.language || 'en',
217
- debug: existing.debug === true,
218
- };
219
- return {
220
- ...agentConfig,
221
- agentConfig,
222
- effectiveConfig: agentConfig,
223
- };
224
189
  }
225
190
 
226
191
  // ─── Yeaft runtime settings (task-318) ────────────────────────────
@@ -261,7 +226,6 @@ export function getYeaftSettings(dir) {
261
226
  */
262
227
  export function updateYeaftSettings(update, dir) {
263
228
  const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
264
- const configPath = join(root, 'config.json');
265
229
 
266
230
  if (!update || typeof update !== 'object') {
267
231
  return { error: 'update payload required' };
@@ -288,37 +252,26 @@ export function updateYeaftSettings(update, dir) {
288
252
  }
289
253
  }
290
254
 
291
- // Preserve all existing fields only when the on-disk document and its
292
- // Plugins policy are valid. A Settings update must not repair bad JSON into
293
- // a config whose missing Plugins fields inherit all capabilities.
294
- let existing;
295
- try {
296
- existing = readConfigForWrite(configPath);
297
- } catch (err) {
298
- return { error: `Failed to read config.json: ${err?.message || err}` };
299
- }
300
-
301
- const prev = normaliseYeaftSection(existing.yeaft);
302
- const merged = {
303
- maxConcurrentThreads: update.maxConcurrentThreads !== undefined
304
- ? Math.floor(Number(update.maxConcurrentThreads))
305
- : prev.maxConcurrentThreads,
306
- autoArchiveIdleDays: update.autoArchiveIdleDays !== undefined
307
- ? Math.floor(Number(update.autoArchiveIdleDays))
308
- : prev.autoArchiveIdleDays,
309
- recentTurnsLimit: update.recentTurnsLimit !== undefined
310
- ? Math.floor(Number(update.recentTurnsLimit))
311
- : prev.recentTurnsLimit,
312
- };
313
- existing.yeaft = merged;
314
-
315
255
  try {
316
- writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
317
- } catch (e) {
318
- return { error: `Failed to write config.json: ${e.message}` };
256
+ return mutateAgentConfig(root, existing => {
257
+ const prev = normaliseYeaftSection(existing.yeaft);
258
+ const merged = {
259
+ maxConcurrentThreads: update.maxConcurrentThreads !== undefined
260
+ ? Math.floor(Number(update.maxConcurrentThreads))
261
+ : prev.maxConcurrentThreads,
262
+ autoArchiveIdleDays: update.autoArchiveIdleDays !== undefined
263
+ ? Math.floor(Number(update.autoArchiveIdleDays))
264
+ : prev.autoArchiveIdleDays,
265
+ recentTurnsLimit: update.recentTurnsLimit !== undefined
266
+ ? Math.floor(Number(update.recentTurnsLimit))
267
+ : prev.recentTurnsLimit,
268
+ };
269
+ existing.yeaft = merged;
270
+ return merged;
271
+ });
272
+ } catch (error) {
273
+ return { error: `Failed to read config.json or persist update: ${error?.message || error}` };
319
274
  }
320
-
321
- return merged;
322
275
  }
323
276
 
324
277
  /**
@@ -357,25 +310,50 @@ export function updateTelemetrySettings(update, dir) {
357
310
  if (Object.keys(update).some(key => !allowed.has(key))) {
358
311
  return { error: 'unknown telemetry setting' };
359
312
  }
313
+ const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
314
+ try {
315
+ return mutateAgentConfig(root, existing => {
316
+ const merged = normaliseTelemetrySection({
317
+ ...(existing.telemetry && typeof existing.telemetry === 'object' ? existing.telemetry : {}),
318
+ ...update,
319
+ });
320
+ existing.telemetry = merged;
321
+ return merged;
322
+ });
323
+ } catch (error) {
324
+ return { error: `Failed to read config.json or persist update: ${error?.message || error}` };
325
+ }
326
+ }
327
+
328
+ // ─── Browser Runtime settings ───────────────────────────────────────
329
+
330
+ export function getBrowserRuntimeSettings(dir) {
360
331
  const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
361
332
  const configPath = join(root, 'config.json');
362
- let existing;
333
+ if (!existsSync(configPath)) return normaliseBrowserRuntimeSection(null);
363
334
  try {
364
- existing = readConfigForWrite(configPath);
365
- } catch (err) {
366
- return { error: `Failed to read config.json: ${err?.message || err}` };
335
+ const json = JSON.parse(readFileSync(configPath, 'utf8'));
336
+ return normaliseBrowserRuntimeSection(json.browserRuntime);
337
+ } catch (error) {
338
+ return { error: `Failed to read config.json: ${error.message}` };
367
339
  }
368
- const merged = normaliseTelemetrySection({
369
- ...(existing.telemetry && typeof existing.telemetry === 'object' ? existing.telemetry : {}),
370
- ...update,
371
- });
372
- existing.telemetry = merged;
340
+ }
341
+
342
+ export function updateBrowserRuntimeSettings(update, dir) {
343
+ const validationError = validateBrowserRuntimeUpdate(update);
344
+ if (validationError) return { error: validationError };
345
+ const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
373
346
  try {
374
- writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
375
- } catch (e) {
376
- return { error: `Failed to write config.json: ${e.message}` };
347
+ return mutateAgentConfig(root, existing => {
348
+ const previous = existing.browserRuntime && typeof existing.browserRuntime === 'object'
349
+ ? existing.browserRuntime
350
+ : {};
351
+ existing.browserRuntime = normaliseBrowserRuntimeSection({ ...previous, ...update });
352
+ return existing.browserRuntime;
353
+ });
354
+ } catch (error) {
355
+ return { error: `Failed to read config.json or persist update: ${error?.message || error}` };
377
356
  }
378
- return merged;
379
357
  }
380
358
 
381
359
  // ─── Search settings (web-search backend selection + Tavily key) ────
@@ -443,7 +421,6 @@ export function getSearchSettings(dir) {
443
421
  */
444
422
  export function updateSearchSettings(update, dir) {
445
423
  const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
446
- const configPath = join(root, 'config.json');
447
424
 
448
425
  if (!update || typeof update !== 'object') {
449
426
  return { error: 'update payload required' };
@@ -455,23 +432,17 @@ export function updateSearchSettings(update, dir) {
455
432
  return { error: 'tavilyApiKey must be a string' };
456
433
  }
457
434
 
458
- let existing;
459
435
  try {
460
- existing = readConfigForWrite(configPath);
461
- } catch (err) {
462
- return { error: `Failed to read config.json: ${err?.message || err}` };
463
- }
464
- const prev = (existing && typeof existing.search === 'object' && existing.search) || {};
465
- const merged = { ...prev };
466
- if (update.backend !== undefined) merged.backend = update.backend;
467
- if (update.tavilyApiKey !== undefined) merged.tavilyApiKey = update.tavilyApiKey;
468
- if (update.disableHtmlFallback !== undefined) merged.disableHtmlFallback = !!update.disableHtmlFallback;
469
- existing.search = merged;
470
-
471
- try {
472
- writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
473
- } catch (e) {
474
- return { error: `Failed to write config.json: ${e.message}` };
436
+ mutateAgentConfig(root, existing => {
437
+ const prev = (existing && typeof existing.search === 'object' && existing.search) || {};
438
+ const merged = { ...prev };
439
+ if (update.backend !== undefined) merged.backend = update.backend;
440
+ if (update.tavilyApiKey !== undefined) merged.tavilyApiKey = update.tavilyApiKey;
441
+ if (update.disableHtmlFallback !== undefined) merged.disableHtmlFallback = !!update.disableHtmlFallback;
442
+ existing.search = merged;
443
+ });
444
+ } catch (error) {
445
+ return { error: `Failed to read config.json or persist update: ${error?.message || error}` };
475
446
  }
476
447
  return getSearchSettings(root);
477
448
  }
@@ -549,24 +520,17 @@ export function getPluginConfig(dir) {
549
520
  */
550
521
  export function updatePluginConfig(plugins, dir) {
551
522
  const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
552
- const configPath = join(root, 'config.json');
553
523
  let normalized;
554
- let existing;
555
524
  try {
556
- existing = readConfigForWrite(configPath);
557
525
  normalized = normalizePluginConfig(plugins);
558
- } catch (err) {
559
- return { error: `Failed to read plugin config: ${err?.message || err}` };
560
- }
561
-
562
- if (Object.keys(normalized).length === 0) delete existing.plugins;
563
- else existing.plugins = normalized;
564
- try {
565
- writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
566
- } catch (err) {
567
- return { error: `Failed to write plugin config: ${err?.message || err}` };
526
+ return mutateAgentConfig(root, existing => {
527
+ if (Object.keys(normalized).length === 0) delete existing.plugins;
528
+ else existing.plugins = normalized;
529
+ return { plugins: normalized };
530
+ });
531
+ } catch (error) {
532
+ return { error: `Failed to read plugin config or persist update: ${error?.message || error}` };
568
533
  }
569
- return { plugins: normalized };
570
534
  }
571
535
 
572
536
  // ─── MCP server config (mcpServers array in config.json) ──
@@ -684,33 +648,21 @@ export function upsertMcpServer(server, dir) {
684
648
  if (err) return { error: err };
685
649
 
686
650
  const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
687
- const configPath = join(root, 'config.json');
688
- let existing;
689
- try {
690
- existing = readConfigForWrite(configPath);
691
- } catch (err) {
692
- return { error: `Failed to read config.json: ${err?.message || err}` };
693
- }
694
- const list = Array.isArray(existing.mcpServers) ? existing.mcpServers.slice() : [];
695
-
696
651
  const normalised = normaliseMcpServer(server);
697
652
  if (!normalised) return { error: 'invalid server payload' };
698
653
 
699
- const idx = list.findIndex(s => s && typeof s === 'object' && s.name === normalised.name);
700
- if (idx >= 0) {
701
- list[idx] = normalised;
702
- } else {
703
- list.push(normalised);
704
- }
705
- existing.mcpServers = list;
706
-
707
654
  try {
708
- writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
709
- } catch (e) {
710
- return { error: `Failed to write config.json: ${e.message}` };
655
+ return mutateAgentConfig(root, existing => {
656
+ const list = Array.isArray(existing.mcpServers) ? existing.mcpServers.slice() : [];
657
+ const index = list.findIndex(entry => entry && typeof entry === 'object' && entry.name === normalised.name);
658
+ if (index >= 0) list[index] = normalised;
659
+ else list.push(normalised);
660
+ existing.mcpServers = list;
661
+ return { servers: list.map(normaliseMcpServer).filter(Boolean), server: normalised };
662
+ });
663
+ } catch (error) {
664
+ return { error: `Failed to read config.json or persist update: ${error?.message || error}` };
711
665
  }
712
-
713
- return { servers: list.map(normaliseMcpServer).filter(Boolean), server: normalised };
714
666
  }
715
667
 
716
668
  /**
@@ -732,24 +684,18 @@ export function removeMcpServer(name, dir) {
732
684
  // matching against the padded string.
733
685
  const target = name.trim();
734
686
  const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
735
- const configPath = join(root, 'config.json');
736
- let existing;
737
- try {
738
- existing = readConfigForWrite(configPath);
739
- } catch (err) {
740
- return { error: `Failed to read config.json: ${err?.message || err}` };
741
- }
742
- const list = Array.isArray(existing.mcpServers) ? existing.mcpServers.slice() : [];
743
- const next = list.filter(s => !(s && typeof s === 'object' && s.name === target));
744
- const removed = next.length !== list.length;
745
- existing.mcpServers = next;
746
-
747
687
  try {
748
- writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
749
- } catch (e) {
750
- return { error: `Failed to write config.json: ${e.message}` };
688
+ return mutateAgentConfig(root, existing => {
689
+ const list = Array.isArray(existing.mcpServers) ? existing.mcpServers.slice() : [];
690
+ const next = list.filter(entry => !(entry && typeof entry === 'object' && entry.name === target));
691
+ existing.mcpServers = next;
692
+ return {
693
+ servers: next.map(normaliseMcpServer).filter(Boolean),
694
+ removed: next.length !== list.length,
695
+ };
696
+ });
697
+ } catch (error) {
698
+ return { error: `Failed to read config.json or persist update: ${error?.message || error}` };
751
699
  }
752
-
753
- return { servers: next.map(normaliseMcpServer).filter(Boolean), removed };
754
700
  }
755
701