@wix/pathgrade 0.29.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.
Files changed (238) hide show
  1. package/bin/pathgrade.js +2 -0
  2. package/dist/affected/anchor.d.ts +18 -0
  3. package/dist/affected/anchor.js +42 -0
  4. package/dist/affected/config.d.ts +25 -0
  5. package/dist/affected/config.js +98 -0
  6. package/dist/affected/format.d.ts +45 -0
  7. package/dist/affected/format.js +87 -0
  8. package/dist/affected/git.d.ts +33 -0
  9. package/dist/affected/git.js +68 -0
  10. package/dist/affected/glob.d.ts +17 -0
  11. package/dist/affected/glob.js +25 -0
  12. package/dist/affected/meta.d.ts +26 -0
  13. package/dist/affected/meta.js +158 -0
  14. package/dist/affected/select.d.ts +30 -0
  15. package/dist/affected/select.js +101 -0
  16. package/dist/affected/sidecar.d.ts +32 -0
  17. package/dist/affected/sidecar.js +86 -0
  18. package/dist/affected/types.d.ts +43 -0
  19. package/dist/affected/types.js +7 -0
  20. package/dist/agents/claude/ask-user-answer-store.d.ts +28 -0
  21. package/dist/agents/claude/ask-user-answer-store.js +29 -0
  22. package/dist/agents/claude/ask-user-bridge.d.ts +56 -0
  23. package/dist/agents/claude/ask-user-bridge.js +162 -0
  24. package/dist/agents/claude/sdk-message-projector.d.ts +43 -0
  25. package/dist/agents/claude/sdk-message-projector.js +208 -0
  26. package/dist/agents/claude/sdk-options.d.ts +52 -0
  27. package/dist/agents/claude/sdk-options.js +79 -0
  28. package/dist/agents/claude.d.ts +56 -0
  29. package/dist/agents/claude.js +143 -0
  30. package/dist/agents/codex-app-server/agent.d.ts +35 -0
  31. package/dist/agents/codex-app-server/agent.js +388 -0
  32. package/dist/agents/codex-app-server/fixtures/app-server-client.d.ts +19 -0
  33. package/dist/agents/codex-app-server/fixtures/app-server-client.js +104 -0
  34. package/dist/agents/codex-app-server/fixtures/run-gate.d.ts +25 -0
  35. package/dist/agents/codex-app-server/fixtures/run-gate.js +48 -0
  36. package/dist/agents/codex-app-server/protocol/ClientRequest.d.ts +27 -0
  37. package/dist/agents/codex-app-server/protocol/ClientRequest.js +7 -0
  38. package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.d.ts +9 -0
  39. package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.js +4 -0
  40. package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.d.ts +6 -0
  41. package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.js +7 -0
  42. package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.d.ts +23 -0
  43. package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.js +8 -0
  44. package/dist/agents/codex-app-server/protocol/Op.d.ts +11 -0
  45. package/dist/agents/codex-app-server/protocol/Op.js +5 -0
  46. package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.d.ts +10 -0
  47. package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.js +6 -0
  48. package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.d.ts +15 -0
  49. package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.js +4 -0
  50. package/dist/agents/codex-app-server/protocol/SandboxMode.d.ts +1 -0
  51. package/dist/agents/codex-app-server/protocol/SandboxMode.js +4 -0
  52. package/dist/agents/codex-app-server/protocol/ServerRequest.d.ts +42 -0
  53. package/dist/agents/codex-app-server/protocol/ServerRequest.js +7 -0
  54. package/dist/agents/codex-app-server/protocol/ThreadStartParams.d.ts +45 -0
  55. package/dist/agents/codex-app-server/protocol/ThreadStartParams.js +10 -0
  56. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.d.ts +6 -0
  57. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.js +4 -0
  58. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.d.ts +7 -0
  59. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.js +4 -0
  60. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.d.ts +10 -0
  61. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.js +4 -0
  62. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.d.ts +12 -0
  63. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.js +4 -0
  64. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.d.ts +9 -0
  65. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.js +4 -0
  66. package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.d.ts +5 -0
  67. package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.js +4 -0
  68. package/dist/agents/codex-app-server/protocol/index.d.ts +16 -0
  69. package/dist/agents/codex-app-server/protocol/index.js +11 -0
  70. package/dist/agents/codex-app-server/transport.d.ts +84 -0
  71. package/dist/agents/codex-app-server/transport.js +246 -0
  72. package/dist/agents/codex-app-server/wire-translators.d.ts +19 -0
  73. package/dist/agents/codex-app-server/wire-translators.js +52 -0
  74. package/dist/agents/codex.d.ts +7 -0
  75. package/dist/agents/codex.js +164 -0
  76. package/dist/agents/cursor.d.ts +39 -0
  77. package/dist/agents/cursor.js +233 -0
  78. package/dist/agents/registry.d.ts +17 -0
  79. package/dist/agents/registry.js +27 -0
  80. package/dist/agents/transcript-agent.d.ts +14 -0
  81. package/dist/agents/transcript-agent.js +59 -0
  82. package/dist/analytics/engine.d.ts +18 -0
  83. package/dist/analytics/engine.js +66 -0
  84. package/dist/commands/affected.d.ts +31 -0
  85. package/dist/commands/affected.js +150 -0
  86. package/dist/commands/analyze.d.ts +21 -0
  87. package/dist/commands/analyze.js +167 -0
  88. package/dist/commands/init.d.ts +3 -0
  89. package/dist/commands/init.js +242 -0
  90. package/dist/commands/preview-reactions.d.ts +10 -0
  91. package/dist/commands/preview-reactions.js +88 -0
  92. package/dist/commands/preview.d.ts +5 -0
  93. package/dist/commands/preview.js +17 -0
  94. package/dist/commands/report.d.ts +39 -0
  95. package/dist/commands/report.js +108 -0
  96. package/dist/commands/run-args.d.ts +22 -0
  97. package/dist/commands/run-args.js +69 -0
  98. package/dist/commands/run-changed.d.ts +26 -0
  99. package/dist/commands/run-changed.js +138 -0
  100. package/dist/commands/validate.d.ts +13 -0
  101. package/dist/commands/validate.js +233 -0
  102. package/dist/core/mcp-mock.d.ts +3 -0
  103. package/dist/core/mcp-mock.js +23 -0
  104. package/dist/core/mcp-mock.types.d.ts +15 -0
  105. package/dist/core/mcp-mock.types.js +1 -0
  106. package/dist/core/skills.d.ts +15 -0
  107. package/dist/core/skills.js +76 -0
  108. package/dist/evals/discovery.d.ts +7 -0
  109. package/dist/evals/discovery.js +69 -0
  110. package/dist/mcp-mock-server.d.ts +1 -0
  111. package/dist/mcp-mock-server.js +116 -0
  112. package/dist/pathgrade.d.ts +10 -0
  113. package/dist/pathgrade.js +243 -0
  114. package/dist/plugin/index.d.ts +9 -0
  115. package/dist/plugin/index.js +81 -0
  116. package/dist/plugin/lifecycle.d.ts +45 -0
  117. package/dist/plugin/lifecycle.js +146 -0
  118. package/dist/plugin/reporter.d.ts +36 -0
  119. package/dist/plugin/reporter.js +275 -0
  120. package/dist/plugin/setup.d.ts +1 -0
  121. package/dist/plugin/setup.js +3 -0
  122. package/dist/providers/copy-filter.d.ts +18 -0
  123. package/dist/providers/copy-filter.js +66 -0
  124. package/dist/providers/credentials.d.ts +36 -0
  125. package/dist/providers/credentials.js +202 -0
  126. package/dist/providers/mcp-config.d.ts +34 -0
  127. package/dist/providers/mcp-config.js +55 -0
  128. package/dist/providers/sandbox-exec.d.ts +8 -0
  129. package/dist/providers/sandbox-exec.js +82 -0
  130. package/dist/providers/sandbox.d.ts +22 -0
  131. package/dist/providers/sandbox.js +158 -0
  132. package/dist/providers/sandboxed-claude-spawn.d.ts +24 -0
  133. package/dist/providers/sandboxed-claude-spawn.js +57 -0
  134. package/dist/providers/workspace.d.ts +15 -0
  135. package/dist/providers/workspace.js +73 -0
  136. package/dist/reporters/browser.d.ts +2 -0
  137. package/dist/reporters/browser.js +85 -0
  138. package/dist/reporters/cli.d.ts +2 -0
  139. package/dist/reporters/cli.js +131 -0
  140. package/dist/reporters/diagnostics.d.ts +46 -0
  141. package/dist/reporters/diagnostics.js +147 -0
  142. package/dist/reporters/github-comment.d.ts +67 -0
  143. package/dist/reporters/github-comment.js +252 -0
  144. package/dist/reporters/loader.d.ts +8 -0
  145. package/dist/reporters/loader.js +36 -0
  146. package/dist/reporters/results-path.d.ts +2 -0
  147. package/dist/reporters/results-path.js +10 -0
  148. package/dist/reporters/verbose-emitter.d.ts +69 -0
  149. package/dist/reporters/verbose-emitter.js +103 -0
  150. package/dist/sdk/agent-crash.d.ts +19 -0
  151. package/dist/sdk/agent-crash.js +20 -0
  152. package/dist/sdk/agent-resolution.d.ts +12 -0
  153. package/dist/sdk/agent-resolution.js +21 -0
  154. package/dist/sdk/agent-result-log.d.ts +19 -0
  155. package/dist/sdk/agent-result-log.js +45 -0
  156. package/dist/sdk/agent.d.ts +4 -0
  157. package/dist/sdk/agent.js +354 -0
  158. package/dist/sdk/ask-bus/bus.d.ts +21 -0
  159. package/dist/sdk/ask-bus/bus.js +129 -0
  160. package/dist/sdk/ask-bus/handler.d.ts +31 -0
  161. package/dist/sdk/ask-bus/handler.js +244 -0
  162. package/dist/sdk/ask-bus/parsers.d.ts +9 -0
  163. package/dist/sdk/ask-bus/parsers.js +30 -0
  164. package/dist/sdk/ask-bus/projection.d.ts +26 -0
  165. package/dist/sdk/ask-bus/projection.js +45 -0
  166. package/dist/sdk/ask-bus/types.d.ts +69 -0
  167. package/dist/sdk/ask-bus/types.js +1 -0
  168. package/dist/sdk/chat.d.ts +29 -0
  169. package/dist/sdk/chat.js +93 -0
  170. package/dist/sdk/conversation-window.d.ts +14 -0
  171. package/dist/sdk/conversation-window.js +38 -0
  172. package/dist/sdk/converse.d.ts +42 -0
  173. package/dist/sdk/converse.js +329 -0
  174. package/dist/sdk/eval-runtime.d.ts +10 -0
  175. package/dist/sdk/eval-runtime.js +17 -0
  176. package/dist/sdk/evaluate.d.ts +13 -0
  177. package/dist/sdk/evaluate.js +325 -0
  178. package/dist/sdk/index.d.ts +31 -0
  179. package/dist/sdk/index.js +20 -0
  180. package/dist/sdk/judge-pipeline.d.ts +7 -0
  181. package/dist/sdk/judge-pipeline.js +178 -0
  182. package/dist/sdk/judge-prompt-builder.d.ts +7 -0
  183. package/dist/sdk/judge-prompt-builder.js +76 -0
  184. package/dist/sdk/judge-tool-runner.d.ts +19 -0
  185. package/dist/sdk/judge-tool-runner.js +51 -0
  186. package/dist/sdk/judge-tool-session.d.ts +47 -0
  187. package/dist/sdk/judge-tool-session.js +213 -0
  188. package/dist/sdk/judge-tools.d.ts +29 -0
  189. package/dist/sdk/judge-tools.js +283 -0
  190. package/dist/sdk/managed-session.d.ts +50 -0
  191. package/dist/sdk/managed-session.js +110 -0
  192. package/dist/sdk/persona.d.ts +5 -0
  193. package/dist/sdk/persona.js +33 -0
  194. package/dist/sdk/reaction-loader.d.ts +4 -0
  195. package/dist/sdk/reaction-loader.js +119 -0
  196. package/dist/sdk/reaction-preview.d.ts +6 -0
  197. package/dist/sdk/reaction-preview.js +94 -0
  198. package/dist/sdk/run-scorer.d.ts +10 -0
  199. package/dist/sdk/run-scorer.js +124 -0
  200. package/dist/sdk/runtime-policy.d.ts +21 -0
  201. package/dist/sdk/runtime-policy.js +89 -0
  202. package/dist/sdk/scorer-utils.d.ts +6 -0
  203. package/dist/sdk/scorer-utils.js +39 -0
  204. package/dist/sdk/scorers.d.ts +64 -0
  205. package/dist/sdk/scorers.js +80 -0
  206. package/dist/sdk/snapshots.d.ts +41 -0
  207. package/dist/sdk/snapshots.js +148 -0
  208. package/dist/sdk/types.d.ts +416 -0
  209. package/dist/sdk/types.js +17 -0
  210. package/dist/sdk/visible-turn.d.ts +4 -0
  211. package/dist/sdk/visible-turn.js +23 -0
  212. package/dist/tool-events.d.ts +45 -0
  213. package/dist/tool-events.js +167 -0
  214. package/dist/types.d.ts +372 -0
  215. package/dist/types.js +37 -0
  216. package/dist/utils/cli.d.ts +55 -0
  217. package/dist/utils/cli.js +131 -0
  218. package/dist/utils/env.d.ts +8 -0
  219. package/dist/utils/env.js +26 -0
  220. package/dist/utils/llm-mocks.d.ts +43 -0
  221. package/dist/utils/llm-mocks.js +108 -0
  222. package/dist/utils/llm-providers/anthropic.d.ts +2 -0
  223. package/dist/utils/llm-providers/anthropic.js +132 -0
  224. package/dist/utils/llm-providers/cli.d.ts +20 -0
  225. package/dist/utils/llm-providers/cli.js +162 -0
  226. package/dist/utils/llm-providers/openai.d.ts +2 -0
  227. package/dist/utils/llm-providers/openai.js +56 -0
  228. package/dist/utils/llm-types.d.ts +107 -0
  229. package/dist/utils/llm-types.js +1 -0
  230. package/dist/utils/llm.d.ts +36 -0
  231. package/dist/utils/llm.js +209 -0
  232. package/dist/utils/shutdown.d.ts +15 -0
  233. package/dist/utils/shutdown.js +41 -0
  234. package/dist/utils/timeout.d.ts +8 -0
  235. package/dist/utils/timeout.js +32 -0
  236. package/dist/viewer.html +1178 -0
  237. package/package.json +88 -0
  238. package/templates/eval.ts.template +28 -0
@@ -0,0 +1,233 @@
1
+ /**
2
+ * `pathgrade validate` command.
3
+ *
4
+ * Validates a .eval.ts file for structural correctness without running it.
5
+ * Uses regex/string pattern matching — no AST parsing, no LLM calls.
6
+ *
7
+ * `runValidateAffected` (Issue 13) is a strict mode: instead of validating
8
+ * one positional file, it iterates every discovered eval and reports
9
+ * onMissing (no SKILL.md + no meta) and malformed-meta as hard errors,
10
+ * for teams that want CI-level declaration enforcement.
11
+ */
12
+ import { createRequire } from 'node:module';
13
+ import * as path from 'path';
14
+ import fs from 'fs-extra';
15
+ import { findSkillRoot } from '../affected/anchor.js';
16
+ import { parsePathgradeMeta } from '../affected/meta.js';
17
+ import { discoverEvalFiles } from './affected.js';
18
+ /**
19
+ * Run the validate command. Returns exit code (0 = valid, 1 = errors).
20
+ */
21
+ export async function runValidate(filePath, opts = {}) {
22
+ const errors = [];
23
+ const warnings = [];
24
+ // Check 1: File exists
25
+ if (!await fs.pathExists(filePath)) {
26
+ errors.push({ check: 'file-exists', message: `File not found: ${filePath}` });
27
+ return writeResult({ valid: false, errors, warnings });
28
+ }
29
+ // Check 1b: File extension
30
+ if (!filePath.endsWith('.eval.ts')) {
31
+ errors.push({ check: 'file-extension', message: `File must have .eval.ts extension, got: ${filePath}` });
32
+ return writeResult({ valid: false, errors, warnings });
33
+ }
34
+ const content = await fs.readFile(filePath, 'utf-8');
35
+ // Check 2: TypeScript syntax (optional, skip in tests)
36
+ if (!opts.skipTsc) {
37
+ const tscResult = await runTsc(filePath);
38
+ if (!tscResult.ok) {
39
+ errors.push({ check: 'typescript-compiles', message: tscResult.message });
40
+ }
41
+ }
42
+ // Check 3: Imports
43
+ if (!content.includes("from '@wix/pathgrade'") && !content.includes('from "@wix/pathgrade"')) {
44
+ errors.push({ check: 'imports-pathgrade', message: "No import from '@wix/pathgrade' found." });
45
+ }
46
+ // Check 4: describe/it blocks
47
+ if (!/describe\s*\(/.test(content) || !/it\s*\(/.test(content)) {
48
+ errors.push({ check: 'has-describe-it', message: 'No describe() or it() blocks found. Eval must use Vitest structure.' });
49
+ }
50
+ // Check 5: createAgent
51
+ if (!/createAgent\s*\(/.test(content)) {
52
+ errors.push({ check: 'has-create-agent', message: 'No createAgent() call found.' });
53
+ }
54
+ // Check 6: evaluate
55
+ if (!/evaluate\s*\(/.test(content)) {
56
+ errors.push({ check: 'has-evaluate', message: 'No evaluate() call found.' });
57
+ }
58
+ // Check 7: Deterministic scorer
59
+ if (!/\bcheck\s*\(/.test(content) && !/\bscore\s*\(/.test(content)) {
60
+ errors.push({ check: 'has-deterministic-scorer', message: 'No check() or score() scorer found. Add at least one deterministic scorer.' });
61
+ }
62
+ // Check 8: Scorer names non-empty
63
+ const scorerCallPattern = /\b(?:check|score|judge|toolUsage)\s*\(\s*(['"])(.*?)\1/g;
64
+ let scorerMatch;
65
+ while ((scorerMatch = scorerCallPattern.exec(content)) !== null) {
66
+ if (scorerMatch[2].trim() === '') {
67
+ errors.push({ check: 'scorer-names-non-empty', message: `Scorer has empty name at position ${scorerMatch.index}.` });
68
+ break; // One error is enough
69
+ }
70
+ }
71
+ // Check 9: Instruction non-empty (>10 chars)
72
+ const instructionPattern = /(?:prompt|startChat)\s*\(\s*(['"`])([\s\S]*?)\1/g;
73
+ let instrMatch;
74
+ let foundInstruction = false;
75
+ while ((instrMatch = instructionPattern.exec(content)) !== null) {
76
+ foundInstruction = true;
77
+ if (instrMatch[2].trim().length <= 10) {
78
+ errors.push({ check: 'instruction-non-empty', message: `Instruction is too short (${instrMatch[2].trim().length} chars). Write a meaningful instruction (>10 chars).` });
79
+ break;
80
+ }
81
+ }
82
+ // Also check template literal instructions
83
+ if (!foundInstruction) {
84
+ const templateInstrPattern = /(?:prompt|startChat)\s*\(\s*`([\s\S]*?)`/g;
85
+ let tmplMatch;
86
+ while ((tmplMatch = templateInstrPattern.exec(content)) !== null) {
87
+ foundInstruction = true;
88
+ if (tmplMatch[1].trim().length <= 10) {
89
+ errors.push({ check: 'instruction-non-empty', message: `Instruction is too short (${tmplMatch[1].trim().length} chars). Write a meaningful instruction (>10 chars).` });
90
+ break;
91
+ }
92
+ }
93
+ }
94
+ // Check 10: Filename consistency
95
+ const filenameWarnings = checkFilenameConsistency(content);
96
+ warnings.push(...filenameWarnings);
97
+ const valid = errors.length === 0;
98
+ return writeResult({ valid, errors, warnings });
99
+ }
100
+ function checkFilenameConsistency(content) {
101
+ const warnings = [];
102
+ // Extract all instruction text from prompt()/startChat() calls
103
+ const instrTexts = [];
104
+ // Match single/double quoted strings
105
+ const instrQuoted = /(?:prompt|startChat)\s*\(\s*(['"])([\s\S]*?)\1/g;
106
+ let m;
107
+ while ((m = instrQuoted.exec(content)) !== null) {
108
+ instrTexts.push(m[2]);
109
+ }
110
+ // Match template literals
111
+ const instrTemplate = /(?:prompt|startChat)\s*\(\s*`([\s\S]*?)`/g;
112
+ while ((m = instrTemplate.exec(content)) !== null) {
113
+ instrTexts.push(m[1]);
114
+ }
115
+ const instructionText = instrTexts.join(' ');
116
+ if (!instructionText)
117
+ return warnings;
118
+ // Find all check() call regions by matching check( ... the callback body
119
+ // Strategy: find each check( occurrence, then scan forward for filenames
120
+ const checkCallPattern = /\bcheck\s*\(/g;
121
+ while ((m = checkCallPattern.exec(content)) !== null) {
122
+ // Extract a reasonable window after the check( — up to 500 chars covers most callbacks
123
+ const window = content.substring(m.index, m.index + 500);
124
+ const filenamePattern = /['"`](?:.*\/)?([a-zA-Z0-9_.-]+\.[a-zA-Z]{1,5})['"`]/g;
125
+ let fm;
126
+ while ((fm = filenamePattern.exec(window)) !== null) {
127
+ const filename = fm[1];
128
+ // Skip the scorer name (first string after check(), and common non-file patterns
129
+ if (fm.index < 10)
130
+ continue; // likely the scorer name
131
+ if (filename.startsWith('0.'))
132
+ continue;
133
+ if (!instructionText.includes(filename)) {
134
+ warnings.push({
135
+ check: 'filename-consistency',
136
+ message: `Scorer checks for '${filename}' but the instruction does not mention this filename.`,
137
+ });
138
+ }
139
+ }
140
+ }
141
+ return warnings;
142
+ }
143
+ function writeResult(result) {
144
+ process.stdout.write(JSON.stringify(result));
145
+ return result.valid ? 0 : 1;
146
+ }
147
+ /**
148
+ * Strict affected-selection validator. Exits 1 if any eval is missing
149
+ * both a SKILL.md ancestor and `__pathgradeMeta`, or has malformed meta.
150
+ */
151
+ export async function runValidateAffected(cwd) {
152
+ const evalFiles = discoverEvalFiles(cwd);
153
+ let errors = 0;
154
+ for (const evalFile of evalFiles) {
155
+ const abs = path.resolve(cwd, evalFile);
156
+ const skillRoot = findSkillRoot(abs, cwd);
157
+ let parseError = null;
158
+ let hasMeta = false;
159
+ try {
160
+ const meta = parsePathgradeMeta(abs);
161
+ hasMeta = meta !== null;
162
+ }
163
+ catch (err) {
164
+ parseError = err instanceof Error ? err.message : String(err);
165
+ }
166
+ if (parseError) {
167
+ errors++;
168
+ process.stdout.write(`${evalFile} — ❌ malformed __pathgradeMeta: ${parseError}\n`);
169
+ continue;
170
+ }
171
+ if (skillRoot) {
172
+ process.stdout.write(`${evalFile} — ✅ anchored at ${skillRoot}\n`);
173
+ continue;
174
+ }
175
+ if (hasMeta) {
176
+ process.stdout.write(`${evalFile} — ✅ __pathgradeMeta present (no SKILL.md anchor)\n`);
177
+ continue;
178
+ }
179
+ errors++;
180
+ process.stdout.write(`${evalFile} — ❌ no SKILL.md ancestor and no __pathgradeMeta\n`);
181
+ }
182
+ process.stdout.write(`\n${evalFiles.length} evals, ${errors} errors\n`);
183
+ return errors === 0 ? 0 : 1;
184
+ }
185
+ async function runTsc(filePath) {
186
+ const { execSync } = await import('child_process');
187
+ const path = await import('path');
188
+ const { writeFileSync, unlinkSync } = await import('fs');
189
+ const os = await import('os');
190
+ // Resolve pathgrade's own tsc binary — avoids npx indirection and the
191
+ // "This is not the tsc command you are looking for" error when the
192
+ // consumer hasn't installed typescript themselves. `createRequire` is
193
+ // needed because this module is emitted as ESM ("type": "module") and
194
+ // `require` is not a free binding there — issue #39.
195
+ const localRequire = createRequire(import.meta.url);
196
+ const tscPath = localRequire.resolve('typescript/bin/tsc');
197
+ // Resolve pathgrade's own @types so Node builtins (fs, path, __dirname)
198
+ // compile without the consumer needing to install @types/node.
199
+ const pathgradeTypesDir = path.dirname(localRequire.resolve('@types/node/package.json'));
200
+ const pathgradeTypeRoots = path.dirname(pathgradeTypesDir);
201
+ const consumerTypeRoots = path.join(process.cwd(), 'node_modules', '@types');
202
+ // Generate a temp tsconfig instead of passing files on the CLI.
203
+ // This avoids TS5112 ("tsconfig.json is present but will not be loaded
204
+ // if files are specified on commandline") in TypeScript 5.6+.
205
+ const tmpConfig = path.join(os.tmpdir(), `pathgrade-tsc-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
206
+ const tsconfig = {
207
+ compilerOptions: {
208
+ noEmit: true,
209
+ esModuleInterop: true,
210
+ module: 'nodenext',
211
+ moduleResolution: 'nodenext',
212
+ typeRoots: [pathgradeTypeRoots, consumerTypeRoots],
213
+ skipLibCheck: true,
214
+ },
215
+ files: [path.resolve(filePath)],
216
+ };
217
+ writeFileSync(tmpConfig, JSON.stringify(tsconfig));
218
+ try {
219
+ execSync(`node "${tscPath}" --project "${tmpConfig}"`, { stdio: 'pipe', timeout: 30000 });
220
+ return { ok: true, message: '' };
221
+ }
222
+ catch (err) {
223
+ const output = (err.stdout?.toString() || '') + (err.stderr?.toString() || '');
224
+ const lines = output.split('\n').filter((l) => l.trim()).slice(0, 5);
225
+ return { ok: false, message: lines.join('\n') || 'TypeScript compilation failed.' };
226
+ }
227
+ finally {
228
+ try {
229
+ unlinkSync(tmpConfig);
230
+ }
231
+ catch { }
232
+ }
233
+ }
@@ -0,0 +1,3 @@
1
+ import { MockMcpServerConfig, MockMcpServerDescriptor } from './mcp-mock.types.js';
2
+ export type { MockMcpTool, MockMcpServerConfig, MockMcpServerDescriptor } from './mcp-mock.types.js';
3
+ export declare function mockMcpServer(config: MockMcpServerConfig): MockMcpServerDescriptor;
@@ -0,0 +1,23 @@
1
+ export function mockMcpServer(config) {
2
+ if (!config.name || typeof config.name !== 'string') {
3
+ throw new Error('mockMcpServer: name must be a non-empty string');
4
+ }
5
+ if (!Array.isArray(config.tools) || config.tools.length === 0) {
6
+ throw new Error('mockMcpServer: must have at least one tool');
7
+ }
8
+ for (let i = 0; i < config.tools.length; i++) {
9
+ const tool = config.tools[i];
10
+ if (!tool.name || typeof tool.name !== 'string') {
11
+ throw new Error(`mockMcpServer: tools[${i}].name must be a non-empty string`);
12
+ }
13
+ if (tool.when !== undefined) {
14
+ try {
15
+ new RegExp(tool.when, 'i');
16
+ }
17
+ catch (e) {
18
+ throw new Error(`mockMcpServer: tools[${i}].when is not a valid regex: ${e.message}`);
19
+ }
20
+ }
21
+ }
22
+ return { __type: 'mock_mcp_server', config };
23
+ }
@@ -0,0 +1,15 @@
1
+ export interface MockMcpTool {
2
+ name: string;
3
+ description?: string;
4
+ inputSchema?: Record<string, unknown>;
5
+ when?: string;
6
+ response: unknown;
7
+ }
8
+ export interface MockMcpServerConfig {
9
+ name: string;
10
+ tools: MockMcpTool[];
11
+ }
12
+ export interface MockMcpServerDescriptor {
13
+ __type: 'mock_mcp_server';
14
+ config: MockMcpServerConfig;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ export interface DetectedSkill {
2
+ name: string;
3
+ path: string;
4
+ skillMd: string;
5
+ }
6
+ /**
7
+ * Detect skills in the given directory.
8
+ *
9
+ * Looks in these locations (in order):
10
+ * 1. SKILL.md in the root directory (the dir itself IS the skill)
11
+ * 2. skill/SKILL.md (example layout with eval.ts at the root)
12
+ * 3. skills/{name}/SKILL.md (sub-skills)
13
+ * 4. .agents/skills/{name}/SKILL.md
14
+ */
15
+ export declare function detectSkills(dir: string): Promise<DetectedSkill[]>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Skill detection — finds skills in the current directory or standard locations.
3
+ */
4
+ import fs from 'fs-extra';
5
+ import * as path from 'path';
6
+ /**
7
+ * Detect skills in the given directory.
8
+ *
9
+ * Looks in these locations (in order):
10
+ * 1. SKILL.md in the root directory (the dir itself IS the skill)
11
+ * 2. skill/SKILL.md (example layout with eval.ts at the root)
12
+ * 3. skills/{name}/SKILL.md (sub-skills)
13
+ * 4. .agents/skills/{name}/SKILL.md
14
+ */
15
+ export async function detectSkills(dir) {
16
+ const skills = [];
17
+ // 1. Check if the directory itself is a skill
18
+ const rootSkillMd = path.join(dir, 'SKILL.md');
19
+ if (await fs.pathExists(rootSkillMd)) {
20
+ const content = await fs.readFile(rootSkillMd, 'utf-8');
21
+ const name = parseSkillName(content) || path.basename(dir);
22
+ skills.push({ name, path: dir, skillMd: content });
23
+ return skills; // If root is a skill, don't look for sub-skills
24
+ }
25
+ // 2. Check singular skill/ directory used by example layouts
26
+ const nestedSkillDir = path.join(dir, 'skill');
27
+ const nestedSkillMd = path.join(nestedSkillDir, 'SKILL.md');
28
+ if (await fs.pathExists(nestedSkillMd)) {
29
+ const content = await fs.readFile(nestedSkillMd, 'utf-8');
30
+ const name = parseSkillName(content) || 'skill';
31
+ skills.push({ name, path: nestedSkillDir, skillMd: content });
32
+ return skills;
33
+ }
34
+ // 3. Check skills/ and agent skill subdirectories
35
+ const searchDirs = ['skills', '.agents/skills', '.claude/skills'];
36
+ for (const searchDir of searchDirs) {
37
+ const fullSearchDir = path.join(dir, searchDir);
38
+ if (!await fs.pathExists(fullSearchDir))
39
+ continue;
40
+ const entries = await fs.readdir(fullSearchDir, { withFileTypes: true });
41
+ for (const entry of entries) {
42
+ if (!entry.isDirectory())
43
+ continue;
44
+ const skillMdPath = path.join(fullSearchDir, entry.name, 'SKILL.md');
45
+ if (await fs.pathExists(skillMdPath)) {
46
+ const content = await fs.readFile(skillMdPath, 'utf-8');
47
+ const name = parseSkillName(content) || entry.name;
48
+ skills.push({
49
+ name,
50
+ path: path.join(fullSearchDir, entry.name),
51
+ skillMd: content,
52
+ });
53
+ }
54
+ }
55
+ }
56
+ return skills;
57
+ }
58
+ /**
59
+ * Parse the skill name from SKILL.md content.
60
+ * Looks for YAML frontmatter `name:` or first `# Heading`.
61
+ */
62
+ function parseSkillName(content) {
63
+ // Try YAML frontmatter
64
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
65
+ if (fmMatch) {
66
+ const nameMatch = fmMatch[1].match(/^name:\s*(.+)$/m);
67
+ if (nameMatch)
68
+ return nameMatch[1].trim().replace(/^['"]|['"]$/g, '');
69
+ }
70
+ // Try first heading
71
+ const headingMatch = content.match(/^#\s+(.+)$/m);
72
+ if (headingMatch) {
73
+ return headingMatch[1].trim().toLowerCase().replace(/\s+/g, '-');
74
+ }
75
+ return undefined;
76
+ }
@@ -0,0 +1,7 @@
1
+ export interface EvalDiscoveryOptions {
2
+ cwd: string;
3
+ include: string[];
4
+ exclude: string[];
5
+ }
6
+ export declare function discoverPathgradeEvalFiles(opts: EvalDiscoveryOptions): string[];
7
+ export declare function isPathgradeEval(absPath: string): boolean;
@@ -0,0 +1,69 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import picomatch from 'picomatch';
4
+ import * as ts from 'typescript';
5
+ const EVAL_SUFFIX = '.eval.ts';
6
+ const PATHGRADE_PACKAGE = '@wix/pathgrade';
7
+ export function discoverPathgradeEvalFiles(opts) {
8
+ const { cwd, include, exclude } = opts;
9
+ const root = path.resolve(cwd);
10
+ const includeMatchers = include.map(g => picomatch(g, { dot: true }));
11
+ const excludeMatchers = exclude.map(g => picomatch(g, { dot: true }));
12
+ const results = [];
13
+ function isIncluded(relPath) {
14
+ return includeMatchers.some(m => m(relPath));
15
+ }
16
+ function isExcluded(relPath) {
17
+ return excludeMatchers.some(m => m(relPath));
18
+ }
19
+ function walk(absDir, relDir) {
20
+ let entries;
21
+ try {
22
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
23
+ }
24
+ catch {
25
+ return;
26
+ }
27
+ for (const ent of entries) {
28
+ const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
29
+ if (isExcluded(rel))
30
+ continue;
31
+ const abs = path.join(absDir, ent.name);
32
+ if (ent.isDirectory()) {
33
+ walk(abs, rel);
34
+ continue;
35
+ }
36
+ if (!ent.isFile() || !ent.name.endsWith(EVAL_SUFFIX) || !isIncluded(rel))
37
+ continue;
38
+ if (!isPathgradeEval(abs))
39
+ continue;
40
+ results.push(rel);
41
+ }
42
+ }
43
+ walk(root, '');
44
+ results.sort();
45
+ return results;
46
+ }
47
+ export function isPathgradeEval(absPath) {
48
+ const source = fs.readFileSync(absPath, 'utf8');
49
+ const sourceFile = ts.createSourceFile(absPath, source, ts.ScriptTarget.Latest,
50
+ /* setParentNodes */ false, ts.ScriptKind.TS);
51
+ for (const stmt of sourceFile.statements) {
52
+ if (isPathgradeImport(stmt) || isPathgradeMetaExport(stmt))
53
+ return true;
54
+ }
55
+ return false;
56
+ }
57
+ function isPathgradeImport(stmt) {
58
+ if (!ts.isImportDeclaration(stmt))
59
+ return false;
60
+ return ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text === PATHGRADE_PACKAGE;
61
+ }
62
+ function isPathgradeMetaExport(stmt) {
63
+ if (!ts.isVariableStatement(stmt))
64
+ return false;
65
+ const hasExport = stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
66
+ if (!hasExport)
67
+ return false;
68
+ return stmt.declarationList.declarations.some(decl => ts.isIdentifier(decl.name) && decl.name.text === '__pathgradeMeta');
69
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,116 @@
1
+ import * as fs from 'fs';
2
+ import * as readline from 'readline';
3
+ function loadFixture(fixturePath) {
4
+ const content = fs.readFileSync(fixturePath, 'utf-8');
5
+ return JSON.parse(content);
6
+ }
7
+ function buildToolSchemas(fixture) {
8
+ const seen = new Set();
9
+ const schemas = [];
10
+ for (const tool of fixture.tools) {
11
+ if (seen.has(tool.name))
12
+ continue;
13
+ seen.add(tool.name);
14
+ schemas.push({
15
+ name: tool.name,
16
+ description: tool.description || `Mock tool: ${tool.name}`,
17
+ inputSchema: tool.inputSchema || { type: 'object' },
18
+ });
19
+ }
20
+ return schemas;
21
+ }
22
+ function matchTool(fixture, toolName, args) {
23
+ const inputStr = JSON.stringify(args);
24
+ let fallback;
25
+ for (const entry of fixture.tools) {
26
+ if (entry.name !== toolName)
27
+ continue;
28
+ if (entry.when) {
29
+ if (new RegExp(entry.when, 'i').test(inputStr)) {
30
+ return { response: entry.response };
31
+ }
32
+ }
33
+ else if (!fallback) {
34
+ fallback = entry;
35
+ }
36
+ }
37
+ if (fallback)
38
+ return { response: fallback.response };
39
+ return null;
40
+ }
41
+ function formatResponse(response) {
42
+ return typeof response === 'string' ? response : JSON.stringify(response);
43
+ }
44
+ function sendResponse(id, result) {
45
+ const msg = JSON.stringify({ jsonrpc: '2.0', id, result });
46
+ process.stdout.write(msg + '\n');
47
+ }
48
+ function sendError(id, code, message) {
49
+ const msg = JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
50
+ process.stdout.write(msg + '\n');
51
+ }
52
+ function main() {
53
+ const fixturePath = process.argv[2];
54
+ if (!fixturePath) {
55
+ process.stderr.write('Usage: mcp-mock-server <fixture.json>\n');
56
+ process.exit(1);
57
+ }
58
+ let fixture;
59
+ try {
60
+ fixture = loadFixture(fixturePath);
61
+ }
62
+ catch (e) {
63
+ process.stderr.write(`Failed to load fixture: ${e.message}\n`);
64
+ process.exit(1);
65
+ }
66
+ const toolSchemas = buildToolSchemas(fixture);
67
+ const rl = readline.createInterface({ input: process.stdin });
68
+ rl.on('line', (line) => {
69
+ if (!line.trim())
70
+ return;
71
+ let msg;
72
+ try {
73
+ msg = JSON.parse(line);
74
+ }
75
+ catch {
76
+ return;
77
+ }
78
+ // Notifications (no id) — acknowledge silently
79
+ if (msg.id === undefined)
80
+ return;
81
+ switch (msg.method) {
82
+ case 'initialize':
83
+ sendResponse(msg.id, {
84
+ protocolVersion: '2024-11-05',
85
+ capabilities: { tools: {} },
86
+ serverInfo: { name: fixture.name, version: '1.0.0' },
87
+ });
88
+ break;
89
+ case 'tools/list':
90
+ sendResponse(msg.id, { tools: toolSchemas });
91
+ break;
92
+ case 'tools/call': {
93
+ const toolName = msg.params?.name;
94
+ const toolArgs = msg.params?.arguments ?? {};
95
+ const match = matchTool(fixture, toolName, toolArgs);
96
+ if (match) {
97
+ sendResponse(msg.id, {
98
+ content: [{ type: 'text', text: formatResponse(match.response) }],
99
+ });
100
+ }
101
+ else {
102
+ sendError(msg.id, -32602, `Unknown tool: ${toolName}`);
103
+ }
104
+ break;
105
+ }
106
+ case 'ping':
107
+ sendResponse(msg.id, {});
108
+ break;
109
+ default:
110
+ sendError(msg.id, -32601, `Method not found: ${msg.method}`);
111
+ break;
112
+ }
113
+ });
114
+ rl.on('close', () => process.exit(0));
115
+ }
116
+ main();
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pathgrade CLI — thin wrapper around vitest
4
+ *
5
+ * Usage:
6
+ * pathgrade run [-- vitest-args] Run evals via vitest (loads .env, validates API keys)
7
+ * pathgrade init [--force] Generate eval scaffolding
8
+ * pathgrade preview [browser] View results (CLI default, or browser)
9
+ */
10
+ export {};