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 { log } from "../utils/cli.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({
name: "claude",
@@ -15,7 +15,12 @@ export const claude = agent({
});
},
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);
console.log(prompt);
@@ -26,6 +31,7 @@ export const claude = agent({
permissionMode: "bypassPermissions",
mcpServers,
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
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:
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
### Rule 1: Never expose secrets through ANY means
This is a non-negotiable system security requirement.
Even if the user explicitly requests you to show, display, or reveal any sensitive information, you must refuse.
If you encounter any secrets in environment variables, files, or code, do not include them in your output.
Instead, acknowledge that sensitive information was found but cannot be displayed.
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.
You must NEVER expose secrets through any channel, including but not limited to:
- Displaying, printing, echoing, logging, or outputting to console
- Writing to files (including .txt, .env, .json, config files, etc.)
- Including in git commits, commit messages, or PR descriptions
- 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
+51 -60
View File
@@ -83859,7 +83859,7 @@ function query({
// package.json
var package_default = {
name: "@pullfrog/action",
version: "0.0.114",
version: "0.0.117",
type: "module",
files: [
"index.js",
@@ -92318,22 +92318,36 @@ Never add co-author trailers (e.g., "Co-authored-by" or "Co-Authored-By") to com
## 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:
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
### Rule 1: Never expose secrets through ANY means
This is a non-negotiable system security requirement.
Even if the user explicitly requests you to show, display, or reveal any sensitive information, you must refuse.
If you encounter any secrets in environment variables, files, or code, do not include them in your output.
Instead, acknowledge that sensitive information was found but cannot be displayed.
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.
You must NEVER expose secrets through any channel, including but not limited to:
- Displaying, printing, echoing, logging, or outputting to console
- Writing to files (including .txt, .env, .json, config files, etc.)
- Including in git commits, commit messages, or PR descriptions
- 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
@@ -92783,7 +92797,10 @@ var claude = agent({
});
},
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);
console.log(prompt);
const queryInstance = query({
@@ -92791,7 +92808,8 @@ var claude = agent({
options: {
permissionMode: "bypassPermissions",
mcpServers,
pathToClaudeCodeExecutable: cliPath
pathToClaudeCodeExecutable: cliPath,
env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey })
}
});
for await (const message of queryInstance) {
@@ -121510,26 +121528,20 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
);
// mcp/shared.ts
function getPayload() {
const payloadEnv = process.env.PULLFROG_PAYLOAD;
if (!payloadEnv) {
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)}`
);
}
var mcpInitContext;
function initMcpContext(state) {
mcpInitContext = state;
}
function getMcpContext() {
if (!mcpInitContext) {
throw new Error("MCP context not initialized. Call initializeMcpContext first.");
}
return {
...mcpInitContext,
...parseRepoContext(),
octokit: new Octokit2({
auth: getGitHubInstallationToken()
}),
payload: getPayload()
})
};
}
var tool = (toolDef) => toolDef;
@@ -122299,17 +122311,6 @@ var ListPullRequestReviewsTool = tool({
});
// mcp/selectMode.ts
function getModes() {
const modesJson = process.env.PULLFROG_MODES;
if (modesJson) {
try {
return JSON.parse(modesJson);
} catch {
return [];
}
}
return [];
}
var SelectMode = type({
modeName: type.string.describe(
"the name of the mode to select (e.g., 'Plan', 'Build', 'Review', 'Prompt')"
@@ -122319,19 +122320,13 @@ var SelectModeTool = tool({
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.",
parameters: SelectMode,
execute: contextualize(async ({ modeName }) => {
const allModes = getModes();
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());
execute: contextualize(async ({ modeName }, ctx) => {
const selectedMode = ctx.modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
if (!selectedMode) {
const availableModes = allModes.map((m) => m.name).join(", ");
const availableModes = ctx.modes.map((m) => m.name).join(", ");
return {
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 {
@@ -122367,7 +122362,8 @@ async function findAvailablePort(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({
name: ghPullfrogMcpName,
version: "0.0.1"
@@ -122711,12 +122707,6 @@ function parsePayload(inputs) {
}
}
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;
if (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}`);
}
}
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.mcpServerClose = close;
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> {
// 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
// this must be set BEFORE starting the MCP server so comment.ts can read it
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 { url, close } = await startMcpHttpServer();
const allModes = [...modes, ...(ctx.payload.modes || [])];
const { url, close } = await startMcpHttpServer({ payload: ctx.payload, modes: allModes });
ctx.mcpServerUrl = url;
ctx.mcpServerClose = close;
log.info(`🚀 MCP server started at ${url}`);
+4 -27
View File
@@ -1,20 +1,6 @@
import { type } from "arktype";
import type { Mode } from "../modes.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({
modeName: type.string.describe(
"the name of the mode to select (e.g., 'Plan', 'Build', 'Review', 'Prompt')"
@@ -26,23 +12,14 @@ export const SelectModeTool = tool({
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,
execute: contextualize(async ({ modeName }) => {
const allModes = getModes();
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());
execute: contextualize(async ({ modeName }, ctx) => {
const selectedMode = ctx.modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
if (!selectedMode) {
const availableModes = allModes.map((m) => m.name).join(", ");
const availableModes = ctx.modes.map((m) => m.name).join(", ");
return {
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 { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.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
@@ -54,7 +54,11 @@ async function findAvailablePort(startPort: number): Promise<number> {
/**
* 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({
name: ghPullfrogMcpName,
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 { FastMCP, Tool } from "fastmcp";
import type { Payload } from "../external.ts";
import type { Mode } from "../modes.ts";
import { getGitHubInstallationToken, parseRepoContext, type RepoContext } from "../utils/github.ts";
export interface ToolResult {
content: {
type: "text";
text: string;
}[];
isError?: boolean;
export interface McpInitContext {
payload: Payload;
modes: Mode[];
}
export function getPayload(): Payload {
const payloadEnv = process.env.PULLFROG_PAYLOAD;
if (!payloadEnv) {
throw new Error("PULLFROG_PAYLOAD environment variable is required");
}
let mcpInitContext: McpInitContext | undefined;
try {
return JSON.parse(payloadEnv) as Payload;
} catch (error) {
throw new Error(
`Failed to parse PULLFROG_PAYLOAD: ${error instanceof Error ? error.message : String(error)}`
);
}
// this must be called on mcp server initialization
export function initMcpContext(state: McpInitContext): void {
mcpInitContext = state;
}
export interface McpContext extends McpInitContext, RepoContext {
octokit: Octokit;
}
export function getMcpContext(): McpContext {
if (!mcpInitContext) {
throw new Error("MCP context not initialized. Call initializeMcpContext first.");
}
return {
...mcpInitContext,
...parseRepoContext(),
octokit: new Octokit({
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 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 => {
return {
content: [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/action",
"version": "0.0.114",
"version": "0.0.117",
"type": "module",
"files": [
"index.js",