npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iinm/plain-agent

v1.10.20

Published

A lightweight coding agent for the terminal.

Readme

Plain Agent

A lightweight coding agent for the terminal.

  • Multi-provider — Use Claude, GPT, Gemini, or any OpenAI-compatible model via direct APIs or through Bedrock, Vertex AI, or Azure.
  • Fine-grained auto-approval — Auto-approve tool calls by matching tool names and inputs against configurable patterns, while validating string inputs as paths for safety.
  • Sandboxed execution — Run commands in a Docker container with filesystem and network isolation.
  • Supports Claude Code resources — Use Claude Code plugins, commands, subagents, and skills from .claude/.
  • Zero external dependencies — Built using only Node.js standard libraries.

Limitations

  • Path validation only covers tool arguments — It blocks paths outside the working directory, directory traversal (..), symlinks that escape the project, and git-ignored files. However, it only applies to paths explicitly passed as tool-use arguments, so it cannot control file access inside arbitrary scripts. Always use sandboxed execution when running arbitrary scripts.
  • Sequential subagent execution — Subagents run one at a time rather than in parallel. The trade-off is that every step is streamed to your terminal, so you can follow exactly what each subagent is doing.

Requirements

  • Node.js 22 or later
  • Credentials for your LLM provider
  • ripgrep, fd
  • Bash and Docker for sandboxed execution

Quick Start

npm install -g @iinm/plain-agent

List the available models.

plain list-models

Create a configuration file.

// ~/.config/plain-agent/config.local.json
{
  // Set default model
  "model": "claude-sonnet-4-6+thinking-high",

  // Configure the providers you want to use
  "platforms": [
    {
      "name": "anthropic",
      "variant": "default",
      "apiKey": "<ANTHROPIC_API_KEY>"
      // Or read from environment variable
      // "apiKey": { "$env": "ANTHROPIC_API_KEY" }
    },
    {
      "name": "gemini",
      "variant": "default",
      "apiKey": "<GEMINI_API_KEY>"
    },
    {
      "name": "openai",
      "variant": "default",
      "apiKey": "<OPENAI_API_KEY>"
    }
  ],

  // Optional: enable web tools
  "tools": {
    "webSearch": {
      "provider": "gemini",
      "apiKey": "<GEMINI_API_KEY>",
      "model": "gemini-3.5-flash"

      // Or use Vertex AI (requires the gcloud CLI for authentication)
      // "provider": "gemini-vertex-ai",
      // "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project_id>/locations/<location>",
      // "model": "gemini-3.5-flash"

      // Or use a custom command
      // "provider": "command",
      // "command": "bash",
      // "args": ["-c", "w3m -dump -o display_link_number=1 \"https://lite.duckduckgo.com/lite?q=$*\"", "-"]
    },

    "webFetch": {
      "provider": "gemini",
      "apiKey": "<GEMINI_API_KEY>",
      "model": "gemini-3.5-flash"

      // Or use Vertex AI (requires the gcloud CLI for authentication)

      // Or use a custom command
      // "provider": "command",
      // "command": "w3m",
      // "args": ["-dump", "-o", "display_link_number=1"]
    }
  }
}
{
  "platforms": [
    // Bedrock: Requires the AWS CLI for authentication
    {
      "name": "bedrock",
      "variant": "default",
      "baseURL": "https://bedrock-runtime.<region>.amazonaws.com",
      "awsProfile": "<AWS_PROFILE>"
    },

    // Vertex AI: Requires the gcloud CLI for authentication
    {
      "name": "vertex-ai",
      "variant": "default",
      "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project>/locations/<location>",
      // Optional: impersonate this service account to obtain an auth token
      "account": "<SERVICE_ACCOUNT_EMAIL>"
    },

    // Azure: Requires the Azure CLI for authentication
    {
      "name": "azure",
      "variant": "default",
      "baseURL": "https://<resource>.services.ai.azure.com",
      // Optional
      "azureConfigDir": "/home/xxx/.azure-for-agent"
    },
    // Azure OpenAI
    {
      "name": "azure",
      "variant": "openai",
      "baseURL": "https://<resource>.openai.azure.com/openai",
      // Optional
      "azureConfigDir": "/home/xxx/.azure-for-agent"
    }
  ]
}
{
  "platforms": [
    {
      "name": "openai-compatible",
      "variant": "fireworks",
      "baseURL": "https://api.fireworks.ai/inference",
      "apiKey": "<FIREWORKS_API_KEY>"
    },
    {
      "name": "openai-compatible",
      "variant": "novita",
      "baseURL": "https://api.novita.ai/openai",
      "apiKey": "<NOVITA_API_KEY>"
    }
  ]
}
// Ollama example with a custom model
{
  "platforms": [
    {
      "name": "openai-compatible",
      "variant": "ollama",
      "baseURL": "https://ollama.com",
      "apiKey": "<API_KEY>"
    }
  ],
  "models": [
    {
      "name": "gpt-oss",
      "variant": "ollama",
      "platform": {
        "name": "openai-compatible",
        "variant": "ollama"
      },
      "model": {
        "format": "openai-responses",
        "config": {
          "model": "gpt-oss:120b-cloud"
        }
      }
    }
  ]
}
{
  "platforms": [
    {
      "name": "bedrock",
      "variant": "jp",
      "baseURL": "https://bedrock-runtime.ap-northeast-1.amazonaws.com",
      "awsProfile": "<AWS_PROFILE>"
    }
  ],
  "models": [
    {
      "name": "claude-haiku-4-5",
      "variant": "thinking-16k-bedrock-jp",
      "platform": {
        "name": "bedrock",
        "variant": "jp"
      },
      "model": {
        "format": "anthropic",
        "config": {
          "model": "jp.anthropic.claude-haiku-4-5-20251001-v1:0",
          "max_tokens": 32768,
          "thinking": { "type": "enabled", "budget_tokens": 16384 }
        }
      },
      "cost": {
        "currency": "USD",
        "unit": "1M",
        "costs": {
          "input_tokens": 1.1,
          "output_tokens": 5.5,
          "cache_read_input_tokens": 0.11,
          "cache_creation_input_tokens": 1.375
        }
      }
    },
    {
      "name": "claude-sonnet-4-6",
      "variant": "thinking-high-bedrock-jp",
      "platform": {
        "name": "bedrock",
        "variant": "jp"
      },
      "model": {
        "format": "anthropic",
        "config": {
          "model": "jp.anthropic.claude-sonnet-4-6",
          "max_tokens": 32768,
          "thinking": { "type": "adaptive" },
          "output_config": { "effort": "high" }
        }
      },
      "cost": {
        "currency": "USD",
        "unit": "1M",
        "costs": {
          "input_tokens": 3.3,
          "output_tokens": 16.5,
          "cache_read_input_tokens": 0.33,
          "cache_creation_input_tokens": 4.125
        }
      }
    }
  ]
}

Run the agent.

plain

# Or
plain -m <model+variant>

Press Ctrl-C to pause auto-approval. The agent will finish the current tool call, then return to the prompt.

Show the help message.

/help

Run in non-interactive batch mode. In batch mode, configuration files are not loaded automatically. Only the files specified with -c are loaded.

plain batch \
      -c ~/.config/plain-agent/config.local.json \
      -c .plain-agent/config.json \
      "Add tests for ..."

Show daily token cost. plain cost reads ~/.local/share/plain-agent/usage.jsonl; use --from / --to to set the period. Costs are shown separately by currency.

plain cost

# Or
plain cost --from 2026-04-01 --to 2026-04-30

Resume a previously interrupted interactive session. Sessions are automatically saved to .plain-agent/sessions/ and can be deleted with rm when no longer needed. If no argument is provided, the most recently updated session is resumed. Use --list to see resumable sessions. Switching models is not supported (-m is not allowed).

plain resume

# Or
plain resume --list
plain resume 2026-05-10-0803-a7k

Set up Plain Agent for your project.

/configure Auto-approve file writes and patches
/configure Set up a sandbox for this project

Configuration

Files are loaded in the following order. Settings in later files override earlier ones.

~/.config/plain-agent/
  ├── (1) config.json        # User configuration
  ├── (2) config.local.json  # User local configuration (including secrets)
  ├── prompts/               # Global/User-defined prompts
  └── agents/                # Global/User-defined agent roles

<project-root>
  └── .plain-agent/
        ├── (3) config.json        # Project-specific configuration
        ├── (4) config.local.json  # Project-specific local configuration (including secrets)
        ├── prompts/               # Project-specific prompts
        └── agents/                # Project-specific agent roles

Example

{
  "autoApproval": {
    "defaultAction": "deny",
    "maxApprovals": 100,
    "patterns": [
      {
        "toolName": { "$regex": "^(write_file|patch_file)$" },
        "action": "allow"
      },
      {
        "toolName": { "$regex": "^(exec_command|tmux_command)$" },
        "action": "allow"
      },
      {
        "toolName": { "$regex": "^(web_search|web_fetch)$" },
        "action": "allow"
      }
      // ⚠️ Never do this. MCP runs outside the sandbox, so it can send anything externally.
      // {
      //   "toolName": { "$regex": "." },
      //   "action": "allow"
      // }
    ]
  },
  "sandbox": {
    "command": "plain-sandbox",
    "args": ["--allow-write", "--skip-build", "--keep-alive", "30"],
    "separator": "--",
    "rules": [
      {
        "pattern": {
          "command": "npm",
          "args": ["ci"]
        },
        "mode": "sandbox",
        "additionalArgs": ["--allow-net", "0.0.0.0/0"]
      }
    ]
  }
}
{
  "autoApproval": {
    // Absolute paths outside the working directory that are allowed. Relative paths are ignored.
    "allowedPaths": ["/tmp"],
    "defaultAction": "ask",
    // Maximum number of automatic approvals.
    "maxApprovals": 50,
    // Patterns are evaluated in order. First match wins.
    "patterns": [
      {
        "toolName": { "$regex": "^(write_file|patch_file)$" },
        "input": { "filePath": { "$regex": "^\\.plain-agent/memory/.+\\.md$" } },
        "action": "allow"
      },
      {
        "toolName": { "$regex": "^(write_file|patch_file)$" },
        "input": { "filePath": { "$regex": "^src/" } },
        "action": "allow"
      },

      // ⚠️ Arbitrary code execution can access unauthorized files and networks. Always use a sandbox.
      {
        "toolName": "exec_command",
        "input": { "command": "npm", "args": ["run", { "$regex": "^(lint|test)$" }] },
        "action": "allow"
      },

      {
        "toolName": { "$regex": "^(web_search|web_fetch)$" },
        "action": "allow"
      },

      // MCP tool naming convention: mcp__<serverName>__<toolName>
      {
        "toolName": { "$regex": "mcp__slack__slack_(read|search)_.+" },
        "action": "allow"
      }
    ]
  },

  // Sandbox environment for the exec_command and tmux_command tools
  "sandbox": {
    "command": "plain-sandbox",
    "args": ["--allow-write", "--skip-build", "--keep-alive", "30"],
    // separator is inserted between sandbox flags and the user command to prevent bypasses
    "separator": "--",

    "rules": [
      // Run specific commands outside the sandbox
      {
        "pattern": {
          "command": { "$regex": "^(gh|docker)$" }
        },
        "mode": "unsandboxed"
      },
      // Run commands in the sandbox with network access
      {
        "pattern": {
          "command": "npm",
          "args": ["install"]
        },
        "mode": "sandbox",
        // Allow access to registry.npmjs.org
        "additionalArgs": ["--allow-net", "registry.npmjs.org"]
      }
    ]
  },

  // Configure MCP servers
  "mcpServers": {
    "chrome_devtools": {
      "command": "npx",
      "args": ["-y", "chrome-devtools-mcp@latest", "--isolated"]
    },
    // ⚠️ Add this to config.local.json to avoid committing secrets to Git
    "slack": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.slack.com/mcp", "--header", "Authorization:Bearer <SLACK_TOKEN>"],
    },
    "notion": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"],
      "options": {
        // Enable only specific tools. If not specified, all tools are enabled.
        "enabledTools": ["notion-search", "notion-fetch"]
      }
    },
    "aws_knowledge": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://knowledge-mcp.global.api.aws"]
    },
    // ⚠️ Add this to config.local.json to avoid committing secrets to Git
    "google_developer-knowledge": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://developerknowledge.googleapis.com/mcp", "--header", "X-Goog-Api-Key:<GOOGLE_API_KEY>"]
    }
  },

  // Override the default notification command
  "notifyCmd": { "command": "plain-notify-desktop", "args": [] }

  // Voice input. See "Voice Input" below.
  // ⚠️ Add this to config.local.json to avoid committing secrets to Git
  "voiceInput": {
    "provider": "openai",
    "apiKey": "<OPENAI_API_KEY>"
  }
}

Available Tools

The agent can use the following tools:

  • read_file: Read a file with line numbers (1-indexed). Supports offset and limit to read a specific range.
  • write_file: Write a file.
  • patch_file: Patch a file.
  • exec_command: Run a command without shell interpretation.
  • tmux_command: Run a tmux command.
  • web_search: Search the web with one or more keyword sets and answer a question based on the combined results (requires Google API key, Vertex AI configuration, or the command provider with a local search command).
  • web_fetch: Fetch the contents of a single URL and answer a question based on it (requires Google API key, Vertex AI configuration, or the command provider with a local fetch command such as w3m, curl, or lynx).
  • switch_to_subagent: Switch to a subagent role within the same conversation, focusing on the specified goal.
  • switch_to_main_agent: Switch back to the main agent role and report the result. After reporting, the subagent's conversation history is removed from the context.
  • compact_context: Compact the conversation context by discarding earlier messages and reloading task state from a memory file. Use this when the context has grown large but the task is not yet complete. You can also invoke it with the /compact slash command.

Prompts

You can define reusable prompts in Markdown files.

Locations

The agent searches for prompts in the following directories:

  • ~/.config/plain-agent/prompts/
  • .plain-agent/prompts/
  • .plain-agent/prompts/skills/
  • .claude/commands/
  • .claude/skills/

The prompt ID is the relative path of the file without the .md extension. For example, .plain-agent/prompts/commit.md becomes /prompts:commit.

Prompt File Format

---
description: Create a commit message based on staged changes
---

Review the staged changes and create a concise commit message following the conventional commits specification.

Shortcuts

Prompts located in a shortcuts/ subdirectory (e.g., .plain-agent/prompts/shortcuts/commit.md) can be invoked directly as a top-level command (e.g., /commit).

Subagents

Subagents are specialized helpers for specific tasks.

Locations

The agent searches for subagent definitions in the following directories:

  • ~/.config/plain-agent/agents/
  • .plain-agent/agents/
  • .claude/agents/

Subagent File Format

---
description: Fetches a web page and answers questions about its content
---

You are a web content reader and analyzer. Given a URL and a question, you:

1. Fetch the page content using `w3m -dump <URL>`.
2. Read and understand the fetched content.
3. Answer the user's question based on the content.

Claude Code Plugin Support

Plugins are installed under .plain-agent/claude-code-plugins/ and must be installed per project by running plain install-claude-code-plugins from the project root. Global installation (e.g., under ~/.plain-agent) is not supported because plugins may include skills the agent invokes autonomously. Keeping them scoped to the project keeps approval rules and permission management straightforward.

Example:

// .plain-agent/config.json
{
  "claudeCodePlugins": [
    {
      "source": "https://github.com/anthropics/claude-code",
      "plugins": [
        { "name": "feature-dev", "path": "plugins/feature-dev" },
        { "name": "code-review", "path": "plugins/code-review" }
      ]
    },
    {
      "source": "https://github.com/anthropics/skills",
      "plugins": [
        { "name": "document-skills", "path": "", "only": "xlsx|docx|pptx|pdf" }
      ]
    }
  ]
}
plain install-claude-code-plugins

Voice Input

Press Ctrl-O to start recording, then press it again to stop. Partial transcripts are inserted into the prompt as you speak, so you can edit and send them like regular text.

Requirements

  • A recording command on PATH: arecord, sox, or ffmpeg.
  • An API key for the chosen provider.
  • Your host must have microphone access.

Providers

OpenAI Realtime

// ~/.config/plain-agent/config.local.json
{
  "voiceInput": {
    "provider": "openai",
    "apiKey": "<OPENAI_API_KEY>"
    // "model": "gpt-4o-transcribe",  // or "gpt-4o-mini-transcribe", "whisper-1"
    // "language": "ja"               // ISO-639-1 code. Improves accuracy and latency.
  }
}

Gemini Live

// ~/.config/plain-agent/config.local.json
{
  "voiceInput": {
    "provider": "gemini",
    "apiKey": "<GEMINI_API_KEY>"
    // "model": "gemini-3.1-flash-live-preview",
    // "language": "ja"
  }
}

Options

  • toggleKey — Rebind the toggle key. Accepts "ctrl-<char>" where <char> is a letter (a-z) or one of [ \ ] ^ _. Defaults to "ctrl-o".
  • recorder — Override automatic recorder detection, e.g. { "command": "sox", "args": ["-q", "-d", "-b", "16", "-c", "1", "-r", "24000", "-e", "signed-integer", "-t", "raw", "-"] }. It must write raw 16-bit little-endian mono PCM to stdout at 24 kHz (OpenAI) or 16 kHz (Gemini).

Development

# Run lint, typecheck, and tests
npm run check

# Fix lint issues
npm run fix
# or
npm run fix -- --unsafe

# Update dependencies
npx npm-check-updates -t minor -c 3
npx npm-check-updates -t minor -c 3 -u

Release

npm version <major|minor|patch>

git push --follow-tags
gh release create $(git describe --tags) --generate-notes

npm publish --access public

Appendix: Creating Least-Privilege Users for Cloud Providers

# IAM Identity Center
identity_center_instance_arn="<IDENTITY_CENTER_INSTANCE_ARN>" # e.g., arn:aws:sso:::instance/ssoins-xxxxxxxxxxxxxxxx"
identity_store_id=<IDENTITY_STORE_ID>
aws_account_id=<AWS_ACCOUNT_ID>

# Create a permission set
permission_set_arn=$(aws sso-admin create-permission-set \
  --instance-arn "$identity_center_instance_arn" \
  --name "BedrockCodingAgent" \
  --description "Allows only Bedrock model invocation" \
  --query "PermissionSet.PermissionSetArn" --output text)

# Add a policy to the permission set
policy='{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "bedrock:InvokeModel",
        "bedrock:InvokeModelWithResponseStream",
        "bedrock:ListInferenceProfiles"
      ],
      "Resource": [
        "arn:aws:bedrock:*:*:foundation-model/*",
        "arn:aws:bedrock:*:*:inference-profile/*",
        "arn:aws:bedrock:*:*:application-inference-profile/*"
      ]
    }
  ]
}'

aws sso-admin put-inline-policy-to-permission-set \
  --instance-arn "$identity_center_instance_arn" \
  --permission-set-arn "$permission_set_arn" \
  --inline-policy "$policy"

# Create an SSO user
sso_user_name=<SSO_USER_NAME>
sso_user_email=<SSO_USER_EMAIL>
sso_user_family_name=<SSO_USER_FAMILY_NAME>
sso_user_given_name=<SSO_USER_GIVEN_NAME>

user_id=$(aws identitystore create-user \
  --identity-store-id "$identity_store_id" \
  --user-name "$sso_user_name" \
  --display-name "$sso_user_name" \
  --name "FamilyName=${sso_user_family_name},GivenName=${sso_user_given_name}" \
  --emails Value=${sso_user_email},Primary=true \
  --query "UserId" --output text)

# Associate the user, permission set, and account
aws sso-admin create-account-assignment \
  --instance-arn "$identity_center_instance_arn" \
  --target-id "$aws_account_id" \
  --target-type AWS_ACCOUNT \
  --permission-set-arn "$permission_set_arn" \
  --principal-type USER \
  --principal-id "$user_id"

# Verify the setup
aws configure sso
# profile: CodingAgent

profile=CodingAgent
aws sso login --profile "$profile"

echo '{"anthropic_version": "bedrock-2023-05-31", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' > request.json

aws bedrock-runtime invoke-model \
  --model-id global.anthropic.claude-haiku-4-5-20251001-v1:0 \
  --body fileb://request.json \
  --profile "$profile" \
  --region ap-northeast-1 \
  response.json
resource_group=<RESOURCE_GROUP>
account_name=<ACCOUNT_NAME> # Resource name

# Create a service principal
service_principal=$(az ad sp create-for-rbac --name "CodingAgentServicePrincipal" --skip-assignment)
echo "$service_principal"
app_id=$(echo "$service_principal" | jq -r .appId)

# Assign role permissions
# https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/role-based-access-control?view=foundry-classic#azure-openai-roles
resource_id=$(az cognitiveservices account show \
    --name "$account_name" \
    --resource-group "$resource_group" \
    --query id --output tsv)

az role assignment create \
  --role "Cognitive Services OpenAI User" \
  --assignee "$app_id" \
  --scope "$resource_id"

# Log in with the service principal
export app_secret=$(echo "$service_principal" | jq -r .password)
export tenant_id=$(echo "$service_principal" | jq -r .tenant)

export AZURE_CONFIG_DIR=$HOME/.azure-for-agent # Change this to store credentials elsewhere
az login --service-principal -u "$app_id" -p "$app_secret" --tenant "$tenant_id"
project_id=<PROJECT_ID>
service_account_name=<SERVICE_ACCOUNT_NAME>
service_account_email="${service_account_name}@${project_id}.iam.gserviceaccount.com"
your_account_email=<YOUR_ACCOUNT_EMAIL>

# Create a service account
gcloud iam service-accounts create "$service_account_name" \
  --project "$project_id" --display-name "Vertex AI Caller Service Account for Coding Agent"

# Grant permissions
gcloud projects add-iam-policy-binding "$project_id" \
  --member "serviceAccount:$service_account_email" \
  --role="roles/aiplatform.serviceAgent"

# Allow your account to impersonate the service account
gcloud iam service-accounts add-iam-policy-binding "$service_account_email" \
  --project "$project_id" \
  --member "user:$your_account_email" \
  --role "roles/iam.serviceAccountTokenCreator"

# Verify that tokens can be issued
gcloud auth print-access-token --impersonate-service-account "$service_account_email"