@salesforce/sfdx-agent-harness-claude 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ import { LogBus } from '@salesforce/agentic-common';
2
+ /**
3
+ * One discovered skill: the absolute path to its `SKILL.md` and the
4
+ * description string the loader will surface to the model via the
5
+ * `skills` MCP tool catalog. The frontmatter `name` (or the
6
+ * skill folder basename, if the frontmatter is missing) is the key in
7
+ * the {@link SkillMap}; this struct holds the rest.
8
+ */
9
+ export type SkillEntry = {
10
+ /** Absolute path to the resolved `SKILL.md` file. */
11
+ path: string;
12
+ /**
13
+ * Skill description from the YAML frontmatter `description:` field, trimmed.
14
+ * Empty string when the frontmatter omits `description:`. Surfaced verbatim
15
+ * in the `load_skill` tool description so the model can pick the right
16
+ * skill without reading the body.
17
+ */
18
+ description: string;
19
+ };
20
+ /**
21
+ * Discovered skill catalog: skill name → entry. The name is what the
22
+ * model passes back when it invokes `mcp__skill_bridge__load_skill`.
23
+ */
24
+ export type SkillMap = Map<string, SkillEntry>;
25
+ /**
26
+ * Resolve `config.skills` paths into a `Map<skillName, SkillEntry>`.
27
+ * Each entry in `skillPaths` may be:
28
+ *
29
+ * - A `SKILL.md` file path → one skill (named from frontmatter or
30
+ * parent folder basename).
31
+ * - A folder containing `SKILL.md` → one skill.
32
+ * - A parent folder containing one or more skill subfolders, each with
33
+ * its own `SKILL.md` → many skills. Subfolders without a `SKILL.md`
34
+ * are silently skipped so consumers can drop arbitrary subdirectories
35
+ * into their skill roots without breaking the loader.
36
+ *
37
+ * Mirrors `composeRulesAppend`'s contract for path validation: a
38
+ * configured entry that does not exist or that the harness cannot
39
+ * `stat` rejects `createAgent`, matching the rules-loader behavior so
40
+ * a typo in `config.skills` fails loud at registration time rather
41
+ * than silently dropping the skill at every turn.
42
+ *
43
+ * Skill name resolution: the frontmatter `name:` field wins; if it is
44
+ * missing or empty the parent-folder basename is used. When two
45
+ * configured paths resolve to the same name, **first writer wins** —
46
+ * the second entry is dropped. (Mirrors the behavior of
47
+ * `Map.set`-with-collision semantics; consumers who configure
48
+ * overlapping skill roots see only the first occurrence.)
49
+ *
50
+ * Frontmatter `description:` is captured verbatim (trimmed) and stored
51
+ * for the MCP server to surface in the tool description. When absent,
52
+ * the entry's `description` is the empty string.
53
+ *
54
+ * Frontmatter parsing is intentionally regex-based, not full YAML —
55
+ * `splitFrontmatterAndBody` from `@salesforce/agentic-common` already
56
+ * keeps the parser dependency-free, and the only fields this loader
57
+ * needs are `name` and `description`. A future requirement that needs
58
+ * structured frontmatter can swap in a YAML parser without changing
59
+ * the public shape of `SkillMap`.
60
+ */
61
+ export declare function buildSkillMap(skillPaths: readonly string[] | undefined, logBus?: LogBus): Promise<SkillMap>;
@@ -0,0 +1,153 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import * as fs from 'node:fs/promises';
6
+ import * as path from 'node:path';
7
+ import { getErrorMessage, LogBus, splitFrontmatterAndBody } from '@salesforce/agentic-common';
8
+ const SKILL_FILE_NAME = 'SKILL.md';
9
+ const FRONTMATTER_NAME_RE = /^name:\s*(.+?)\s*$/m;
10
+ const FRONTMATTER_DESCRIPTION_RE = /^description:\s*(.+?)\s*$/m;
11
+ /**
12
+ * Resolve `config.skills` paths into a `Map<skillName, SkillEntry>`.
13
+ * Each entry in `skillPaths` may be:
14
+ *
15
+ * - A `SKILL.md` file path → one skill (named from frontmatter or
16
+ * parent folder basename).
17
+ * - A folder containing `SKILL.md` → one skill.
18
+ * - A parent folder containing one or more skill subfolders, each with
19
+ * its own `SKILL.md` → many skills. Subfolders without a `SKILL.md`
20
+ * are silently skipped so consumers can drop arbitrary subdirectories
21
+ * into their skill roots without breaking the loader.
22
+ *
23
+ * Mirrors `composeRulesAppend`'s contract for path validation: a
24
+ * configured entry that does not exist or that the harness cannot
25
+ * `stat` rejects `createAgent`, matching the rules-loader behavior so
26
+ * a typo in `config.skills` fails loud at registration time rather
27
+ * than silently dropping the skill at every turn.
28
+ *
29
+ * Skill name resolution: the frontmatter `name:` field wins; if it is
30
+ * missing or empty the parent-folder basename is used. When two
31
+ * configured paths resolve to the same name, **first writer wins** —
32
+ * the second entry is dropped. (Mirrors the behavior of
33
+ * `Map.set`-with-collision semantics; consumers who configure
34
+ * overlapping skill roots see only the first occurrence.)
35
+ *
36
+ * Frontmatter `description:` is captured verbatim (trimmed) and stored
37
+ * for the MCP server to surface in the tool description. When absent,
38
+ * the entry's `description` is the empty string.
39
+ *
40
+ * Frontmatter parsing is intentionally regex-based, not full YAML —
41
+ * `splitFrontmatterAndBody` from `@salesforce/agentic-common` already
42
+ * keeps the parser dependency-free, and the only fields this loader
43
+ * needs are `name` and `description`. A future requirement that needs
44
+ * structured frontmatter can swap in a YAML parser without changing
45
+ * the public shape of `SkillMap`.
46
+ */
47
+ export async function buildSkillMap(skillPaths, logBus) {
48
+ const map = new Map();
49
+ if (!skillPaths || skillPaths.length === 0)
50
+ return map;
51
+ for (const entry of skillPaths) {
52
+ let stats;
53
+ try {
54
+ stats = await fs.stat(entry);
55
+ }
56
+ catch (err) {
57
+ throw new Error(`Failed to load skills from "${entry}": ${getErrorMessage(err)}`, { cause: err });
58
+ }
59
+ if (stats.isFile()) {
60
+ if (path.basename(entry) !== SKILL_FILE_NAME)
61
+ continue;
62
+ await ingestSkillFile(entry, map, logBus);
63
+ continue;
64
+ }
65
+ if (!stats.isDirectory())
66
+ continue;
67
+ // Single-skill folder containing SKILL.md?
68
+ const candidate = path.join(entry, SKILL_FILE_NAME);
69
+ if (await isFile(candidate)) {
70
+ await ingestSkillFile(candidate, map, logBus);
71
+ continue;
72
+ }
73
+ // Parent folder: enumerate immediate subdirectories.
74
+ let children;
75
+ try {
76
+ children = await fs.readdir(entry, { withFileTypes: true });
77
+ }
78
+ catch (err) {
79
+ throw new Error(`Failed to load skills from "${entry}": ${getErrorMessage(err)}`, { cause: err });
80
+ }
81
+ for (const child of children) {
82
+ if (!child.isDirectory())
83
+ continue;
84
+ const childSkillMd = path.join(entry, child.name, SKILL_FILE_NAME);
85
+ if (!(await isFile(childSkillMd)))
86
+ continue;
87
+ await ingestSkillFile(childSkillMd, map, logBus);
88
+ }
89
+ }
90
+ return map;
91
+ }
92
+ async function ingestSkillFile(skillMdPath, map, logBus) {
93
+ let raw;
94
+ try {
95
+ raw = await fs.readFile(skillMdPath, 'utf8');
96
+ }
97
+ catch (err) {
98
+ throw new Error(`Failed to read skill file "${skillMdPath}": ${getErrorMessage(err)}`, { cause: err });
99
+ }
100
+ const { frontmatter } = splitFrontmatterAndBody(raw);
101
+ const fm = frontmatter ?? '';
102
+ const name = readField(fm, FRONTMATTER_NAME_RE) ?? path.basename(path.dirname(skillMdPath));
103
+ const description = readField(fm, FRONTMATTER_DESCRIPTION_RE) ?? '';
104
+ if (!name)
105
+ return;
106
+ if (map.has(name)) {
107
+ // First-writer-wins by design (consumers can layer skill folders),
108
+ // but warn so the silent shadow doesn't burn debugging time.
109
+ const winner = map.get(name);
110
+ logBus?.warn(`Skill name "${name}" already loaded; ignoring duplicate at "${skillMdPath}".`, {
111
+ skillName: name,
112
+ keptPath: winner.path,
113
+ ignoredPath: skillMdPath,
114
+ });
115
+ return;
116
+ }
117
+ map.set(name, { path: skillMdPath, description });
118
+ }
119
+ function readField(frontmatter, re) {
120
+ const match = re.exec(frontmatter);
121
+ if (!match?.[1])
122
+ return undefined;
123
+ const trimmed = match[1].trim();
124
+ if (trimmed.length === 0)
125
+ return undefined;
126
+ // Strip a single matched pair of YAML scalar quotes (`"..."` or
127
+ // `'...'`). YAML allows quoting frontmatter scalars to escape colons,
128
+ // leading whitespace, etc., and most authors with descriptions
129
+ // containing punctuation will reach for quotes. A regex-based reader
130
+ // (kept for the no-YAML-runtime stance) would otherwise capture the
131
+ // quotes verbatim — the resulting `name:` key would include them and
132
+ // the model's `load_skill({ skill: '<name>' })` call would miss the
133
+ // catalog entry. Mastra's full YAML parser strips quotes; this keeps
134
+ // the cross-harness contract on the same observable shape.
135
+ if (trimmed.length >= 2) {
136
+ const first = trimmed.charAt(0);
137
+ const last = trimmed.charAt(trimmed.length - 1);
138
+ if ((first === '"' || first === "'") && first === last) {
139
+ return trimmed.slice(1, -1);
140
+ }
141
+ }
142
+ return trimmed;
143
+ }
144
+ async function isFile(p) {
145
+ try {
146
+ const s = await fs.stat(p);
147
+ return s.isFile();
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ }
153
+ //# sourceMappingURL=skill-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-loader.js","sourceRoot":"","sources":["../src/skill-loader.ts"],"names":[],"mappings":"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"}