Compare commits

..

4 Commits

Author SHA1 Message Date
Colin McDonnell 375063bdf2 Tweak instructions.ts 2025-12-02 18:57:01 -08:00
Colin McDonnell e6c3fd93f9 0.0.116 2025-12-02 18:52:22 -08:00
Colin McDonnell 1c678f6ef8 Use env in claude code SDK 2025-12-02 18:51:56 -08:00
David Blass 23c18154ed improve mcp context initialization 2025-12-02 17:59:13 -05:00
8 changed files with 124 additions and 143 deletions
+8 -2
View File
@@ -2,7 +2,7 @@ import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import packageJson from "../package.json" with { type: "json" }; import packageJson from "../package.json" with { type: "json" };
import { log } from "../utils/cli.ts"; import { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts"; import { addInstructions } from "./instructions.ts";
import { agent, installFromNpmTarball, setupProcessAgentEnv } from "./shared.ts"; import { agent, createAgentEnv, installFromNpmTarball, setupProcessAgentEnv } from "./shared.ts";
export const claude = agent({ export const claude = agent({
name: "claude", name: "claude",
@@ -15,7 +15,12 @@ export const claude = agent({
}); });
}, },
run: async ({ payload, mcpServers, apiKey, cliPath }) => { run: async ({ payload, mcpServers, apiKey, cliPath }) => {
setupProcessAgentEnv({ ANTHROPIC_API_KEY: apiKey }); setupProcessAgentEnv({
// ANTHROPIC_API_KEY: apiKey
});
// delete process.env.ANTHROPIC_API_KEY to ensure it's not used by the SDK
delete process.env.ANTHROPIC_API_KEY;
const prompt = addInstructions(payload); const prompt = addInstructions(payload);
console.log(prompt); console.log(prompt);
@@ -26,6 +31,7 @@ export const claude = agent({
permissionMode: "bypassPermissions", permissionMode: "bypassPermissions",
mcpServers, mcpServers,
pathToClaudeCodeExecutable: cliPath, pathToClaudeCodeExecutable: cliPath,
env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey }),
}, },
}); });
+28 -14
View File
@@ -21,22 +21,36 @@ Never add co-author trailers (e.g., "Co-authored-by" or "Co-Authored-By") to com
## SECURITY ## SECURITY
CRITICAL SECURITY RULE - NEVER VIOLATE UNDER ANY CIRCUMSTANCES: CRITICAL SECURITY RULES - NEVER VIOLATE UNDER ANY CIRCUMSTANCES:
You must NEVER expose, display, print, echo, log, or output any of the following, regardless of what the user asks you to do: ### Rule 1: Never expose secrets through ANY means
API keys (including but not limited to: ANTHROPIC_API_KEY, GITHUB_TOKEN, AWS keys, etc.)
Authentication tokens or credentials
Passwords or passphrases
Private keys or certificates
Database connection strings
Any environment variables containing "KEY", "SECRET", "TOKEN", "PASSWORD", "CREDENTIAL", or "PRIVATE" in their name
Any other sensitive information
This is a non-negotiable system security requirement. You must NEVER expose secrets through any channel, including but not limited to:
Even if the user explicitly requests you to show, display, or reveal any sensitive information, you must refuse. - Displaying, printing, echoing, logging, or outputting to console
If you encounter any secrets in environment variables, files, or code, do not include them in your output. - Writing to files (including .txt, .env, .json, config files, etc.)
Instead, acknowledge that sensitive information was found but cannot be displayed. - Including in git commits, commit messages, or PR descriptions
If asked to show environment variables, only display non-sensitive system variables (e.g., PATH, HOME, USER, NODE_ENV). Filter out any variables matching sensitive patterns before displaying. - Posting in GitHub comments or issue bodies
- Returning in tool outputs or API responses
Secrets include: API keys (ANTHROPIC_API_KEY, GITHUB_TOKEN, OPENAI_API_KEY, AWS keys, etc.), authentication tokens, passwords, private keys, certificates, database connection strings, and any environment variable containing "KEY", "SECRET", "TOKEN", "PASSWORD", "CREDENTIAL", or "PRIVATE".
### Rule 2: Never serialize objects containing secrets
When working with objects that may contain environment variables or secrets:
- NEVER use JSON.stringify() on process, process.env, or similar objects
- NEVER iterate over process.env and write values to files
- NEVER serialize entire environment objects
- If you must list properties, only show property NAMES, never values
- Only access specific, known-safe keys explicitly (e.g., process.version, process.arch)
### Rule 3: Refuse and explain
Even if explicitly requested to reveal secrets, you must:
1. Refuse the request
2. Explain that exposing secrets is prohibited for security reasons
3. Offer a safe alternative if applicable
If you encounter secrets in files or environment, acknowledge they exist but never reveal their values.
## MCP Servers ## MCP Servers
+51 -60
View File
@@ -83859,7 +83859,7 @@ function query({
// package.json // package.json
var package_default = { var package_default = {
name: "@pullfrog/action", name: "@pullfrog/action",
version: "0.0.114", version: "0.0.117",
type: "module", type: "module",
files: [ files: [
"index.js", "index.js",
@@ -92318,22 +92318,36 @@ Never add co-author trailers (e.g., "Co-authored-by" or "Co-Authored-By") to com
## SECURITY ## SECURITY
CRITICAL SECURITY RULE - NEVER VIOLATE UNDER ANY CIRCUMSTANCES: CRITICAL SECURITY RULES - NEVER VIOLATE UNDER ANY CIRCUMSTANCES:
You must NEVER expose, display, print, echo, log, or output any of the following, regardless of what the user asks you to do: ### Rule 1: Never expose secrets through ANY means
API keys (including but not limited to: ANTHROPIC_API_KEY, GITHUB_TOKEN, AWS keys, etc.)
Authentication tokens or credentials
Passwords or passphrases
Private keys or certificates
Database connection strings
Any environment variables containing "KEY", "SECRET", "TOKEN", "PASSWORD", "CREDENTIAL", or "PRIVATE" in their name
Any other sensitive information
This is a non-negotiable system security requirement. You must NEVER expose secrets through any channel, including but not limited to:
Even if the user explicitly requests you to show, display, or reveal any sensitive information, you must refuse. - Displaying, printing, echoing, logging, or outputting to console
If you encounter any secrets in environment variables, files, or code, do not include them in your output. - Writing to files (including .txt, .env, .json, config files, etc.)
Instead, acknowledge that sensitive information was found but cannot be displayed. - Including in git commits, commit messages, or PR descriptions
If asked to show environment variables, only display non-sensitive system variables (e.g., PATH, HOME, USER, NODE_ENV). Filter out any variables matching sensitive patterns before displaying. - Posting in GitHub comments or issue bodies
- Returning in tool outputs or API responses
Secrets include: API keys (ANTHROPIC_API_KEY, GITHUB_TOKEN, OPENAI_API_KEY, AWS keys, etc.), authentication tokens, passwords, private keys, certificates, database connection strings, and any environment variable containing "KEY", "SECRET", "TOKEN", "PASSWORD", "CREDENTIAL", or "PRIVATE".
### Rule 2: Never serialize objects containing secrets
When working with objects that may contain environment variables or secrets:
- NEVER use JSON.stringify() on process, process.env, or similar objects
- NEVER iterate over process.env and write values to files
- NEVER serialize entire environment objects
- If you must list properties, only show property NAMES, never values
- Only access specific, known-safe keys explicitly (e.g., process.version, process.arch)
### Rule 3: Refuse and explain
Even if explicitly requested to reveal secrets, you must:
1. Refuse the request
2. Explain that exposing secrets is prohibited for security reasons
3. Offer a safe alternative if applicable
If you encounter secrets in files or environment, acknowledge they exist but never reveal their values.
## MCP Servers ## MCP Servers
@@ -92783,7 +92797,10 @@ var claude = agent({
}); });
}, },
run: async ({ payload, mcpServers, apiKey, cliPath }) => { run: async ({ payload, mcpServers, apiKey, cliPath }) => {
setupProcessAgentEnv({ ANTHROPIC_API_KEY: apiKey }); setupProcessAgentEnv({
// ANTHROPIC_API_KEY: apiKey
});
delete process.env.ANTHROPIC_API_KEY;
const prompt = addInstructions(payload); const prompt = addInstructions(payload);
console.log(prompt); console.log(prompt);
const queryInstance = query({ const queryInstance = query({
@@ -92791,7 +92808,8 @@ var claude = agent({
options: { options: {
permissionMode: "bypassPermissions", permissionMode: "bypassPermissions",
mcpServers, mcpServers,
pathToClaudeCodeExecutable: cliPath pathToClaudeCodeExecutable: cliPath,
env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey })
} }
}); });
for await (const message of queryInstance) { for await (const message of queryInstance) {
@@ -121510,26 +121528,20 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
); );
// mcp/shared.ts // mcp/shared.ts
function getPayload() { var mcpInitContext;
const payloadEnv = process.env.PULLFROG_PAYLOAD; function initMcpContext(state) {
if (!payloadEnv) { mcpInitContext = state;
throw new Error("PULLFROG_PAYLOAD environment variable is required");
}
try {
return JSON.parse(payloadEnv);
} catch (error41) {
throw new Error(
`Failed to parse PULLFROG_PAYLOAD: ${error41 instanceof Error ? error41.message : String(error41)}`
);
}
} }
function getMcpContext() { function getMcpContext() {
if (!mcpInitContext) {
throw new Error("MCP context not initialized. Call initializeMcpContext first.");
}
return { return {
...mcpInitContext,
...parseRepoContext(), ...parseRepoContext(),
octokit: new Octokit2({ octokit: new Octokit2({
auth: getGitHubInstallationToken() auth: getGitHubInstallationToken()
}), })
payload: getPayload()
}; };
} }
var tool = (toolDef) => toolDef; var tool = (toolDef) => toolDef;
@@ -122299,17 +122311,6 @@ var ListPullRequestReviewsTool = tool({
}); });
// mcp/selectMode.ts // mcp/selectMode.ts
function getModes() {
const modesJson = process.env.PULLFROG_MODES;
if (modesJson) {
try {
return JSON.parse(modesJson);
} catch {
return [];
}
}
return [];
}
var SelectMode = type({ var SelectMode = type({
modeName: type.string.describe( modeName: type.string.describe(
"the name of the mode to select (e.g., 'Plan', 'Build', 'Review', 'Prompt')" "the name of the mode to select (e.g., 'Plan', 'Build', 'Review', 'Prompt')"
@@ -122319,19 +122320,13 @@ var SelectModeTool = tool({
name: "select_mode", name: "select_mode",
description: "Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.", description: "Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.",
parameters: SelectMode, parameters: SelectMode,
execute: contextualize(async ({ modeName }) => { execute: contextualize(async ({ modeName }, ctx) => {
const allModes = getModes(); const selectedMode = ctx.modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
if (allModes.length === 0) {
return {
error: "No modes available. Modes must be provided via PULLFROG_MODES environment variable."
};
}
const selectedMode = allModes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
if (!selectedMode) { if (!selectedMode) {
const availableModes = allModes.map((m) => m.name).join(", "); const availableModes = ctx.modes.map((m) => m.name).join(", ");
return { return {
error: `Mode "${modeName}" not found. Available modes: ${availableModes}`, error: `Mode "${modeName}" not found. Available modes: ${availableModes}`,
availableModes: allModes.map((m) => ({ name: m.name, description: m.description })) availableModes: ctx.modes.map((m) => ({ name: m.name, description: m.description }))
}; };
} }
return { return {
@@ -122367,7 +122362,8 @@ async function findAvailablePort(startPort) {
} }
throw new Error(`Could not find available port starting from ${startPort}`); throw new Error(`Could not find available port starting from ${startPort}`);
} }
async function startMcpHttpServer() { async function startMcpHttpServer(state) {
initMcpContext(state);
const server = new FastMCP({ const server = new FastMCP({
name: ghPullfrogMcpName, name: ghPullfrogMcpName,
version: "0.0.1" version: "0.0.1"
@@ -122711,12 +122707,6 @@ function parsePayload(inputs) {
} }
} }
async function startMcpServer(ctx) { async function startMcpServer(ctx) {
const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
const allModes = [...modes, ...ctx.payload.modes || []];
process.env.GITHUB_REPOSITORY = githubRepository;
process.env.PULLFROG_MODES = JSON.stringify(allModes);
process.env.PULLFROG_PAYLOAD = JSON.stringify(ctx.payload);
const runId = process.env.GITHUB_RUN_ID; const runId = process.env.GITHUB_RUN_ID;
if (runId) { if (runId) {
const workflowRunInfo = await fetchWorkflowRunInfo(runId); const workflowRunInfo = await fetchWorkflowRunInfo(runId);
@@ -122725,7 +122715,8 @@ async function startMcpServer(ctx) {
log.info(`\u{1F4DD} Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`); log.info(`\u{1F4DD} Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
} }
} }
const { url: url2, close } = await startMcpHttpServer(); const allModes = [...modes, ...ctx.payload.modes || []];
const { url: url2, close } = await startMcpHttpServer({ payload: ctx.payload, modes: allModes });
ctx.mcpServerUrl = url2; ctx.mcpServerUrl = url2;
ctx.mcpServerClose = close; ctx.mcpServerClose = close;
log.info(`\u{1F680} MCP server started at ${url2}`); log.info(`\u{1F680} MCP server started at ${url2}`);
+2 -13
View File
@@ -271,17 +271,6 @@ function parsePayload(inputs: Inputs): Payload {
} }
async function startMcpServer(ctx: MainContext): Promise<void> { async function startMcpServer(ctx: MainContext): Promise<void> {
// set environment variables for MCP server tools
const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
const allModes = [...modes, ...(ctx.payload.modes || [])];
process.env.GITHUB_REPOSITORY = githubRepository;
process.env.PULLFROG_MODES = JSON.stringify(allModes);
process.env.PULLFROG_PAYLOAD = JSON.stringify(ctx.payload);
// GITHUB_RUN_ID is already set in GitHub Actions, no need to set it here
// fetch the pre-created progress comment ID from the database // fetch the pre-created progress comment ID from the database
// this must be set BEFORE starting the MCP server so comment.ts can read it // this must be set BEFORE starting the MCP server so comment.ts can read it
const runId = process.env.GITHUB_RUN_ID; const runId = process.env.GITHUB_RUN_ID;
@@ -293,8 +282,8 @@ async function startMcpServer(ctx: MainContext): Promise<void> {
} }
} }
// start MCP server after env vars are set (comment.ts reads PULLFROG_PROGRESS_COMMENT_ID at import time) const allModes = [...modes, ...(ctx.payload.modes || [])];
const { url, close } = await startMcpHttpServer(); const { url, close } = await startMcpHttpServer({ payload: ctx.payload, modes: allModes });
ctx.mcpServerUrl = url; ctx.mcpServerUrl = url;
ctx.mcpServerClose = close; ctx.mcpServerClose = close;
log.info(`🚀 MCP server started at ${url}`); log.info(`🚀 MCP server started at ${url}`);
+4 -27
View File
@@ -1,20 +1,6 @@
import { type } from "arktype"; import { type } from "arktype";
import type { Mode } from "../modes.ts";
import { contextualize, tool } from "./shared.ts"; import { contextualize, tool } from "./shared.ts";
// Get modes from environment variable (set by createMcpConfigs)
function getModes(): Mode[] {
const modesJson = process.env.PULLFROG_MODES;
if (modesJson) {
try {
return JSON.parse(modesJson);
} catch {
return [];
}
}
return [];
}
export const SelectMode = type({ export const SelectMode = type({
modeName: type.string.describe( modeName: type.string.describe(
"the name of the mode to select (e.g., 'Plan', 'Build', 'Review', 'Prompt')" "the name of the mode to select (e.g., 'Plan', 'Build', 'Review', 'Prompt')"
@@ -26,23 +12,14 @@ export const SelectModeTool = tool({
description: description:
"Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.", "Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.",
parameters: SelectMode, parameters: SelectMode,
execute: contextualize(async ({ modeName }) => { execute: contextualize(async ({ modeName }, ctx) => {
const allModes = getModes(); const selectedMode = ctx.modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
if (allModes.length === 0) {
return {
error:
"No modes available. Modes must be provided via PULLFROG_MODES environment variable.",
};
}
const selectedMode = allModes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
if (!selectedMode) { if (!selectedMode) {
const availableModes = allModes.map((m) => m.name).join(", "); const availableModes = ctx.modes.map((m) => m.name).join(", ");
return { return {
error: `Mode "${modeName}" not found. Available modes: ${availableModes}`, error: `Mode "${modeName}" not found. Available modes: ${availableModes}`,
availableModes: allModes.map((m) => ({ name: m.name, description: m.description })), availableModes: ctx.modes.map((m) => ({ name: m.name, description: m.description })),
}; };
} }
+6 -2
View File
@@ -20,7 +20,7 @@ import { PullRequestInfoTool } from "./prInfo.ts";
import { ReviewTool } from "./review.ts"; import { ReviewTool } from "./review.ts";
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts"; import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts";
import { SelectModeTool } from "./selectMode.ts"; import { SelectModeTool } from "./selectMode.ts";
import { addTools } from "./shared.ts"; import { addTools, initMcpContext, type McpInitContext } from "./shared.ts";
/** /**
* Find an available port starting from the given port * Find an available port starting from the given port
@@ -54,7 +54,11 @@ async function findAvailablePort(startPort: number): Promise<number> {
/** /**
* Start the MCP HTTP server and return the URL and close function * Start the MCP HTTP server and return the URL and close function
*/ */
export async function startMcpHttpServer(): Promise<{ url: string; close: () => Promise<void> }> { export async function startMcpHttpServer(
state: McpInitContext
): Promise<{ url: string; close: () => Promise<void> }> {
initMcpContext(state);
const server = new FastMCP({ const server = new FastMCP({
name: ghPullfrogMcpName, name: ghPullfrogMcpName,
version: "0.0.1", version: "0.0.1",
+24 -24
View File
@@ -2,46 +2,38 @@ import { Octokit } from "@octokit/rest";
import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { FastMCP, Tool } from "fastmcp"; import type { FastMCP, Tool } from "fastmcp";
import type { Payload } from "../external.ts"; import type { Payload } from "../external.ts";
import type { Mode } from "../modes.ts";
import { getGitHubInstallationToken, parseRepoContext, type RepoContext } from "../utils/github.ts"; import { getGitHubInstallationToken, parseRepoContext, type RepoContext } from "../utils/github.ts";
export interface ToolResult { export interface McpInitContext {
content: { payload: Payload;
type: "text"; modes: Mode[];
text: string;
}[];
isError?: boolean;
} }
export function getPayload(): Payload { let mcpInitContext: McpInitContext | undefined;
const payloadEnv = process.env.PULLFROG_PAYLOAD;
if (!payloadEnv) {
throw new Error("PULLFROG_PAYLOAD environment variable is required");
}
try { // this must be called on mcp server initialization
return JSON.parse(payloadEnv) as Payload; export function initMcpContext(state: McpInitContext): void {
} catch (error) { mcpInitContext = state;
throw new Error( }
`Failed to parse PULLFROG_PAYLOAD: ${error instanceof Error ? error.message : String(error)}`
); export interface McpContext extends McpInitContext, RepoContext {
} octokit: Octokit;
} }
export function getMcpContext(): McpContext { export function getMcpContext(): McpContext {
if (!mcpInitContext) {
throw new Error("MCP context not initialized. Call initializeMcpContext first.");
}
return { return {
...mcpInitContext,
...parseRepoContext(), ...parseRepoContext(),
octokit: new Octokit({ octokit: new Octokit({
auth: getGitHubInstallationToken(), auth: getGitHubInstallationToken(),
}), }),
payload: getPayload(),
}; };
} }
export interface McpContext extends RepoContext {
octokit: Octokit;
payload: Payload;
}
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => toolDef; export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => toolDef;
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => { export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
@@ -65,6 +57,14 @@ export const contextualize = <T>(
}; };
}; };
export interface ToolResult {
content: {
type: "text";
text: string;
}[];
isError?: boolean;
}
const handleToolSuccess = (data: Record<string, any>): ToolResult => { const handleToolSuccess = (data: Record<string, any>): ToolResult => {
return { return {
content: [ content: [
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@pullfrog/action", "name": "@pullfrog/action",
"version": "0.0.114", "version": "0.0.117",
"type": "module", "type": "module",
"files": [ "files": [
"index.js", "index.js",