@yeaft/webchat-agent 1.0.413 → 1.0.415
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/browser-runtime/browser-install.js +497 -0
- package/browser-runtime/cli.js +88 -0
- package/browser-runtime/config.js +116 -0
- package/browser-runtime/errors.js +8 -0
- package/browser-runtime/extension/manifest.json +18 -0
- package/browser-runtime/extension/offscreen.html +5 -0
- package/browser-runtime/extension/offscreen.js +101 -0
- package/browser-runtime/extension/popup.html +5 -0
- package/browser-runtime/extension/popup.js +1 -0
- package/browser-runtime/extension/service-worker.js +48 -0
- package/browser-runtime/extension.js +45 -0
- package/browser-runtime/index.js +5 -0
- package/browser-runtime/probe.js +427 -0
- package/browser-runtime/protocol.js +71 -0
- package/browser-runtime/service.js +132 -0
- package/browser-runtime/windows-version-job.ps1 +233 -0
- package/browser-runtime/windows-version-worker.js +75 -0
- package/browser-runtime/windows-version.js +85 -0
- package/cli.js +24 -7
- package/connection/index.js +12 -0
- package/context.js +1 -0
- package/index.js +19 -2
- package/llm-config-cli.js +24 -21
- package/local-runtime/server/client-protocol.js +14 -0
- package/local-runtime/server/context.js +3 -2
- package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
- package/local-runtime/server/handlers/agent-output.js +3 -0
- package/local-runtime/server/handlers/client-misc.js +21 -4
- package/local-runtime/server/handlers/client-workbench.js +222 -41
- package/local-runtime/server/workbench-correlation.js +184 -0
- package/local-runtime/server/workbench-route.js +180 -0
- package/local-runtime/server/ws-agent.js +4 -0
- package/local-runtime/server/ws-client.js +25 -3
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +191 -135
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +5 -1
- package/service/config.js +23 -2
- package/service/index.js +1 -0
- package/service/linux.js +3 -2
- package/terminal.js +167 -30
- package/workbench/file-ops.js +21 -20
- package/workbench/file-search.js +4 -3
- package/workbench/git-ops.js +23 -22
- package/workbench/request-routing.js +16 -0
- package/yeaft/cli.js +57 -1
- package/yeaft/config-api.js +138 -192
- package/yeaft/config-store.js +192 -0
- package/yeaft/config.js +3 -0
- package/yeaft/init.js +20 -7
- package/yeaft/sessions/feature-flag.js +15 -33
- package/yeaft/sessions/session-manifest.js +114 -10
- package/yeaft/stdio-protocol.js +57 -0
- package/yeaft/storage/atomic.js +43 -17
- package/yeaft/tools/process-runner.js +86 -13
package/yeaft/config-api.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
166
|
-
if (!
|
|
167
|
-
return { error: `Provider "${
|
|
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(
|
|
170
|
-
return { error: `Provider "${
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
333
|
+
if (!existsSync(configPath)) return normaliseBrowserRuntimeSection(null);
|
|
363
334
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { hostname } from 'node:os';
|
|
14
|
+
import { normalizePluginConfig } from './plugins.js';
|
|
15
|
+
import { writeAtomic } from './storage/atomic.js';
|
|
16
|
+
|
|
17
|
+
const LOCK_WAIT_MS = 10_000;
|
|
18
|
+
const LOCK_STALE_MS = 5 * 60_000;
|
|
19
|
+
const LOCK_RETRY_MS = 10;
|
|
20
|
+
|
|
21
|
+
function sleepSync(ms) {
|
|
22
|
+
const buffer = new Int32Array(new SharedArrayBuffer(4));
|
|
23
|
+
Atomics.wait(buffer, 0, 0, ms);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ensureOwnerDirectory(path) {
|
|
27
|
+
if (existsSync(path)) {
|
|
28
|
+
const details = lstatSync(path);
|
|
29
|
+
if (!details.isDirectory()) throw new Error('Yeaft data root is not a directory');
|
|
30
|
+
if (process.platform !== 'win32') {
|
|
31
|
+
const currentMode = details.mode & 0o777;
|
|
32
|
+
const restrictedMode = currentMode & 0o700;
|
|
33
|
+
if (currentMode !== restrictedMode) chmodSync(path, restrictedMode);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
38
|
+
if (process.platform !== 'win32') chmodSync(path, 0o700);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readConfigForWrite(configPath) {
|
|
42
|
+
if (!existsSync(configPath)) return {};
|
|
43
|
+
const json = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
44
|
+
if (!json || typeof json !== 'object' || Array.isArray(json)
|
|
45
|
+
|| Object.getPrototypeOf(json) !== Object.prototype) {
|
|
46
|
+
throw new Error('config.json must contain an object');
|
|
47
|
+
}
|
|
48
|
+
if (Object.prototype.hasOwnProperty.call(json, 'plugins')) {
|
|
49
|
+
normalizePluginConfig(json.plugins);
|
|
50
|
+
}
|
|
51
|
+
return json;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function processIsAlive(pid) {
|
|
55
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, 0);
|
|
58
|
+
return true;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return error?.code === 'EPERM';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readConfigLockOwner(lockDir) {
|
|
65
|
+
const lockStat = lstatSync(lockDir);
|
|
66
|
+
if (!lockStat.isDirectory()) throw new Error('config.json lock path is not a directory');
|
|
67
|
+
try {
|
|
68
|
+
const owner = JSON.parse(readFileSync(join(lockDir, 'owner.json'), 'utf8'));
|
|
69
|
+
return { owner, lockStat };
|
|
70
|
+
} catch {
|
|
71
|
+
return { owner: null, lockStat };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function configLockCanBeTaken(lockDir) {
|
|
76
|
+
const { owner, lockStat } = readConfigLockOwner(lockDir);
|
|
77
|
+
if (owner?.host === hostname()) return !processIsAlive(Number(owner.pid));
|
|
78
|
+
if (owner) return false;
|
|
79
|
+
return Date.now() - lockStat.mtimeMs > LOCK_STALE_MS;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function lockIsOwned(lockDir, token) {
|
|
83
|
+
try {
|
|
84
|
+
const owner = JSON.parse(readFileSync(join(lockDir, 'owner.json'), 'utf8'));
|
|
85
|
+
return owner?.token === token;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function removeConfigLockIfOwned(lockDir, token) {
|
|
92
|
+
if (!lockIsOwned(lockDir, token)) return false;
|
|
93
|
+
const claimed = `${lockDir}.release-${token}`;
|
|
94
|
+
try {
|
|
95
|
+
renameSync(lockDir, claimed);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error?.code === 'ENOENT') return false;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
if (!lockIsOwned(claimed, token)) {
|
|
101
|
+
try { renameSync(claimed, lockDir); } catch {}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
rmSync(claimed, { recursive: true, force: true });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function lockOwnerIdentity(owner) {
|
|
109
|
+
if (!owner) return null;
|
|
110
|
+
if (typeof owner.token === 'string' && owner.token) return `token:${owner.token}`;
|
|
111
|
+
return `legacy:${owner.host || ''}:${Number(owner.pid) || 0}:${Number(owner.startedAt) || 0}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function takeConfigLock(lockDir) {
|
|
115
|
+
const observed = readConfigLockOwner(lockDir).owner;
|
|
116
|
+
if (!configLockCanBeTaken(lockDir)) return false;
|
|
117
|
+
const observedIdentity = lockOwnerIdentity(observed);
|
|
118
|
+
const claimed = `${lockDir}.stale-${randomUUID()}`;
|
|
119
|
+
try {
|
|
120
|
+
renameSync(lockDir, claimed);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error?.code === 'ENOENT') return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const claimedOwner = readConfigLockOwner(claimed).owner;
|
|
126
|
+
const ownerChanged = lockOwnerIdentity(claimedOwner) !== observedIdentity;
|
|
127
|
+
const ownerRevived = claimedOwner?.host === hostname()
|
|
128
|
+
&& processIsAlive(Number(claimedOwner.pid));
|
|
129
|
+
if (ownerChanged || ownerRevived) {
|
|
130
|
+
try { renameSync(claimed, lockDir); } catch {}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
rmSync(claimed, { recursive: true, force: true });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function acquireConfigLock(root, { waitMs = LOCK_WAIT_MS } = {}) {
|
|
138
|
+
ensureOwnerDirectory(root);
|
|
139
|
+
const lockDir = join(root, '.config.json.lock');
|
|
140
|
+
const deadline = Date.now() + waitMs;
|
|
141
|
+
for (;;) {
|
|
142
|
+
const token = randomUUID();
|
|
143
|
+
try {
|
|
144
|
+
mkdirSync(lockDir, { mode: 0o700 });
|
|
145
|
+
writeFileSync(join(lockDir, 'owner.json'), JSON.stringify({
|
|
146
|
+
pid: process.pid,
|
|
147
|
+
host: hostname(),
|
|
148
|
+
token,
|
|
149
|
+
startedAt: Date.now(),
|
|
150
|
+
}), { flag: 'wx', mode: 0o600 });
|
|
151
|
+
return () => removeConfigLockIfOwned(lockDir, token);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
154
|
+
try {
|
|
155
|
+
if (takeConfigLock(lockDir)) continue;
|
|
156
|
+
} catch (inspectionError) {
|
|
157
|
+
if (inspectionError?.code === 'ENOENT') continue;
|
|
158
|
+
throw inspectionError;
|
|
159
|
+
}
|
|
160
|
+
if (Date.now() >= deadline) throw new Error('config.json is busy');
|
|
161
|
+
sleepSync(Math.min(LOCK_RETRY_MS, Math.max(1, deadline - Date.now())));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Mutate one Agent-owned config.json under a cross-process lock.
|
|
168
|
+
* The callback runs after the file is re-read and validated inside the lock.
|
|
169
|
+
*/
|
|
170
|
+
export function mutateAgentConfig(root, mutate, options = {}) {
|
|
171
|
+
if (!root) throw new Error('Yeaft data root required');
|
|
172
|
+
if (typeof mutate !== 'function') throw new Error('config mutator required');
|
|
173
|
+
const release = acquireConfigLock(root, options);
|
|
174
|
+
const configPath = join(root, 'config.json');
|
|
175
|
+
try {
|
|
176
|
+
const exists = existsSync(configPath);
|
|
177
|
+
const current = readConfigForWrite(configPath);
|
|
178
|
+
const result = mutate(current, { exists, configPath });
|
|
179
|
+
writeAtomic(configPath, `${JSON.stringify(current, null, 2)}\n`, { mode: 0o600 });
|
|
180
|
+
return result;
|
|
181
|
+
} finally {
|
|
182
|
+
release();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function mutateAgentConfigPath(configPath, mutate, options = {}) {
|
|
187
|
+
return mutateAgentConfig(dirname(configPath), mutate, options);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function readAgentConfigForWrite(configPath) {
|
|
191
|
+
return readConfigForWrite(configPath);
|
|
192
|
+
}
|
package/yeaft/config.js
CHANGED
|
@@ -27,6 +27,7 @@ import { getModelEffortOptions, getThinkingCapability, modelSupportsEffort, reso
|
|
|
27
27
|
import { inferProtocolFromModelId } from './llm/router.js';
|
|
28
28
|
import { normalizeKnownProviderForRuntime } from './llm/known-providers.js';
|
|
29
29
|
import { createDenyAllPluginConfig, normalizePluginConfig } from './plugins.js';
|
|
30
|
+
import { normaliseBrowserRuntimeSection } from '../browser-runtime/config.js';
|
|
30
31
|
import { readWorkspaceFile } from './workspace-file.js';
|
|
31
32
|
|
|
32
33
|
/** Default configuration values. */
|
|
@@ -368,6 +369,7 @@ function loadLegacyConfig(dir, overrides) {
|
|
|
368
369
|
// task-318: legacy path never had the `yeaft` section — defaults.
|
|
369
370
|
yeaft: normaliseYeaftSection(null),
|
|
370
371
|
telemetry: normaliseTelemetrySection(null),
|
|
372
|
+
browserRuntime: normaliseBrowserRuntimeSection(null),
|
|
371
373
|
plugins: {},
|
|
372
374
|
providers: null,
|
|
373
375
|
primaryModel: null,
|
|
@@ -524,6 +526,7 @@ export function loadConfig(overrides = {}) {
|
|
|
524
526
|
// don't pollute the flat config namespace used by chat code.
|
|
525
527
|
yeaft: normaliseYeaftSection(jsonConfig.yeaft),
|
|
526
528
|
telemetry: normaliseTelemetrySection(jsonConfig.telemetry),
|
|
529
|
+
browserRuntime: normaliseBrowserRuntimeSection(jsonConfig.browserRuntime),
|
|
527
530
|
|
|
528
531
|
// Agent-level tools / skills / MCP server allowlists. Missing fields mean
|
|
529
532
|
// all currently discovered capabilities remain enabled. A persisted schema
|