@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.
- package/dist/claude-harness.d.ts +20 -6
- package/dist/claude-harness.js +66 -7
- package/dist/claude-harness.js.map +1 -1
- package/dist/claude-query-options-builder.js +16 -1
- package/dist/claude-query-options-builder.js.map +1 -1
- package/dist/claude-skill-mcp-server.d.ts +36 -0
- package/dist/claude-skill-mcp-server.js +164 -0
- package/dist/claude-skill-mcp-server.js.map +1 -0
- package/dist/skill-loader.d.ts +61 -0
- package/dist/skill-loader.js +153 -0
- package/dist/skill-loader.js.map +1 -0
- package/dist/test/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -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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"}
|