@yeaft/webchat-agent 0.1.1075 → 0.1.1076
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 +218 -55
- package/yeaft/skills.js +64 -14
package/package.json
CHANGED
package/yeaft/config.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { existsSync, readFileSync } from 'fs';
|
|
23
|
+
import { homedir } from 'os';
|
|
23
24
|
import { join } from 'path';
|
|
24
25
|
import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
25
26
|
import { getModelEffortOptions, getThinkingCapability, modelSupportsEffort, resolveModel, parseModelRef, normalizeProviderModels, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
|
|
@@ -513,13 +514,16 @@ export function loadConfig(overrides = {}) {
|
|
|
513
514
|
/**
|
|
514
515
|
* Load MCP server configuration.
|
|
515
516
|
*
|
|
516
|
-
* Merges
|
|
517
|
-
* 1. global
|
|
517
|
+
* Merges compatibility tiers, in priority order (highest first):
|
|
518
|
+
* 1. yeaft-global — ~/.yeaft authoritative config: config.json "mcpServers"
|
|
518
519
|
* array, else standalone ~/.yeaft/mcp.json ({ servers: [...] }).
|
|
519
|
-
* 2.
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
520
|
+
* 2. claude-user — ~/.claude.json (Claude Code user-scope MCP config).
|
|
521
|
+
* 3. codex-user — ~/.codex/config.toml (Codex user-scope MCP config).
|
|
522
|
+
* 4. project — `<workDir>/.mcp.json` (Claude Code project location)
|
|
523
|
+
* plus `<workDir>/.codex/config.toml` (Codex project location).
|
|
524
|
+
*
|
|
525
|
+
* Higher tiers win. Borrowed Claude/Codex configs supplement Yeaft, but never
|
|
526
|
+
* override explicit ~/.yeaft settings.
|
|
523
527
|
*
|
|
524
528
|
* @param {string} yeaftDir
|
|
525
529
|
* @param {object} [jsonConfig] — Already-parsed config.json (optional, avoids re-read)
|
|
@@ -527,24 +531,23 @@ export function loadConfig(overrides = {}) {
|
|
|
527
531
|
* @returns {{ servers: object[], skipped: { name: string, reason: string, source: string }[] }}
|
|
528
532
|
*/
|
|
529
533
|
export function loadMCPConfig(yeaftDir, jsonConfig, workDir) {
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
// ── Project tier: <workDir>/.mcp.json (Claude Code standard) ──
|
|
534
|
+
const yeaftGlobal = loadGlobalMCPServers(yeaftDir, jsonConfig);
|
|
535
|
+
const externalUser = loadExternalUserMCPServers();
|
|
534
536
|
const project = workDir
|
|
535
537
|
? loadProjectMCPServers(workDir)
|
|
536
538
|
: { servers: [], skipped: [] };
|
|
537
539
|
|
|
538
|
-
|
|
539
|
-
const seen = new Set(
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
540
|
+
const servers = [];
|
|
541
|
+
const seen = new Set();
|
|
542
|
+
for (const tier of [yeaftGlobal, externalUser.servers, project.servers]) {
|
|
543
|
+
for (const s of tier) {
|
|
544
|
+
if (!s?.name || seen.has(s.name)) continue;
|
|
545
|
+
seen.add(s.name);
|
|
546
|
+
servers.push(s);
|
|
547
|
+
}
|
|
545
548
|
}
|
|
546
549
|
|
|
547
|
-
return { servers, skipped: project.skipped };
|
|
550
|
+
return { servers, skipped: [...externalUser.skipped, ...project.skipped] };
|
|
548
551
|
}
|
|
549
552
|
|
|
550
553
|
/**
|
|
@@ -579,35 +582,27 @@ function loadGlobalMCPServers(yeaftDir, jsonConfig) {
|
|
|
579
582
|
}
|
|
580
583
|
}
|
|
581
584
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
* `mcpServers` field all return gracefully with empty arrays — a broken
|
|
596
|
-
* project `.mcp.json` must never fail session creation.
|
|
597
|
-
*
|
|
598
|
-
* @param {string} workDir — project working directory
|
|
599
|
-
* @returns {{ servers: object[], skipped: { name: string, reason: string, source: string }[] }}
|
|
600
|
-
*/
|
|
601
|
-
export function loadProjectMCPServers(workDir) {
|
|
602
|
-
const empty = { servers: [], skipped: [] };
|
|
603
|
-
if (!workDir || typeof workDir !== 'string') return empty;
|
|
585
|
+
function normaliseStdioMCPServer(name, raw, source) {
|
|
586
|
+
if (!name || !raw || typeof raw !== 'object') return { server: null, skipped: null };
|
|
587
|
+
if (typeof raw.command === 'string' && raw.command.length > 0) {
|
|
588
|
+
const server = { name, command: raw.command };
|
|
589
|
+
if (Array.isArray(raw.args)) server.args = raw.args;
|
|
590
|
+
if (raw.env && typeof raw.env === 'object' && !Array.isArray(raw.env)) server.env = raw.env;
|
|
591
|
+
return { server, skipped: null };
|
|
592
|
+
}
|
|
593
|
+
if (typeof raw.url === 'string' || typeof raw.type === 'string') {
|
|
594
|
+
return { server: null, skipped: { name, reason: 'unsupported-transport', source } };
|
|
595
|
+
}
|
|
596
|
+
return { server: null, skipped: { name, reason: 'invalid-config', source } };
|
|
597
|
+
}
|
|
604
598
|
|
|
605
|
-
|
|
606
|
-
|
|
599
|
+
function loadClaudeMCPJsonFile(filePath, source) {
|
|
600
|
+
const empty = { servers: [], skipped: [] };
|
|
601
|
+
if (!filePath || !existsSync(filePath)) return empty;
|
|
607
602
|
|
|
608
603
|
let parsed;
|
|
609
604
|
try {
|
|
610
|
-
parsed = JSON.parse(readFileSync(
|
|
605
|
+
parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
611
606
|
} catch {
|
|
612
607
|
return empty;
|
|
613
608
|
}
|
|
@@ -620,21 +615,189 @@ export function loadProjectMCPServers(workDir) {
|
|
|
620
615
|
const servers = [];
|
|
621
616
|
const skipped = [];
|
|
622
617
|
for (const [name, raw] of Object.entries(mcpServers)) {
|
|
623
|
-
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
618
|
+
const normalised = normaliseStdioMCPServer(name, raw, source);
|
|
619
|
+
if (normalised.server) servers.push(normalised.server);
|
|
620
|
+
if (normalised.skipped) skipped.push(normalised.skipped);
|
|
621
|
+
}
|
|
622
|
+
return { servers, skipped };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function stripTomlInlineComment(raw) {
|
|
626
|
+
const text = String(raw || '');
|
|
627
|
+
let inSingle = false;
|
|
628
|
+
let inDouble = false;
|
|
629
|
+
let escaped = false;
|
|
630
|
+
let depth = 0;
|
|
631
|
+
|
|
632
|
+
for (let i = 0; i < text.length; i++) {
|
|
633
|
+
const ch = text[i];
|
|
634
|
+
if (inDouble) {
|
|
635
|
+
if (escaped) {
|
|
636
|
+
escaped = false;
|
|
637
|
+
} else if (ch === '\\') {
|
|
638
|
+
escaped = true;
|
|
639
|
+
} else if (ch === '"') {
|
|
640
|
+
inDouble = false;
|
|
641
|
+
}
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (inSingle) {
|
|
645
|
+
if (ch === "'") inSingle = false;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (ch === '"') {
|
|
649
|
+
inDouble = true;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (ch === "'") {
|
|
653
|
+
inSingle = true;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (ch === '[' || ch === '{') {
|
|
657
|
+
depth++;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if ((ch === ']' || ch === '}') && depth > 0) {
|
|
661
|
+
depth--;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (ch === '#' && depth === 0) return text.slice(0, i).trim();
|
|
665
|
+
}
|
|
666
|
+
return text.trim();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function parseTomlValue(raw) {
|
|
670
|
+
const value = stripTomlInlineComment(raw);
|
|
671
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
672
|
+
try { return JSON.parse(value); } catch { return value.slice(1, -1); }
|
|
673
|
+
}
|
|
674
|
+
if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
675
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
676
|
+
try { return JSON.parse(value.replace(/'/g, '"')); } catch { return undefined; }
|
|
677
|
+
}
|
|
678
|
+
if (value.startsWith('{') && value.endsWith('}')) {
|
|
679
|
+
const obj = {};
|
|
680
|
+
const inner = value.slice(1, -1).trim();
|
|
681
|
+
if (!inner) return obj;
|
|
682
|
+
for (const part of inner.split(',')) {
|
|
683
|
+
const eq = part.indexOf('=');
|
|
684
|
+
if (eq <= 0) return undefined;
|
|
685
|
+
const key = part.slice(0, eq).trim().replace(/^['"]|['"]$/g, '');
|
|
686
|
+
const parsed = parseTomlValue(part.slice(eq + 1));
|
|
687
|
+
if (!key || parsed === undefined) return undefined;
|
|
688
|
+
obj[key] = parsed;
|
|
689
|
+
}
|
|
690
|
+
return obj;
|
|
691
|
+
}
|
|
692
|
+
if (value === 'true') return true;
|
|
693
|
+
if (value === 'false') return false;
|
|
694
|
+
return value;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function parseCodexMCPServersToml(content, source) {
|
|
698
|
+
const rawServers = {};
|
|
699
|
+
let current = null;
|
|
700
|
+
for (const line of String(content || '').split(/\r?\n/)) {
|
|
701
|
+
const trimmed = line.trim();
|
|
702
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
703
|
+
const section = trimmed.match(/^\[mcp_servers\.("[^"]+"|'[^']+'|[^\].]+)(?:\.(env))?\]$/);
|
|
704
|
+
if (section) {
|
|
705
|
+
const name = section[1].replace(/^['"]|['"]$/g, '');
|
|
706
|
+
rawServers[name] ||= {};
|
|
707
|
+
current = { name, env: section[2] === 'env' };
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (!current) continue;
|
|
711
|
+
const eq = trimmed.indexOf('=');
|
|
712
|
+
if (eq <= 0) continue;
|
|
713
|
+
const key = trimmed.slice(0, eq).trim();
|
|
714
|
+
const value = parseTomlValue(trimmed.slice(eq + 1));
|
|
715
|
+
if (value === undefined) continue;
|
|
716
|
+
if (current.env) {
|
|
717
|
+
rawServers[current.name].env ||= {};
|
|
718
|
+
rawServers[current.name].env[key] = String(value);
|
|
719
|
+
} else if (key === 'env' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
720
|
+
rawServers[current.name].env = Object.fromEntries(Object.entries(value).map(([k, v]) => [k, String(v)]));
|
|
633
721
|
} else {
|
|
634
|
-
|
|
635
|
-
skipped.push({ name, reason: 'invalid-config', source: '.mcp.json' });
|
|
722
|
+
rawServers[current.name][key] = value;
|
|
636
723
|
}
|
|
637
724
|
}
|
|
638
725
|
|
|
726
|
+
const servers = [];
|
|
727
|
+
const skipped = [];
|
|
728
|
+
for (const [name, raw] of Object.entries(rawServers)) {
|
|
729
|
+
const normalised = normaliseStdioMCPServer(name, raw, source);
|
|
730
|
+
if (normalised.server) servers.push(normalised.server);
|
|
731
|
+
if (normalised.skipped) skipped.push(normalised.skipped);
|
|
732
|
+
}
|
|
733
|
+
return { servers, skipped };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function loadCodexMCPConfigFile(filePath, source) {
|
|
737
|
+
const empty = { servers: [], skipped: [] };
|
|
738
|
+
if (!filePath || !existsSync(filePath)) return empty;
|
|
739
|
+
try {
|
|
740
|
+
return parseCodexMCPServersToml(readFileSync(filePath, 'utf8'), source);
|
|
741
|
+
} catch {
|
|
742
|
+
return empty;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function mergeMCPConfigResults(results) {
|
|
747
|
+
const servers = [];
|
|
748
|
+
const skipped = [];
|
|
749
|
+
const seen = new Set();
|
|
750
|
+
for (const result of results) {
|
|
751
|
+
for (const s of result.servers || []) {
|
|
752
|
+
if (!s?.name || seen.has(s.name)) continue;
|
|
753
|
+
seen.add(s.name);
|
|
754
|
+
servers.push(s);
|
|
755
|
+
}
|
|
756
|
+
skipped.push(...(result.skipped || []));
|
|
757
|
+
}
|
|
639
758
|
return { servers, skipped };
|
|
640
759
|
}
|
|
760
|
+
|
|
761
|
+
function loadExternalUserMCPServers() {
|
|
762
|
+
const home = homedir();
|
|
763
|
+
if (!home) return { servers: [], skipped: [] };
|
|
764
|
+
return mergeMCPConfigResults([
|
|
765
|
+
loadClaudeMCPJsonFile(join(home, '.claude.json'), '~/.claude.json'),
|
|
766
|
+
loadCodexMCPConfigFile(join(home, '.codex', 'config.toml'), '~/.codex/config.toml'),
|
|
767
|
+
]);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Parse project-level borrowed MCP configs from `<workDir>/.mcp.json` (Claude
|
|
772
|
+
* Code) and `<workDir>/.codex/config.toml` (Codex).
|
|
773
|
+
*
|
|
774
|
+
* Claude Code format:
|
|
775
|
+
* { "mcpServers": { "<name>": { command, args?, env?, url?, type? } } }
|
|
776
|
+
*
|
|
777
|
+
* Codex format subset:
|
|
778
|
+
* [mcp_servers.<name>]
|
|
779
|
+
* command = "..."
|
|
780
|
+
* args = ["..."]
|
|
781
|
+
*
|
|
782
|
+
* Only stdio servers (those with a `command`) are adapted into yeaft's
|
|
783
|
+
* { name, command, args?, env? } shape. SSE/HTTP servers (url/type, no
|
|
784
|
+
* command) cannot be spawned by the current MCPManager, so they're reported
|
|
785
|
+
* in `skipped` with reason 'unsupported-transport' rather than silently
|
|
786
|
+
* dropped or surfaced later as spawn failures.
|
|
787
|
+
*
|
|
788
|
+
* Robust by design: missing files, malformed JSON/TOML, or non-object
|
|
789
|
+
* `mcpServers` fields all return gracefully with empty arrays — broken borrowed
|
|
790
|
+
* configs must never fail session creation.
|
|
791
|
+
*
|
|
792
|
+
* @param {string} workDir — project working directory
|
|
793
|
+
* @returns {{ servers: object[], skipped: { name: string, reason: string, source: string }[] }}
|
|
794
|
+
*/
|
|
795
|
+
export function loadProjectMCPServers(workDir) {
|
|
796
|
+
const empty = { servers: [], skipped: [] };
|
|
797
|
+
if (!workDir || typeof workDir !== 'string') return empty;
|
|
798
|
+
|
|
799
|
+
return mergeMCPConfigResults([
|
|
800
|
+
loadClaudeMCPJsonFile(join(workDir, '.mcp.json'), '.mcp.json'),
|
|
801
|
+
loadCodexMCPConfigFile(join(workDir, '.codex', 'config.toml'), '.codex/config.toml'),
|
|
802
|
+
]);
|
|
803
|
+
}
|
package/yeaft/skills.js
CHANGED
|
@@ -31,17 +31,23 @@
|
|
|
31
31
|
* tier 1 (bundled): wherever yeaft-skills is installed on disk — typically
|
|
32
32
|
* ~/.claude/skills/yeaft-skills/skills/. Read-only — `save()` and
|
|
33
33
|
* `remove()` never target this tier.
|
|
34
|
-
* tier 2 (user):
|
|
34
|
+
* tier 2 (user-claude): ~/.claude/skills. User-level Claude Code assets,
|
|
35
|
+
* loaded read-only for cross-tool compatibility.
|
|
36
|
+
* tier 3 (user-codex): ~/.codex/skills. User-level Codex assets, loaded
|
|
37
|
+
* read-only for cross-tool compatibility.
|
|
38
|
+
* tier 4 (user): <yeaftDir>/skills (e.g. ~/.yeaft/skills). User edits
|
|
35
39
|
* land here. `save()` writes here. `init.js` seeds it from tier 1
|
|
36
40
|
* on first boot so users start with the full bundled set.
|
|
37
|
-
* tier
|
|
41
|
+
* tier 5 (project-claude): <workDir>/.claude/skills (if provided). Claude
|
|
38
42
|
* Code project assets, loaded so a Claude-Code-integrated project
|
|
39
43
|
* works out of the box. Higher than user (project-local beats
|
|
40
44
|
* user-global), lower than the yeaft-native project tier.
|
|
41
|
-
* tier
|
|
45
|
+
* tier 6 (project-codex): <workDir>/.agents/skills (if provided). Codex
|
|
46
|
+
* project assets, loaded with the same project-local precedence.
|
|
47
|
+
* tier 7 (project): <workDir>/.yeaft/skills (if provided). Highest
|
|
42
48
|
* priority — a project can pin a skill version without affecting
|
|
43
49
|
* the user's other projects, and overrides a borrowed
|
|
44
|
-
* `.claude/skills` skill of the same name.
|
|
50
|
+
* `.claude/skills` / `.agents/skills` skill of the same name.
|
|
45
51
|
*
|
|
46
52
|
* Reference: yeaft-yeaft-design.md §8, yeaft-yeaft-core-systems.md
|
|
47
53
|
*/
|
|
@@ -207,8 +213,19 @@ function listSubdirFiles(dir) {
|
|
|
207
213
|
* @param {string} [subPath] — relative path from root (for category derivation)
|
|
208
214
|
* @returns {{ skills: Skill[], errors: string[] }}
|
|
209
215
|
*/
|
|
210
|
-
function
|
|
216
|
+
function pathIsInside(childPath, parentPath) {
|
|
217
|
+
const child = resolve(childPath);
|
|
218
|
+
const parent = resolve(parentPath);
|
|
219
|
+
return child === parent || child.startsWith(parent + sep);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function shouldIgnorePath(candidatePath, ignorePaths) {
|
|
223
|
+
return ignorePaths.some(ignorePath => pathIsInside(candidatePath, ignorePath));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function discoverSkills(rootDir, subPath = '', opts = {}) {
|
|
211
227
|
const dir = subPath ? join(rootDir, subPath) : rootDir;
|
|
228
|
+
const ignorePaths = Array.isArray(opts.ignorePaths) ? opts.ignorePaths : [];
|
|
212
229
|
const skills = [];
|
|
213
230
|
const errors = [];
|
|
214
231
|
|
|
@@ -226,6 +243,8 @@ function discoverSkills(rootDir, subPath = '') {
|
|
|
226
243
|
const entryPath = join(dir, entry.name);
|
|
227
244
|
const relPath = subPath ? join(subPath, entry.name) : entry.name;
|
|
228
245
|
|
|
246
|
+
if (shouldIgnorePath(entryPath, ignorePaths)) continue;
|
|
247
|
+
|
|
229
248
|
if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'SKILL.md') {
|
|
230
249
|
// Single-file skill (legacy format)
|
|
231
250
|
try {
|
|
@@ -272,7 +291,7 @@ function discoverSkills(rootDir, subPath = '') {
|
|
|
272
291
|
}
|
|
273
292
|
} else {
|
|
274
293
|
// No SKILL.md — treat as category directory, recurse
|
|
275
|
-
const sub = discoverSkills(rootDir, relPath);
|
|
294
|
+
const sub = discoverSkills(rootDir, relPath, opts);
|
|
276
295
|
skills.push(...sub.skills);
|
|
277
296
|
errors.push(...sub.errors);
|
|
278
297
|
}
|
|
@@ -362,17 +381,22 @@ export class SkillManager {
|
|
|
362
381
|
/** @type {Map<string, string>} — dir path → tier label */
|
|
363
382
|
#tierByDir;
|
|
364
383
|
|
|
384
|
+
/** @type {Map<string, string[]>} — dir path → resolved ignore paths */
|
|
385
|
+
#ignorePathsByDir;
|
|
386
|
+
|
|
365
387
|
/**
|
|
366
388
|
* @param {string | string[]} dirs — single directory (back-compat) or array of
|
|
367
389
|
* directories in priority order (lowest → highest). Falsy entries are
|
|
368
390
|
* filtered out so callers can write `[bundled, user, projectOrNull]`.
|
|
369
|
-
* @param {{ userDir?: string, tierByDir?: Record<string, string> }} [opts]
|
|
391
|
+
* @param {{ userDir?: string, tierByDir?: Record<string, string>, ignorePathsByDir?: Record<string, string[]> }} [opts]
|
|
370
392
|
* userDir: directory where `save()` and `remove()` write. Defaults to the
|
|
371
393
|
* last entry in `dirs` (typical case: user dir is highest priority that
|
|
372
394
|
* isn't a per-project layer).
|
|
373
395
|
* tierByDir: optional label map — dir path → 'bundled' | 'user' | 'project'.
|
|
374
396
|
* Decorates each discovered Skill with `_tier` for diagnostics (Settings
|
|
375
397
|
* UI uses this to show "where this skill came from").
|
|
398
|
+
* ignorePathsByDir: optional map of scan dir → subtrees to skip while
|
|
399
|
+
* recursively discovering skills in that dir.
|
|
376
400
|
*/
|
|
377
401
|
constructor(dirs, opts = {}) {
|
|
378
402
|
const list = Array.isArray(dirs)
|
|
@@ -393,6 +417,13 @@ export class SkillManager {
|
|
|
393
417
|
}
|
|
394
418
|
}
|
|
395
419
|
}
|
|
420
|
+
this.#ignorePathsByDir = new Map();
|
|
421
|
+
if (opts && opts.ignorePathsByDir && typeof opts.ignorePathsByDir === 'object') {
|
|
422
|
+
for (const [d, ignorePaths] of Object.entries(opts.ignorePathsByDir)) {
|
|
423
|
+
if (typeof d !== 'string' || !Array.isArray(ignorePaths)) continue;
|
|
424
|
+
this.#ignorePathsByDir.set(d, ignorePaths.filter(p => typeof p === 'string' && p.length > 0).map(p => resolve(p)));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
396
427
|
}
|
|
397
428
|
|
|
398
429
|
/** The user-writable skills directory (save/remove target). */
|
|
@@ -425,7 +456,7 @@ export class SkillManager {
|
|
|
425
456
|
|
|
426
457
|
for (const dir of this.#skillsDirs) {
|
|
427
458
|
if (!existsSync(dir)) continue;
|
|
428
|
-
const { skills, errors } = discoverSkills(dir);
|
|
459
|
+
const { skills, errors } = discoverSkills(dir, '', { ignorePaths: this.#ignorePathsByDir.get(dir) || [] });
|
|
429
460
|
const tier = this.#tierByDir.get(dir) || basename(dir);
|
|
430
461
|
for (const skill of skills) {
|
|
431
462
|
// Platform filtering at load time
|
|
@@ -701,14 +732,19 @@ export class SkillManager {
|
|
|
701
732
|
* Tier order (lowest → highest priority):
|
|
702
733
|
* 1. bundled — the yeaft-skills package on disk, located via
|
|
703
734
|
* `bundledYeaftSkillsDir()` (typically ~/.claude/skills/yeaft-skills/skills/).
|
|
704
|
-
* 2. user
|
|
705
|
-
* 3.
|
|
735
|
+
* 2. user-claude — `~/.claude/skills`, read-only borrowed Claude Code assets.
|
|
736
|
+
* 3. user-codex — `~/.codex/skills`, read-only borrowed Codex assets.
|
|
737
|
+
* 4. user — `<yeaftDir>/skills` (e.g. ~/.yeaft/skills). User edits + saves.
|
|
738
|
+
* 5. project-claude — `<workDir>/.claude/skills` when a workDir is provided.
|
|
706
739
|
* Claude Code project assets, loaded so a project that integrates Claude
|
|
707
740
|
* Code works out of the box. Ranks above `user` (project-local beats
|
|
708
741
|
* user-global) but below the yeaft-native project tier.
|
|
709
|
-
*
|
|
742
|
+
* 6. project-codex — `<workDir>/.agents/skills` when a workDir is provided.
|
|
743
|
+
* Codex project assets, loaded so Codex-integrated repositories work out
|
|
744
|
+
* of the box. Same precedence band as project Claude Code assets.
|
|
745
|
+
* 7. project — `<workDir>/.yeaft/skills` when a workDir is provided.
|
|
710
746
|
* Highest priority: a yeaft-native skill pinned in the project overrides
|
|
711
|
-
* a borrowed `.claude/skills` skill of the same name.
|
|
747
|
+
* a borrowed `.claude/skills` / `.agents/skills` skill of the same name.
|
|
712
748
|
*
|
|
713
749
|
* `save()` / `remove()` always target the USER tier, matching Claude Code.
|
|
714
750
|
*
|
|
@@ -718,18 +754,32 @@ export class SkillManager {
|
|
|
718
754
|
*/
|
|
719
755
|
export function createSkillManager(yeaftDir, workDir) {
|
|
720
756
|
const bundled = bundledYeaftSkillsDir();
|
|
757
|
+
const home = homedir();
|
|
758
|
+
const claudeUserDir = home ? join(home, '.claude', 'skills') : null;
|
|
759
|
+
const codexUserDir = home ? join(home, '.codex', 'skills') : null;
|
|
721
760
|
const userDir = join(yeaftDir, 'skills');
|
|
722
761
|
const claudeProjectDir = workDir ? join(workDir, '.claude', 'skills') : null;
|
|
762
|
+
const codexProjectDir = workDir ? join(workDir, '.agents', 'skills') : null;
|
|
723
763
|
const projectDir = workDir ? join(workDir, '.yeaft', 'skills') : null;
|
|
724
764
|
|
|
725
|
-
const dirs = [bundled, userDir, claudeProjectDir, projectDir].filter(Boolean);
|
|
765
|
+
const dirs = [bundled, claudeUserDir, codexUserDir, userDir, claudeProjectDir, codexProjectDir, projectDir].filter(Boolean);
|
|
726
766
|
const tierByDir = {};
|
|
727
767
|
if (bundled) tierByDir[bundled] = 'bundled';
|
|
768
|
+
if (claudeUserDir) tierByDir[claudeUserDir] = 'user-claude';
|
|
769
|
+
if (codexUserDir) tierByDir[codexUserDir] = 'user-codex';
|
|
728
770
|
tierByDir[userDir] = 'user';
|
|
729
771
|
if (claudeProjectDir) tierByDir[claudeProjectDir] = 'project-claude';
|
|
772
|
+
if (codexProjectDir) tierByDir[codexProjectDir] = 'project-codex';
|
|
730
773
|
if (projectDir) tierByDir[projectDir] = 'project';
|
|
731
774
|
|
|
732
|
-
const
|
|
775
|
+
const ignorePathsByDir = {};
|
|
776
|
+
if (claudeUserDir && bundled && pathIsInside(bundled, claudeUserDir)) {
|
|
777
|
+
// Borrowed ~/.claude/skills must not recurse into the Yeaft bundled plugin
|
|
778
|
+
// package; bundled skills have their own lower-priority tier/provenance.
|
|
779
|
+
ignorePathsByDir[claudeUserDir] = [join(claudeUserDir, 'yeaft-skills')];
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const manager = new SkillManager(dirs, { userDir, tierByDir, ignorePathsByDir });
|
|
733
783
|
manager.load();
|
|
734
784
|
return manager;
|
|
735
785
|
}
|