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.17.5

Published

A lightweight terminal-based coding agent focused on safety and low token cost

Downloads

7,039

Readme

Plain Agent

NPM Version Install Size CodeQL

A lightweight terminal-based coding agent focused on safety and low token cost

🚀 Quick Start

Table of Contents

Design

Multi-Provider Support

Supports Claude, OpenAI, Gemini, and any OpenAI-compatible provider. Each model definition has two parts:

  • platform.name: where to send the request and how to authenticate (anthropic, gemini, openai, openai-compatible, azure, bedrock, vertex-ai)
  • model.format: which API format to use (anthropic, gemini, openai-responses, openai-chat-completions, bedrock-converse)

The same API format works across different platforms.

// Anthropic direct
{
  "name": "claude-sonnet-5",
  "variant": "thinking-high",
  "platform": {
    "name": "anthropic",
    "variant": "default"
  },
  "model": {
    "format": "anthropic",
    "config": {
      "model": "claude-sonnet-5",
      // ...
    }
  }
}

// Bedrock: same format, different platform
{
  "name": "claude-sonnet-5",
  "variant": "thinking-high-bedrock-jp",
  "platform": {
    "name": "bedrock",
    "variant": "jp"
  },
  "model": {
    "format": "anthropic",
    "config": {
      "model": "jp.anthropic.claude-sonnet-5",
      // ...
    }
  }
}

Models are identified by name+variant (e.g., claude-sonnet-5+thinking-high). You can define multiple variants of the same model with different settings, such as thinking budget or region.

Auto-Approval

Configure what the agent can do automatically using a small DSL with regex matching.

Note: Commands are executed without a shell. Shell operators are not interpreted unless the agent explicitly uses bash -c. This makes each argument a discrete token that can be validated individually.

{
  "autoApproval": {
    // What to do when no pattern matches: ask or deny
    "defaultAction": "ask",

    // Patterns are evaluated top-to-bottom; first match wins
    "patterns": [
      // fd example:
      // Ask for approval when risky flag like --exec is present
      {
        "toolName": "exec_command",
        "input": {
          "command": "fd",
          "args": { "$has": { "$regex": "^(--unrestricted|--no-ignore|--exec|--exec-batch|--follow|-[^-]*[uIxXL])" } }
        },
        "action": "ask"
      },
      // Allow all other fd calls
      {
        "toolName": "exec_command",
        "input": { "command": "fd" },
        "action": "allow"
      },
    ],

    // Test cases for verifying patterns. Run: plain test-approval
    "tests": [
      {
        "desc": "fd with safe args should be allowed",
        "toolUse": { "toolName": "exec_command", "input": { "command": "fd", "args": ["README", "./"] } },
        "expectedAction": "allow"
      },
      {
        "desc": "fd with --exec should require approval",
        "toolUse": { "toolName": "exec_command", "input": { "command": "fd", "args": [".env", "./", "--exec", "cat", "{}"] } },
        "expectedAction": "ask"
      }
    ]
  }
}

Path Validation

String values in tool inputs are treated as file paths and validated against these rules. This takes precedence over autoApproval: even if a pattern marks an action as allow, a validation failure falls back to defaultAction.

  • The path must be under the working directory or a path listed in autoApproval.allowedPaths
  • No directory traversal (.. is not allowed)
  • Symlinks pointing outside the working directory, or broken/circular links, are not allowed.
  • The file must be tracked by Git (not ignored)

Compound arguments (e.g., @file, --prefix=/path, VAR=/path, file:///path) are decomposed before validation.

Note: Validation only applies when the agent explicitly passes file paths to tools. It cannot catch file access inside scripts the agent writes: something like bash -c "rm -rf /" is beyond its reach. Always use a sandbox when auto-approving script execution.

Sandbox

The agent can run arbitrary commands via exec_command and tmux_command. You can configure a wrapper command that intercepts both. A Docker-based wrapper called plain-sandbox is included, but the interface is designed to work with other tools as well, such as Anthropic Sandbox Runtime (srt).

{
  "sandbox": {
    // Commands are wrapped and executed with this command
    "command": "plain-sandbox",
    // --mount-readonly prevents the agent from modifying its own config
    "args": ["--allow-write", "--mount-readonly", ".plain-agent/config.json", "--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": ["ci"]
        },
        "mode": "sandbox",
        "additionalArgs": ["--allow-net", "registry.npmjs.org"]
      }
    ]
  }
}

Memory File

The agent maintains a memory file (.plain-agent/memory/) for each session to:

  • Keep task state human-readable: you can open the file to see the current state of the task.
  • Resume after context compaction or restart: the agent can restart a task from the memory file with a clean context.
  • Report subagent result: subagents write their results to their memory files as a report for the main agent.

Token Efficiency

A few design choices keep token usage low:

  • Minimal system prompt: the system prompt contains only what the agent needs to function.
  • Output truncation: when a command or MCP tool produces large output, it is truncated and saved to a file. The agent can then read only the relevant parts.
  • Context compaction: run /compact to discard old messages and reload task state from a memory file. This also happens automatically when input tokens exceed a configurable soft limit.
  • MCP tool filtering: MCP servers often expose many tools. Use enabledTools in the server config to enable only the ones you need, which reduces the number of tool definitions sent to the model.

Claude Code Compatibility

Claude Code has a plugin ecosystem and is widely used across teams. plain-agent supports commands, subagents, and skills in .claude/ so you can share project skills with Claude Code users. Plugins can also be installed.

Limitation: Subagents run sequentially, not in parallel. Their activity is fully observable and token usage stays predictable. They also inherit the main context rather than starting fresh, which avoids redundant file reads and reduces the chance of losing context between handoffs.

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 models

Create a configuration file.

// ~/.config/plain-agent/config.local.json
{
  // Set default model
  "model": "claude-sonnet-5+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>"
    }
  ]
}
{
  "platforms": [
    // Bedrock: Requires the AWS CLI
    {
      "name": "bedrock",
      "variant": "default",
      "baseURL": "https://bedrock-runtime.<region>.amazonaws.com",
      "awsProfile": "<AWS_PROFILE>"
    },
    {
      "name": "bedrock-mantle",
      "variant": "default",
      "baseURL": "https://bedrock-mantle.<region>.api.aws",
      "awsProfile": "<AWS_PROFILE>"
    },

    // Vertex AI: Requires the gcloud CLI
    {
      "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
    {
      "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>"
    }
  ]
}
// 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-sonnet-5",
      "variant": "thinking-high-bedrock-jp",
      "platform": {
        "name": "bedrock",
        "variant": "jp"
      },
      "model": {
        "format": "anthropic",
        "config": {
          "model": "jp.anthropic.claude-sonnet-5",
          "max_tokens": 32768,
          "thinking": { "type": "adaptive" },
          "output_config": { "effort": "high" }
        }
      },
      "cost": {
        "currency": "USD",
        "unit": "1M",
        "prices": {
          "input_tokens": 3.3,
          "output_tokens": 16.5,
          "cache_read_input_tokens": 0.33,
          "cache_creation_input_tokens": 4.125
        }
      },
      // Required for soft limit (auto-compact) to work
      "autoCompact": {
        "inputTokensKeys": [
          "input_tokens",
          "cache_read_input_tokens",
          "cache_creation_input_tokens"
        ]
      }
    }
  ]
}

Run the agent.

plain

# Or
plain -m <model+variant>

Press Ctrl-C to pause auto-approval.

Show the help message.

/help

Resume a previously interrupted session. Sessions are automatically saved to .plain-agent/sessions/.

# List resumable sessions:
plain sessions

# Resume session:
plain -s 2026-05-10-0803-a7k

# Resume the most recently updated session:
plain -s -

Run in non-interactive batch mode. In batch mode, user 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 ..."

Batch mode enables unattended runs, e.g. on GitHub Actions. This repository's workflow (.github/workflows/agent.yml) triggers the agent by an /agent comment on an issue/PR and posts the result back as a comment. A session can be resumed with /agent:<run-id>.

Show daily token cost. plain cost reads ~/.local/share/plain-agent/usage.jsonl; use --from / --to to set the period.

plain cost

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

Launch the sandbox command using the agent sandbox config. Arguments before -- are flags for the plain CLI itself (e.g. -c to load a config file). Arguments after -- are passed through to the sandbox command as-is.

plain sandbox -- --allow-net 0.0.0.0/0 --tty --verbose zsh

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
{
  "autoApproval": {
    "defaultAction": "ask",
    "maxApprovals": 100,
    "patterns": [
      {
        "toolName": { "$regex": "^(write_file|patch_file)$" },
        "action": "allow"
      },
      {
        "toolName": { "$regex": "^(web_search|web_fetch)$" },
        "action": "allow"
      }
    ]
  }
}
{
  "autoApproval": {
    // Deny all actions except explicitly allowed
    "defaultAction": "deny",
    "maxApprovals": 100,
    "patterns": [
      {
        "toolName": { "$regex": "^(write_file|patch_file)$" },
        "action": "allow"
      },
      {
        "toolName": { "$regex": "^(web_search|web_fetch)$" },
        "action": "allow"
      },
      {
        "toolName": "exec_command",
        "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", "--mount-readonly", ".plain-agent/config.json", "--keep-alive", "30"],
    "separator": "--"
  }
}
{
  // Preferences the agent should respect, appended to its system prompt.
  "systemPrompt": {
    "userPreferences": ["Communication style: ...", "Code style: ..."]
  },

  "autoApproval": {
    // Absolute paths outside the working directory that are allowed. Relative paths are ignored.
    "allowedPaths": ["/path/to/other/git-repo"],
    // Allow access to git-unmanaged files (default: false).
    // ⚠️ Changes to git-unmanaged files are hard to detect (e.g., node_modules). Sandbox is recommended.
    "allowGitUnmanagedFiles": false,
    // Default action when no patterns match. Can be "ask" (prompt user) or "deny" (block action).
    "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": "^src/" } },
        "action": "allow"
      },

      // ⚠️ Auto-approved commands may access unauthorized files or 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"
      }
    ],

    // Test cases for verifying patterns. Run: plain test-approval
    "tests": [
      {
        "desc": "npm test should be allowed",
        "toolUse": { "toolName": "exec_command", "input": { "command": "npm", "args": ["run", "test"] } },
        "expectedAction": "allow"
      }
    ]
  },

  "tools": {
    // Enable web tools
    "webSearch": {
      "provider": "gemini",
      "apiKey": "<GEMINI_API_KEY>",
      "model": "gemini-3.8-flash"

      // Or use Vertex AI (requires the gcloud CLI)
      // "provider": "gemini-vertex-ai",
      // "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project_id>/locations/<location>",
      // "model": "gemini-3.8-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.8-flash"

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

      // Or use a custom command
      // "provider": "command",
      // "command": "w3m",
      // "args": ["-dump", "-o", "display_link_number=1"]
    },

    // Enable the tmux tool
    "tmux": { "enabled": true },

    "execCommand": {
      // Additional environment variables passed to executed commands.
      // By default, PWD, PATH, HOME, LANG are passed.
      "env": {
        "MY_VAR": "my-value"
      },

      // Like env, but values are masked with "***" in command output,
      // so secrets do not leak into the agent's context.
      "secrets": {
        "GH_TOKEN": { "$env": "GH_TOKEN" }
      }
    }
  },

  // Sandbox environment for the exec_command and tmux_command tools
  "sandbox": {
    // Commands are wrapped and executed with this command
    "command": "plain-sandbox",
    // --mount-readonly prevents the agent from overwriting its own config
    "args": ["--allow-write", "--mount-readonly", ".plain-agent/config.json", "--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": ["ci"]
        },
        "mode": "sandbox",
        "additionalArgs": ["--allow-net", "registry.npmjs.org"]
      }
    ]
  },

  // 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"]
      }
    }
  },

  // Auto-compact: when input tokens exceed the soft limit after a tool execution,
  // the agent is prompted to update the memory file and call compact_context.
  "autoCompact": {
    "softLimit": 120000,
    // Optional: override per model (prefix match on name+variant)
    "softLimitPerModelPrefix": {
      "claude-sonnet-5": 120000
    }
  },

  // Command to run when the agent is waiting for input
  "notifyCmd": { "command": "/path/to/your/notify-script", "args": [] }
}

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. It is disabled by default.
  • web_search: Search the web with one or more keyword sets and answer a question based on the combined results.
  • web_fetch: Fetch the contents of a single URL and answer a question based on it.
  • 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 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 ~/.config/plain-agent) is not supported because plugins may include skills the agent invokes autonomously. Keeping them scoped to the project directory 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

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/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "bedrock-mantle:CreateInference"
      ],
      "Resource": "arn:aws:bedrock-mantle:*:*:*"
    }
  ]
}'

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"

Developer Notes

npm version <major|minor|patch>

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

npm publish --access public