fathom-cli 0.3.1 → 0.3.3

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,135 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { describe, it, expect } from "vitest";
4
+ import yaml from "js-yaml";
5
+
6
+ const SKILLS_ROOT = resolve(__dirname, "../skills");
7
+
8
+ interface SkillFrontmatter {
9
+ name?: string;
10
+ description?: string;
11
+ "argument-hint"?: string;
12
+ "user-invocable"?: boolean;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ function parseSkillMd(skillDir: string): {
17
+ frontmatter: SkillFrontmatter;
18
+ body: string;
19
+ } {
20
+ const filePath = resolve(SKILLS_ROOT, skillDir, "SKILL.md");
21
+ const raw = readFileSync(filePath, "utf-8");
22
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
23
+ if (!match) {
24
+ throw new Error(`Invalid SKILL.md format in ${skillDir}: no frontmatter`);
25
+ }
26
+ return {
27
+ frontmatter: yaml.load(match[1]) as SkillFrontmatter,
28
+ body: match[2],
29
+ };
30
+ }
31
+
32
+ const INTERACTIVE_SKILLS = [
33
+ {
34
+ dir: "estimate",
35
+ mcpTool: "fathom_estimate_task_cost",
36
+ },
37
+ {
38
+ dir: "recommend-model",
39
+ mcpTool: "fathom_recommend_model",
40
+ },
41
+ {
42
+ dir: "budget",
43
+ mcpTool: "fathom_check_budget",
44
+ },
45
+ {
46
+ dir: "status",
47
+ mcpTool: "fathom_get_project_status",
48
+ },
49
+ {
50
+ dir: "projections",
51
+ mcpTool: "fathom_generate_tool_configs",
52
+ },
53
+ {
54
+ dir: "gsd",
55
+ mcpTool: "fathom_estimate_task_cost",
56
+ },
57
+ ];
58
+
59
+ const BACKGROUND_SKILLS = [
60
+ {
61
+ dir: "cost-preview",
62
+ mcpTool: "fathom_estimate_task_cost",
63
+ },
64
+ {
65
+ dir: "budget-warning",
66
+ mcpTool: "fathom_check_budget",
67
+ },
68
+ ];
69
+
70
+ const ALL_SKILLS = [...INTERACTIVE_SKILLS, ...BACKGROUND_SKILLS];
71
+
72
+ for (const skill of INTERACTIVE_SKILLS) {
73
+ describe(skill.dir, () => {
74
+ const skillPath = resolve(SKILLS_ROOT, skill.dir, "SKILL.md");
75
+
76
+ it("SKILL.md exists", () => {
77
+ expect(existsSync(skillPath)).toBe(true);
78
+ });
79
+
80
+ it("has valid frontmatter with required fields", () => {
81
+ const { frontmatter } = parseSkillMd(skill.dir);
82
+
83
+ expect(frontmatter.name).toBeTypeOf("string");
84
+ expect(frontmatter.name!.length).toBeGreaterThan(0);
85
+ expect(frontmatter.description).toBeTypeOf("string");
86
+ expect(frontmatter.description!.length).toBeGreaterThan(0);
87
+ expect(frontmatter.description!.length).toBeLessThan(300);
88
+ });
89
+
90
+ it("is user-invocable (not false)", () => {
91
+ const { frontmatter } = parseSkillMd(skill.dir);
92
+ expect(frontmatter["user-invocable"]).not.toBe(false);
93
+ });
94
+
95
+ it("has argument-hint for interactive use", () => {
96
+ const { frontmatter } = parseSkillMd(skill.dir);
97
+ expect(frontmatter["argument-hint"]).toBeTypeOf("string");
98
+ });
99
+
100
+ it(`references MCP tool ${skill.mcpTool}`, () => {
101
+ const { body } = parseSkillMd(skill.dir);
102
+ expect(body).toContain(skill.mcpTool);
103
+ });
104
+ });
105
+ }
106
+
107
+ for (const skill of BACKGROUND_SKILLS) {
108
+ describe(skill.dir, () => {
109
+ const skillPath = resolve(SKILLS_ROOT, skill.dir, "SKILL.md");
110
+
111
+ it("SKILL.md exists", () => {
112
+ expect(existsSync(skillPath)).toBe(true);
113
+ });
114
+
115
+ it("has valid frontmatter with required fields", () => {
116
+ const { frontmatter } = parseSkillMd(skill.dir);
117
+
118
+ expect(frontmatter.name).toBeTypeOf("string");
119
+ expect(frontmatter.name!.length).toBeGreaterThan(0);
120
+ expect(frontmatter.description).toBeTypeOf("string");
121
+ expect(frontmatter.description!.length).toBeGreaterThan(0);
122
+ expect(frontmatter.description!.length).toBeLessThan(300);
123
+ });
124
+
125
+ it("user-invocable is exactly false", () => {
126
+ const { frontmatter } = parseSkillMd(skill.dir);
127
+ expect(frontmatter["user-invocable"]).toBe(false);
128
+ });
129
+
130
+ it(`references MCP tool ${skill.mcpTool}`, () => {
131
+ const { body } = parseSkillMd(skill.dir);
132
+ expect(body).toContain(skill.mcpTool);
133
+ });
134
+ });
135
+ }