@ryanstark24/sfgraph-skills 1.1.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/__tests__/cursor-format.test.d.ts +2 -0
- package/dist/__tests__/cursor-format.test.d.ts.map +1 -0
- package/dist/__tests__/cursor-format.test.js +44 -0
- package/dist/__tests__/cursor-format.test.js.map +1 -0
- package/dist/__tests__/installer.test.d.ts +2 -0
- package/dist/__tests__/installer.test.d.ts.map +1 -0
- package/dist/__tests__/installer.test.js +64 -0
- package/dist/__tests__/installer.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/installer.d.ts +39 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +187 -0
- package/dist/installer.js.map +1 -0
- package/package.json +51 -0
- package/skills/sf-cross-layer-trace/SKILL.md +97 -0
- package/skills/sf-cross-org-diff/SKILL.md +70 -0
- package/skills/sf-dead-code-audit/SKILL.md +72 -0
- package/skills/sf-deployment-manifest/SKILL.md +70 -0
- package/skills/sf-explain-code/SKILL.md +66 -0
- package/skills/sf-flow-impact/SKILL.md +67 -0
- package/skills/sf-governor-risk-fix/SKILL.md +64 -0
- package/skills/sf-impact-from-diff/SKILL.md +64 -0
- package/skills/sf-metadata-refresh/SKILL.md +71 -0
- package/skills/sf-omnistudio-migration-audit/SKILL.md +71 -0
- package/skills/sf-schema-overview/SKILL.md +66 -0
- package/skills/sf-security-audit/SKILL.md +66 -0
- package/skills/sf-snapshot-compare/SKILL.md +68 -0
- package/skills/sf-web-explorer/SKILL.md +56 -0
- package/skills/sf-what-broke/SKILL.md +64 -0
- package/skills/sf-wip-impact/SKILL.md +74 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-format.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cursor-format.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toClaudeFormat, toCursorFormat } from "../installer.js";
|
|
3
|
+
const SAMPLE = `---
|
|
4
|
+
name: foo
|
|
5
|
+
description: bar
|
|
6
|
+
triggers:
|
|
7
|
+
- "x"
|
|
8
|
+
tools_used:
|
|
9
|
+
- tool_a
|
|
10
|
+
- tool_b
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Body
|
|
14
|
+
|
|
15
|
+
Some content.
|
|
16
|
+
|
|
17
|
+
- bullet
|
|
18
|
+
`;
|
|
19
|
+
describe("format transforms", () => {
|
|
20
|
+
it("toCursorFormat rewrites frontmatter into Cursor's contract", () => {
|
|
21
|
+
const out = toCursorFormat(SAMPLE);
|
|
22
|
+
// Cursor-shaped frontmatter (the only three keys it understands)
|
|
23
|
+
expect(out).toMatch(/^description:.*bar/m);
|
|
24
|
+
// globs now contains the SF file patterns so the rule auto-attaches
|
|
25
|
+
// whenever the user opens any Salesforce metadata file.
|
|
26
|
+
expect(out).toMatch(/^globs:.*sfdx-project\.json.*\*\*\/\*\.cls/m);
|
|
27
|
+
expect(out).toMatch(/^alwaysApply:\s*false$/m);
|
|
28
|
+
// Trigger phrases folded into the description so the agent matches them
|
|
29
|
+
expect(out).toMatch(/Triggers:.*"?x"?/);
|
|
30
|
+
// The agent-requested hint mentions the skill name + sfgraph routing
|
|
31
|
+
expect(out).toMatch(/sfgraph MCP tools/);
|
|
32
|
+
// Body preserved verbatim
|
|
33
|
+
expect(out).toMatch(/^# Body/m);
|
|
34
|
+
expect(out).toMatch(/^- bullet/m);
|
|
35
|
+
// Old frontmatter is gone (it would confuse Cursor's parser)
|
|
36
|
+
expect(out).not.toMatch(/^name: foo/m);
|
|
37
|
+
expect(out).not.toMatch(/^triggers:/m);
|
|
38
|
+
expect(out).not.toMatch(/^tools_used:/m);
|
|
39
|
+
});
|
|
40
|
+
it("toClaudeFormat is identity", () => {
|
|
41
|
+
expect(toClaudeFormat(SAMPLE)).toBe(SAMPLE);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
//# sourceMappingURL=cursor-format.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-format.test.js","sourceRoot":"","sources":["../../src/__tests__/cursor-format.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjE,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;CAed,CAAC;AAEF,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,GAAG,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACnC,iEAAiE;QACjE,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;QAC3C,oEAAoE;QACpE,wDAAwD;QACxD,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,6CAA6C,CAAC,CAAC;QACnE,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;QAC/C,wEAAwE;QACxE,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;QACxC,qEAAqE;QACrE,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACzC,0BAA0B;QAC1B,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QAClC,6DAA6D;QAC7D,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QACvC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QACvC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installer.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/installer.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { install, listSkillsBundled } from "../installer.js";
|
|
6
|
+
let home;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
home = mkdtempSync(join(tmpdir(), "sfgraph-home-"));
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
rmSync(home, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
describe("skills installer", () => {
|
|
14
|
+
it("installs all 16 skills to the claude target", async () => {
|
|
15
|
+
const results = await install("claude", { homeOverride: home });
|
|
16
|
+
const skillNames = await listSkillsBundled();
|
|
17
|
+
expect(skillNames.length).toBe(16);
|
|
18
|
+
expect(results.length).toBe(16);
|
|
19
|
+
for (const r of results) {
|
|
20
|
+
expect(r.target).toBe("claude");
|
|
21
|
+
expect(r.action).toBe("created");
|
|
22
|
+
expect(existsSync(r.path)).toBe(true);
|
|
23
|
+
expect(r.path).toContain(join(home, ".claude", "skills"));
|
|
24
|
+
expect(r.path.endsWith("SKILL.md")).toBe(true);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
it("installs to the cursor target as .mdc files in Cursor's frontmatter shape", async () => {
|
|
28
|
+
const results = await install("cursor", { homeOverride: home });
|
|
29
|
+
expect(results.length).toBe(15);
|
|
30
|
+
const cursorDir = join(home, ".cursor", "rules");
|
|
31
|
+
const files = readdirSync(cursorDir);
|
|
32
|
+
expect(files.every((f) => f.endsWith(".mdc"))).toBe(true);
|
|
33
|
+
expect(files.length).toBe(15);
|
|
34
|
+
// Cursor-shaped frontmatter (the three keys Cursor's rule loader reads).
|
|
35
|
+
const sample = readFileSync(join(cursorDir, files[0] ?? ""), "utf8");
|
|
36
|
+
expect(sample).toMatch(/^description:/m);
|
|
37
|
+
expect(sample).toMatch(/^globs:.*\*\*\/\*\.cls/m);
|
|
38
|
+
expect(sample).toMatch(/^alwaysApply:\s*false$/m);
|
|
39
|
+
// Old frontmatter is rewritten away (would confuse Cursor's parser).
|
|
40
|
+
expect(sample).not.toMatch(/^name:/m);
|
|
41
|
+
expect(sample).not.toMatch(/^triggers:/m);
|
|
42
|
+
expect(sample).not.toMatch(/^tools_used:/m);
|
|
43
|
+
// Body content preserved.
|
|
44
|
+
expect(sample).toMatch(/## Playbook/);
|
|
45
|
+
});
|
|
46
|
+
it("installs to all targets in one call", async () => {
|
|
47
|
+
const results = await install("all", { homeOverride: home });
|
|
48
|
+
const claudeCount = results.filter((r) => r.target === "claude").length;
|
|
49
|
+
const cursorCount = results.filter((r) => r.target === "cursor").length;
|
|
50
|
+
const vscodeCount = results.filter((r) => r.target === "vscode").length;
|
|
51
|
+
expect(claudeCount).toBe(15);
|
|
52
|
+
expect(cursorCount).toBe(15);
|
|
53
|
+
expect(vscodeCount).toBe(15);
|
|
54
|
+
expect(existsSync(join(home, ".claude", "skills"))).toBe(true);
|
|
55
|
+
expect(existsSync(join(home, ".cursor", "rules"))).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it("dry-run returns would-write actions and writes nothing", async () => {
|
|
58
|
+
const results = await install("all", { homeOverride: home, dryRun: true });
|
|
59
|
+
expect(results.every((r) => r.action === "would-write")).toBe(true);
|
|
60
|
+
expect(existsSync(join(home, ".claude", "skills"))).toBe(false);
|
|
61
|
+
expect(existsSync(join(home, ".cursor", "rules"))).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=installer.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installer.test.js","sourceRoot":"","sources":["../../src/__tests__/installer.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { type InstallOptions, type InstallResult, type SkillTarget, findSkillsRoot, install, listSkillsBundled, toClaudeFormat, toCursorFormat, } from "./installer.js";
|
|
2
|
+
export declare const SKILLS: readonly ["sf-impact-from-diff", "sf-what-broke", "sf-cross-layer-trace", "sf-dead-code-audit", "sf-governor-risk-fix", "sf-flow-impact", "sf-security-audit", "sf-cross-org-diff", "sf-deployment-manifest", "sf-omnistudio-migration-audit"];
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,cAAc,EACd,OAAO,EACP,iBAAiB,EACjB,cAAc,EACd,cAAc,GACf,MAAM,gBAAgB,CAAC;AAExB,eAAO,MAAM,MAAM,gPAWT,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { findSkillsRoot, install, listSkillsBundled, toClaudeFormat, toCursorFormat, } from "./installer.js";
|
|
2
|
+
export const SKILLS = [
|
|
3
|
+
"sf-impact-from-diff",
|
|
4
|
+
"sf-what-broke",
|
|
5
|
+
"sf-cross-layer-trace",
|
|
6
|
+
"sf-dead-code-audit",
|
|
7
|
+
"sf-governor-risk-fix",
|
|
8
|
+
"sf-flow-impact",
|
|
9
|
+
"sf-security-audit",
|
|
10
|
+
"sf-cross-org-diff",
|
|
11
|
+
"sf-deployment-manifest",
|
|
12
|
+
"sf-omnistudio-migration-audit",
|
|
13
|
+
];
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,cAAc,EACd,OAAO,EACP,iBAAiB,EACjB,cAAc,EACd,cAAc,GACf,MAAM,gBAAgB,CAAC;AAExB,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,qBAAqB;IACrB,eAAe;IACf,sBAAsB;IACtB,oBAAoB;IACpB,sBAAsB;IACtB,gBAAgB;IAChB,mBAAmB;IACnB,mBAAmB;IACnB,wBAAwB;IACxB,+BAA+B;CACvB,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type SkillTarget = "claude" | "cursor" | "vscode" | "all";
|
|
2
|
+
export interface InstallResult {
|
|
3
|
+
target: "claude" | "cursor" | "vscode";
|
|
4
|
+
skill: string;
|
|
5
|
+
path: string;
|
|
6
|
+
action: "created" | "updated" | "would-write" | "skipped";
|
|
7
|
+
}
|
|
8
|
+
export interface InstallOptions {
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
homeOverride?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Walk up from the current source file (works in dist/ and src/) to locate
|
|
14
|
+
* the bundled `skills/` directory next to this package's root.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findSkillsRoot(): string;
|
|
17
|
+
export declare function listSkillsBundled(): Promise<string[]>;
|
|
18
|
+
/** Claude consumes the SKILL.md format directly. */
|
|
19
|
+
export declare function toClaudeFormat(md: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Cursor expects `.mdc` rule files with a specific frontmatter contract:
|
|
22
|
+
* description: <short description the agent matches against>
|
|
23
|
+
* globs: <file patterns; can be empty for non-file-scoped rules>
|
|
24
|
+
* alwaysApply: <true = always loaded; false = agent-requested>
|
|
25
|
+
*
|
|
26
|
+
* Our skill files use a different frontmatter (name/description/triggers/
|
|
27
|
+
* tools_used). Rewrite the frontmatter into Cursor's shape so rules are
|
|
28
|
+
* actually picked up. We use 'agent-requested' mode (alwaysApply: false +
|
|
29
|
+
* rich description) which lets Cursor's agent pull the skill in when the
|
|
30
|
+
* user's prompt matches the description — without polluting every chat in
|
|
31
|
+
* unrelated projects.
|
|
32
|
+
*
|
|
33
|
+
* The body of the skill markdown (playbook, response shape, visualization,
|
|
34
|
+
* don't list) is preserved verbatim — that's the actual instructions the
|
|
35
|
+
* agent reads.
|
|
36
|
+
*/
|
|
37
|
+
export declare function toCursorFormat(md: string): string;
|
|
38
|
+
export declare function install(target: SkillTarget, opts?: InstallOptions): Promise<InstallResult[]>;
|
|
39
|
+
//# sourceMappingURL=installer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,CAAC;CAC3D;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAYD;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAevC;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAU3D;AAED,oDAAoD;AACpD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAoEjD;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,aAAa,EAAE,CAAC,CAwD1B"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
function targetDirs(home) {
|
|
7
|
+
return {
|
|
8
|
+
claude: join(home, ".claude", "skills"),
|
|
9
|
+
// Cursor uses ~/.cursor/rules with .mdc extension
|
|
10
|
+
cursor: join(home, ".cursor", "rules"),
|
|
11
|
+
// VS Code Claude extension shares ~/.claude
|
|
12
|
+
vscode: join(home, ".claude", "skills"),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Walk up from the current source file (works in dist/ and src/) to locate
|
|
17
|
+
* the bundled `skills/` directory next to this package's root.
|
|
18
|
+
*/
|
|
19
|
+
export function findSkillsRoot() {
|
|
20
|
+
// import.meta.url -> /…/packages/skills/{dist,src}/installer.{js,ts}
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
// candidates: ../skills, ../../skills (when inside dist/ subfolder)
|
|
23
|
+
const candidates = [
|
|
24
|
+
join(here, "..", "skills"),
|
|
25
|
+
join(here, "..", "..", "skills"),
|
|
26
|
+
join(here, "..", "..", "..", "skills"),
|
|
27
|
+
];
|
|
28
|
+
for (const c of candidates) {
|
|
29
|
+
if (existsSync(c))
|
|
30
|
+
return c;
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`@ryanstark24/sfgraph-skills: bundled skills directory not found (looked in ${candidates.join(", ")})`);
|
|
33
|
+
}
|
|
34
|
+
export async function listSkillsBundled() {
|
|
35
|
+
const root = findSkillsRoot();
|
|
36
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
37
|
+
const names = [];
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
if (!e.isDirectory())
|
|
40
|
+
continue;
|
|
41
|
+
const skillPath = join(root, e.name, "SKILL.md");
|
|
42
|
+
if (existsSync(skillPath))
|
|
43
|
+
names.push(e.name);
|
|
44
|
+
}
|
|
45
|
+
return names.sort();
|
|
46
|
+
}
|
|
47
|
+
/** Claude consumes the SKILL.md format directly. */
|
|
48
|
+
export function toClaudeFormat(md) {
|
|
49
|
+
return md;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Cursor expects `.mdc` rule files with a specific frontmatter contract:
|
|
53
|
+
* description: <short description the agent matches against>
|
|
54
|
+
* globs: <file patterns; can be empty for non-file-scoped rules>
|
|
55
|
+
* alwaysApply: <true = always loaded; false = agent-requested>
|
|
56
|
+
*
|
|
57
|
+
* Our skill files use a different frontmatter (name/description/triggers/
|
|
58
|
+
* tools_used). Rewrite the frontmatter into Cursor's shape so rules are
|
|
59
|
+
* actually picked up. We use 'agent-requested' mode (alwaysApply: false +
|
|
60
|
+
* rich description) which lets Cursor's agent pull the skill in when the
|
|
61
|
+
* user's prompt matches the description — without polluting every chat in
|
|
62
|
+
* unrelated projects.
|
|
63
|
+
*
|
|
64
|
+
* The body of the skill markdown (playbook, response shape, visualization,
|
|
65
|
+
* don't list) is preserved verbatim — that's the actual instructions the
|
|
66
|
+
* agent reads.
|
|
67
|
+
*/
|
|
68
|
+
export function toCursorFormat(md) {
|
|
69
|
+
// Parse the leading YAML frontmatter.
|
|
70
|
+
const fmMatch = md.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
71
|
+
if (!fmMatch)
|
|
72
|
+
return md; // no frontmatter — leave as-is
|
|
73
|
+
const frontmatter = fmMatch[1] ?? "";
|
|
74
|
+
const body = fmMatch[2] ?? "";
|
|
75
|
+
// Pull out the name, description, and triggers list.
|
|
76
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+?)\s*$/m);
|
|
77
|
+
const descMatch = frontmatter.match(/^description:\s*(.+?)\s*$/m);
|
|
78
|
+
const triggersMatch = frontmatter.match(/^triggers:\s*\n((?:[ \t]+-[^\n]*\n?)+)/m);
|
|
79
|
+
const name = nameMatch?.[1] ?? "sfgraph-skill";
|
|
80
|
+
const baseDescription = descMatch?.[1] ?? "";
|
|
81
|
+
const triggerPhrases = [];
|
|
82
|
+
if (triggersMatch?.[1]) {
|
|
83
|
+
for (const line of triggersMatch[1].split("\n")) {
|
|
84
|
+
const m = line.match(/^\s*-\s*"?(.+?)"?\s*$/);
|
|
85
|
+
if (m?.[1])
|
|
86
|
+
triggerPhrases.push(m[1]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Compose a rich description: base + trigger phrases. Cursor's agent
|
|
90
|
+
// matches the conversation against this string to decide whether to
|
|
91
|
+
// pull the rule in. Mention sfgraph + Salesforce explicitly so the
|
|
92
|
+
// match fires on tool-relevant questions.
|
|
93
|
+
const descriptionParts = [baseDescription];
|
|
94
|
+
if (triggerPhrases.length > 0) {
|
|
95
|
+
descriptionParts.push(`Triggers: ${triggerPhrases.join("; ")}.`);
|
|
96
|
+
}
|
|
97
|
+
descriptionParts.push(`Use this skill (${name}) for Salesforce questions via the sfgraph MCP tools.`);
|
|
98
|
+
const description = descriptionParts.filter(Boolean).join(" ");
|
|
99
|
+
// Salesforce file patterns that should auto-attach the rule when the user
|
|
100
|
+
// opens any of them. Once any of these are in scope, Cursor's agent gets
|
|
101
|
+
// the rule loaded into its context automatically — no need for the user
|
|
102
|
+
// to mention sfgraph or invoke the rule by name.
|
|
103
|
+
const globs = [
|
|
104
|
+
"**/sfdx-project.json",
|
|
105
|
+
"**/force-app/**",
|
|
106
|
+
"**/*.cls",
|
|
107
|
+
"**/*.cls-meta.xml",
|
|
108
|
+
"**/*.trigger",
|
|
109
|
+
"**/*.trigger-meta.xml",
|
|
110
|
+
"**/lwc/**",
|
|
111
|
+
"**/aura/**",
|
|
112
|
+
"**/*.flow-meta.xml",
|
|
113
|
+
"**/*.object-meta.xml",
|
|
114
|
+
"**/*.field-meta.xml",
|
|
115
|
+
"**/*.permissionset-meta.xml",
|
|
116
|
+
"**/*.permissionsetgroup-meta.xml",
|
|
117
|
+
"**/*.profile-meta.xml",
|
|
118
|
+
"**/*.sharingRules-meta.xml",
|
|
119
|
+
"**/*.layout-meta.xml",
|
|
120
|
+
"**/*.flexipage-meta.xml",
|
|
121
|
+
"**/*.namedCredential-meta.xml",
|
|
122
|
+
"**/*.workflow-meta.xml",
|
|
123
|
+
].join(",");
|
|
124
|
+
return `---
|
|
125
|
+
description: ${description.replace(/\n/g, " ").replace(/"/g, "'")}
|
|
126
|
+
globs: ${globs}
|
|
127
|
+
alwaysApply: false
|
|
128
|
+
---
|
|
129
|
+
${body}`;
|
|
130
|
+
}
|
|
131
|
+
export async function install(target, opts = {}) {
|
|
132
|
+
const home = opts.homeOverride ?? homedir();
|
|
133
|
+
const dirs = targetDirs(home);
|
|
134
|
+
const targets = target === "all" ? ["claude", "cursor", "vscode"] : [target];
|
|
135
|
+
const results = [];
|
|
136
|
+
const root = findSkillsRoot();
|
|
137
|
+
const skills = await listSkillsBundled();
|
|
138
|
+
for (const t of targets) {
|
|
139
|
+
const outDir = dirs[t];
|
|
140
|
+
if (!opts.dryRun) {
|
|
141
|
+
await mkdir(outDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
for (const skill of skills) {
|
|
144
|
+
const src = join(root, skill, "SKILL.md");
|
|
145
|
+
const body = await readFile(src, "utf8");
|
|
146
|
+
let outPath;
|
|
147
|
+
let transformed;
|
|
148
|
+
if (t === "cursor") {
|
|
149
|
+
outPath = join(outDir, `${skill}.mdc`);
|
|
150
|
+
transformed = toCursorFormat(body);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// claude + vscode: install as a directory with SKILL.md inside
|
|
154
|
+
const skillDir = join(outDir, skill);
|
|
155
|
+
outPath = join(skillDir, "SKILL.md");
|
|
156
|
+
transformed = toClaudeFormat(body);
|
|
157
|
+
if (!opts.dryRun) {
|
|
158
|
+
await mkdir(skillDir, { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
let action;
|
|
162
|
+
if (opts.dryRun) {
|
|
163
|
+
action = "would-write";
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const existed = existsSync(outPath);
|
|
167
|
+
if (existed) {
|
|
168
|
+
const prev = await readFile(outPath, "utf8");
|
|
169
|
+
if (prev === transformed) {
|
|
170
|
+
action = "skipped";
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
await writeFile(outPath, transformed, "utf8");
|
|
174
|
+
action = "updated";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
await writeFile(outPath, transformed, "utf8");
|
|
179
|
+
action = "created";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
results.push({ target: t, skill, path: outPath, action });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return results;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=installer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installer.js","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ryanstark24/sfgraph-skills",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@biomejs/biome": "^1.9.0",
|
|
15
|
+
"@types/node": "^22.0.0",
|
|
16
|
+
"typescript": "^5.6.0",
|
|
17
|
+
"vitest": "^3.2.4"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/ryanStark24/sfgraph"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"description": "sfgraph SKILL.md playbooks for Cursor and Claude",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"skills",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"keywords": [
|
|
37
|
+
"salesforce",
|
|
38
|
+
"sfgraph",
|
|
39
|
+
"mcp",
|
|
40
|
+
"skills",
|
|
41
|
+
"playbook"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.json",
|
|
45
|
+
"test": "vitest run --passWithNoTests",
|
|
46
|
+
"lint": "biome check src",
|
|
47
|
+
"lint:fix": "biome check --write src",
|
|
48
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
49
|
+
"clean": "rm -rf dist"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-cross-layer-trace
|
|
3
|
+
description: Trace a field, component, or method end-to-end across LWC, Apex, Flow, and data layer. PROACTIVELY volunteer this skill whenever the focal artifact is an LWC bundle, Aura component, ApexPage, or a custom field — even on explain-style questions ("explain this LWC", "what does this component do", "where is X used"). After answering the surface question, offer a cross-layer trace so the user sees what the artifact ultimately touches through Apex / Flow / SObject.
|
|
4
|
+
triggers:
|
|
5
|
+
- "UI to DB"
|
|
6
|
+
- "trace this LWC"
|
|
7
|
+
- "end-to-end"
|
|
8
|
+
- "where is this field used"
|
|
9
|
+
- "where does this come from"
|
|
10
|
+
- "explain this LWC"
|
|
11
|
+
- "explain this component"
|
|
12
|
+
- "what does this component do"
|
|
13
|
+
- "what does this LWC do"
|
|
14
|
+
- "explain this Aura"
|
|
15
|
+
- "explain this VisualForce"
|
|
16
|
+
- "explain this ApexPage"
|
|
17
|
+
- "where is this component used"
|
|
18
|
+
- "what pages embed"
|
|
19
|
+
- "which pages use"
|
|
20
|
+
tools_used:
|
|
21
|
+
- cross_layer_flow_map
|
|
22
|
+
- analyze_field
|
|
23
|
+
- staleness_check
|
|
24
|
+
- explain_code
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# sf-cross-layer-trace
|
|
28
|
+
|
|
29
|
+
Use when the user needs to follow a single artifact (field, LWC, Apex method) across every layer it touches — UI rendering, Apex controller, flow automation, data model.
|
|
30
|
+
|
|
31
|
+
## When to volunteer this skill
|
|
32
|
+
|
|
33
|
+
Source-only tools (file grep, repo search, `explain_code`) can answer "where is this used?" inside the sfdx-source repo, but they cannot see:
|
|
34
|
+
- Lightning App Builder page bindings (FlexiPages embedding the component)
|
|
35
|
+
- Quick Action / Lightning Action targets
|
|
36
|
+
- Flow screen components referencing the LWC
|
|
37
|
+
- Apex controllers the LWC `@wire`s into, and what those controllers ultimately read/write
|
|
38
|
+
|
|
39
|
+
So whenever the focal node is an **LWC bundle, Aura component, ApexPage, custom field, or Apex class/method**, after answering the surface question proactively offer:
|
|
40
|
+
|
|
41
|
+
> "I answered using the repo. Want me to run `cross_layer_flow_map` on `<node>` against the ingested org graph? That will surface page bindings, controller chains, and the SObject fields it ultimately touches — things the source files alone don't show."
|
|
42
|
+
|
|
43
|
+
Only skip the offer when the user has explicitly scoped the question to source (e.g. "just look at the repo, don't query the graph") or when the graph staleness check shows the org isn't ingested at all.
|
|
44
|
+
|
|
45
|
+
## Playbook
|
|
46
|
+
|
|
47
|
+
1. Resolve the starting node. If the user gave a field, call `analyze_field` first to canonicalise `SObject.Field` and gather direct readers/writers.
|
|
48
|
+
2. Call `cross_layer_flow_map` with the resolved node. Receive ordered paths across layers (UI -> Controller -> Service -> Selector -> SObject and any Flow / Process Builder branches).
|
|
49
|
+
3. For each distinct path, summarise the layer transitions with the edge type that bridges them (`@wire`, `@AuraEnabled`, `READS_FIELD`, `UPDATES_FIELD`, `INVOKES_FLOW`).
|
|
50
|
+
4. Surface anything anomalous: cycles, missing `with sharing`, fields read by multiple LWCs but written only by Flow, etc.
|
|
51
|
+
5. Render the Mermaid layered diagram from `cross_layer_flow_map`. If the graph has more than 30 nodes, ask whether to filter to a single layer pair.
|
|
52
|
+
6. End with a one-line recommendation of which related skill to invoke next (e.g. `sf-security-audit` if FLS gaps appear).
|
|
53
|
+
|
|
54
|
+
## Visualization
|
|
55
|
+
|
|
56
|
+
Render a **`flowchart TD`** layered diagram with one subgraph per layer (UI / Controller / Service / Selector / Data). Edges are labelled with the edge type that bridges layers (`@wire`, `@AuraEnabled`, `READS_FIELD`, `UPDATES_FIELD`, `INVOKES_FLOW`).
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
flowchart TD
|
|
60
|
+
subgraph UI
|
|
61
|
+
LWC[accountList.js]
|
|
62
|
+
end
|
|
63
|
+
subgraph Apex
|
|
64
|
+
Ctl[AccountController.getAccounts]
|
|
65
|
+
Svc[AccountSelector.queryAll]
|
|
66
|
+
end
|
|
67
|
+
subgraph Data
|
|
68
|
+
F[(Account.Name)]
|
|
69
|
+
end
|
|
70
|
+
LWC -->|@wire| Ctl --> Svc -->|READS_FIELD| F
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If the resolved trace has >30 nodes, ask the user to narrow to a single layer-pair before rendering — a sprawling layered diagram is more misleading than no diagram.
|
|
74
|
+
|
|
75
|
+
## Staleness check
|
|
76
|
+
|
|
77
|
+
Before calling `cross_layer_flow_map`, invoke `staleness_check` for the target org. If the report says stale, surface a warning to the user:
|
|
78
|
+
|
|
79
|
+
> Your ingest is N days old. Run `sfgraph ingest --org <alias>` to refresh, or proceed with the understanding that the graph may not reflect recent changes.
|
|
80
|
+
|
|
81
|
+
Then continue with the playbook.
|
|
82
|
+
|
|
83
|
+
## Response Shape
|
|
84
|
+
|
|
85
|
+
- **Subject** — the resolved node id and type.
|
|
86
|
+
- **Paths** — numbered list of distinct UI->DB paths with layer transitions.
|
|
87
|
+
- **Anomalies** — bullets calling out anything that looks structurally suspicious.
|
|
88
|
+
- **Mermaid layered diagram** — embedded from tool output.
|
|
89
|
+
- **Suggested next skill** — one line.
|
|
90
|
+
|
|
91
|
+
## Don't
|
|
92
|
+
|
|
93
|
+
- Do not collapse layers — the value of this skill is showing the layer boundaries explicitly.
|
|
94
|
+
- Do not include nodes outside the requested traversal depth; defer breadth to a follow-up call.
|
|
95
|
+
- Do not modify anything. Pure read.
|
|
96
|
+
- Do not assume the starting node exists; if `analyze_field` returns empty, ask the user to refine.
|
|
97
|
+
- Do not stop at a source-only answer for LWC / Aura / ApexPage / field / Apex method questions without at least *offering* the cross-layer trace. Repo grep misses FlexiPage bindings, Quick Action targets, and the Apex→SObject tail — say so and offer to fill it in.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-cross-org-diff
|
|
3
|
+
description: Compare metadata between two orgs (sandbox vs prod, two sandboxes) and summarise drift.
|
|
4
|
+
triggers:
|
|
5
|
+
- "sandbox vs prod"
|
|
6
|
+
- "what changed"
|
|
7
|
+
- "org diff"
|
|
8
|
+
- "compare orgs"
|
|
9
|
+
- "drift"
|
|
10
|
+
tools_used:
|
|
11
|
+
- cross_org_diff
|
|
12
|
+
- point_in_time_diff
|
|
13
|
+
- list_orgs
|
|
14
|
+
- staleness_check
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# sf-cross-org-diff
|
|
18
|
+
|
|
19
|
+
Use when the user wants to understand drift between two orgs — typically pre-deploy verification or post-deploy reconciliation.
|
|
20
|
+
|
|
21
|
+
## Playbook
|
|
22
|
+
|
|
23
|
+
1. Call `list_orgs` first. Present the user with the authenticated orgs, each org's `ingested` flag, last sync timestamp, and `stale` flag. Ask the user to pick the two orgs to compare. Refuse to proceed if either selected org has `ingested=false` — instruct them to run `sfgraph ingest --org <alias>` first.
|
|
24
|
+
2. Call `cross_org_diff(orgA, orgB)`. Capture added / removed / modified nodes grouped by metadata category.
|
|
25
|
+
3. For high-churn categories, call `point_in_time_diff` inside each org to confirm the drift originated there (vs being a stale snapshot artifact).
|
|
26
|
+
4. Categorise drift: **structural** (new objects/fields), **logic** (Apex, Flow body changes), **security** (profile / sharing), **config** (custom metadata / settings).
|
|
27
|
+
5. Render the Mermaid drift summary from the tool. For each category, list the top-N items by impact.
|
|
28
|
+
6. Recommend `sf-deployment-manifest` if the user wants to bring orgs into sync.
|
|
29
|
+
|
|
30
|
+
## Visualization
|
|
31
|
+
|
|
32
|
+
Render a **`flowchart LR`** drift summary. Two side-by-side subgraphs (one per org) joined by edges that classify drift (added / removed / modified). Use colour to indicate severity by metadata category.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
flowchart LR
|
|
36
|
+
subgraph A[orgA]
|
|
37
|
+
A1[ApexClass:Foo]
|
|
38
|
+
end
|
|
39
|
+
subgraph B[orgB]
|
|
40
|
+
B1[ApexClass:Foo']
|
|
41
|
+
end
|
|
42
|
+
A1 -->|modified| B1
|
|
43
|
+
A2[Field:Account.X]:::removed -.->|absent| B
|
|
44
|
+
classDef removed fill:#fdd,stroke:#900
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For diffs with >50 nodes, switch to a bucketed table per category and skip the diagram.
|
|
48
|
+
|
|
49
|
+
## Staleness check
|
|
50
|
+
|
|
51
|
+
Before calling `cross_org_diff`, invoke `staleness_check` for both orgs. If either is stale, surface a warning to the user:
|
|
52
|
+
|
|
53
|
+
> Your ingest for `<alias>` is N days old. Run `sfgraph ingest --org <alias>` to refresh, or proceed with the understanding that the graph may not reflect recent changes.
|
|
54
|
+
|
|
55
|
+
Then continue with the playbook.
|
|
56
|
+
|
|
57
|
+
## Response Shape
|
|
58
|
+
|
|
59
|
+
- **Orgs compared** — `orgA -> orgB`.
|
|
60
|
+
- **Drift by category** — structural / logic / security / config, each with a count and top items.
|
|
61
|
+
- **Notable single-side items** — present in A only or B only.
|
|
62
|
+
- **Mermaid drift summary** — embedded.
|
|
63
|
+
- **Next step** — single line, typically pointing at deployment manifest skill.
|
|
64
|
+
|
|
65
|
+
## Don't
|
|
66
|
+
|
|
67
|
+
- Do not deploy or copy metadata between orgs from this skill.
|
|
68
|
+
- Do not assume `orgA` is "source of truth"; report drift symmetrically.
|
|
69
|
+
- Do not include freshness drift (last-modified-by user) in the structural bucket — call it out separately.
|
|
70
|
+
- Do not run if either org has not been ingested; instruct the user instead.
|