@yeaft/webchat-agent 0.1.1031 → 0.1.1033
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/package.json +1 -1
- package/yeaft/config.js +107 -10
- package/yeaft/engine.js +14 -0
- package/yeaft/session.js +13 -7
- package/yeaft/skills.js +19 -6
package/package.json
CHANGED
package/yeaft/config.js
CHANGED
|
@@ -498,31 +498,128 @@ export function loadConfig(overrides = {}) {
|
|
|
498
498
|
/**
|
|
499
499
|
* Load MCP server configuration.
|
|
500
500
|
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
501
|
+
* Merges two tiers, in priority order (highest first):
|
|
502
|
+
* 1. global — ~/.yeaft authoritative config: config.json "mcpServers"
|
|
503
|
+
* array, else standalone ~/.yeaft/mcp.json ({ servers: [...] }).
|
|
504
|
+
* 2. project — `<workDir>/.mcp.json` (Claude Code standard location), so a
|
|
505
|
+
* project that integrates Claude Code works out of the box. Supplementary:
|
|
506
|
+
* a project server whose name collides with a global one is dropped (the
|
|
507
|
+
* explicit ~/.yeaft config wins).
|
|
503
508
|
*
|
|
504
509
|
* @param {string} yeaftDir
|
|
505
510
|
* @param {object} [jsonConfig] — Already-parsed config.json (optional, avoids re-read)
|
|
506
|
-
* @
|
|
511
|
+
* @param {string} [workDir] — optional project working directory (project tier root)
|
|
512
|
+
* @returns {{ servers: object[], skipped: { name: string, reason: string, source: string }[] }}
|
|
513
|
+
*/
|
|
514
|
+
export function loadMCPConfig(yeaftDir, jsonConfig, workDir) {
|
|
515
|
+
// ── Global tier: ~/.yeaft authoritative config ──
|
|
516
|
+
const globalServers = loadGlobalMCPServers(yeaftDir, jsonConfig);
|
|
517
|
+
|
|
518
|
+
// ── Project tier: <workDir>/.mcp.json (Claude Code standard) ──
|
|
519
|
+
const project = workDir
|
|
520
|
+
? loadProjectMCPServers(workDir)
|
|
521
|
+
: { servers: [], skipped: [] };
|
|
522
|
+
|
|
523
|
+
// Dedup: global (~/.yeaft explicit config) wins over project supplements.
|
|
524
|
+
const seen = new Set(globalServers.map(s => s.name));
|
|
525
|
+
const servers = [...globalServers];
|
|
526
|
+
for (const s of project.servers) {
|
|
527
|
+
if (seen.has(s.name)) continue;
|
|
528
|
+
seen.add(s.name);
|
|
529
|
+
servers.push(s);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return { servers, skipped: project.skipped };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Load the global (~/.yeaft) MCP server list.
|
|
537
|
+
*
|
|
538
|
+
* Reads from config.json "mcpServers" field first, then falls back to
|
|
539
|
+
* standalone ~/.yeaft/mcp.json for backward compatibility. Returns a plain
|
|
540
|
+
* array (not wrapped) so `loadMCPConfig` can merge tiers.
|
|
541
|
+
*
|
|
542
|
+
* @param {string} yeaftDir
|
|
543
|
+
* @param {object} [jsonConfig]
|
|
544
|
+
* @returns {object[]}
|
|
507
545
|
*/
|
|
508
|
-
|
|
546
|
+
function loadGlobalMCPServers(yeaftDir, jsonConfig) {
|
|
509
547
|
// Check config.json mcpServers field
|
|
510
548
|
if (jsonConfig && Array.isArray(jsonConfig.mcpServers)) {
|
|
511
549
|
const valid = jsonConfig.mcpServers.filter(s => s.name && s.command);
|
|
512
|
-
if (valid.length > 0) return
|
|
550
|
+
if (valid.length > 0) return valid;
|
|
513
551
|
}
|
|
514
552
|
|
|
515
553
|
// Fallback: standalone mcp.json
|
|
516
554
|
const mcpPath = join(yeaftDir, 'mcp.json');
|
|
517
|
-
if (!existsSync(mcpPath)) return
|
|
555
|
+
if (!existsSync(mcpPath)) return [];
|
|
518
556
|
|
|
519
557
|
try {
|
|
520
558
|
const raw = readFileSync(mcpPath, 'utf8');
|
|
521
559
|
const parsed = JSON.parse(raw);
|
|
522
|
-
if (!parsed.servers || !Array.isArray(parsed.servers)) return
|
|
523
|
-
|
|
524
|
-
return { servers: valid };
|
|
560
|
+
if (!parsed.servers || !Array.isArray(parsed.servers)) return [];
|
|
561
|
+
return parsed.servers.filter(s => s.name && s.command);
|
|
525
562
|
} catch {
|
|
526
|
-
return
|
|
563
|
+
return [];
|
|
527
564
|
}
|
|
528
565
|
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Parse a project's Claude Code MCP config at `<workDir>/.mcp.json`.
|
|
569
|
+
*
|
|
570
|
+
* Format (Claude Code standard):
|
|
571
|
+
* { "mcpServers": { "<name>": { command, args?, env?, url?, type? } } }
|
|
572
|
+
*
|
|
573
|
+
* Only stdio servers (those with a `command`) are adapted into yeaft's
|
|
574
|
+
* { name, command, args?, env? } shape. SSE/HTTP servers (url/type, no
|
|
575
|
+
* command) cannot be spawned by the current MCPManager, so they're reported
|
|
576
|
+
* in `skipped` with reason 'unsupported-transport' rather than silently
|
|
577
|
+
* dropped or surfaced later as spawn failures.
|
|
578
|
+
*
|
|
579
|
+
* Robust by design: a missing file, malformed JSON, or a non-object
|
|
580
|
+
* `mcpServers` field all return gracefully with empty arrays — a broken
|
|
581
|
+
* project `.mcp.json` must never fail session creation.
|
|
582
|
+
*
|
|
583
|
+
* @param {string} workDir — project working directory
|
|
584
|
+
* @returns {{ servers: object[], skipped: { name: string, reason: string, source: string }[] }}
|
|
585
|
+
*/
|
|
586
|
+
export function loadProjectMCPServers(workDir) {
|
|
587
|
+
const empty = { servers: [], skipped: [] };
|
|
588
|
+
if (!workDir || typeof workDir !== 'string') return empty;
|
|
589
|
+
|
|
590
|
+
const mcpPath = join(workDir, '.mcp.json');
|
|
591
|
+
if (!existsSync(mcpPath)) return empty;
|
|
592
|
+
|
|
593
|
+
let parsed;
|
|
594
|
+
try {
|
|
595
|
+
parsed = JSON.parse(readFileSync(mcpPath, 'utf8'));
|
|
596
|
+
} catch {
|
|
597
|
+
return empty;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const mcpServers = parsed && parsed.mcpServers;
|
|
601
|
+
if (!mcpServers || typeof mcpServers !== 'object' || Array.isArray(mcpServers)) {
|
|
602
|
+
return empty;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const servers = [];
|
|
606
|
+
const skipped = [];
|
|
607
|
+
for (const [name, raw] of Object.entries(mcpServers)) {
|
|
608
|
+
if (!name || !raw || typeof raw !== 'object') continue;
|
|
609
|
+
if (typeof raw.command === 'string' && raw.command.length > 0) {
|
|
610
|
+
// stdio server → adapt to yeaft shape
|
|
611
|
+
const server = { name, command: raw.command };
|
|
612
|
+
if (Array.isArray(raw.args)) server.args = raw.args;
|
|
613
|
+
if (raw.env && typeof raw.env === 'object') server.env = raw.env;
|
|
614
|
+
servers.push(server);
|
|
615
|
+
} else if (typeof raw.url === 'string' || typeof raw.type === 'string') {
|
|
616
|
+
// SSE/HTTP transport — not spawnable by current MCPManager.
|
|
617
|
+
skipped.push({ name, reason: 'unsupported-transport', source: '.mcp.json' });
|
|
618
|
+
} else {
|
|
619
|
+
// No command and no url/type — malformed entry.
|
|
620
|
+
skipped.push({ name, reason: 'invalid-config', source: '.mcp.json' });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return { servers, skipped };
|
|
625
|
+
}
|
package/yeaft/engine.js
CHANGED
|
@@ -2051,6 +2051,20 @@ export class Engine {
|
|
|
2051
2051
|
signal,
|
|
2052
2052
|
onRawExchange: captureRawExchange,
|
|
2053
2053
|
})) {
|
|
2054
|
+
// task-325a (abort-stop fix): per-event abort short-circuit.
|
|
2055
|
+
// The adapter is expected to throw AbortError when fetch's
|
|
2056
|
+
// signal fires, but in practice undici/HTTP-2/proxy layers
|
|
2057
|
+
// can hand us a batch of SSE chunks that were already buffered
|
|
2058
|
+
// when abort was requested. Those chunks would otherwise be
|
|
2059
|
+
// forwarded to the caller (web-bridge → WS → browser) for
|
|
2060
|
+
// 1–2s after the user pressed Stop, producing the exact
|
|
2061
|
+
// symptom "Stop button doesn't stop the turn". Drop every
|
|
2062
|
+
// post-abort event by throwing into the outer catch, which
|
|
2063
|
+
// converges on the same `aborted` + `turn_end` terminal pair
|
|
2064
|
+
// as the adapter-throws-AbortError path.
|
|
2065
|
+
if (signal?.aborted) {
|
|
2066
|
+
throw new LLMAbortError();
|
|
2067
|
+
}
|
|
2054
2068
|
switch (event.type) {
|
|
2055
2069
|
case 'text_delta':
|
|
2056
2070
|
if (ttfbMs === null) ttfbMs = Date.now() - startTime;
|
package/yeaft/session.js
CHANGED
|
@@ -370,6 +370,14 @@ export async function loadSession(options = {}) {
|
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
// ─── 6. Load skills ────────────────────────────────────
|
|
373
|
+
// Project tier root for skills + MCP project assets. Per-session/per-group
|
|
374
|
+
// workdirs override at tool-execution time via ToolContext.cwd; for the
|
|
375
|
+
// SYSTEM-PROMPT skill set and the project MCP servers we use the agent
|
|
376
|
+
// process cwd, the common case when an agent is launched inside a project
|
|
377
|
+
// the user wants project-tier assets for. Shared so skills (.claude/skills,
|
|
378
|
+
// .yeaft/skills) and MCP (.mcp.json) resolve from the same root.
|
|
379
|
+
const projectTierRoot = process.cwd();
|
|
380
|
+
|
|
373
381
|
let skillManager;
|
|
374
382
|
if (skipSkills) {
|
|
375
383
|
// Pass the literal user-tier dir (matches the normal branch's tier 2)
|
|
@@ -378,17 +386,11 @@ export async function loadSession(options = {}) {
|
|
|
378
386
|
skillManager = new SkillManager(join(yeaftDir, 'skills'));
|
|
379
387
|
// Don't call .load() — empty skill manager
|
|
380
388
|
} else {
|
|
381
|
-
// Pass the agent's current working directory as the project tier root.
|
|
382
|
-
// Per-session/per-group workdirs override at tool-execution time via
|
|
383
|
-
// ToolContext.cwd; for the SYSTEM-PROMPT skill set we just use the
|
|
384
|
-
// agent process cwd, which is the common case when an agent is
|
|
385
|
-
// launched inside a project the user wants project-tier skills for.
|
|
386
|
-
const projectTierRoot = process.cwd();
|
|
387
389
|
skillManager = createSkillManager(yeaftDir, projectTierRoot);
|
|
388
390
|
}
|
|
389
391
|
|
|
390
392
|
// ─── 7. Connect MCP servers ────────────────────────────
|
|
391
|
-
const mcpConfig = loadMCPConfig(yeaftDir);
|
|
393
|
+
const mcpConfig = loadMCPConfig(yeaftDir, undefined, projectTierRoot);
|
|
392
394
|
const mcpManager = new MCPManager();
|
|
393
395
|
let mcpStatus = { connected: [], failed: [] };
|
|
394
396
|
|
|
@@ -548,6 +550,10 @@ export async function loadSession(options = {}) {
|
|
|
548
550
|
skills: skillManager.size,
|
|
549
551
|
mcpServers: mcpStatus.connected,
|
|
550
552
|
mcpFailed: mcpStatus.failed,
|
|
553
|
+
// Project `.mcp.json` servers we couldn't spawn (e.g. SSE/HTTP transport).
|
|
554
|
+
// Surfaced (not silently dropped) so the UI can explain why a configured
|
|
555
|
+
// server isn't available.
|
|
556
|
+
mcpSkipped: mcpConfig.skipped || [],
|
|
551
557
|
tools: toolRegistry.size,
|
|
552
558
|
};
|
|
553
559
|
|
package/yeaft/skills.js
CHANGED
|
@@ -34,9 +34,14 @@
|
|
|
34
34
|
* tier 2 (user): <yeaftDir>/skills (e.g. ~/.yeaft/skills). User edits
|
|
35
35
|
* land here. `save()` writes here. `init.js` seeds it from tier 1
|
|
36
36
|
* on first boot so users start with the full bundled set.
|
|
37
|
-
* tier 3 (project): <workDir>/.
|
|
37
|
+
* tier 3 (project-claude): <workDir>/.claude/skills (if provided). Claude
|
|
38
|
+
* Code project assets, loaded so a Claude-Code-integrated project
|
|
39
|
+
* works out of the box. Higher than user (project-local beats
|
|
40
|
+
* user-global), lower than the yeaft-native project tier.
|
|
41
|
+
* tier 4 (project): <workDir>/.yeaft/skills (if provided). Highest
|
|
38
42
|
* priority — a project can pin a skill version without affecting
|
|
39
|
-
* the user's other projects
|
|
43
|
+
* the user's other projects, and overrides a borrowed
|
|
44
|
+
* `.claude/skills` skill of the same name.
|
|
40
45
|
*
|
|
41
46
|
* Reference: yeaft-yeaft-design.md §8, yeaft-yeaft-core-systems.md
|
|
42
47
|
*/
|
|
@@ -694,10 +699,16 @@ export class SkillManager {
|
|
|
694
699
|
* Create a SkillManager wired with the standard layered tier list and load it.
|
|
695
700
|
*
|
|
696
701
|
* Tier order (lowest → highest priority):
|
|
697
|
-
* 1. bundled
|
|
702
|
+
* 1. bundled — the yeaft-skills package on disk, located via
|
|
698
703
|
* `bundledYeaftSkillsDir()` (typically ~/.claude/skills/yeaft-skills/skills/).
|
|
699
|
-
* 2. user
|
|
700
|
-
* 3. project — `<workDir>/.
|
|
704
|
+
* 2. user — `<yeaftDir>/skills` (e.g. ~/.yeaft/skills). User edits + saves.
|
|
705
|
+
* 3. project-claude — `<workDir>/.claude/skills` when a workDir is provided.
|
|
706
|
+
* Claude Code project assets, loaded so a project that integrates Claude
|
|
707
|
+
* Code works out of the box. Ranks above `user` (project-local beats
|
|
708
|
+
* user-global) but below the yeaft-native project tier.
|
|
709
|
+
* 4. project — `<workDir>/.yeaft/skills` when a workDir is provided.
|
|
710
|
+
* Highest priority: a yeaft-native skill pinned in the project overrides
|
|
711
|
+
* a borrowed `.claude/skills` skill of the same name.
|
|
701
712
|
*
|
|
702
713
|
* `save()` / `remove()` always target the USER tier, matching Claude Code.
|
|
703
714
|
*
|
|
@@ -708,12 +719,14 @@ export class SkillManager {
|
|
|
708
719
|
export function createSkillManager(yeaftDir, workDir) {
|
|
709
720
|
const bundled = bundledYeaftSkillsDir();
|
|
710
721
|
const userDir = join(yeaftDir, 'skills');
|
|
722
|
+
const claudeProjectDir = workDir ? join(workDir, '.claude', 'skills') : null;
|
|
711
723
|
const projectDir = workDir ? join(workDir, '.yeaft', 'skills') : null;
|
|
712
724
|
|
|
713
|
-
const dirs = [bundled, userDir, projectDir].filter(Boolean);
|
|
725
|
+
const dirs = [bundled, userDir, claudeProjectDir, projectDir].filter(Boolean);
|
|
714
726
|
const tierByDir = {};
|
|
715
727
|
if (bundled) tierByDir[bundled] = 'bundled';
|
|
716
728
|
tierByDir[userDir] = 'user';
|
|
729
|
+
if (claudeProjectDir) tierByDir[claudeProjectDir] = 'project-claude';
|
|
717
730
|
if (projectDir) tierByDir[projectDir] = 'project';
|
|
718
731
|
|
|
719
732
|
const manager = new SkillManager(dirs, { userDir, tierByDir });
|