@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.
- package/bin/pathgrade.js +2 -0
- package/dist/affected/anchor.d.ts +18 -0
- package/dist/affected/anchor.js +42 -0
- package/dist/affected/config.d.ts +25 -0
- package/dist/affected/config.js +98 -0
- package/dist/affected/format.d.ts +45 -0
- package/dist/affected/format.js +87 -0
- package/dist/affected/git.d.ts +33 -0
- package/dist/affected/git.js +68 -0
- package/dist/affected/glob.d.ts +17 -0
- package/dist/affected/glob.js +25 -0
- package/dist/affected/meta.d.ts +26 -0
- package/dist/affected/meta.js +158 -0
- package/dist/affected/select.d.ts +30 -0
- package/dist/affected/select.js +101 -0
- package/dist/affected/sidecar.d.ts +32 -0
- package/dist/affected/sidecar.js +86 -0
- package/dist/affected/types.d.ts +43 -0
- package/dist/affected/types.js +7 -0
- package/dist/agents/claude/ask-user-answer-store.d.ts +28 -0
- package/dist/agents/claude/ask-user-answer-store.js +29 -0
- package/dist/agents/claude/ask-user-bridge.d.ts +56 -0
- package/dist/agents/claude/ask-user-bridge.js +162 -0
- package/dist/agents/claude/sdk-message-projector.d.ts +43 -0
- package/dist/agents/claude/sdk-message-projector.js +208 -0
- package/dist/agents/claude/sdk-options.d.ts +52 -0
- package/dist/agents/claude/sdk-options.js +79 -0
- package/dist/agents/claude.d.ts +56 -0
- package/dist/agents/claude.js +143 -0
- package/dist/agents/codex-app-server/agent.d.ts +35 -0
- package/dist/agents/codex-app-server/agent.js +388 -0
- package/dist/agents/codex-app-server/fixtures/app-server-client.d.ts +19 -0
- package/dist/agents/codex-app-server/fixtures/app-server-client.js +104 -0
- package/dist/agents/codex-app-server/fixtures/run-gate.d.ts +25 -0
- package/dist/agents/codex-app-server/fixtures/run-gate.js +48 -0
- package/dist/agents/codex-app-server/protocol/ClientRequest.d.ts +27 -0
- package/dist/agents/codex-app-server/protocol/ClientRequest.js +7 -0
- package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.d.ts +9 -0
- package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.js +4 -0
- package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.d.ts +6 -0
- package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.js +7 -0
- package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.d.ts +23 -0
- package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.js +8 -0
- package/dist/agents/codex-app-server/protocol/Op.d.ts +11 -0
- package/dist/agents/codex-app-server/protocol/Op.js +5 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.d.ts +10 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.js +6 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.d.ts +15 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.js +4 -0
- package/dist/agents/codex-app-server/protocol/SandboxMode.d.ts +1 -0
- package/dist/agents/codex-app-server/protocol/SandboxMode.js +4 -0
- package/dist/agents/codex-app-server/protocol/ServerRequest.d.ts +42 -0
- package/dist/agents/codex-app-server/protocol/ServerRequest.js +7 -0
- package/dist/agents/codex-app-server/protocol/ThreadStartParams.d.ts +45 -0
- package/dist/agents/codex-app-server/protocol/ThreadStartParams.js +10 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.d.ts +6 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.d.ts +7 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.d.ts +10 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.d.ts +12 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.d.ts +9 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.js +4 -0
- package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.d.ts +5 -0
- package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.js +4 -0
- package/dist/agents/codex-app-server/protocol/index.d.ts +16 -0
- package/dist/agents/codex-app-server/protocol/index.js +11 -0
- package/dist/agents/codex-app-server/transport.d.ts +84 -0
- package/dist/agents/codex-app-server/transport.js +246 -0
- package/dist/agents/codex-app-server/wire-translators.d.ts +19 -0
- package/dist/agents/codex-app-server/wire-translators.js +52 -0
- package/dist/agents/codex.d.ts +7 -0
- package/dist/agents/codex.js +164 -0
- package/dist/agents/cursor.d.ts +39 -0
- package/dist/agents/cursor.js +233 -0
- package/dist/agents/registry.d.ts +17 -0
- package/dist/agents/registry.js +27 -0
- package/dist/agents/transcript-agent.d.ts +14 -0
- package/dist/agents/transcript-agent.js +59 -0
- package/dist/analytics/engine.d.ts +18 -0
- package/dist/analytics/engine.js +66 -0
- package/dist/commands/affected.d.ts +31 -0
- package/dist/commands/affected.js +150 -0
- package/dist/commands/analyze.d.ts +21 -0
- package/dist/commands/analyze.js +167 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +242 -0
- package/dist/commands/preview-reactions.d.ts +10 -0
- package/dist/commands/preview-reactions.js +88 -0
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.js +17 -0
- package/dist/commands/report.d.ts +39 -0
- package/dist/commands/report.js +108 -0
- package/dist/commands/run-args.d.ts +22 -0
- package/dist/commands/run-args.js +69 -0
- package/dist/commands/run-changed.d.ts +26 -0
- package/dist/commands/run-changed.js +138 -0
- package/dist/commands/validate.d.ts +13 -0
- package/dist/commands/validate.js +233 -0
- package/dist/core/mcp-mock.d.ts +3 -0
- package/dist/core/mcp-mock.js +23 -0
- package/dist/core/mcp-mock.types.d.ts +15 -0
- package/dist/core/mcp-mock.types.js +1 -0
- package/dist/core/skills.d.ts +15 -0
- package/dist/core/skills.js +76 -0
- package/dist/evals/discovery.d.ts +7 -0
- package/dist/evals/discovery.js +69 -0
- package/dist/mcp-mock-server.d.ts +1 -0
- package/dist/mcp-mock-server.js +116 -0
- package/dist/pathgrade.d.ts +10 -0
- package/dist/pathgrade.js +243 -0
- package/dist/plugin/index.d.ts +9 -0
- package/dist/plugin/index.js +81 -0
- package/dist/plugin/lifecycle.d.ts +45 -0
- package/dist/plugin/lifecycle.js +146 -0
- package/dist/plugin/reporter.d.ts +36 -0
- package/dist/plugin/reporter.js +275 -0
- package/dist/plugin/setup.d.ts +1 -0
- package/dist/plugin/setup.js +3 -0
- package/dist/providers/copy-filter.d.ts +18 -0
- package/dist/providers/copy-filter.js +66 -0
- package/dist/providers/credentials.d.ts +36 -0
- package/dist/providers/credentials.js +202 -0
- package/dist/providers/mcp-config.d.ts +34 -0
- package/dist/providers/mcp-config.js +55 -0
- package/dist/providers/sandbox-exec.d.ts +8 -0
- package/dist/providers/sandbox-exec.js +82 -0
- package/dist/providers/sandbox.d.ts +22 -0
- package/dist/providers/sandbox.js +158 -0
- package/dist/providers/sandboxed-claude-spawn.d.ts +24 -0
- package/dist/providers/sandboxed-claude-spawn.js +57 -0
- package/dist/providers/workspace.d.ts +15 -0
- package/dist/providers/workspace.js +73 -0
- package/dist/reporters/browser.d.ts +2 -0
- package/dist/reporters/browser.js +85 -0
- package/dist/reporters/cli.d.ts +2 -0
- package/dist/reporters/cli.js +131 -0
- package/dist/reporters/diagnostics.d.ts +46 -0
- package/dist/reporters/diagnostics.js +147 -0
- package/dist/reporters/github-comment.d.ts +67 -0
- package/dist/reporters/github-comment.js +252 -0
- package/dist/reporters/loader.d.ts +8 -0
- package/dist/reporters/loader.js +36 -0
- package/dist/reporters/results-path.d.ts +2 -0
- package/dist/reporters/results-path.js +10 -0
- package/dist/reporters/verbose-emitter.d.ts +69 -0
- package/dist/reporters/verbose-emitter.js +103 -0
- package/dist/sdk/agent-crash.d.ts +19 -0
- package/dist/sdk/agent-crash.js +20 -0
- package/dist/sdk/agent-resolution.d.ts +12 -0
- package/dist/sdk/agent-resolution.js +21 -0
- package/dist/sdk/agent-result-log.d.ts +19 -0
- package/dist/sdk/agent-result-log.js +45 -0
- package/dist/sdk/agent.d.ts +4 -0
- package/dist/sdk/agent.js +354 -0
- package/dist/sdk/ask-bus/bus.d.ts +21 -0
- package/dist/sdk/ask-bus/bus.js +129 -0
- package/dist/sdk/ask-bus/handler.d.ts +31 -0
- package/dist/sdk/ask-bus/handler.js +244 -0
- package/dist/sdk/ask-bus/parsers.d.ts +9 -0
- package/dist/sdk/ask-bus/parsers.js +30 -0
- package/dist/sdk/ask-bus/projection.d.ts +26 -0
- package/dist/sdk/ask-bus/projection.js +45 -0
- package/dist/sdk/ask-bus/types.d.ts +69 -0
- package/dist/sdk/ask-bus/types.js +1 -0
- package/dist/sdk/chat.d.ts +29 -0
- package/dist/sdk/chat.js +93 -0
- package/dist/sdk/conversation-window.d.ts +14 -0
- package/dist/sdk/conversation-window.js +38 -0
- package/dist/sdk/converse.d.ts +42 -0
- package/dist/sdk/converse.js +329 -0
- package/dist/sdk/eval-runtime.d.ts +10 -0
- package/dist/sdk/eval-runtime.js +17 -0
- package/dist/sdk/evaluate.d.ts +13 -0
- package/dist/sdk/evaluate.js +325 -0
- package/dist/sdk/index.d.ts +31 -0
- package/dist/sdk/index.js +20 -0
- package/dist/sdk/judge-pipeline.d.ts +7 -0
- package/dist/sdk/judge-pipeline.js +178 -0
- package/dist/sdk/judge-prompt-builder.d.ts +7 -0
- package/dist/sdk/judge-prompt-builder.js +76 -0
- package/dist/sdk/judge-tool-runner.d.ts +19 -0
- package/dist/sdk/judge-tool-runner.js +51 -0
- package/dist/sdk/judge-tool-session.d.ts +47 -0
- package/dist/sdk/judge-tool-session.js +213 -0
- package/dist/sdk/judge-tools.d.ts +29 -0
- package/dist/sdk/judge-tools.js +283 -0
- package/dist/sdk/managed-session.d.ts +50 -0
- package/dist/sdk/managed-session.js +110 -0
- package/dist/sdk/persona.d.ts +5 -0
- package/dist/sdk/persona.js +33 -0
- package/dist/sdk/reaction-loader.d.ts +4 -0
- package/dist/sdk/reaction-loader.js +119 -0
- package/dist/sdk/reaction-preview.d.ts +6 -0
- package/dist/sdk/reaction-preview.js +94 -0
- package/dist/sdk/run-scorer.d.ts +10 -0
- package/dist/sdk/run-scorer.js +124 -0
- package/dist/sdk/runtime-policy.d.ts +21 -0
- package/dist/sdk/runtime-policy.js +89 -0
- package/dist/sdk/scorer-utils.d.ts +6 -0
- package/dist/sdk/scorer-utils.js +39 -0
- package/dist/sdk/scorers.d.ts +64 -0
- package/dist/sdk/scorers.js +80 -0
- package/dist/sdk/snapshots.d.ts +41 -0
- package/dist/sdk/snapshots.js +148 -0
- package/dist/sdk/types.d.ts +416 -0
- package/dist/sdk/types.js +17 -0
- package/dist/sdk/visible-turn.d.ts +4 -0
- package/dist/sdk/visible-turn.js +23 -0
- package/dist/tool-events.d.ts +45 -0
- package/dist/tool-events.js +167 -0
- package/dist/types.d.ts +372 -0
- package/dist/types.js +37 -0
- package/dist/utils/cli.d.ts +55 -0
- package/dist/utils/cli.js +131 -0
- package/dist/utils/env.d.ts +8 -0
- package/dist/utils/env.js +26 -0
- package/dist/utils/llm-mocks.d.ts +43 -0
- package/dist/utils/llm-mocks.js +108 -0
- package/dist/utils/llm-providers/anthropic.d.ts +2 -0
- package/dist/utils/llm-providers/anthropic.js +132 -0
- package/dist/utils/llm-providers/cli.d.ts +20 -0
- package/dist/utils/llm-providers/cli.js +162 -0
- package/dist/utils/llm-providers/openai.d.ts +2 -0
- package/dist/utils/llm-providers/openai.js +56 -0
- package/dist/utils/llm-types.d.ts +107 -0
- package/dist/utils/llm-types.js +1 -0
- package/dist/utils/llm.d.ts +36 -0
- package/dist/utils/llm.js +209 -0
- package/dist/utils/shutdown.d.ts +15 -0
- package/dist/utils/shutdown.js +41 -0
- package/dist/utils/timeout.d.ts +8 -0
- package/dist/utils/timeout.js +32 -0
- package/dist/viewer.html +1178 -0
- package/package.json +88 -0
- 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,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 {};
|